From e1beb7306cd01c8355f80c671e76a7922f1bdf6c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 26 Dec 2020 12:56:59 -0800 Subject: [PATCH 001/131] fix: miscellaneous small Python 3 build-time fixes --- .travis.yml | 2 +- docs/api/style.rst | 2 +- docx/compat.py | 4 ++-- docx/opc/compat.py | 7 +++---- tox.ini | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 105c42594..6ce09e8e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python python: + - "3.8" - "3.6" - - "3.5" - "2.7" # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -r requirements.txt diff --git a/docs/api/style.rst b/docs/api/style.rst index e1647caac..9e05ab351 100644 --- a/docs/api/style.rst +++ b/docs/api/style.rst @@ -6,7 +6,7 @@ Style-related objects A style is used to collect a set of formatting properties under a single name and apply those properties to a content object all at once. This promotes -formatting consistency thoroughout a document and across related documents +formatting consistency throughout a document and across related documents and allows formatting changes to be made globally by changing the definition in the appropriate style. diff --git a/docx/compat.py b/docx/compat.py index 98ab9051c..1e3012d95 100644 --- a/docx/compat.py +++ b/docx/compat.py @@ -34,6 +34,6 @@ def is_string(obj): def is_string(obj): """Return True if *obj* is a string, False otherwise.""" - return isinstance(obj, basestring) + return isinstance(obj, basestring) # noqa - Unicode = unicode + Unicode = unicode # noqa diff --git a/docx/opc/compat.py b/docx/opc/compat.py index d944fe43b..d5f63ac19 100644 --- a/docx/opc/compat.py +++ b/docx/opc/compat.py @@ -4,9 +4,7 @@ Provides Python 2/3 compatibility objects """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import sys @@ -29,6 +27,7 @@ def is_string(obj): """ return isinstance(obj, str) + # =========================================================================== # Python 2 versions # =========================================================================== @@ -47,4 +46,4 @@ def is_string(obj): """ Return True if *obj* is a string, False otherwise. """ - return isinstance(obj, basestring) + return isinstance(obj, basestring) # noqa diff --git a/tox.ini b/tox.ini index 4f75c628e..6ced79b71 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ python_classes = Test Describe python_functions = it_ they_ and_it_ but_it_ [tox] -envlist = py26, py27, py34, py35, py36 +envlist = py26, py27, py34, py35, py36, py38 [testenv] deps = From e1ffdcc4ba1ed348aac51e54a260f015ff9a38f7 Mon Sep 17 00:00:00 2001 From: Brian Seitz Date: Thu, 24 Oct 2019 18:57:31 -0400 Subject: [PATCH 002/131] Add 'zip_safe' == False to setup.py --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index f0b3ef54d..0eaeb50c8 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ def text_of(relpath): LONG_DESCRIPTION = text_of('README.rst') + '\n\n' + text_of('HISTORY.rst') +ZIP_SAFE = False params = { 'name': NAME, @@ -77,6 +78,7 @@ def text_of(relpath): 'tests_require': TESTS_REQUIRE, 'test_suite': TEST_SUITE, 'classifiers': CLASSIFIERS, + 'zip_safe': ZIP_SAFE, } setup(**params) From 24b9af76be07e633ca648250ff0c26bfcd1bad0e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 15 May 2021 13:54:30 -0700 Subject: [PATCH 003/131] fix: documentation fixes --- docs/api/enum/WdLineSpacing.rst | 2 +- docs/dev/analysis/features/shapes/picture.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/enum/WdLineSpacing.rst b/docs/api/enum/WdLineSpacing.rst index b03e7dd17..f28142e2d 100644 --- a/docs/api/enum/WdLineSpacing.rst +++ b/docs/api/enum/WdLineSpacing.rst @@ -10,7 +10,7 @@ Example:: from docx.enum.text import WD_LINE_SPACING paragraph = document.add_paragraph() - paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY + paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY ---- diff --git a/docs/dev/analysis/features/shapes/picture.rst b/docs/dev/analysis/features/shapes/picture.rst index a98c6e906..ca327512a 100644 --- a/docs/dev/analysis/features/shapes/picture.rst +++ b/docs/dev/analysis/features/shapes/picture.rst @@ -12,7 +12,7 @@ Candidate protocol :: >>> run = paragraph.add_run() - >>> inline_shape = run.add_inline_picture(file_like_image, MIME_type=None) + >>> inline_shape = run.add_picture(file_like_image, MIME_type=None) >>> inline_shape.width = width >>> inline_shape.height = height From eafffeac96acd5e880440a90fc5006d87bd023cb Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 15 May 2021 14:37:48 -0700 Subject: [PATCH 004/131] black: blacken setup.py This produces a lot of line changes for single to double quotes, but no logic changes were made. --- setup.py | 90 +++++++++++++++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/setup.py b/setup.py index 0eaeb50c8..5958749f6 100644 --- a/setup.py +++ b/setup.py @@ -21,64 +21,62 @@ def text_of(relpath): # 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( - "__version__ = '([^']+)'", text_of('docx/__init__.py') -).group(1) +version = re.search("__version__ = '([^']+)'", text_of("docx/__init__.py")).group(1) -NAME = 'python-docx' +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']} +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'] +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' + "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') +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, + "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) From 36cac78de080d412e9e50d56c2784e33655cad59 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 15 May 2021 14:36:34 -0700 Subject: [PATCH 005/131] release: prepare v0.8.11 release --- HISTORY.rst | 6 ++++++ docx/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5612cbf05..2b33a4d5d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +0.8.11 (2021-05-15) ++++++++++++++++++++ + +- Small build changes and Python 3.8 version changes like collections.abc location. + + 0.8.10 (2019-01-08) +++++++++++++++++++ diff --git a/docx/__init__.py b/docx/__init__.py index 4dae2946b..59756c021 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -2,7 +2,7 @@ from docx.api import Document # noqa -__version__ = '0.8.10' +__version__ = "0.8.11" # register custom Part classes with opc package reader diff --git a/setup.py b/setup.py index 5958749f6..7c34edcca 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def text_of(relpath): # 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("__version__ = '([^']+)'", text_of("docx/__init__.py")).group(1) +version = re.search(r'__version__ = "([^"]+)"', text_of("docx/__init__.py")).group(1) NAME = "python-docx" From 629f942ee2cdeb42bbe48460010cba354b2ab28e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 24 Sep 2023 12:18:38 -0700 Subject: [PATCH 006/131] rfctr: Blacken code base --- docs/conf.py | 61 ++- docx/api.py | 2 +- docx/blkcntnr.py | 4 +- docx/compat.py | 2 - docx/dml/color.py | 4 +- docx/document.py | 9 +- docx/enum/__init__.py | 1 - docx/enum/base.py | 74 ++-- docx/enum/dml.py | 87 ++-- docx/enum/section.py | 40 +- docx/enum/shape.py | 1 + docx/enum/style.py | 560 ++++++----------------- docx/enum/table.py | 91 ++-- docx/enum/text.py | 343 ++++++--------- docx/image/__init__.py | 20 +- docx/image/bmp.py | 3 +- docx/image/constants.py | 227 +++++----- docx/image/gif.py | 5 +- docx/image/helpers.py | 21 +- docx/image/image.py | 14 +- docx/image/jpeg.py | 75 ++-- docx/image/png.py | 22 +- docx/image/tiff.py | 38 +- docx/opc/constants.py | 610 +++++++++++--------------- docx/opc/coreprops.py | 5 +- docx/opc/oxml.py | 84 ++-- docx/opc/package.py | 13 +- docx/opc/packuri.py | 17 +- docx/opc/part.py | 15 +- docx/opc/parts/coreprops.py | 15 +- docx/opc/phys_pkg.py | 15 +- docx/opc/pkgreader.py | 23 +- docx/opc/pkgwriter.py | 6 +- docx/opc/rel.py | 25 +- docx/opc/shared.py | 7 +- docx/opc/spec.py | 36 +- docx/oxml/__init__.py | 211 ++++----- docx/oxml/coreprops.py | 137 +++--- docx/oxml/document.py | 12 +- docx/oxml/ns.py | 17 +- docx/oxml/numbering.py | 40 +- docx/oxml/section.py | 57 ++- docx/oxml/settings.py | 131 ++++-- docx/oxml/shape.py | 134 +++--- docx/oxml/shared.py | 11 +- docx/oxml/simpletypes.py | 122 ++---- docx/oxml/styles.py | 125 +++--- docx/oxml/table.py | 244 +++++++---- docx/oxml/text/font.py | 139 +++--- docx/oxml/text/paragraph.py | 9 +- docx/oxml/text/parfmt.py | 113 +++-- docx/oxml/text/run.py | 45 +- docx/oxml/xmlchemy.py | 175 ++++---- docx/package.py | 8 +- docx/parts/hdrftr.py | 8 +- docx/parts/image.py | 7 +- docx/parts/numbering.py | 6 +- docx/parts/settings.py | 12 +- docx/parts/story.py | 2 +- docx/parts/styles.py | 12 +- docx/shape.py | 14 +- docx/shared.py | 19 +- docx/styles/__init__.py | 32 +- docx/styles/latent.py | 36 +- docx/styles/style.py | 12 +- docx/styles/styles.py | 11 +- docx/table.py | 13 +- docx/text/font.py | 84 ++-- docx/text/paragraph.py | 11 +- docx/text/parfmt.py | 10 +- docx/text/run.py | 18 +- docx/text/tabstops.py | 17 +- features/environment.py | 4 +- features/steps/api.py | 21 +- features/steps/block.py | 17 +- features/steps/cell.py | 17 +- features/steps/coreprops.py | 99 +++-- features/steps/document.py | 106 ++--- features/steps/font.py | 167 ++++--- features/steps/hdrftr.py | 11 +- features/steps/helpers.py | 21 +- features/steps/image.py | 42 +- features/steps/numbering.py | 11 +- features/steps/paragraph.py | 107 ++--- features/steps/parfmt.py | 161 ++++--- features/steps/section.py | 133 +++--- features/steps/settings.py | 19 +- features/steps/shape.py | 95 ++-- features/steps/shared.py | 6 +- features/steps/styles.py | 305 ++++++------- features/steps/table.py | 311 +++++++------ features/steps/tabstops.py | 57 +-- features/steps/text.py | 160 +++---- tests/dml/test_color.py | 142 +++--- tests/image/test_bmp.py | 7 +- tests/image/test_gif.py | 5 +- tests/image/test_helpers.py | 20 +- tests/image/test_image.py | 126 +++--- tests/image/test_jpeg.py | 245 +++++------ tests/image/test_png.py | 96 ++-- tests/image/test_tiff.py | 151 ++++--- tests/opc/parts/test_coreprops.py | 11 +- tests/opc/test_coreprops.py | 168 +++---- tests/opc/test_oxml.py | 61 +-- tests/opc/test_package.py | 219 ++++----- tests/opc/test_packuri.py | 49 ++- tests/opc/test_part.py | 117 +++-- tests/opc/test_phys_pkg.py | 64 ++- tests/opc/test_pkgreader.py | 305 +++++++------ tests/opc/test_pkgwriter.py | 100 ++--- tests/opc/test_rel.py | 154 +++---- tests/opc/unitdata/rels.py | 122 +++--- tests/opc/unitdata/types.py | 18 +- tests/oxml/parts/test_document.py | 27 +- tests/oxml/parts/unitdata/document.py | 8 +- tests/oxml/test__init__.py | 62 ++- tests/oxml/test_ns.py | 20 +- tests/oxml/test_styles.py | 33 +- tests/oxml/test_table.py | 287 +++++++----- tests/oxml/test_xmlchemy.py | 426 +++++++++--------- tests/oxml/text/test_run.py | 22 +- tests/oxml/unitdata/dml.py | 102 ++--- tests/oxml/unitdata/numbering.py | 10 +- tests/oxml/unitdata/section.py | 29 +- tests/oxml/unitdata/shared.py | 10 +- tests/oxml/unitdata/styles.py | 10 +- tests/oxml/unitdata/table.py | 44 +- tests/oxml/unitdata/text.py | 96 ++-- tests/parts/test_document.py | 38 +- tests/parts/test_hdrftr.py | 2 - tests/parts/test_image.py | 33 +- tests/parts/test_numbering.py | 31 +- tests/parts/test_settings.py | 11 +- tests/parts/test_story.py | 1 - tests/parts/test_styles.py | 7 +- tests/styles/test_latent.py | 297 +++++++------ tests/styles/test_style.py | 391 +++++++++-------- tests/styles/test_styles.py | 219 +++++---- tests/test_api.py | 17 +- tests/test_blkcntnr.py | 58 +-- tests/test_document.py | 94 ++-- tests/test_enum.py | 55 +-- tests/test_package.py | 19 +- tests/test_section.py | 230 +++++----- tests/test_settings.py | 3 +- tests/test_shape.py | 109 +++-- tests/test_shared.py | 63 ++- tests/test_table.py | 528 ++++++++++++---------- tests/text/test_font.py | 475 +++++++++++--------- tests/text/test_paragraph.py | 179 ++++---- tests/text/test_parfmt.py | 473 +++++++++++--------- tests/text/test_run.py | 285 ++++++------ tests/text/test_tabstops.py | 261 ++++++----- tests/unitdata.py | 42 +- tests/unitutil/cxml.py | 67 +-- tests/unitutil/file.py | 20 +- tests/unitutil/mock.py | 6 +- 157 files changed, 7141 insertions(+), 6738 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1a988453a..ceea98093 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 @@ -193,7 +193,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 +211,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 +221,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 +229,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 +250,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 +263,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 +297,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 +305,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 +318,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 +346,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 +358,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 +380,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/docx/api.py b/docx/api.py index 63e18c406..4cf6acd1b 100644 --- a/docx/api.py +++ b/docx/api.py @@ -34,4 +34,4 @@ def _default_docx_path(): 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/docx/blkcntnr.py index a80903e52..b4d4d8b04 100644 --- a/docx/blkcntnr.py +++ b/docx/blkcntnr.py @@ -25,7 +25,7 @@ def __init__(self, element, parent): super(BlockItemContainer, self).__init__(parent) self._element = element - def add_paragraph(self, text='', style=None): + 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 @@ -46,6 +46,7 @@ def add_table(self, rows, cols, width): distributed between the table columns. """ from .table import Table + tbl = CT_Tbl.new_tbl(rows, cols, width) self._element._insert_tbl(tbl) return Table(tbl, self) @@ -65,6 +66,7 @@ def tables(self): Read-only. """ from .table import Table + return [Table(tbl, self) for tbl in self._element.tbl_lst] def _add_paragraph(self): diff --git a/docx/compat.py b/docx/compat.py index 1e3012d95..dfa8ea054 100644 --- a/docx/compat.py +++ b/docx/compat.py @@ -13,7 +13,6 @@ # =========================================================================== if sys.version_info >= (3, 0): - from collections.abc import Sequence from io import BytesIO @@ -28,7 +27,6 @@ def is_string(obj): # =========================================================================== else: - from collections import Sequence # noqa from StringIO import StringIO as BytesIO # noqa diff --git a/docx/dml/color.py b/docx/dml/color.py index 2f2f25cb2..cfdf097a4 100644 --- a/docx/dml/color.py +++ b/docx/dml/color.py @@ -4,9 +4,7 @@ DrawingML objects related to color, ColorFormat being the most prominent. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from ..enum.dml import MSO_COLOR_TYPE from ..oxml.simpletypes import ST_HexColorAuto diff --git a/docx/document.py b/docx/document.py index 6493c458b..6acdb839a 100644 --- a/docx/document.py +++ b/docx/document.py @@ -18,7 +18,7 @@ class Document(ElementProxy): a document. """ - __slots__ = ('_part', '__body') + __slots__ = ("_part", "__body") def __init__(self, element, part): super(Document, self).__init__(element) @@ -44,7 +44,7 @@ def add_page_break(self): paragraph.add_run().add_break(WD_BREAK.PAGE) return paragraph - def add_paragraph(self, text='', style=None): + 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 @@ -172,9 +172,7 @@ def _block_width(self): 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 - ) + return Emu(section.page_width - section.left_margin - section.right_margin) @property def _body(self): @@ -191,6 +189,7 @@ 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 diff --git a/docx/enum/__init__.py b/docx/enum/__init__.py index dd49faafd..e1bbd47f2 100644 --- a/docx/enum/__init__.py +++ b/docx/enum/__init__.py @@ -8,7 +8,6 @@ class Enumeration(object): - @classmethod def from_xml(cls, xml_val): return cls._xml_to_idx[xml_val] diff --git a/docx/enum/base.py b/docx/enum/base.py index 36764b1a6..0bcdbd6dd 100644 --- a/docx/enum/base.py +++ b/docx/enum/base.py @@ -17,6 +17,7 @@ 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) @@ -24,6 +25,7 @@ def decorator(cls): for alias in aliases: globals_dict[alias] = cls return cls + return decorator @@ -45,10 +47,12 @@ 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' + 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 + self._ms_name, + self._page_title, + self._intro_text, + self._member_defs, ) return tmpl % components @@ -56,12 +60,12 @@ def page_str(self): def _intro_text(self): """Docstring of the enumeration, formatted for documentation page.""" try: - cls_docstring = self._clsdict['__doc__'] + cls_docstring = self._clsdict["__doc__"] except KeyError: - cls_docstring = '' + cls_docstring = "" if cls_docstring is None: - return '' + return "" return textwrap.dedent(cls_docstring).strip() @@ -72,10 +76,12 @@ def _member_def(self, member): """ member_docstring = textwrap.dedent(member.docstring).strip() member_docstring = textwrap.fill( - member_docstring, width=78, initial_indent=' '*4, - subsequent_indent=' '*4 + member_docstring, + width=78, + initial_indent=" " * 4, + subsequent_indent=" " * 4, ) - return '%s\n%s\n' % (member.name, member_docstring) + return "%s\n%s\n" % (member.name, member_docstring) @property def _member_defs(self): @@ -83,19 +89,18 @@ def _member_defs(self): A single string containing the aggregated member definitions section of the documentation page """ - members = self._clsdict['__members__'] + members = self._clsdict["__members__"] member_defs = [ - self._member_def(member) for member in members - if member.name is not None + self._member_def(member) for member in members if member.name is not None ] - return '\n'.join(member_defs) + return "\n".join(member_defs) @property def _ms_name(self): """ The Microsoft API name for this enumeration """ - return self._clsdict['__ms_name__'] + return self._clsdict["__ms_name__"] @property def _page_title(self): @@ -103,8 +108,8 @@ 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) + title_underscore = "=" * (len(self._clsname) + 4) + return "``%s``\n%s" % (self._clsname, title_underscore) class MetaEnumeration(type): @@ -113,6 +118,7 @@ class MetaEnumeration(type): 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) @@ -126,7 +132,7 @@ def _add_enum_members(meta, clsdict): thing to properly add itself to the enumeration class. This delegation allows member sub-classes to add specialized behaviors. """ - enum_members = clsdict['__members__'] + enum_members = clsdict["__members__"] for member in enum_members: member.add_to_enum(clsdict) @@ -136,20 +142,18 @@ 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__'] + enum_members = clsdict["__members__"] valid_settings = [] for member in enum_members: valid_settings.extend(member.valid_settings) - clsdict['_valid_settings'] = 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 - ) + clsdict["__docs_rst__"] = _DocsPageFormatter(clsname, clsdict).page_str class EnumerationBase(object): @@ -158,8 +162,9 @@ class EnumerationBase(object): only basic behavior. It's __dict__ is used below in the Python 2+3 compatible metaclass definition. """ + __members__ = () - __ms_name__ = '' + __ms_name__ = "" @classmethod def validate(cls, value): @@ -172,9 +177,7 @@ def validate(cls, value): ) -Enumeration = MetaEnumeration( - 'Enumeration', (object,), dict(EnumerationBase.__dict__) -) +Enumeration = MetaEnumeration("Enumeration", (object,), dict(EnumerationBase.__dict__)) class XmlEnumeration(Enumeration): @@ -182,8 +185,9 @@ class XmlEnumeration(Enumeration): Provides ``to_xml()`` and ``from_xml()`` methods in addition to base enumeration features """ + __members__ = () - __ms_name__ = '' + __ms_name__ = "" @classmethod def from_xml(cls, xml_val): @@ -214,6 +218,7 @@ 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): @@ -278,6 +283,7 @@ class EnumValue(int): 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) @@ -305,6 +311,7 @@ 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): """ @@ -317,6 +324,7 @@ 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 @@ -349,15 +357,15 @@ 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'] + 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'] + 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 index 1ad0eaa87..133d824d6 100644 --- a/docx/enum/dml.py +++ b/docx/enum/dml.py @@ -6,9 +6,7 @@ from __future__ import absolute_import -from .base import ( - alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember -) +from .base import alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember class MSO_COLOR_TYPE(Enumeration): @@ -22,28 +20,22 @@ class MSO_COLOR_TYPE(Enumeration): assert font.color.type == MSO_COLOR_TYPE.SCHEME """ - __ms_name__ = 'MsoColorType' + __ms_name__ = "MsoColorType" __url__ = ( - 'http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15' - ').aspx' + "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( - '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.' + "AUTO", 101, "Color is determined automatically by the " "application." ), ) -@alias('MSO_THEME_COLOR') +@alias("MSO_THEME_COLOR") class MSO_THEME_COLOR_INDEX(XmlEnumeration): """ Indicates the Office theme color, one of those shown in the color gallery @@ -58,67 +50,64 @@ class MSO_THEME_COLOR_INDEX(XmlEnumeration): font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 """ - __ms_name__ = 'MsoThemeColorIndex' + __ms_name__ = "MsoThemeColorIndex" __url__ = ( - 'http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15' - ').aspx' + "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.' - ), + EnumMember("NOT_THEME_COLOR", 0, "Indicates the color is not a theme color."), XmlMappedEnumMember( - 'ACCENT_5', 9, 'accent5', 'Specifies the Accent 5 theme color.' + "ACCENT_1", 5, "accent1", "Specifies the Accent 1 theme color." ), XmlMappedEnumMember( - 'ACCENT_6', 10, 'accent6', 'Specifies the Accent 6 theme color.' + "ACCENT_2", 6, "accent2", "Specifies the Accent 2 theme color." ), XmlMappedEnumMember( - 'BACKGROUND_1', 14, 'background1', 'Specifies the Background 1 ' - 'theme color.' + "ACCENT_3", 7, "accent3", "Specifies the Accent 3 theme color." ), XmlMappedEnumMember( - 'BACKGROUND_2', 16, 'background2', 'Specifies the Background 2 ' - 'theme color.' + "ACCENT_4", 8, "accent4", "Specifies the Accent 4 theme color." ), XmlMappedEnumMember( - 'DARK_1', 1, 'dark1', 'Specifies the Dark 1 theme color.' + "ACCENT_5", 9, "accent5", "Specifies the Accent 5 theme color." ), XmlMappedEnumMember( - 'DARK_2', 3, 'dark2', 'Specifies the Dark 2 theme color.' + "ACCENT_6", 10, "accent6", "Specifies the Accent 6 theme color." ), XmlMappedEnumMember( - 'FOLLOWED_HYPERLINK', 12, 'followedHyperlink', 'Specifies the ' - 'theme color for a clicked hyperlink.' + "BACKGROUND_1", + 14, + "background1", + "Specifies the Background 1 " "theme color.", ), XmlMappedEnumMember( - 'HYPERLINK', 11, 'hyperlink', 'Specifies the theme color for a ' - 'hyperlink.' + "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( - 'LIGHT_1', 2, 'light1', 'Specifies the Light 1 theme color.' + "FOLLOWED_HYPERLINK", + 12, + "followedHyperlink", + "Specifies the " "theme color for a clicked hyperlink.", ), XmlMappedEnumMember( - 'LIGHT_2', 4, 'light2', 'Specifies the Light 2 theme color.' + "HYPERLINK", + 11, + "hyperlink", + "Specifies the theme color for a " "hyperlink.", ), XmlMappedEnumMember( - 'TEXT_1', 13, 'text1', 'Specifies the Text 1 theme color.' + "LIGHT_1", 2, "light1", "Specifies the Light 1 theme color." ), XmlMappedEnumMember( - 'TEXT_2', 15, 'text2', 'Specifies the Text 2 theme color.' + "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 index 381e81877..814e4cfba 100644 --- a/docx/enum/section.py +++ b/docx/enum/section.py @@ -9,7 +9,7 @@ from .base import alias, XmlEnumeration, XmlMappedEnumMember -@alias('WD_HEADER_FOOTER') +@alias("WD_HEADER_FOOTER") class WD_HEADER_FOOTER_INDEX(XmlEnumeration): """ alias: **WD_HEADER_FOOTER** @@ -36,7 +36,7 @@ class WD_HEADER_FOOTER_INDEX(XmlEnumeration): ) -@alias('WD_ORIENT') +@alias("WD_ORIENT") class WD_ORIENTATION(XmlEnumeration): """ alias: **WD_ORIENT** @@ -51,21 +51,17 @@ class WD_ORIENTATION(XmlEnumeration): section.orientation = WD_ORIENT.LANDSCAPE """ - __ms_name__ = 'WdOrientation' + __ms_name__ = "WdOrientation" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff837902.aspx' + __url__ = "http://msdn.microsoft.com/en-us/library/office/ff837902.aspx" __members__ = ( - XmlMappedEnumMember( - 'PORTRAIT', 0, 'portrait', 'Portrait orientation.' - ), - XmlMappedEnumMember( - 'LANDSCAPE', 1, 'landscape', 'Landscape orientation.' - ), + XmlMappedEnumMember("PORTRAIT", 0, "portrait", "Portrait orientation."), + XmlMappedEnumMember("LANDSCAPE", 1, "landscape", "Landscape orientation."), ) -@alias('WD_SECTION') +@alias("WD_SECTION") class WD_SECTION_START(XmlEnumeration): """ alias: **WD_SECTION** @@ -80,24 +76,16 @@ class WD_SECTION_START(XmlEnumeration): section.start_type = WD_SECTION.NEW_PAGE """ - __ms_name__ = 'WdSectionStart' + __ms_name__ = "WdSectionStart" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff840975.aspx' + __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( - '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.' + "ODD_PAGE", 4, "oddPage", "Section begins on next odd page." ), ) diff --git a/docx/enum/shape.py b/docx/enum/shape.py index 937f30a9f..b785ab9c1 100644 --- a/docx/enum/shape.py +++ b/docx/enum/shape.py @@ -12,6 +12,7 @@ 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 diff --git a/docx/enum/style.py b/docx/enum/style.py index 515c594ce..1915429c0 100644 --- a/docx/enum/style.py +++ b/docx/enum/style.py @@ -9,7 +9,7 @@ from .base import alias, EnumMember, XmlEnumeration, XmlMappedEnumMember -@alias('WD_STYLE') +@alias("WD_STYLE") class WD_BUILTIN_STYLE(XmlEnumeration): """ alias: **WD_STYLE** @@ -26,409 +26,147 @@ class WD_BUILTIN_STYLE(XmlEnumeration): style = styles[WD_STYLE.BODY_TEXT] """ - __ms_name__ = 'WdBuiltinStyle' + __ms_name__ = "WdBuiltinStyle" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff835210.aspx' + __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.' - ), + 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."), ) @@ -446,21 +184,13 @@ class WD_STYLE_TYPE(XmlEnumeration): assert styles[0].type == WD_STYLE_TYPE.PARAGRAPH """ - __ms_name__ = 'WdStyleType' + __ms_name__ = "WdStyleType" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff196870.aspx' + __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.' - ), + 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 index eedab082e..f66fdccd5 100644 --- a/docx/enum/table.py +++ b/docx/enum/table.py @@ -4,16 +4,12 @@ Enumerations related to tables in WordprocessingML files """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals -from .base import ( - alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember -) +from .base import alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember -@alias('WD_ALIGN_VERTICAL') +@alias("WD_ALIGN_VERTICAL") class WD_CELL_VERTICAL_ALIGNMENT(XmlEnumeration): """ alias: **WD_ALIGN_VERTICAL** @@ -28,33 +24,37 @@ class WD_CELL_VERTICAL_ALIGNMENT(XmlEnumeration): table.cell(0, 0).vertical_alignment = WD_ALIGN_VERTICAL.BOTTOM """ - __ms_name__ = 'WdCellVerticalAlignment' + __ms_name__ = "WdCellVerticalAlignment" - __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff193345.aspx' + __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.' + "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.' + "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.' + "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.' + "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') +@alias("WD_ROW_HEIGHT") class WD_ROW_HEIGHT_RULE(XmlEnumeration): """ alias: **WD_ROW_HEIGHT** @@ -71,20 +71,23 @@ class WD_ROW_HEIGHT_RULE(XmlEnumeration): __ms_name__ = "WdRowHeightRule" - __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff193620.aspx' + __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.' + "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.' + "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."), ) @@ -100,20 +103,14 @@ class WD_TABLE_ALIGNMENT(XmlEnumeration): table.alignment = WD_TABLE_ALIGNMENT.CENTER """ - __ms_name__ = 'WdRowAlignment' + __ms_name__ = "WdRowAlignment" - __url__ = ' http://office.microsoft.com/en-us/word-help/HV080607259.aspx' + __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.' - ), + XmlMappedEnumMember("LEFT", 0, "left", "Left-aligned"), + XmlMappedEnumMember("CENTER", 1, "center", "Center-aligned."), + XmlMappedEnumMember("RIGHT", 2, "right", "Right-aligned."), ) @@ -130,17 +127,21 @@ class WD_TABLE_DIRECTION(Enumeration): table.direction = WD_TABLE_DIRECTION.RTL """ - __ms_name__ = 'WdTableDirection' + __ms_name__ = "WdTableDirection" - __url__ = ' http://msdn.microsoft.com/en-us/library/ff835141.aspx' + __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.' + "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.' + "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 index 67f6a66af..1de05326f 100644 --- a/docx/enum/text.py +++ b/docx/enum/text.py @@ -9,7 +9,7 @@ from .base import alias, EnumMember, XmlEnumeration, XmlMappedEnumMember -@alias('WD_ALIGN_PARAGRAPH') +@alias("WD_ALIGN_PARAGRAPH") class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): """ alias: **WD_ALIGN_PARAGRAPH** @@ -24,42 +24,45 @@ class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER """ - __ms_name__ = 'WdParagraphAlignment' + __ms_name__ = "WdParagraphAlignment" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff835817.aspx' + __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( - 'LEFT', 0, 'left', 'Left-aligned' + "DISTRIBUTE", + 4, + "distribute", + "Paragraph characters are distrib" + "uted to fill the entire width of the paragraph.", ), XmlMappedEnumMember( - 'CENTER', 1, 'center', 'Center-aligned.' + "JUSTIFY_MED", + 5, + "mediumKashida", + "Justified with a medium char" "acter compression ratio.", ), XmlMappedEnumMember( - 'RIGHT', 2, 'right', 'Right-aligned.' + "JUSTIFY_HI", + 7, + "highKashida", + "Justified with a high character" " compression ratio.", ), XmlMappedEnumMember( - 'JUSTIFY', 3, 'both', 'Fully justified.' + "JUSTIFY_LOW", + 8, + "lowKashida", + "Justified with a low character " "compression ratio.", ), 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.' + "THAI_JUSTIFY", + 9, + "thaiDistribute", + "Justified according to Tha" "i formatting layout.", ), ) @@ -69,6 +72,7 @@ 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 @@ -85,72 +89,40 @@ class WD_BREAK_TYPE(object): WD_BREAK = WD_BREAK_TYPE -@alias('WD_COLOR') +@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' + __ms_name__ = "WdColorIndex" - __url__ = 'https://msdn.microsoft.com/EN-US/library/office/ff195343.aspx' + __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.' - ), + 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."), ) @@ -166,33 +138,36 @@ class WD_LINE_SPACING(XmlEnumeration): paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY """ - __ms_name__ = 'WdLineSpacing' + __ms_name__ = "WdLineSpacing" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff844910.aspx' + __url__ = "http://msdn.microsoft.com/en-us/library/office/ff844910.aspx" __members__ = ( - EnumMember( - 'ONE_POINT_FIVE', 1, 'Space-and-a-half line spacing.' - ), + 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.' + "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.' + "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).' + "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)."), ) @@ -201,41 +176,21 @@ class WD_TAB_ALIGNMENT(XmlEnumeration): Specifies the tab stop alignment to apply. """ - __ms_name__ = 'WdTabAlignment' + __ms_name__ = "WdTabAlignment" - __url__ = 'https://msdn.microsoft.com/EN-US/library/office/ff195609.aspx' + __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)' - ), + 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)"), ) @@ -244,29 +199,17 @@ class WD_TAB_LEADER(XmlEnumeration): Specifies the character to use as the leader with formatted tabs. """ - __ms_name__ = 'WdTabLeader' + __ms_name__ = "WdTabLeader" - __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff845050.aspx' + __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.' - ), + 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."), ) @@ -275,78 +218,62 @@ class WD_UNDERLINE(XmlEnumeration): Specifies the style of underline applied to a run of characters. """ - __ms_name__ = 'WdUnderline' + __ms_name__ = "WdUnderline" - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff822388.aspx' + __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.' + None, None, None, "Inherit underline setting from containing par" "agraph." ), XmlMappedEnumMember( - 'DOT_DASH_HEAVY', 25, 'dashDotHeavy', 'Alternating heavy dots an' - 'd heavy dashes.' + "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( - 'DOT_DOT_DASH_HEAVY', 26, 'dashDotDotHeavy', 'An alternating hea' - 'vy dot-dot-dash pattern.' + "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( - 'WAVY_HEAVY', 27, 'wavyHeavy', 'A heavy wavy line.' + "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( - 'DASH_LONG', 39, 'dashLong', 'Long dashes.' + "DOT_DASH_HEAVY", + 25, + "dashDotHeavy", + "Alternating heavy dots an" "d heavy dashes.", ), XmlMappedEnumMember( - 'WAVY_DOUBLE', 43, 'wavyDouble', 'A double wavy line.' + "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.' + "DASH_LONG_HEAVY", 55, "dashLongHeavy", "Long heavy dashes." ), ) diff --git a/docx/image/__init__.py b/docx/image/__init__.py index 8ab3ada68..e85234545 100644 --- a/docx/image/__init__.py +++ b/docx/image/__init__.py @@ -5,9 +5,7 @@ size, as a required step in including them in a document. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from docx.image.bmp import Bmp from docx.image.gif import Gif @@ -18,12 +16,12 @@ 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'), + (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/docx/image/bmp.py index d22f25871..aebc6b9cc 100644 --- a/docx/image/bmp.py +++ b/docx/image/bmp.py @@ -11,6 +11,7 @@ class Bmp(BaseImageHeader): """ Image header parser for BMP images """ + @classmethod def from_stream(cls, stream): """ @@ -43,7 +44,7 @@ def default_ext(self): """ Default filename extension, always 'bmp' for BMP images. """ - return 'bmp' + return "bmp" @staticmethod def _dpi(px_per_meter): diff --git a/docx/image/constants.py b/docx/image/constants.py index 90b469705..e4fa17fb3 100644 --- a/docx/image/constants.py +++ b/docx/image/constants.py @@ -9,83 +9,93 @@ 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 - ) + + 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 + 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', + 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 @@ -97,26 +107,29 @@ class MIME_TYPE(object): """ Image content types """ - BMP = 'image/bmp' - GIF = 'image/gif' - JPEG = 'image/jpeg' - PNG = 'image/png' - TIFF = 'image/tiff' + + 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' + + 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 @@ -124,8 +137,11 @@ class TIFF_FLD_TYPE(object): RATIONAL = 5 field_type_names = { - 1: 'BYTE', 2: 'ASCII char', 3: 'SHORT', 4: 'LONG', - 5: 'RATIONAL' + 1: "BYTE", + 2: "ASCII char", + 3: "SHORT", + 4: "LONG", + 5: "RATIONAL", } @@ -136,6 +152,7 @@ class TIFF_TAG(object): """ Tag codes for TIFF Image File Directory (IFD) entries. """ + IMAGE_WIDTH = 0x0100 IMAGE_LENGTH = 0x0101 X_RESOLUTION = 0x011A @@ -143,27 +160,27 @@ class TIFF_TAG(object): 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', + 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/gif.py b/docx/image/gif.py index 57f037d80..07f2b1c77 100644 --- a/docx/image/gif.py +++ b/docx/image/gif.py @@ -14,6 +14,7 @@ class Gif(BaseImageHeader): support resolution (DPI) information. Both horizontal and vertical DPI default to 72. """ + @classmethod def from_stream(cls, stream): """ @@ -36,12 +37,12 @@ def default_ext(self): """ Default filename extension, always 'gif' for GIF images. """ - return 'gif' + return "gif" @classmethod def _dimensions_from_stream(cls, stream): stream.seek(6) bytes_ = stream.read(4) - struct = Struct('L' + fmt = "L" return self._read_int(fmt, base, offset) def read_short(self, base, offset=0): @@ -55,7 +54,7 @@ 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' + fmt = b"H" return self._read_int(fmt, base, offset) def read_str(self, char_count, base, offset=0): @@ -63,12 +62,14 @@ 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 + 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') + unicode_str = chars.decode("UTF-8") return unicode_str def seek(self, base, offset=0): diff --git a/docx/image/image.py b/docx/image/image.py index ba2158e72..8fddaabc4 100644 --- a/docx/image/image.py +++ b/docx/image/image.py @@ -20,6 +20,7 @@ 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 @@ -43,7 +44,7 @@ def from_file(cls, image_descriptor): """ if is_string(image_descriptor): path = image_descriptor - with open(path, 'rb') as f: + with open(path, "rb") as f: blob = f.read() stream = BytesIO(blob) filename = os.path.basename(path) @@ -175,7 +176,7 @@ def _from_stream(cls, stream, blob, filename=None): """ image_header = _ImageHeaderFactory(stream) if filename is None: - filename = 'image.%s' % image_header.default_ext + filename = "image.%s" % image_header.default_ext return cls(blob, filename, image_header) @@ -203,6 +204,7 @@ 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 @@ -215,8 +217,8 @@ 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' + "content_type property must be implemented by all subclasses of " + "BaseImageHeader" ) raise NotImplementedError(msg) @@ -227,8 +229,8 @@ def default_ext(self): property definition, must be implemented by all subclasses. """ msg = ( - 'default_ext property must be implemented by all subclasses of ' - 'BaseImageHeader' + "default_ext property must be implemented by all subclasses of " + "BaseImageHeader" ) raise NotImplementedError(msg) diff --git a/docx/image/jpeg.py b/docx/image/jpeg.py index 8a263b6c5..adba5c1ad 100644 --- a/docx/image/jpeg.py +++ b/docx/image/jpeg.py @@ -18,6 +18,7 @@ class Jpeg(BaseImageHeader): """ Base class for JFIF and EXIF subclasses. """ + @property def content_type(self): """ @@ -31,13 +32,14 @@ def default_ext(self): """ Default filename extension, always 'jpg' for JPG images. """ - return 'jpg' + return "jpg" class Exif(Jpeg): """ Image header parser for Exif image format """ + @classmethod def from_stream(cls, stream): """ @@ -59,6 +61,7 @@ class Jfif(Jpeg): """ Image header parser for JFIF image format """ + @classmethod def from_stream(cls, stream): """ @@ -80,6 +83,7 @@ class _JfifMarkers(object): 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) @@ -89,16 +93,21 @@ 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' + 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): @@ -122,7 +131,7 @@ def app0(self): 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): @@ -132,7 +141,7 @@ def app1(self): 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): @@ -142,7 +151,7 @@ def sof(self): 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): @@ -150,6 +159,7 @@ class _MarkerParser(object): 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 @@ -173,9 +183,7 @@ def iter_markers(self): 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 @@ -184,6 +192,7 @@ class _MarkerFinder(object): """ 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 @@ -208,12 +217,12 @@ def next(self, start): # 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 @@ -226,7 +235,7 @@ def _next_non_ff_byte(self, start): """ 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_ @@ -239,7 +248,7 @@ def _offset_of_next_ff_byte(self, start): """ 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 @@ -251,7 +260,7 @@ def _read_byte(self): """ 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_ @@ -276,6 +285,7 @@ class _Marker(object): 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 @@ -322,9 +332,10 @@ class _App0Marker(_Marker): """ 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 @@ -379,8 +390,7 @@ 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 ) @@ -388,6 +398,7 @@ class _App1Marker(_Marker): """ 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 @@ -411,9 +422,7 @@ 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): @@ -438,9 +447,9 @@ def _is_non_Exif_APP1_segment(cls, stream, offset): Exif segment, as determined by the ``'Exif\x00\x00'`` signature at offset 2 in the segment. """ - stream.seek(offset+2) + 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): @@ -449,8 +458,8 @@ def _tiff_from_exif_segment(cls, stream, offset, segment_length): *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) + stream.seek(offset + 8) + segment_bytes = stream.read(segment_length - 8) substream = BytesIO(segment_bytes) return Tiff.from_stream(substream) @@ -459,8 +468,8 @@ class _SofMarker(_Marker): """ Represents a JFIF start of frame (SOFx) marker segment. """ - def __init__( - self, marker_code, offset, segment_length, px_width, px_height): + + 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 diff --git a/docx/image/png.py b/docx/image/png.py index 4e899fa5c..c2e4ae820 100644 --- a/docx/image/png.py +++ b/docx/image/png.py @@ -12,6 +12,7 @@ class Png(BaseImageHeader): """ Image header parser for PNG images """ + @property def content_type(self): """ @@ -25,7 +26,7 @@ def default_ext(self): """ Default filename extension, always 'png' for PNG images. """ - return 'png' + return "png" @classmethod def from_stream(cls, stream): @@ -48,6 +49,7 @@ class _PngParser(object): 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 @@ -114,6 +116,7 @@ class _Chunks(object): """ Collection of the chunks parsed from a PNG image stream """ + def __init__(self, chunk_iterable): super(_Chunks, self).__init__() self._chunks = list(chunk_iterable) @@ -135,7 +138,7 @@ def IHDR(self): 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 @@ -161,6 +164,7 @@ class _ChunkParser(object): """ Extracts chunks from a PNG image stream """ + def __init__(self, stream_rdr): super(_ChunkParser, self).__init__() self._stream_rdr = stream_rdr @@ -195,10 +199,10 @@ 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): @@ -219,6 +223,7 @@ class _Chunk(object): 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 @@ -242,6 +247,7 @@ class _IHDRChunk(_Chunk): """ 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 @@ -270,8 +276,8 @@ 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): + + 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 @@ -286,9 +292,7 @@ def from_offset(cls, chunk_type, stream_rdr, 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/docx/image/tiff.py b/docx/image/tiff.py index c38242360..5b5e8b748 100644 --- a/docx/image/tiff.py +++ b/docx/image/tiff.py @@ -12,6 +12,7 @@ class Tiff(BaseImageHeader): Image header parser for TIFF images. Handles both big and little endian byte ordering. """ + @property def content_type(self): """ @@ -25,7 +26,7 @@ def default_ext(self): """ Default filename extension, always 'tiff' for TIFF images. """ - return 'tiff' + return "tiff" @classmethod def from_stream(cls, stream): @@ -48,6 +49,7 @@ 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 @@ -107,7 +109,7 @@ def _detect_endian(cls, stream): """ stream.seek(0) endian_str = stream.read(2) - return BIG_ENDIAN if endian_str == b'MM' else LITTLE_ENDIAN + return BIG_ENDIAN if endian_str == b"MM" else LITTLE_ENDIAN def _dpi(self, resolution_tag): """ @@ -124,7 +126,8 @@ def _dpi(self, resolution_tag): # 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 TIFF_TAG.RESOLUTION_UNIT in ifd_entries + else 2 ) if resolution_unit == 1: # aspect ratio only @@ -150,6 +153,7 @@ 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 @@ -189,6 +193,7 @@ 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 @@ -200,7 +205,7 @@ def iter_entries(self): directory. """ for idx in range(self._entry_count): - dir_entry_offset = self._offset + 2 + (idx*12) + dir_entry_offset = self._offset + 2 + (idx * 12) ifd_entry = _IfdEntryFactory(self._stream_rdr, dir_entry_offset) yield ifd_entry @@ -218,9 +223,9 @@ def _IfdEntryFactory(stream_rdr, offset): directory entry at *offset* in *stream_rdr*. """ ifd_entry_classes = { - TIFF_FLD.ASCII: _AsciiIfdEntry, - TIFF_FLD.SHORT: _ShortIfdEntry, - TIFF_FLD.LONG: _LongIfdEntry, + TIFF_FLD.ASCII: _AsciiIfdEntry, + TIFF_FLD.SHORT: _ShortIfdEntry, + TIFF_FLD.LONG: _LongIfdEntry, TIFF_FLD.RATIONAL: _RationalIfdEntry, } field_type = stream_rdr.read_short(offset, 2) @@ -236,6 +241,7 @@ 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 @@ -252,9 +258,7 @@ def from_stream(cls, stream_rdr, offset): 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 - ) + value = cls._parse_value(stream_rdr, offset, value_count, value_offset) return cls(tag_code, value) @classmethod @@ -263,7 +267,7 @@ 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 + return "UNIMPLEMENTED FIELD TYPE" # pragma: no cover @property def tag(self): @@ -284,6 +288,7 @@ 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): """ @@ -291,13 +296,14 @@ def _parse_value(cls, stream_rdr, offset, value_count, 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) + 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): """ @@ -307,13 +313,14 @@ def _parse_value(cls, stream_rdr, offset, value_count, value_offset): if value_count == 1: return stream_rdr.read_short(offset, 8) else: # pragma: no cover - return 'Multi-value short integer NOT IMPLEMENTED' + 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): """ @@ -323,13 +330,14 @@ def _parse_value(cls, stream_rdr, offset, value_count, value_offset): if value_count == 1: return stream_rdr.read_long(offset, 8) else: # pragma: no cover - return 'Multi-value long integer NOT IMPLEMENTED' + 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): """ @@ -342,4 +350,4 @@ def _parse_value(cls, stream_rdr, offset, value_count, value_offset): denominator = stream_rdr.read_long(value_offset, 4) return numerator / denominator else: # pragma: no cover - return 'Multi-value Rational NOT IMPLEMENTED' + return "Multi-value Rational NOT IMPLEMENTED" diff --git a/docx/opc/constants.py b/docx/opc/constants.py index b90aa394a..425435d92 100644 --- a/docx/opc/constants.py +++ b/docx/opc/constants.py @@ -10,649 +10,547 @@ 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' - ) + + BMP = "image/bmp" + DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" DML_CHARTSHAPES = ( - 'application/vnd.openxmlformats-officedocument.drawingml.chartshapes' - '+xml' + "application/vnd.openxmlformats-officedocument.drawingml.chartshapes" "+xml" ) DML_DIAGRAM_COLORS = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramColo' - 'rs+xml' + "application/vnd.openxmlformats-officedocument.drawingml.diagramColo" "rs+xml" ) DML_DIAGRAM_DATA = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramData' - '+xml' + "application/vnd.openxmlformats-officedocument.drawingml.diagramData" "+xml" ) DML_DIAGRAM_LAYOUT = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramLayo' - 'ut+xml' + "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' + "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' + "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' + "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' + "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' + "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' + "application/vnd.openxmlformats-package.digital-signature-certificat" "e" ) OPC_DIGITAL_SIGNATURE_ORIGIN = ( - 'application/vnd.openxmlformats-package.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' + "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' + "application/vnd.openxmlformats-officedocument.presentationml.commen" "ts+xml" ) PML_COMMENT_AUTHORS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.commen' - 'tAuthors+xml' + "application/vnd.openxmlformats-officedocument.presentationml.commen" + "tAuthors+xml" ) PML_HANDOUT_MASTER = ( - 'application/vnd.openxmlformats-officedocument.presentationml.handou' - 'tMaster+xml' + "application/vnd.openxmlformats-officedocument.presentationml.handou" + "tMaster+xml" ) PML_NOTES_MASTER = ( - 'application/vnd.openxmlformats-officedocument.presentationml.notesM' - 'aster+xml' + "application/vnd.openxmlformats-officedocument.presentationml.notesM" + "aster+xml" ) PML_NOTES_SLIDE = ( - 'application/vnd.openxmlformats-officedocument.presentationml.notesS' - 'lide+xml' + "application/vnd.openxmlformats-officedocument.presentationml.notesS" "lide+xml" ) PML_PRESENTATION_MAIN = ( - 'application/vnd.openxmlformats-officedocument.presentationml.presen' - 'tation.main+xml' + "application/vnd.openxmlformats-officedocument.presentationml.presen" + "tation.main+xml" ) PML_PRES_PROPS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.presPr' - 'ops+xml' + "application/vnd.openxmlformats-officedocument.presentationml.presPr" "ops+xml" ) PML_PRINTER_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.printe' - 'rSettings' + "application/vnd.openxmlformats-officedocument.presentationml.printe" + "rSettings" ) PML_SLIDE = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slide+' - 'xml' + "application/vnd.openxmlformats-officedocument.presentationml.slide+" "xml" ) PML_SLIDESHOW_MAIN = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slides' - 'how.main+xml' + "application/vnd.openxmlformats-officedocument.presentationml.slides" + "how.main+xml" ) PML_SLIDE_LAYOUT = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slideL' - 'ayout+xml' + "application/vnd.openxmlformats-officedocument.presentationml.slideL" + "ayout+xml" ) PML_SLIDE_MASTER = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slideM' - 'aster+xml' + "application/vnd.openxmlformats-officedocument.presentationml.slideM" + "aster+xml" ) PML_SLIDE_UPDATE_INFO = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slideU' - 'pdateInfo+xml' + "application/vnd.openxmlformats-officedocument.presentationml.slideU" + "pdateInfo+xml" ) PML_TABLE_STYLES = ( - 'application/vnd.openxmlformats-officedocument.presentationml.tableS' - 'tyles+xml' + "application/vnd.openxmlformats-officedocument.presentationml.tableS" + "tyles+xml" ) PML_TAGS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.tags+x' - 'ml' + "application/vnd.openxmlformats-officedocument.presentationml.tags+x" "ml" ) PML_TEMPLATE_MAIN = ( - 'application/vnd.openxmlformats-officedocument.presentationml.templa' - 'te.main+xml' + "application/vnd.openxmlformats-officedocument.presentationml.templa" + "te.main+xml" ) PML_VIEW_PROPS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.viewPr' - 'ops+xml' - ) - PNG = ( - 'image/png' + "application/vnd.openxmlformats-officedocument.presentationml.viewPr" "ops+xml" ) + PNG = "image/png" SML_CALC_CHAIN = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.calcCha' - 'in+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.calcCha" "in+xml" ) SML_CHARTSHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.chartsh' - 'eet+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsh" "eet+xml" ) SML_COMMENTS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.comment' - 's+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.comment" "s+xml" ) SML_CONNECTIONS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.connect' - 'ions+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.connect" "ions+xml" ) SML_CUSTOM_PROPERTY = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.customP' - 'roperty' + "application/vnd.openxmlformats-officedocument.spreadsheetml.customP" "roperty" ) SML_DIALOGSHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.dialogs' - 'heet+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogs" "heet+xml" ) SML_EXTERNAL_LINK = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.externa' - 'lLink+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.externa" + "lLink+xml" ) SML_PIVOT_CACHE_DEFINITION = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa' - 'cheDefinition+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa" + "cheDefinition+xml" ) SML_PIVOT_CACHE_RECORDS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa' - 'cheRecords+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa" + "cheRecords+xml" ) SML_PIVOT_TABLE = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTa' - 'ble+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTa" "ble+xml" ) SML_PRINTER_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.printer' - 'Settings' + "application/vnd.openxmlformats-officedocument.spreadsheetml.printer" "Settings" ) SML_QUERY_TABLE = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.queryTa' - 'ble+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTa" "ble+xml" ) SML_REVISION_HEADERS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.revisio' - 'nHeaders+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisio" + "nHeaders+xml" ) SML_REVISION_LOG = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.revisio' - 'nLog+xml' + "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' + "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' + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.m" "ain+xml" ) SML_SHEET_METADATA = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMe' - 'tadata+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMe" + "tadata+xml" ) SML_STYLES = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+' - 'xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+" "xml" ) SML_TABLE = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+x' - 'ml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.table+x" "ml" ) SML_TABLE_SINGLE_CELLS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.tableSi' - 'ngleCells+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSi" + "ngleCells+xml" ) SML_TEMPLATE_MAIN = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.templat' - 'e.main+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.templat" + "e.main+xml" ) SML_USER_NAMES = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.userNam' - 'es+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.userNam" "es+xml" ) SML_VOLATILE_DEPENDENCIES = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.volatil' - 'eDependencies+xml' + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatil" + "eDependencies+xml" ) SML_WORKSHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.workshe' - 'et+xml' - ) - TIFF = ( - 'image/tiff' + "application/vnd.openxmlformats-officedocument.spreadsheetml.workshe" "et+xml" ) + TIFF = "image/tiff" WML_COMMENTS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.comm' - 'ents+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.comm" "ents+xml" ) WML_DOCUMENT = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.docu' - 'ment' + "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" "ment" ) WML_DOCUMENT_GLOSSARY = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.docu' - 'ment.glossary+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" + "ment.glossary+xml" ) WML_DOCUMENT_MAIN = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.docu' - 'ment.main+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" + "ment.main+xml" ) WML_ENDNOTES = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.endn' - 'otes+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.endn" "otes+xml" ) WML_FONT_TABLE = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.font' - 'Table+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.font" + "Table+xml" ) WML_FOOTER = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.foot' - 'er+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.foot" "er+xml" ) WML_FOOTNOTES = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.foot' - 'notes+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.foot" + "notes+xml" ) WML_HEADER = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.head' - 'er+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.head" "er+xml" ) WML_NUMBERING = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.numb' - 'ering+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.numb" + "ering+xml" ) WML_PRINTER_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.prin' - 'terSettings' + "application/vnd.openxmlformats-officedocument.wordprocessingml.prin" + "terSettings" ) WML_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.sett' - 'ings+xml' + "application/vnd.openxmlformats-officedocument.wordprocessingml.sett" "ings+xml" ) WML_STYLES = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.styl' - 'es+xml' + "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' - ) + "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' + "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' + "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' + + EXTERNAL = "External" + INTERNAL = "Internal" class RELATIONSHIP_TYPE(object): AUDIO = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/audio' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/audio" ) A_F_CHUNK = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/aFChunk' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/aFChunk" ) CALC_CHAIN = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/calcChain' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/calcChain" ) CERTIFICATE = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/digita' - 'l-signature/certificate' + "http://schemas.openxmlformats.org/package/2006/relationships/digita" + "l-signature/certificate" ) CHART = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/chart' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/chart" ) CHARTSHEET = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/chartsheet' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/chartsheet" ) CHART_USER_SHAPES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/chartUserShapes' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/chartUserShapes" ) COMMENTS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/comments' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/comments" ) COMMENT_AUTHORS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/commentAuthors' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/commentAuthors" ) CONNECTIONS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/connections' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/connections" ) CONTROL = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/control' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/control" ) CORE_PROPERTIES = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/metada' - 'ta/core-properties' + "http://schemas.openxmlformats.org/package/2006/relationships/metada" + "ta/core-properties" ) CUSTOM_PROPERTIES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/custom-properties' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/custom-properties" ) CUSTOM_PROPERTY = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/customProperty' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/customProperty" ) CUSTOM_XML = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/customXml' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/customXml" ) CUSTOM_XML_PROPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/customXmlProps' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/customXmlProps" ) DIAGRAM_COLORS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramColors' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramColors" ) DIAGRAM_DATA = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramData' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramData" ) DIAGRAM_LAYOUT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramLayout' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramLayout" ) DIAGRAM_QUICK_STYLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramQuickStyle' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramQuickStyle" ) DIALOGSHEET = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/dialogsheet' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/dialogsheet" ) DRAWING = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/drawing' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/drawing" ) ENDNOTES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/endnotes' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/endnotes" ) EXTENDED_PROPERTIES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/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' + "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' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/fontTable" ) FOOTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/footer' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/footer" ) FOOTNOTES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/footnotes' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/footnotes" ) GLOSSARY_DOCUMENT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/glossaryDocument' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/glossaryDocument" ) HANDOUT_MASTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/handoutMaster' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/handoutMaster" ) HEADER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/header' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/header" ) HYPERLINK = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/hyperlink' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/hyperlink" ) IMAGE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/image' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/image" ) NOTES_MASTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/notesMaster' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/notesMaster" ) NOTES_SLIDE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/notesSlide' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/notesSlide" ) NUMBERING = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/numbering' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/numbering" ) OFFICE_DOCUMENT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/officeDocument' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/officeDocument" ) OLE_OBJECT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/oleObject' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/oleObject" ) ORIGIN = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/digita' - 'l-signature/origin' + "http://schemas.openxmlformats.org/package/2006/relationships/digita" + "l-signature/origin" ) PACKAGE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/package' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/package" ) PIVOT_CACHE_DEFINITION = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/pivotCacheDefinition' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/pivotCacheDefinition" ) PIVOT_CACHE_RECORDS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/spreadsheetml/pivotCacheRecords' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/spreadsheetml/pivotCacheRecords" ) PIVOT_TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/pivotTable' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/pivotTable" ) PRES_PROPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/presProps' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/presProps" ) PRINTER_SETTINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/printerSettings' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/printerSettings" ) QUERY_TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/queryTable' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/queryTable" ) REVISION_HEADERS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/revisionHeaders' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/revisionHeaders" ) REVISION_LOG = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/revisionLog' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/revisionLog" ) SETTINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/settings' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/settings" ) SHARED_STRINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/sharedStrings' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/sharedStrings" ) SHEET_METADATA = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/sheetMetadata' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/sheetMetadata" ) SIGNATURE = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/digita' - 'l-signature/signature' + "http://schemas.openxmlformats.org/package/2006/relationships/digita" + "l-signature/signature" ) SLIDE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slide' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/slide" ) SLIDE_LAYOUT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slideLayout' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/slideLayout" ) SLIDE_MASTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slideMaster' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/slideMaster" ) SLIDE_UPDATE_INFO = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slideUpdateInfo' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/slideUpdateInfo" ) STYLES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/styles' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/styles" ) TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/table' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/table" ) TABLE_SINGLE_CELLS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/tableSingleCells' + "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' + "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' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/theme" ) THEME_OVERRIDE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/themeOverride' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/themeOverride" ) THUMBNAIL = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/metada' - 'ta/thumbnail' + "http://schemas.openxmlformats.org/package/2006/relationships/metada" + "ta/thumbnail" ) USERNAMES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/usernames' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/usernames" ) VIDEO = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/video' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/video" ) VIEW_PROPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/viewProps' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/viewProps" ) VML_DRAWING = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/vmlDrawing' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/vmlDrawing" ) VOLATILE_DEPENDENCIES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/volatileDependencies' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/volatileDependencies" ) WEB_SETTINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/webSettings' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/webSettings" ) WORKSHEET_SOURCE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/worksheetSource' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/worksheetSource" ) XML_MAPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/xmlMaps' + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/xmlMaps" ) diff --git a/docx/opc/coreprops.py b/docx/opc/coreprops.py index 2d38dabd3..186e851df 100644 --- a/docx/opc/coreprops.py +++ b/docx/opc/coreprops.py @@ -5,9 +5,7 @@ writing presentations to and from a .pptx file. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals class CoreProperties(object): @@ -15,6 +13,7 @@ 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/docx/opc/oxml.py b/docx/opc/oxml.py index 494b31dca..432dcf9f7 100644 --- a/docx/opc/oxml.py +++ b/docx/opc/oxml.py @@ -20,9 +20,9 @@ oxml_parser.set_element_class_lookup(element_class_lookup) nsmap = { - 'ct': NS.OPC_CONTENT_TYPES, - 'pr': NS.OPC_RELATIONSHIPS, - 'r': NS.OFC_RELATIONSHIPS, + "ct": NS.OPC_CONTENT_TYPES, + "pr": NS.OPC_RELATIONSHIPS, + "r": NS.OFC_RELATIONSHIPS, } @@ -30,6 +30,7 @@ # functions # =========================================================================== + def parse_xml(text): """ ``etree.fromstring()`` replacement that uses oxml parser @@ -43,9 +44,9 @@ def qn(tag): 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(':') + prefix, tagroot = tag.split(":") uri = nsmap[prefix] - return '{%s}%s' % (uri, tagroot) + return "{%s}%s" % (uri, tagroot) def serialize_part_xml(part_elm): @@ -54,7 +55,7 @@ def serialize_part_xml(part_elm): 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) + return etree.tostring(part_elm, encoding="UTF-8", standalone=True) def serialize_for_reading(element): @@ -62,18 +63,20 @@ 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) + 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): """ @@ -89,13 +92,14 @@ 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') + return self.get("ContentType") @property def extension(self): @@ -103,7 +107,7 @@ def extension(self): String held in the ``Extension`` attribute of this ```` element. """ - return self.get('Extension') + return self.get("Extension") @staticmethod def new(ext, content_type): @@ -111,10 +115,10 @@ def new(ext, content_type): Return a new ```` element with attributes set to parameter values. """ - xml = '' % nsmap['ct'] + xml = '' % nsmap["ct"] default = parse_xml(xml) - default.set('Extension', ext) - default.set('ContentType', content_type) + default.set("Extension", ext) + default.set("ContentType", content_type) return default @@ -123,13 +127,14 @@ 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') + return self.get("ContentType") @staticmethod def new(partname, content_type): @@ -137,10 +142,10 @@ def new(partname, content_type): Return a new ```` element with attributes set to parameter values. """ - xml = '' % nsmap['ct'] + xml = '' % nsmap["ct"] override = parse_xml(xml) - override.set('PartName', partname) - override.set('ContentType', content_type) + override.set("PartName", partname) + override.set("ContentType", content_type) return override @property @@ -149,7 +154,7 @@ def partname(self): String held in the ``PartName`` attribute of this ```` element. """ - return self.get('PartName') + return self.get("PartName") class CT_Relationship(BaseOxmlElement): @@ -157,18 +162,19 @@ 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'] + xml = '' % nsmap["pr"] relationship = parse_xml(xml) - relationship.set('Id', rId) - relationship.set('Type', reltype) - relationship.set('Target', target) + relationship.set("Id", rId) + relationship.set("Type", reltype) + relationship.set("Target", target) if target_mode == RTM.EXTERNAL: - relationship.set('TargetMode', RTM.EXTERNAL) + relationship.set("TargetMode", RTM.EXTERNAL) return relationship @property @@ -177,7 +183,7 @@ def rId(self): String held in the ``Id`` attribute of this ```` element. """ - return self.get('Id') + return self.get("Id") @property def reltype(self): @@ -185,7 +191,7 @@ def reltype(self): String held in the ``Type`` attribute of this ```` element. """ - return self.get('Type') + return self.get("Type") @property def target_ref(self): @@ -193,7 +199,7 @@ def target_ref(self): String held in the ``Target`` attribute of this ```` element. """ - return self.get('Target') + return self.get("Target") @property def target_mode(self): @@ -202,13 +208,14 @@ def target_mode(self): ```` element, either ``Internal`` or ``External``. Defaults to ``Internal``. """ - return self.get('TargetMode', RTM.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 @@ -223,7 +230,7 @@ def new(): """ Return a new ```` element. """ - xml = '' % nsmap['pr'] + xml = '' % nsmap["pr"] relationships = parse_xml(xml) return relationships @@ -232,7 +239,7 @@ def Relationship_lst(self): """ Return a list containing all the ```` child elements. """ - return self.findall(qn('pr:Relationship')) + return self.findall(qn("pr:Relationship")) @property def xml(self): @@ -248,6 +255,7 @@ 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 @@ -266,27 +274,27 @@ def add_override(self, partname, content_type): @property def defaults(self): - return self.findall(qn('ct:Default')) + return self.findall(qn("ct:Default")) @staticmethod def new(): """ Return a new ```` element. """ - xml = '' % nsmap['ct'] + xml = '' % nsmap["ct"] types = parse_xml(xml) return types @property def overrides(self): - return self.findall(qn('ct:Override')) + 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 +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 +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/docx/opc/package.py index 7ba87bab5..2e81d93c6 100644 --- a/docx/opc/package.py +++ b/docx/opc/package.py @@ -46,6 +46,7 @@ def iter_rels(self): 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(): @@ -68,6 +69,7 @@ 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()): for rel in source.rels.values(): if rel.is_external: @@ -195,9 +197,7 @@ def unmarshal(pkg_reader, package, part_factory): 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() @@ -225,7 +225,8 @@ def _unmarshal_relationships(pkg_reader, package, parts): 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/docx/opc/packuri.py b/docx/opc/packuri.py index 621ed92e5..f20af84f8 100644 --- a/docx/opc/packuri.py +++ b/docx/opc/packuri.py @@ -14,10 +14,11 @@ 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]*)?') + + _filename_re = re.compile("([a-zA-Z]+)([1-9][0-9]*)?") def __new__(cls, pack_uri_str): - if not pack_uri_str[0] == '/': + 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) @@ -49,7 +50,7 @@ def ext(self): """ # 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 + return raw_ext[1:] if raw_ext.startswith(".") else raw_ext @property def filename(self): @@ -95,7 +96,7 @@ def relative_ref(self, baseURI): """ # workaround for posixpath bug in 2.6, doesn't generate correct # relative path when *start* (second) parameter is root ('/') - if baseURI == '/': + if baseURI == "/": relpath = self[1:] else: relpath = posixpath.relpath(self, baseURI) @@ -108,10 +109,10 @@ def rels_uri(self): 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) + 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') +PACKAGE_URI = PackURI("/") +CONTENT_TYPES_URI = PackURI("/[Content_Types].xml") diff --git a/docx/opc/part.py b/docx/opc/part.py index 928d3c183..df20253e7 100644 --- a/docx/opc/part.py +++ b/docx/opc/part.py @@ -4,9 +4,7 @@ Open Packaging Convention (OPC) objects related to package parts. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from .compat import cls_method_fn from .oxml import serialize_part_xml @@ -22,6 +20,7 @@ class Part(object): 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 @@ -160,7 +159,7 @@ 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') + rIds = self._element.xpath("//@r:id") return len([_rId for _rId in rIds if _rId == rId]) @@ -177,6 +176,7 @@ class PartFactory(object): 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 @@ -184,7 +184,7 @@ class PartFactory(object): 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') + 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) @@ -209,10 +209,9 @@ class XmlPart(Part): 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 - ) + super(XmlPart, self).__init__(partname, content_type, package=package) self._element = element @property diff --git a/docx/opc/parts/coreprops.py b/docx/opc/parts/coreprops.py index 3c692fb99..7e5b8c17b 100644 --- a/docx/opc/parts/coreprops.py +++ b/docx/opc/parts/coreprops.py @@ -4,9 +4,7 @@ Core properties part, corresponds to ``/docProps/core.xml`` part in package. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from datetime import datetime @@ -22,6 +20,7 @@ class CorePropertiesPart(XmlPart): Corresponds to part named ``/docProps/core.xml``, containing the core document properties for this document package. """ + @classmethod def default(cls, package): """ @@ -30,8 +29,8 @@ def default(cls, package): """ 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.title = "Word Document" + core_properties.last_modified_by = "python-docx" core_properties.revision = 1 core_properties.modified = datetime.utcnow() return core_properties_part @@ -46,9 +45,7 @@ def core_properties(self): @classmethod def _new(cls, package): - partname = PackURI('/docProps/core.xml') + partname = PackURI("/docProps/core.xml") content_type = CT.OPC_CORE_PROPERTIES coreProperties = CT_CoreProperties.new() - return CorePropertiesPart( - partname, content_type, coreProperties, package - ) + return CorePropertiesPart(partname, content_type, coreProperties, package) diff --git a/docx/opc/phys_pkg.py b/docx/opc/phys_pkg.py index c86a51994..6d68198df 100644 --- a/docx/opc/phys_pkg.py +++ b/docx/opc/phys_pkg.py @@ -19,6 +19,7 @@ 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): @@ -27,9 +28,7 @@ def __new__(cls, pkg_file): elif is_zipfile(pkg_file): reader_cls = _ZipPkgReader else: - raise PackageNotFoundError( - "Package not found at '%s'" % pkg_file - ) + 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 @@ -40,6 +39,7 @@ class PhysPkgWriter(object): """ Factory for physical package writer objects. """ + def __new__(cls, pkg_file): return super(PhysPkgWriter, cls).__new__(_ZipPkgWriter) @@ -49,6 +49,7 @@ 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. @@ -62,7 +63,7 @@ def blob_for(self, pack_uri): directory. """ path = os.path.join(self._path, pack_uri.membername) - with open(path, 'rb') as f: + with open(path, "rb") as f: blob = f.read() return blob @@ -96,9 +97,10 @@ 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') + self._zipf = ZipFile(pkg_file, "r") def blob_for(self, pack_uri): """ @@ -136,9 +138,10 @@ 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) + self._zipf = ZipFile(pkg_file, "w", compression=ZIP_DEFLATED) def close(self): """ diff --git a/docx/opc/pkgreader.py b/docx/opc/pkgreader.py index ae80b3586..b5180192e 100644 --- a/docx/opc/pkgreader.py +++ b/docx/opc/pkgreader.py @@ -19,6 +19,7 @@ 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 @@ -68,9 +69,7 @@ def _load_serialized_parts(phys_reader, pkg_srels, content_types): 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) @@ -81,8 +80,7 @@ def _srels_for(phys_reader, source_uri): 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): @@ -116,6 +114,7 @@ class _ContentTypeMap(object): 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() @@ -169,6 +168,7 @@ class _SerializedPart(object): 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 @@ -207,6 +207,7 @@ class _SerializedRelationship(object): 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 @@ -260,13 +261,14 @@ def target_partname(self): 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 @@ -275,6 +277,7 @@ class _SerializedRelationships(object): Read-only sequence of |_SerializedRelationship| instances corresponding to the relationships item XML passed to constructor. """ + def __init__(self): super(_SerializedRelationships, self).__init__() self._srels = [] diff --git a/docx/opc/pkgwriter.py b/docx/opc/pkgwriter.py index fccda6cd8..1610cbb06 100644 --- a/docx/opc/pkgwriter.py +++ b/docx/opc/pkgwriter.py @@ -22,6 +22,7 @@ class PackageWriter(object): API method, :meth:`write`, is static, so this class is not intended to be instantiated. """ + @staticmethod def write(pkg_file, pkg_rels, parts): """ @@ -71,6 +72,7 @@ class _ContentTypesItem(object): single interface method is xml_for(), e.g. ``_ContentTypesItem.xml_for(parts)``. """ + def __init__(self): self._defaults = CaseInsensitiveDict() self._overrides = dict() @@ -91,8 +93,8 @@ def from_parts(cls, parts): ``[Content_Types].xml`` in an OPC package. """ cti = cls() - cti._defaults['rels'] = CT.OPC_RELATIONSHIPS - cti._defaults['xml'] = CT.XML + 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 diff --git a/docx/opc/rel.py b/docx/opc/rel.py index 7dba2af8e..a57d02f79 100644 --- a/docx/opc/rel.py +++ b/docx/opc/rel.py @@ -4,9 +4,7 @@ Relationship-related objects. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from .oxml import CT_Relationships @@ -15,6 +13,7 @@ class Relationships(dict): """ Collection object for |_Relationship| instances, having list semantics. """ + def __init__(self, baseURI): super(Relationships, self).__init__() self._baseURI = baseURI @@ -49,9 +48,7 @@ def get_or_add_ext_rel(self, reltype, target_ref): 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): @@ -79,9 +76,7 @@ def xml(self): """ 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): @@ -89,6 +84,7 @@ 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. """ + def matches(rel, reltype, target, is_external): if rel.reltype != reltype: return False @@ -125,8 +121,8 @@ 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' + for n in range(1, len(self) + 2): + rId_candidate = "rId%d" % n # like 'rId19' if rId_candidate not in self: return rId_candidate @@ -135,6 +131,7 @@ class _Relationship(object): """ Value object for relationship to part. """ + def __init__(self, rId, reltype, target, baseURI, external=False): super(_Relationship, self).__init__() self._rId = rId @@ -158,8 +155,10 @@ 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 diff --git a/docx/opc/shared.py b/docx/opc/shared.py index 55344483d..15eed35d9 100644 --- a/docx/opc/shared.py +++ b/docx/opc/shared.py @@ -15,6 +15,7 @@ class CaseInsensitiveDict(dict): 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()) @@ -22,9 +23,7 @@ def __getitem__(self, key): return super(CaseInsensitiveDict, self).__getitem__(key.lower()) def __setitem__(self, key, value): - return super(CaseInsensitiveDict, self).__setitem__( - key.lower(), value - ) + return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) def lazyproperty(f): @@ -33,7 +32,7 @@ def lazyproperty(f): to calculate a cached property value. After that, the cached value is returned. """ - cache_attr_name = '_%s' % f.__name__ # like '_foobar' for prop 'foobar' + cache_attr_name = "_%s" % f.__name__ # like '_foobar' for prop 'foobar' docstring = f.__doc__ def get_prop_value(obj): diff --git a/docx/opc/spec.py b/docx/opc/spec.py index 60fc38564..87489fcfb 100644 --- a/docx/opc/spec.py +++ b/docx/opc/spec.py @@ -8,22 +8,22 @@ 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), + ("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 index 093c1b45b..36539340c 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -35,7 +35,7 @@ def register_element_cls(tag, cls): element with matching *tag*. *tag* is a string of the form ``nspfx:tagroot``, e.g. ``'w:document'``. """ - nspfx, tagroot = tag.split(':') + nspfx, tagroot = tag.split(":") namespace = element_class_lookup.get_namespace(nsmap[nspfx]) namespace[tagroot] = cls @@ -55,9 +55,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): nsptag = NamespacePrefixedTag(nsptag_str) if nsdecls is None: nsdecls = nsptag.nsmap - return oxml_parser.makeelement( - nsptag.clark_name, attrib=attrs, nsmap=nsdecls - ) + return oxml_parser.makeelement(nsptag.clark_name, attrib=attrs, nsmap=nsdecls) # =========================================================================== @@ -65,26 +63,30 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): # =========================================================================== 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) + +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) + +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) + +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, @@ -94,6 +96,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): 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) @@ -104,6 +107,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls("w:type", CT_SectType) from .settings import CT_Settings # noqa + register_element_cls("w:settings", CT_Settings) from .shape import ( # noqa @@ -120,34 +124,36 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): 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) + +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) + +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, @@ -164,22 +170,23 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): 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) + +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, @@ -190,37 +197,39 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): 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) + +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) + +register_element_cls("w:p", CT_P) from .text.parfmt import ( # noqa CT_Ind, @@ -230,19 +239,21 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): 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) + +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) + +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 index ed3dd1001..3d4f64e75 100644 --- a/docx/oxml/coreprops.py +++ b/docx/oxml/coreprops.py @@ -2,9 +2,7 @@ """Custom element classes for core properties-related XML elements""" -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import re @@ -24,25 +22,24 @@ class CT_CoreProperties(BaseOxmlElement): ('') 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') - ) + + 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): @@ -58,91 +55,91 @@ def author_text(self): """ The text in the `dc:creator` child element. """ - return self._text_of_element('creator') + return self._text_of_element("creator") @author_text.setter def author_text(self, value): - self._set_element_text('creator', value) + self._set_element_text("creator", value) @property def category_text(self): - return self._text_of_element('category') + return self._text_of_element("category") @category_text.setter def category_text(self, value): - self._set_element_text('category', value) + self._set_element_text("category", value) @property def comments_text(self): - return self._text_of_element('description') + return self._text_of_element("description") @comments_text.setter def comments_text(self, value): - self._set_element_text('description', value) + self._set_element_text("description", value) @property def contentStatus_text(self): - return self._text_of_element('contentStatus') + return self._text_of_element("contentStatus") @contentStatus_text.setter def contentStatus_text(self, value): - self._set_element_text('contentStatus', value) + self._set_element_text("contentStatus", value) @property def created_datetime(self): - return self._datetime_of_element('created') + return self._datetime_of_element("created") @created_datetime.setter def created_datetime(self, value): - self._set_element_datetime('created', value) + self._set_element_datetime("created", value) @property def identifier_text(self): - return self._text_of_element('identifier') + return self._text_of_element("identifier") @identifier_text.setter def identifier_text(self, value): - self._set_element_text('identifier', value) + self._set_element_text("identifier", value) @property def keywords_text(self): - return self._text_of_element('keywords') + return self._text_of_element("keywords") @keywords_text.setter def keywords_text(self, value): - self._set_element_text('keywords', value) + self._set_element_text("keywords", value) @property def language_text(self): - return self._text_of_element('language') + return self._text_of_element("language") @language_text.setter def language_text(self, value): - self._set_element_text('language', value) + self._set_element_text("language", value) @property def lastModifiedBy_text(self): - return self._text_of_element('lastModifiedBy') + return self._text_of_element("lastModifiedBy") @lastModifiedBy_text.setter def lastModifiedBy_text(self, value): - self._set_element_text('lastModifiedBy', value) + self._set_element_text("lastModifiedBy", value) @property def lastPrinted_datetime(self): - return self._datetime_of_element('lastPrinted') + return self._datetime_of_element("lastPrinted") @lastPrinted_datetime.setter def lastPrinted_datetime(self, value): - self._set_element_datetime('lastPrinted', value) + self._set_element_datetime("lastPrinted", value) @property def modified_datetime(self): - return self._datetime_of_element('modified') + return self._datetime_of_element("modified") @modified_datetime.setter def modified_datetime(self, value): - self._set_element_datetime('modified', value) + self._set_element_datetime("modified", value) @property def revision_number(self): @@ -176,27 +173,27 @@ def revision_number(self, value): @property def subject_text(self): - return self._text_of_element('subject') + return self._text_of_element("subject") @subject_text.setter def subject_text(self, value): - self._set_element_text('subject', value) + self._set_element_text("subject", value) @property def title_text(self): - return self._text_of_element('title') + return self._text_of_element("title") @title_text.setter def title_text(self, value): - self._set_element_text('title', value) + self._set_element_text("title", value) @property def version_text(self): - return self._text_of_element('version') + return self._text_of_element("version") @version_text.setter def version_text(self, value): - self._set_element_text('version', value) + self._set_element_text("version", value) def _datetime_of_element(self, property_name): element = getattr(self, property_name) @@ -213,7 +210,7 @@ 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_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 @@ -227,17 +224,15 @@ def _offset_dt(cls, dt, offset_str): """ match = cls._offset_pattern.match(offset_str) if match is None: - raise ValueError( - "'%s' is not a valid offset string" % offset_str - ) + 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 + 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)') + _offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)") @classmethod def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): @@ -248,10 +243,10 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): # 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', + "%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 @@ -275,21 +270,19 @@ 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" - ) + 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') + dt_str = value.strftime("%Y-%m-%dT%H:%M:%SZ") element.text = dt_str - if prop_name in ('created', 'modified'): + 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')] + 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*.""" @@ -297,9 +290,7 @@ def _set_element_text(self, prop_name, value): value = str(value) if len(value) > 255: - tmpl = ( - "exceeded 255 char limit for property, got:\n\n'%s'" - ) + 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 @@ -311,7 +302,7 @@ def _text_of_element(self, property_name): """ element = getattr(self, property_name) if element is None: - return '' + return "" if element.text is None: - return '' + return "" return element.text diff --git a/docx/oxml/document.py b/docx/oxml/document.py index 4211b8ed1..5c3e6f2be 100644 --- a/docx/oxml/document.py +++ b/docx/oxml/document.py @@ -12,7 +12,8 @@ class CT_Document(BaseOxmlElement): """ ```` element, the root element of a document.xml file. """ - body = ZeroOrOne('w:body') + + body = ZeroOrOne("w:body") @property def sectPr_lst(self): @@ -20,7 +21,7 @@ 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') + return self.xpath(".//w:sectPr") class CT_Body(BaseOxmlElement): @@ -28,9 +29,10 @@ 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=()) + + 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. diff --git a/docx/oxml/ns.py b/docx/oxml/ns.py index 6b0861284..192f73ce0 100644 --- a/docx/oxml/ns.py +++ b/docx/oxml/ns.py @@ -20,7 +20,7 @@ "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", + "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", @@ -34,21 +34,22 @@ 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._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) + 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) + nsuri, local_name = clark_name[1:].split("}") + nstag = "%s:%s" % (pfxmap[nsuri], local_name) return cls(nstag) @property @@ -91,7 +92,7 @@ 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]) + return " ".join(['xmlns:%s="%s"' % (pfx, nsmap[pfx]) for pfx in prefixes]) def nspfxmap(*nspfxs): @@ -109,6 +110,6 @@ def qn(tag): 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(':') + prefix, tagroot = tag.split(":") uri = nsmap[prefix] - return '{%s}%s' % (uri, tagroot) + return "{%s}%s" % (uri, tagroot) diff --git a/docx/oxml/numbering.py b/docx/oxml/numbering.py index aeedfa9a0..d5297b2a5 100644 --- a/docx/oxml/numbering.py +++ b/docx/oxml/numbering.py @@ -8,7 +8,11 @@ from .shared import CT_DecimalNumber from .simpletypes import ST_DecimalNumber from .xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrMore, ZeroOrOne + BaseOxmlElement, + OneAndOnlyOne, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, ) @@ -18,9 +22,10 @@ class CT_Num(BaseOxmlElement): 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) + + abstractNumId = OneAndOnlyOne("w:abstractNumId") + lvlOverride = ZeroOrMore("w:lvlOverride") + numId = RequiredAttribute("w:numId", ST_DecimalNumber) def add_lvlOverride(self, ilvl): """ @@ -36,11 +41,9 @@ def new(cls, num_id, abstractNum_id): a ```` child with val attribute set to *abstractNum_id*. """ - num = OxmlElement('w:num') + num = OxmlElement("w:num") num.numId = num_id - abstractNumId = CT_DecimalNumber.new( - 'w:abstractNumId', abstractNum_id - ) + abstractNumId = CT_DecimalNumber.new("w:abstractNumId", abstractNum_id) num.append(abstractNumId) return num @@ -50,8 +53,9 @@ 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) + + startOverride = ZeroOrOne("w:startOverride", successors=("w:lvl",)) + ilvl = RequiredAttribute("w:ilvl", ST_DecimalNumber) def add_startOverride(self, val): """ @@ -66,10 +70,9 @@ 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 = 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): @@ -94,7 +97,8 @@ class CT_Numbering(BaseOxmlElement): ```` element, the root element of a numbering part, i.e. numbering.xml """ - num = ZeroOrMore('w:num', successors=('w:numIdMacAtCleanup',)) + + num = ZeroOrMore("w:num", successors=("w:numIdMacAtCleanup",)) def add_num(self, abstractNum_id): """ @@ -114,7 +118,7 @@ def num_having_numId(self, numId): try: return self.xpath(xpath)[0] except IndexError: - raise KeyError('no element with numId %d' % numId) + raise KeyError("no element with numId %d" % numId) @property def _next_numId(self): @@ -123,9 +127,9 @@ def _next_numId(self): 1 and filling any gaps in numbering between existing ```` elements. """ - numId_strs = self.xpath('./w:num/@w:numId') + 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): + 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 index fc953e74d..e71936774 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -20,38 +20,40 @@ 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=()) + 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) + 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) + + 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) + + w = OptionalAttribute("w:w", ST_TwipsMeasure) + h = OptionalAttribute("w:h", ST_TwipsMeasure) orient = OptionalAttribute( - 'w:orient', WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT + "w:orient", WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT ) @@ -59,10 +61,26 @@ 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', + "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) @@ -348,4 +366,5 @@ class CT_SectType(BaseOxmlElement): """ ```` element, defining the section start type. """ - val = OptionalAttribute('w:val', WD_SECTION_START) + + val = OptionalAttribute("w:val", WD_SECTION_START) diff --git a/docx/oxml/settings.py b/docx/oxml/settings.py index fd319ad70..3fc72e27f 100644 --- a/docx/oxml/settings.py +++ b/docx/oxml/settings.py @@ -11,39 +11,104 @@ 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" + "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 diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 77ca7db8a..885535c71 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -7,12 +7,19 @@ from . import parse_xml from .ns import nsdecls from .simpletypes import ( - ST_Coordinate, ST_DrawingElementId, ST_PositiveCoordinate, - ST_RelationshipId, XsdString, XsdToken + ST_Coordinate, + ST_DrawingElementId, + ST_PositiveCoordinate, + ST_RelationshipId, + XsdString, + XsdToken, ) from .xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, RequiredAttribute, - ZeroOrOne + BaseOxmlElement, + OneAndOnlyOne, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, ) @@ -21,41 +28,44 @@ 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) + + 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' - )) + + 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') + + 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) + + 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') + + extent = OneAndOnlyOne("wp:extent") + docPr = OneAndOnlyOne("wp:docPr") + graphic = OneAndOnlyOne("a:graphic") @classmethod def new(cls, cx, cy, shape_id, pic): @@ -67,9 +77,9 @@ def new(cls, cx, cy, shape_id, pic): inline.extent.cx = cx inline.extent.cy = cy inline.docPr.id = shape_id - inline.docPr.name = 'Picture %d' % shape_id + inline.docPr.name = "Picture %d" % shape_id inline.graphic.graphicData.uri = ( - 'http://schemas.openxmlformats.org/drawingml/2006/picture' + "http://schemas.openxmlformats.org/drawingml/2006/picture" ) inline.graphic.graphicData._insert_pic(pic) return inline @@ -89,16 +99,16 @@ def new_pic_inline(cls, shape_id, rId, filename, cx, cy): @classmethod def _inline_xml(cls): return ( - '\n' + "\n" ' \n' ' \n' - ' \n' + " \n" ' \n' - ' \n' - ' \n' + " \n" + " \n" ' \n' - ' \n' - '' % nsdecls('wp', 'a', 'pic', 'r') + " \n" + "" % nsdecls("wp", "a", "pic", "r") ) @@ -107,8 +117,9 @@ 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) + + id = RequiredAttribute("id", ST_DrawingElementId) + name = RequiredAttribute("name", XsdString) class CT_NonVisualPictureProperties(BaseOxmlElement): @@ -122,9 +133,10 @@ class CT_Picture(BaseOxmlElement): """ ```` element, a DrawingML picture """ - nvPicPr = OneAndOnlyOne('pic:nvPicPr') - blipFill = OneAndOnlyOne('pic:blipFill') - spPr = OneAndOnlyOne('pic:spPr') + + nvPicPr = OneAndOnlyOne("pic:nvPicPr") + blipFill = OneAndOnlyOne("pic:blipFill") + spPr = OneAndOnlyOne("pic:spPr") @classmethod def new(cls, pic_id, filename, rId, cx, cy): @@ -144,25 +156,25 @@ def new(cls, pic_id, filename, rId, cx, cy): @classmethod def _pic_xml(cls): return ( - '\n' - ' \n' + "\n" + " \n" ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" ' \n' ' \n' - ' \n' + " \n" ' \n' - ' \n' - '' % nsdecls('pic', 'a', 'r') + " \n" + "" % nsdecls("pic", "a", "r") ) @@ -170,7 +182,8 @@ class CT_PictureNonVisual(BaseOxmlElement): """ ```` element, non-visual picture properties """ - cNvPr = OneAndOnlyOne('pic:cNvPr') + + cNvPr = OneAndOnlyOne("pic:cNvPr") class CT_Point2D(BaseOxmlElement): @@ -178,8 +191,9 @@ 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) + + x = RequiredAttribute("x", ST_Coordinate) + y = RequiredAttribute("y", ST_Coordinate) class CT_PositiveSize2D(BaseOxmlElement): @@ -187,8 +201,9 @@ 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) + + cx = RequiredAttribute("cx", ST_PositiveCoordinate) + cy = RequiredAttribute("cy", ST_PositiveCoordinate) class CT_PresetGeometry2D(BaseOxmlElement): @@ -209,10 +224,20 @@ 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' - )) + + 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): @@ -256,8 +281,9 @@ class CT_Transform2D(BaseOxmlElement): """ ```` element, specifies size and shape of picture container. """ - off = ZeroOrOne('a:off', successors=('a:ext',)) - ext = ZeroOrOne('a:ext', successors=()) + + off = ZeroOrOne("a:off", successors=("a:ext",)) + ext = ZeroOrOne("a:ext", successors=()) @property def cx(self): diff --git a/docx/oxml/shared.py b/docx/oxml/shared.py index 1e21ba366..4981e9200 100644 --- a/docx/oxml/shared.py +++ b/docx/oxml/shared.py @@ -18,7 +18,8 @@ class CT_DecimalNumber(BaseOxmlElement): others, containing a text representation of a decimal number (e.g. 42) in its ``val`` attribute. """ - val = RequiredAttribute('w:val', ST_DecimalNumber) + + val = RequiredAttribute("w:val", ST_DecimalNumber) @classmethod def new(cls, nsptagname, val): @@ -26,7 +27,7 @@ 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)}) + return OxmlElement(nsptagname, attrs={qn("w:val"): str(val)}) class CT_OnOff(BaseOxmlElement): @@ -34,7 +35,8 @@ 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) + + val = OptionalAttribute("w:val", ST_OnOff, default=True) class CT_String(BaseOxmlElement): @@ -42,7 +44,8 @@ class CT_String(BaseOxmlElement): Used for ```` and ```` elements and others, containing a style name in its ``val`` attribute. """ - val = RequiredAttribute('w:val', ST_String) + + val = RequiredAttribute("w:val", ST_String) @classmethod def new(cls, nsptagname, val): diff --git a/docx/oxml/simpletypes.py b/docx/oxml/simpletypes.py index 400a23700..8fb6cb89d 100644 --- a/docx/oxml/simpletypes.py +++ b/docx/oxml/simpletypes.py @@ -6,16 +6,13 @@ type in the associated XML schema. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from ..exceptions import InvalidXmlError from ..shared import Emu, Pt, RGBColor, Twips class BaseSimpleType(object): - @classmethod def from_xml(cls, str_value): return cls.convert_from_xml(str_value) @@ -29,17 +26,15 @@ def to_xml(cls, 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 @@ -51,13 +46,10 @@ def validate_string(cls, value): return value except NameError: # means we're on Python 3 pass - raise TypeError( - "value must be a string, got %s" % type(value) - ) + raise TypeError("value must be a string, got %s" % type(value)) class BaseIntType(BaseSimpleType): - @classmethod def convert_from_xml(cls, str_value): return int(str_value) @@ -72,7 +64,6 @@ def validate(cls, value): class BaseStringType(BaseSimpleType): - @classmethod def convert_from_xml(cls, str_value): return str_value @@ -87,14 +78,11 @@ def validate(cls, 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): @@ -106,19 +94,17 @@ class XsdAnyUri(BaseStringType): 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): @@ -134,23 +120,20 @@ 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. """ + 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): @@ -168,52 +151,44 @@ class XsdToken(BaseStringType): 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): 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,10 +212,9 @@ 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) @@ -251,7 +224,7 @@ def convert_to_xml(cls, value): 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): @@ -267,7 +240,8 @@ class ST_HexColorAuto(XsdStringEnumeration): """ Value for `w:color/[@val="auto"] attribute setting """ - AUTO = 'auto' + + AUTO = "auto" _members = (AUTO,) @@ -276,11 +250,12 @@ class ST_HpsMeasure(XsdUnsignedLong): """ 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: + 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): @@ -293,26 +268,25 @@ class ST_Merge(XsdStringEnumeration): """ Valid values for attribute """ - CONTINUE = 'continue' - RESTART = 'restart' + + 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 +301,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 +319,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,14 +351,17 @@ def convert_to_xml(cls, value): class ST_UniversalMeasure(BaseSimpleType): - @classmethod def convert_from_xml(cls, str_value): 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 @@ -402,8 +371,9 @@ class ST_VerticalAlignRun(XsdStringEnumeration): """ Valid values for `w:vertAlign/@val`. """ - BASELINE = 'baseline' - SUPERSCRIPT = 'superscript' - SUBSCRIPT = 'subscript' + + BASELINE = "baseline" + SUPERSCRIPT = "superscript" + SUBSCRIPT = "subscript" _members = (BASELINE, SUPERSCRIPT, SUBSCRIPT) diff --git a/docx/oxml/styles.py b/docx/oxml/styles.py index 6f27e45eb..96b390baf 100644 --- a/docx/oxml/styles.py +++ b/docx/oxml/styles.py @@ -7,8 +7,11 @@ from ..enum.style import WD_STYLE_TYPE from .simpletypes import ST_DecimalNumber, ST_OnOff, ST_String from .xmlchemy import ( - BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore, - ZeroOrOne + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, ) @@ -18,17 +21,17 @@ def styleId_from_name(name): 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(' ', '')) + "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): @@ -37,14 +40,15 @@ class CT_LatentStyles(BaseOxmlElement): 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) + 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): """ @@ -78,12 +82,13 @@ 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) + + 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): """ @@ -109,29 +114,47 @@ 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' + "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:]) + 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) + 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): @@ -291,9 +314,10 @@ 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=()) + + _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): @@ -314,8 +338,7 @@ 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 + s for s in self._iter_styles() if s.type == style_type and s.default ] if not default_styles_for_type: return None @@ -348,4 +371,4 @@ def _iter_styles(self): """ Generate each of the `w:style` child elements in document order. """ - return (style for style in self.xpath('w:style')) + return (style for style in self.xpath("w:style")) diff --git a/docx/oxml/table.py b/docx/oxml/table.py index e55bf9126..f9423da6b 100644 --- a/docx/oxml/table.py +++ b/docx/oxml/table.py @@ -2,9 +2,7 @@ """Custom element classes for tables""" -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +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 @@ -12,11 +10,20 @@ from .ns import nsdecls, qn from ..shared import Emu, Twips from .simpletypes import ( - ST_Merge, ST_TblLayoutType, ST_TblWidth, ST_TwipsMeasure, XsdInt + ST_Merge, + ST_TblLayoutType, + ST_TblWidth, + ST_TwipsMeasure, + XsdInt, ) from .xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, OneOrMore, OptionalAttribute, - RequiredAttribute, ZeroOrOne, ZeroOrMore + BaseOxmlElement, + OneAndOnlyOne, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, + ZeroOrMore, ) @@ -24,17 +31,19 @@ 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) + + 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') + + 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): """ @@ -47,8 +56,8 @@ 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): @@ -108,9 +117,10 @@ class CT_Tbl(BaseOxmlElement): """ ```` element """ - tblPr = OneAndOnlyOne('w:tblPr') - tblGrid = OneAndOnlyOne('w:tblGrid') - tr = ZeroOrMore('w:tr') + + tblPr = OneAndOnlyOne("w:tblPr") + tblGrid = OneAndOnlyOne("w:tblGrid") + tr = ZeroOrMore("w:tr") @property def bidiVisual_val(self): @@ -182,54 +192,52 @@ 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 @@ -239,7 +247,8 @@ class CT_TblGrid(BaseOxmlElement): ```` element, child of ````, holds ```` elements that define column count, width, etc. """ - gridCol = ZeroOrMore('w:gridCol', successors=('w:tblGridChange',)) + + gridCol = ZeroOrMore("w:gridCol", successors=("w:tblGridChange",)) class CT_TblGridCol(BaseOxmlElement): @@ -247,7 +256,8 @@ class CT_TblGridCol(BaseOxmlElement): ```` element, child of ````, defines a table column. """ - w = OptionalAttribute('w:w', ST_TwipsMeasure) + + w = OptionalAttribute("w:w", ST_TwipsMeasure) @property def gridCol_idx(self): @@ -263,7 +273,8 @@ 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) + + type = OptionalAttribute("w:type", ST_TblLayoutType) class CT_TblPr(BaseOxmlElement): @@ -271,17 +282,31 @@ class CT_TblPr(BaseOxmlElement): ```` 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 @@ -313,12 +338,12 @@ def autofit(self): tblLayout = self.tblLayout if tblLayout is None: return True - return False if tblLayout.type == 'fixed' else True + return False if tblLayout.type == "fixed" else True @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): @@ -344,11 +369,12 @@ class CT_TblWidth(BaseOxmlElement): 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): @@ -356,22 +382,22 @@ def width(self): Return the EMU length value represented by the combined ``w:w`` and ``w:type`` attributes. """ - if self.type != 'dxa': + 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""" - 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): @@ -422,7 +448,7 @@ 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')) + 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 @@ -451,11 +477,7 @@ def new(cls): Return a new ```` element, containing an empty paragraph as the required EG_BlockLevelElt. """ - return parse_xml( - '\n' - ' \n' - '' % nsdecls('w') - ) + return parse_xml("\n" " \n" "" % nsdecls("w")) @property def right(self): @@ -532,6 +554,7 @@ def _grow_to(self, width, height, top_tc=None): 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,7 +565,7 @@ 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): """ @@ -591,7 +614,7 @@ 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') + following_tcs = self.xpath("./following-sibling::w:tc") return following_tcs[0] if following_tcs else None def _remove(self): @@ -607,7 +630,7 @@ def _remove_trailing_empty_p(self): """ 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: @@ -620,20 +643,21 @@ def _span_dimensions(self, other_tc): 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) @@ -671,11 +695,12 @@ def _swallow_next_tc(self, grid_width, top_tc): |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) @@ -689,7 +714,7 @@ def _tbl(self): """ The tbl element this tc element appears in. """ - return self.xpath('./ancestor::w:tbl[position()=1]')[0] + return self.xpath("./ancestor::w:tbl[position()=1]")[0] @property def _tc_above(self): @@ -713,7 +738,7 @@ def _tr(self): """ The tr element this tc element appears in. """ - return self.xpath('./ancestor::w:tr[position()=1]')[0] + return self.xpath("./ancestor::w:tr[position()=1]")[0] @property def _tr_above(self): @@ -724,8 +749,8 @@ def _tr_above(self): 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): @@ -736,7 +761,7 @@ def _tr_below(self): 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 @@ -752,16 +777,31 @@ class CT_TcPr(BaseOxmlElement): """ ```` 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 @@ -838,13 +878,25 @@ class CT_TrPr(BaseOxmlElement): """ ```` 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 @@ -884,11 +936,13 @@ 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) + + val = OptionalAttribute("w:val", ST_Merge, default=ST_Merge.CONTINUE) diff --git a/docx/oxml/text/font.py b/docx/oxml/text/font.py index 810ec2b30..73913af9a 100644 --- a/docx/oxml/text/font.py +++ b/docx/oxml/text/font.py @@ -8,12 +8,8 @@ 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 -) +from ..simpletypes import ST_HexColor, ST_HpsMeasure, ST_String, ST_VerticalAlignRun +from ..xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrOne class CT_Color(BaseOxmlElement): @@ -21,8 +17,9 @@ 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) + + val = RequiredAttribute("w:val", ST_HexColor) + themeColor = OptionalAttribute("w:themeColor", MSO_THEME_COLOR) class CT_Fonts(BaseOxmlElement): @@ -30,15 +27,17 @@ 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) + + 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) + + val = RequiredAttribute("w:val", WD_COLOR) class CT_HpsMeasure(BaseOxmlElement): @@ -46,49 +45,83 @@ class CT_HpsMeasure(BaseOxmlElement): Used for ```` element and others, specifying font size in half-points. """ - val = RequiredAttribute('w:val', ST_HpsMeasure) + + 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' + "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:]) + 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): @@ -96,7 +129,7 @@ def _new_color(self): Override metaclass method to set `w:color/@val` to RGB black on create. """ - return parse_xml('' % nsdecls('w')) + return parse_xml('' % nsdecls("w")) @property def highlight_val(self): @@ -276,9 +309,9 @@ def _get_bool_val(self, name): def _set_bool_val(self, name, value): if value is None: - getattr(self, '_remove_%s' % name)() + getattr(self, "_remove_%s" % name)() return - element = getattr(self, 'get_or_add_%s' % name)() + element = getattr(self, "get_or_add_%s" % name)() element.val = value @@ -286,12 +319,13 @@ 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')) + val = self.get(qn("w:val")) underline = WD_UNDERLINE.from_xml(val) if underline == WD_UNDERLINE.SINGLE: return True @@ -310,11 +344,12 @@ def val(self, value): value = WD_UNDERLINE.NONE val = WD_UNDERLINE.to_xml(value) - self.set(qn('w:val'), val) + self.set(qn("w:val"), val) class CT_VerticalAlignRun(BaseOxmlElement): """ ```` element, specifying subscript or superscript. """ - val = RequiredAttribute('w:val', ST_VerticalAlignRun) + + val = RequiredAttribute("w:val", ST_VerticalAlignRun) diff --git a/docx/oxml/text/paragraph.py b/docx/oxml/text/paragraph.py index 5e4213776..8386420f6 100644 --- a/docx/oxml/text/paragraph.py +++ b/docx/oxml/text/paragraph.py @@ -12,8 +12,9 @@ class CT_P(BaseOxmlElement): """ ```` element, containing the properties and text for a paragraph. """ - pPr = ZeroOrOne('w:pPr') - r = ZeroOrMore('w:r') + + pPr = ZeroOrOne("w:pPr") + r = ZeroOrMore("w:r") def _insert_pPr(self, pPr): self.insert(0, pPr) @@ -23,7 +24,7 @@ def add_p_before(self): """ Return a new ```` element inserted directly prior to this one. """ - new_p = OxmlElement('w:p') + new_p = OxmlElement("w:p") self.addprevious(new_p) return new_p @@ -48,7 +49,7 @@ def clear_content(self): Remove all child elements, except the ```` element if present. """ for child in self[:]: - if child.tag == qn('w:pPr'): + if child.tag == qn("w:pPr"): continue self.remove(child) diff --git a/docx/oxml/text/parfmt.py b/docx/oxml/text/parfmt.py index 466b11b1b..cae729d57 100644 --- a/docx/oxml/text/parfmt.py +++ b/docx/oxml/text/parfmt.py @@ -5,13 +5,19 @@ """ from ...enum.text import ( - WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, WD_TAB_ALIGNMENT, WD_TAB_LEADER + 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 + BaseOxmlElement, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, ) @@ -19,45 +25,75 @@ 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) + + 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) + + 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' + "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:]) + 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 @@ -311,28 +347,29 @@ 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) + + 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) + + 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=()) + + tab = OneOrMore("w:tab", successors=()) def insert_tab_in_order(self, pos, align, leader): """ diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index 8f0a62e82..3d3d3cdf6 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -6,29 +6,29 @@ from ..ns import qn from ..simpletypes import ST_BrClear, ST_BrType -from ..xmlchemy import ( - BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne -) +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) + + 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') + + 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) @@ -40,7 +40,7 @@ def add_t(self, text): """ t = self._add_t(text=text) if len(text.strip()) < len(text): - t.set(qn('xml:space'), 'preserve') + t.set(qn("xml:space"), "preserve") return t def add_drawing(self, inline_or_anchor): @@ -87,15 +87,15 @@ def text(self): child elements like ```` translated to their Python equivalent. """ - text = '' + text = "" for child in self: - if child.tag == qn('w:t'): + 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' + 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 @@ -119,6 +119,7 @@ class _RunContentAppender(object): 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 = [] @@ -150,17 +151,17 @@ def add_char(self, char): which must be called at the end of text to ensure any pending ```` element is written. """ - if char == '\t': + if char == "\t": self.flush() self._r.add_tab() - elif char in '\r\n': + elif char in "\r\n": self.flush() self._r.add_br() else: self._bfr.append(char) def flush(self): - text = ''.join(self._bfr) + text = "".join(self._bfr) if text: self._r.add_t(text) del self._bfr[:] diff --git a/docx/oxml/xmlchemy.py b/docx/oxml/xmlchemy.py index 46dbf462b..0fff364dc 100644 --- a/docx/oxml/xmlchemy.py +++ b/docx/oxml/xmlchemy.py @@ -23,7 +23,7 @@ def serialize_for_reading(element): 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) @@ -39,7 +39,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() @@ -95,10 +95,16 @@ class MetaOxmlElement(type): """ Metaclass for BaseOxmlElement """ + def __init__(cls, clsname, bases, clsdict): dispatchable = ( - OneAndOnlyOne, OneOrMore, OptionalAttribute, RequiredAttribute, - ZeroOrMore, ZeroOrOne, ZeroOrOneChoice + OneAndOnlyOne, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, + ZeroOrOneChoice, ) for key, value in clsdict.items(): if isinstance(value, dispatchable): @@ -110,6 +116,7 @@ class BaseAttribute(object): Base class for OptionalAttribute and RequiredAttribute, providing common methods. """ + def __init__(self, attr_name, simple_type): super(BaseAttribute, self).__init__() self._attr_name = attr_name @@ -136,7 +143,7 @@ def _add_attr_property(self): @property def _clark_name(self): - if ':' in self._attr_name: + if ":" in self._attr_name: return qn(self._attr_name) return self._attr_name @@ -147,6 +154,7 @@ class OptionalAttribute(BaseAttribute): 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): super(OptionalAttribute, self).__init__(attr_name, simple_type) self._default = default @@ -157,11 +165,13 @@ def _getter(self): 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: return self._default return self._simple_type.from_xml(attr_str_value) + get_attr_value.__doc__ = self._docstring return get_attr_value @@ -172,10 +182,10 @@ def _docstring(self): 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) + "%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) ) @property @@ -184,6 +194,7 @@ def _setter(self): Return a function object suitable for the "set" side of the attribute property descriptor. """ + def set_attr_value(obj, value): if value is None or value == self._default: if self._clark_name in obj.attrib: @@ -191,6 +202,7 @@ def set_attr_value(obj, value): return str_value = self._simple_type.to_xml(value) obj.set(self._clark_name, str_value) + return set_attr_value @@ -203,20 +215,23 @@ class RequiredAttribute(BaseAttribute): |None| is assigned. Assigning |None| raises |TypeError| or |ValueError|, depending on the simple type of the attribute. """ + @property def _getter(self): """ 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 @@ -226,9 +241,9 @@ 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) + return "%s type-converted value of ``%s`` attribute." % ( + self._simple_type.__name__, + self._attr_name, ) @property @@ -237,9 +252,11 @@ def _setter(self): 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 @@ -248,6 +265,7 @@ class _BaseChildElement(object): 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 @@ -266,6 +284,7 @@ def _add_adder(self): 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,8 +295,8 @@ 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) @@ -289,7 +308,7 @@ def _add_creator(self): 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) @@ -307,13 +326,14 @@ def _add_inserter(self): 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) @@ -322,26 +342,27 @@ 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 + 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. """ + 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) @@ -360,8 +381,10 @@ def _creator(self): Return a function object that creates a new, empty element of the right type, having no attributes. """ + def new_child_element(obj): return OxmlElement(self._nsptagname) + return new_child_element @property @@ -371,17 +394,18 @@ def _getter(self): 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): @@ -389,11 +413,13 @@ def _list_getter(self): 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 @@ -405,15 +431,15 @@ def _public_add_method_name(self): 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): @@ -421,12 +447,12 @@ class Choice(_BaseChildElement): 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): + def populate_class_members(self, element_cls, group_prop_name, successors): """ Add the appropriate methods to *element_cls*. """ @@ -445,50 +471,47 @@ def _add_get_or_change_to_method(self): 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 + if ":" in self._nsptagname: + start = self._nsptagname.index(":") + 1 else: start = 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. """ + def __init__(self, nsptagname): super(OneAndOnlyOne, self).__init__(nsptagname, None) @@ -496,9 +519,7 @@ 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 - ) + super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @property @@ -507,18 +528,17 @@ def _getter(self): 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 @@ -528,13 +548,12 @@ class OneOrMore(_BaseChildElement): 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 - ) + super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() self._add_inserter() @@ -547,13 +566,12 @@ class ZeroOrMore(_BaseChildElement): """ 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 - ) + super(ZeroOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() self._add_inserter() @@ -566,6 +584,7 @@ class ZeroOrOne(_BaseChildElement): """ Defines an optional child element for MetaOxmlElement. """ + def populate_class_members(self, element_cls, prop_name): """ Add the appropriate methods to *element_cls*. @@ -583,14 +602,16 @@ def _add_get_or_adder(self): 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) @@ -599,16 +620,18 @@ def _add_remover(self): 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): @@ -616,6 +639,7 @@ class ZeroOrOneChoice(_BaseChildElement): 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 @@ -624,9 +648,7 @@ 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 - ) + super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name) self._add_choice_getter() for choice in self._choices: choice.populate_class_members( @@ -649,16 +671,15 @@ def _add_group_remover(self): 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): @@ -666,11 +687,13 @@ def _choice_getter(self): 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 @@ -684,7 +707,7 @@ def _member_nsptagnames(self): @lazyproperty def _remove_choice_group_method_name(self): - return '_remove_%s' % self._prop_name + return "_remove_%s" % self._prop_name class _OxmlElementBase(etree.ElementBase): @@ -699,7 +722,9 @@ class _OxmlElementBase(etree.ElementBase): 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): @@ -745,9 +770,7 @@ def xpath(self, xpath_str): Override of ``lxml`` _Element.xpath() method to provide standard Open XML namespace mapping (``nsmap``) in centralized location. """ - return super(BaseOxmlElement, self).xpath( - xpath_str, namespaces=nsmap - ) + return super(BaseOxmlElement, self).xpath(xpath_str, namespaces=nsmap) @property def _nsptag(self): @@ -755,5 +778,5 @@ def _nsptag(self): BaseOxmlElement = MetaOxmlElement( - 'BaseOxmlElement', (etree.ElementBase,), dict(_OxmlElementBase.__dict__) + "BaseOxmlElement", (etree.ElementBase,), dict(_OxmlElementBase.__dict__) ) diff --git a/docx/package.py b/docx/package.py index 9f5ccc667..07f0718a2 100644 --- a/docx/package.py +++ b/docx/package.py @@ -104,10 +104,12 @@ def _next_image_partname(self, ext): 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/hdrftr.py b/docx/parts/hdrftr.py index 549805b2a..22ea874a0 100644 --- a/docx/parts/hdrftr.py +++ b/docx/parts/hdrftr.py @@ -26,9 +26,9 @@ 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 @@ -48,8 +48,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/docx/parts/image.py b/docx/parts/image.py index 6ece20d80..a235b465f 100644 --- a/docx/parts/image.py +++ b/docx/parts/image.py @@ -4,9 +4,7 @@ The proxy class for an image part, and related objects. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import hashlib @@ -20,6 +18,7 @@ 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 @@ -57,7 +56,7 @@ def filename(self): """ if self._image is not None: return self._image.filename - return 'image.%s' % self.partname.ext + return "image.%s" % self.partname.ext @classmethod def from_image(cls, image, partname): diff --git a/docx/parts/numbering.py b/docx/parts/numbering.py index e324c5aac..8bcd271a3 100644 --- a/docx/parts/numbering.py +++ b/docx/parts/numbering.py @@ -4,9 +4,7 @@ |NumberingPart| and closely related objects """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from ..opc.part import XmlPart from ..shared import lazyproperty @@ -17,6 +15,7 @@ class NumberingPart(XmlPart): Proxy for the numbering.xml part containing numbering definitions for a document or glossary. """ + @classmethod def new(cls): """ @@ -39,6 +38,7 @@ 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 diff --git a/docx/parts/settings.py b/docx/parts/settings.py index a701b1726..e0f751b34 100644 --- a/docx/parts/settings.py +++ b/docx/parts/settings.py @@ -4,9 +4,7 @@ |SettingsPart| and closely related objects """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import os @@ -21,13 +19,14 @@ 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') + partname = PackURI("/word/settings.xml") content_type = CT.WML_SETTINGS element = parse_xml(cls._default_settings_xml()) return cls(partname, content_type, element, package) @@ -46,9 +45,8 @@ 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' + os.path.split(__file__)[0], "..", "templates", "default-settings.xml" ) - with open(path, 'rb') as f: + 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 index 129b8f1cc..49cfa396b 100644 --- a/docx/parts/story.py +++ b/docx/parts/story.py @@ -66,7 +66,7 @@ def next_id(self): 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') + 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 diff --git a/docx/parts/styles.py b/docx/parts/styles.py index 00c7cb3c3..a16b4188f 100644 --- a/docx/parts/styles.py +++ b/docx/parts/styles.py @@ -4,9 +4,7 @@ Provides StylesPart and related objects """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import os @@ -22,13 +20,14 @@ 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') + partname = PackURI("/word/styles.xml") content_type = CT.WML_STYLES element = parse_xml(cls._default_styles_xml()) return cls(partname, content_type, element, package) @@ -47,9 +46,8 @@ 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' + os.path.split(__file__)[0], "..", "templates", "default-styles.xml" ) - with open(path, 'rb') as f: + with open(path, "rb") as f: xml_bytes = f.read() return xml_bytes diff --git a/docx/shape.py b/docx/shape.py index e4f885d73..036118d46 100644 --- a/docx/shape.py +++ b/docx/shape.py @@ -5,9 +5,7 @@ a document. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from .enum.shape import WD_INLINE_SHAPE from .oxml.ns import nsmap @@ -19,6 +17,7 @@ class InlineShapes(Parented): 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 @@ -43,7 +42,7 @@ 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) @@ -52,6 +51,7 @@ class InlineShape(object): Proxy for an ```` element, representing the container for an inline graphical object. """ + def __init__(self, inline): super(InlineShape, self).__init__() self._inline = inline @@ -78,14 +78,14 @@ def type(self): """ 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 diff --git a/docx/shared.py b/docx/shared.py index 919964325..ea855a21c 100644 --- a/docx/shared.py +++ b/docx/shared.py @@ -14,6 +14,7 @@ class Length(int): 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 @@ -71,6 +72,7 @@ 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) @@ -81,6 +83,7 @@ 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) @@ -91,6 +94,7 @@ 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)) @@ -100,6 +104,7 @@ 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) @@ -109,6 +114,7 @@ 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) @@ -119,6 +125,7 @@ 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) @@ -128,21 +135,22 @@ 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' + 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 + 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 + return "%02X%02X%02X" % self @classmethod def from_string(cls, rgb_hex_str): @@ -161,7 +169,7 @@ def lazyproperty(f): to calculate a cached property value. After that, the cached value is returned. """ - cache_attr_name = '_%s' % f.__name__ # like '_foobar' for prop 'foobar' + cache_attr_name = "_%s" % f.__name__ # like '_foobar' for prop 'foobar' docstring = f.__doc__ def get_prop_value(obj): @@ -193,7 +201,7 @@ class ElementProxy(object): type of class in python-docx other than custom element (oxml) classes. """ - __slots__ = ('_element', '_parent') + __slots__ = ("_element", "_parent") def __init__(self, element, parent=None): self._element = element @@ -238,6 +246,7 @@ class Parented(object): such as add or drop a relationship. Provides ``self._parent`` attribute to subclasses. """ + def __init__(self, parent): super(Parented, self).__init__() self._parent = parent diff --git a/docx/styles/__init__.py b/docx/styles/__init__.py index 63ebaa2b6..61d011d38 100644 --- a/docx/styles/__init__.py +++ b/docx/styles/__init__.py @@ -4,9 +4,7 @@ Sub-package module for docx.styles sub-package. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals class BabelFish(object): @@ -16,18 +14,18 @@ class BabelFish(object): """ 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'), + ("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) @@ -47,6 +45,4 @@ 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 - ) + return cls.ui_style_names.get(internal_style_name, internal_style_name) diff --git a/docx/styles/latent.py b/docx/styles/latent.py index 99b1514ff..6f723692b 100644 --- a/docx/styles/latent.py +++ b/docx/styles/latent.py @@ -4,9 +4,7 @@ Latent style-related objects. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from . import BabelFish from ..shared import ElementProxy @@ -67,11 +65,11 @@ def default_to_hidden(self): to be hidden. A hidden style does not appear in the recommended list or in the style gallery. """ - return self._element.bool_prop('defSemiHidden') + return self._element.bool_prop("defSemiHidden") @default_to_hidden.setter def default_to_hidden(self, value): - self._element.set_bool_prop('defSemiHidden', value) + self._element.set_bool_prop("defSemiHidden", value) @property def default_to_locked(self): @@ -82,11 +80,11 @@ def default_to_locked(self): behavior is only active when formatting protection is turned on for the document (via the Developer menu). """ - return self._element.bool_prop('defLockedState') + return self._element.bool_prop("defLockedState") @default_to_locked.setter def default_to_locked(self, value): - self._element.set_bool_prop('defLockedState', value) + self._element.set_bool_prop("defLockedState", value) @property def default_to_quick_style(self): @@ -94,11 +92,11 @@ 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') + 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) + self._element.set_bool_prop("defQFormat", value) @property def default_to_unhide_when_used(self): @@ -106,11 +104,11 @@ 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') + 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) + self._element.set_bool_prop("defUnhideWhenUsed", value) @property def load_count(self): @@ -155,11 +153,11 @@ def hidden(self): the recommended list. |None| indicates the effective value is inherited from the parent ```` element. """ - return self._element.on_off_prop('semiHidden') + return self._element.on_off_prop("semiHidden") @hidden.setter def hidden(self, value): - self._element.set_on_off_prop('semiHidden', value) + self._element.set_on_off_prop("semiHidden", value) @property def locked(self): @@ -170,11 +168,11 @@ def locked(self): only active when formatting protection is turned on for the document (via the Developer menu). """ - return self._element.on_off_prop('locked') + return self._element.on_off_prop("locked") @locked.setter def locked(self, value): - self._element.set_on_off_prop('locked', value) + self._element.set_on_off_prop("locked", value) @property def name(self): @@ -202,11 +200,11 @@ def quick_style(self): effective value should be inherited from the default values in its parent |LatentStyles| object. """ - return self._element.on_off_prop('qFormat') + return self._element.on_off_prop("qFormat") @quick_style.setter def quick_style(self, value): - self._element.set_on_off_prop('qFormat', value) + self._element.set_on_off_prop("qFormat", value) @property def unhide_when_used(self): @@ -217,8 +215,8 @@ def unhide_when_used(self): inherited from the default specified by its parent |LatentStyles| object. """ - return self._element.on_off_prop('unhideWhenUsed') + 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) + self._element.set_on_off_prop("unhideWhenUsed", value) diff --git a/docx/styles/style.py b/docx/styles/style.py index 24371b231..ff63a819b 100644 --- a/docx/styles/style.py +++ b/docx/styles/style.py @@ -4,9 +4,7 @@ Style object hierarchy. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from . import BabelFish from ..enum.style import WD_STYLE_TYPE @@ -23,8 +21,8 @@ def StyleFactory(style_elm): style_cls = { WD_STYLE_TYPE.PARAGRAPH: _ParagraphStyle, WD_STYLE_TYPE.CHARACTER: _CharacterStyle, - WD_STYLE_TYPE.TABLE: _TableStyle, - WD_STYLE_TYPE.LIST: _NumberingStyle + WD_STYLE_TYPE.TABLE: _TableStyle, + WD_STYLE_TYPE.LIST: _NumberingStyle, }[style_elm.type] return style_cls(style_elm) @@ -211,7 +209,7 @@ class _ParagraphStyle(_CharacterStyle): __slots__ = () def __repr__(self): - return '_ParagraphStyle(\'%s\') id: %s' % (self.name, id(self)) + return "_ParagraphStyle('%s') id: %s" % (self.name, id(self)) @property def next_paragraph_style(self): @@ -254,7 +252,7 @@ class _TableStyle(_ParagraphStyle): __slots__ = () def __repr__(self): - return '_TableStyle(\'%s\') id: %s' % (self.name, id(self)) + return "_TableStyle('%s') id: %s" % (self.name, id(self)) class _NumberingStyle(BaseStyle): diff --git a/docx/styles/styles.py b/docx/styles/styles.py index f9f1cd2fb..d09fbd137 100644 --- a/docx/styles/styles.py +++ b/docx/styles/styles.py @@ -44,8 +44,8 @@ def __getitem__(self, key): 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.' + "style lookup by style_id is deprecated. Use style name as " + "key instead." ) warn(msg, UserWarning, stacklevel=2) return StyleFactory(style_elm) @@ -67,9 +67,7 @@ def add_style(self, name, style_type, builtin=False): 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 - ) + style = self._element.add_style_of_type(style_name, style_type, builtin) return StyleFactory(style) def default(self, style_type): @@ -145,8 +143,7 @@ def _get_style_id_from_style(self, style, style_type): """ if style.type != style_type: raise ValueError( - "assigned style is type %s, need type %s" % - (style.type, style_type) + "assigned style is type %s, need type %s" % (style.type, style_type) ) if style == self.default(style_type): return None diff --git a/docx/table.py b/docx/table.py index b3bc090fb..bd4e1e277 100644 --- a/docx/table.py +++ b/docx/table.py @@ -16,6 +16,7 @@ class Table(Parented): """ Proxy class for a WordprocessingML ```` element. """ + def __init__(self, tbl, parent): super(Table, self).__init__(parent) self._element = self._tbl = tbl @@ -130,9 +131,7 @@ def style(self): @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 @@ -196,7 +195,7 @@ def __init__(self, tc, parent): super(_Cell, self).__init__(tc, parent) self._tc = self._element = tc - def add_paragraph(self, text='', style=None): + 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. @@ -255,7 +254,7 @@ def text(self): 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): @@ -303,6 +302,7 @@ class _Column(Parented): """ Table column """ + def __init__(self, gridCol, parent): super(_Column, self).__init__(parent) self._gridCol = gridCol @@ -346,6 +346,7 @@ class _Columns(Parented): 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 @@ -389,6 +390,7 @@ class _Row(Parented): """ Table row """ + def __init__(self, tr, parent): super(_Row, self).__init__(parent) self._tr = self._element = tr @@ -445,6 +447,7 @@ class _Rows(Parented): 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 diff --git a/docx/text/font.py b/docx/text/font.py index 162832101..5a88e8af5 100644 --- a/docx/text/font.py +++ b/docx/text/font.py @@ -4,9 +4,7 @@ Font-related proxy objects. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from ..dml.color import ColorFormat from ..shared import ElementProxy @@ -26,22 +24,22 @@ def all_caps(self): """ Read/write. Causes text in this font to appear in capital letters. """ - return self._get_bool_prop('caps') + return self._get_bool_prop("caps") @all_caps.setter def all_caps(self, value): - self._set_bool_prop('caps', 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') + return self._get_bool_prop("b") @bold.setter def bold(self, value): - self._set_bool_prop('b', value) + self._set_bool_prop("b", value) @property def color(self): @@ -58,11 +56,11 @@ def complex_script(self): run to be treated as complex script regardless of their Unicode values. """ - return self._get_bool_prop('cs') + return self._get_bool_prop("cs") @complex_script.setter def complex_script(self, value): - self._set_bool_prop('cs', value) + self._set_bool_prop("cs", value) @property def cs_bold(self): @@ -70,11 +68,11 @@ 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') + return self._get_bool_prop("bCs") @cs_bold.setter def cs_bold(self, value): - self._set_bool_prop('bCs', value) + self._set_bool_prop("bCs", value) @property def cs_italic(self): @@ -82,11 +80,11 @@ 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') + return self._get_bool_prop("iCs") @cs_italic.setter def cs_italic(self, value): - self._set_bool_prop('iCs', value) + self._set_bool_prop("iCs", value) @property def double_strike(self): @@ -94,11 +92,11 @@ 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') + return self._get_bool_prop("dstrike") @double_strike.setter def double_strike(self, value): - self._set_bool_prop('dstrike', value) + self._set_bool_prop("dstrike", value) @property def emboss(self): @@ -106,11 +104,11 @@ 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') + return self._get_bool_prop("emboss") @emboss.setter def emboss(self, value): - self._set_bool_prop('emboss', value) + self._set_bool_prop("emboss", value) @property def hidden(self): @@ -119,11 +117,11 @@ def hidden(self): to be hidden from display, unless applications settings force hidden text to be shown. """ - return self._get_bool_prop('vanish') + return self._get_bool_prop("vanish") @hidden.setter def hidden(self, value): - self._set_bool_prop('vanish', value) + self._set_bool_prop("vanish", value) @property def highlight_color(self): @@ -148,11 +146,11 @@ def italic(self): to appear in italics. |None| indicates the effective value is inherited from the style hierarchy. """ - return self._get_bool_prop('i') + return self._get_bool_prop("i") @italic.setter def italic(self, value): - self._set_bool_prop('i', value) + self._set_bool_prop("i", value) @property def imprint(self): @@ -160,11 +158,11 @@ 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') + return self._get_bool_prop("imprint") @imprint.setter def imprint(self, value): - self._set_bool_prop('imprint', value) + self._set_bool_prop("imprint", value) @property def math(self): @@ -172,11 +170,11 @@ 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') + return self._get_bool_prop("oMath") @math.setter def math(self, value): - self._set_bool_prop('oMath', value) + self._set_bool_prop("oMath", value) @property def name(self): @@ -204,11 +202,11 @@ def no_proof(self): of this run should not report any errors when the document is scanned for spelling and grammar. """ - return self._get_bool_prop('noProof') + return self._get_bool_prop("noProof") @no_proof.setter def no_proof(self, value): - self._set_bool_prop('noProof', value) + self._set_bool_prop("noProof", value) @property def outline(self): @@ -217,11 +215,11 @@ def outline(self): 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') + return self._get_bool_prop("outline") @outline.setter def outline(self, value): - self._set_bool_prop('outline', value) + self._set_bool_prop("outline", value) @property def rtl(self): @@ -229,11 +227,11 @@ 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') + return self._get_bool_prop("rtl") @rtl.setter def rtl(self, value): - self._set_bool_prop('rtl', value) + self._set_bool_prop("rtl", value) @property def shadow(self): @@ -241,11 +239,11 @@ 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') + return self._get_bool_prop("shadow") @shadow.setter def shadow(self, value): - self._set_bool_prop('shadow', value) + self._set_bool_prop("shadow", value) @property def size(self): @@ -280,11 +278,11 @@ def small_caps(self): 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') + return self._get_bool_prop("smallCaps") @small_caps.setter def small_caps(self, value): - self._set_bool_prop('smallCaps', value) + self._set_bool_prop("smallCaps", value) @property def snap_to_grid(self): @@ -293,11 +291,11 @@ def snap_to_grid(self): 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') + return self._get_bool_prop("snapToGrid") @snap_to_grid.setter def snap_to_grid(self, value): - self._set_bool_prop('snapToGrid', value) + self._set_bool_prop("snapToGrid", value) @property def spec_vanish(self): @@ -308,11 +306,11 @@ def spec_vanish(self): 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') + return self._get_bool_prop("specVanish") @spec_vanish.setter def spec_vanish(self, value): - self._set_bool_prop('specVanish', value) + self._set_bool_prop("specVanish", value) @property def strike(self): @@ -321,11 +319,11 @@ def strike(self): to appear with a single horizontal line through the center of the line. """ - return self._get_bool_prop('strike') + return self._get_bool_prop("strike") @strike.setter def strike(self, value): - self._set_bool_prop('strike', value) + self._set_bool_prop("strike", value) @property def subscript(self): @@ -388,11 +386,11 @@ def web_hidden(self): of this run shall be hidden when the document is displayed in web page view. """ - return self._get_bool_prop('webHidden') + return self._get_bool_prop("webHidden") @web_hidden.setter def web_hidden(self, value): - self._set_bool_prop('webHidden', value) + self._set_bool_prop("webHidden", value) def _get_bool_prop(self, name): """ diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index 4fb583b94..d349e5378 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -4,9 +4,7 @@ Paragraph-related proxy types. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from ..enum.style import WD_STYLE_TYPE from .parfmt import ParagraphFormat @@ -18,6 +16,7 @@ class Paragraph(Parented): """ Proxy object wrapping ```` element. """ + def __init__(self, p, parent): super(Paragraph, self).__init__(parent) self._p = self._element = p @@ -107,9 +106,7 @@ def style(self): @style.setter def style(self, style_or_name): - style_id = self.part.get_style_id( - style_or_name, WD_STYLE_TYPE.PARAGRAPH - ) + style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.PARAGRAPH) self._p.style = style_id @property @@ -126,7 +123,7 @@ def text(self): Paragraph-level formatting, such as style, is preserved. All run-level formatting, such as bold or italic, is removed. """ - text = '' + text = "" for run in self.runs: text += run.text return text diff --git a/docx/text/parfmt.py b/docx/text/parfmt.py index 37206729c..6d9215549 100644 --- a/docx/text/parfmt.py +++ b/docx/text/parfmt.py @@ -4,9 +4,7 @@ Paragraph-related proxy types. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +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 @@ -20,7 +18,7 @@ class ParagraphFormat(ElementProxy): control. """ - __slots__ = ('_tab_stops',) + __slots__ = ("_tab_stops",) @property def alignment(self): @@ -153,9 +151,7 @@ def line_spacing_rule(self): 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): diff --git a/docx/text/run.py b/docx/text/run.py index 97d6da7db..88c37fa3c 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -21,6 +21,7 @@ class Run(Parented): 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 @@ -33,12 +34,12 @@ def add_break(self, break_type=WD_BREAK.LINE): *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'), + 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: @@ -133,9 +134,7 @@ def style(self): @style.setter def style(self, style_or_name): - style_id = self.part.get_style_id( - style_or_name, WD_STYLE_TYPE.CHARACTER - ) + style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.CHARACTER) self._r.style = style_id @property @@ -186,6 +185,7 @@ 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 index c22b9bc91..6915285cf 100644 --- a/docx/text/tabstops.py +++ b/docx/text/tabstops.py @@ -4,9 +4,7 @@ Tabstop-related proxy types. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +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 @@ -21,7 +19,7 @@ class TabStops(ElementProxy): directly. """ - __slots__ = ('_pPr') + __slots__ = "_pPr" def __init__(self, element): super(TabStops, self).__init__(element, None) @@ -35,7 +33,7 @@ def __delitem__(self, idx): try: tabs.remove(tabs[idx]) except (AttributeError, IndexError): - raise IndexError('tab index out of range') + raise IndexError("tab index out of range") if len(tabs) == 0: self._pPr.remove(tabs) @@ -46,7 +44,7 @@ def __getitem__(self, idx): """ tabs = self._pPr.tabs if tabs is None: - raise IndexError('TabStops object is empty') + raise IndexError("TabStops object is empty") tab = tabs.tab_lst[idx] return TabStop(tab) @@ -66,8 +64,9 @@ def __len__(self): return 0 return len(tabs.tab_lst) - def add_tab_stop(self, position, alignment=WD_TAB_ALIGNMENT.LEFT, - leader=WD_TAB_LEADER.SPACES): + 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 @@ -94,7 +93,7 @@ class TabStop(ElementProxy): list semantics on its containing |TabStops| object. """ - __slots__ = ('_tab') + __slots__ = "_tab" def __init__(self, element): super(TabStop, self).__init__(element, None) diff --git a/features/environment.py b/features/environment.py index e144106cf..f180da73c 100644 --- a/features/environment.py +++ b/features/environment.py @@ -7,9 +7,7 @@ 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/steps/api.py b/features/steps/api.py index a3325567b..1c66c4c89 100644 --- a/features/steps/api.py +++ b/features/steps/api.py @@ -15,32 +15,35 @@ # 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 +51,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..686da2c99 100644 --- a/features/steps/block.py +++ b/features/steps/block.py @@ -14,12 +14,13 @@ # 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 +28,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 +43,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..4ec633ae3 100644 --- a/features/steps/cell.py +++ b/features/steps/cell.py @@ -15,30 +15,33 @@ # 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 +49,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..e13c5f8fb 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -4,9 +4,7 @@ Gherkin step implementations for core properties-related features. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from datetime import datetime, timedelta @@ -20,19 +18,21 @@ # 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 +40,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 +63,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 +77,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..2aae3e191 100644 --- a/features/steps/document.py +++ b/features/steps/document.py @@ -22,39 +22,40 @@ # 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 +67,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 +126,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 +173,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 +191,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 +214,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 +223,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 +237,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 +254,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 +263,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..ed6e51f25 100644 --- a/features/steps/font.py +++ b/features/steps/font.py @@ -4,9 +4,7 @@ Step implementations for font-related features. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from behave import given, then, when @@ -21,137 +19,137 @@ # 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 +157,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..4f0e3b915 100644 --- a/features/steps/hdrftr.py +++ b/features/steps/hdrftr.py @@ -13,6 +13,7 @@ # 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 +52,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 +80,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 +88,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 +116,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..cd64a7861 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -12,22 +12,19 @@ def absjoin(*paths): thisdir = os.path.split(__file__)[0] -scratch_dir = absjoin(thisdir, '../_scratch') +scratch_dir = absjoin(thisdir, "../_scratch") # scratch output docx file ------------- -saved_docx_path = absjoin(scratch_dir, 'test_out.docx') +saved_docx_path = 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, } @@ -35,11 +32,11 @@ def test_docx(name): """ Return the absolute path to test .docx file with root name *name*. """ - return absjoin(thisdir, 'test_files', '%s.docx' % 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) + return absjoin(thisdir, "test_files", "%s" % name) diff --git a/features/steps/image.py b/features/steps/image.py index ee2a35c17..7f80baa59 100644 --- a/features/steps/image.py +++ b/features/steps/image.py @@ -15,59 +15,67 @@ # 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..2a20fd087 100644 --- a/features/steps/numbering.py +++ b/features/steps/numbering.py @@ -13,14 +13,16 @@ # 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 +30,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/paragraph.py b/features/steps/paragraph.py index 3f47df9f1..d372983a0 100644 --- a/features/steps/paragraph.py +++ b/features/steps/paragraph.py @@ -15,83 +15,86 @@ # given =================================================== -@given('a document containing three paragraphs') + +@given("a document containing three paragraphs") def given_a_document_containing_three_paragraphs(context): document = Document() - document.add_paragraph('foo') - document.add_paragraph('bar') - document.add_paragraph('baz') + document.add_paragraph("foo") + document.add_paragraph("bar") + document.add_paragraph("baz") context.document = document -@given('a paragraph having {align_type} alignment') +@given("a paragraph having {align_type} alignment") def given_a_paragraph_align_type_alignment(context, align_type): paragraph_idx = { - 'inherited': 0, - 'left': 1, - 'center': 2, - 'right': 3, - 'justified': 4, + "inherited": 0, + "left": 1, + "center": 2, + "right": 3, + "justified": 4, }[align_type] - document = Document(test_docx('par-alignment')) + document = Document(test_docx("par-alignment")) context.paragraph = document.paragraphs[paragraph_idx] -@given('a paragraph having {style_state} style') +@given("a paragraph having {style_state} style") def given_a_paragraph_having_style(context, style_state): paragraph_idx = { - 'no specified': 0, - 'a missing': 1, - 'Heading 1': 2, - 'Body Text': 3, + "no specified": 0, + "a missing": 1, + "Heading 1": 2, + "Body Text": 3, }[style_state] - document = context.document = Document(test_docx('par-known-styles')) + document = context.document = Document(test_docx("par-known-styles")) context.paragraph = document.paragraphs[paragraph_idx] -@given('a paragraph with content and formatting') +@given("a paragraph with content and formatting") def given_a_paragraph_with_content_and_formatting(context): - document = Document(test_docx('par-known-paragraphs')) + document = Document(test_docx("par-known-paragraphs")) context.paragraph = document.paragraphs[0] # when ==================================================== -@when('I add a run to the paragraph') + +@when("I add a run to the paragraph") def when_add_new_run_to_paragraph(context): context.run = context.p.add_run() -@when('I assign a {style_type} to paragraph.style') +@when("I assign a {style_type} to paragraph.style") def when_I_assign_a_style_type_to_paragraph_style(context, style_type): paragraph = context.paragraph - style = context.style = context.document.styles['Heading 1'] + style = context.style = context.document.styles["Heading 1"] style_spec = { - 'style object': style, - 'style name': 'Heading 1', + "style object": style, + "style name": "Heading 1", }[style_type] paragraph.style = style_spec -@when('I clear the paragraph content') +@when("I clear the paragraph content") def when_I_clear_the_paragraph_content(context): context.paragraph.clear() -@when('I insert a paragraph above the second paragraph') +@when("I insert a paragraph above the second paragraph") def when_I_insert_a_paragraph_above_the_second_paragraph(context): paragraph = context.document.paragraphs[1] - paragraph.insert_paragraph_before('foobar', 'Heading1') + paragraph.insert_paragraph_before("foobar", "Heading1") -@when('I set the paragraph text') +@when("I set the paragraph text") def when_I_set_the_paragraph_text(context): - context.paragraph.text = 'bar\tfoo\r' + context.paragraph.text = "bar\tfoo\r" # then ===================================================== -@then('paragraph.paragraph_format is its ParagraphFormat object') + +@then("paragraph.paragraph_format is its ParagraphFormat object") def then_paragraph_paragraph_format_is_its_parfmt_object(context): paragraph = context.paragraph paragraph_format = paragraph.paragraph_format @@ -99,24 +102,24 @@ def then_paragraph_paragraph_format_is_its_parfmt_object(context): assert paragraph_format.element is paragraph._element -@then('paragraph.style is {value_key}') +@then("paragraph.style is {value_key}") def then_paragraph_style_is_value(context, value_key): styles = context.document.styles expected_value = { - 'Normal': styles['Normal'], - 'Heading 1': styles['Heading 1'], - 'Body Text': styles['Body Text'], + "Normal": styles["Normal"], + "Heading 1": styles["Heading 1"], + "Body Text": styles["Body Text"], }[value_key] paragraph = context.paragraph assert paragraph.style == expected_value -@then('the document contains four paragraphs') +@then("the document contains four paragraphs") def then_the_document_contains_four_paragraphs(context): assert len(context.document.paragraphs) == 4 -@then('the document contains the text I added') +@then("the document contains the text I added") def then_document_contains_text_I_added(context): document = Document(saved_docx_path) paragraphs = document.paragraphs @@ -125,46 +128,46 @@ def then_document_contains_text_I_added(context): assert r.text == test_text -@then('the paragraph alignment property value is {align_value}') +@then("the paragraph alignment property value is {align_value}") def then_the_paragraph_alignment_prop_value_is_value(context, align_value): expected_value = { - 'None': None, - 'WD_ALIGN_PARAGRAPH.LEFT': WD_ALIGN_PARAGRAPH.LEFT, - 'WD_ALIGN_PARAGRAPH.CENTER': WD_ALIGN_PARAGRAPH.CENTER, - 'WD_ALIGN_PARAGRAPH.RIGHT': WD_ALIGN_PARAGRAPH.RIGHT, + "None": None, + "WD_ALIGN_PARAGRAPH.LEFT": WD_ALIGN_PARAGRAPH.LEFT, + "WD_ALIGN_PARAGRAPH.CENTER": WD_ALIGN_PARAGRAPH.CENTER, + "WD_ALIGN_PARAGRAPH.RIGHT": WD_ALIGN_PARAGRAPH.RIGHT, }[align_value] assert context.paragraph.alignment == expected_value -@then('the paragraph formatting is preserved') +@then("the paragraph formatting is preserved") def then_the_paragraph_formatting_is_preserved(context): paragraph = context.paragraph - assert paragraph.style.name == 'Heading 1' + assert paragraph.style.name == "Heading 1" -@then('the paragraph has no content') +@then("the paragraph has no content") def then_the_paragraph_has_no_content(context): - assert context.paragraph.text == '' + assert context.paragraph.text == "" -@then('the paragraph has the style I set') +@then("the paragraph has the style I set") def then_the_paragraph_has_the_style_I_set(context): paragraph, expected_style = context.paragraph, context.style assert paragraph.style == expected_style -@then('the paragraph has the text I set') +@then("the paragraph has the text I set") def then_the_paragraph_has_the_text_I_set(context): - assert context.paragraph.text == 'bar\tfoo\n' + assert context.paragraph.text == "bar\tfoo\n" -@then('the style of the second paragraph matches the style I set') +@then("the style of the second paragraph matches the style I set") def then_the_style_of_the_second_paragraph_matches_the_style_I_set(context): second_paragraph = context.document.paragraphs[1] - assert second_paragraph.style.name == 'Heading 1' + assert second_paragraph.style.name == "Heading 1" -@then('the text of the second paragraph matches the text I set') +@then("the text of the second paragraph matches the text I set") def then_the_text_of_the_second_paragraph_matches_the_text_I_set(context): second_paragraph = context.document.paragraphs[1] - assert second_paragraph.text == 'foobar' + assert second_paragraph.text == "foobar" diff --git a/features/steps/parfmt.py b/features/steps/parfmt.py index f3203f7e5..197cf1626 100644 --- a/features/steps/parfmt.py +++ b/features/steps/parfmt.py @@ -4,9 +4,7 @@ Step implementations for paragraph format-related features. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from behave import given, then, when @@ -20,156 +18,157 @@ # given =================================================== -@given('a paragraph format') + +@given("a paragraph format") def given_a_paragraph_format(context): - document = Document(test_docx('tab-stops')) + document = Document(test_docx("tab-stops")) context.paragraph_format = document.paragraphs[0].paragraph_format -@given('a paragraph format having {prop_name} set {setting}') +@given("a paragraph format having {prop_name} set {setting}") def given_a_paragraph_format_having_prop_set(context, prop_name, setting): style_name = { - 'to inherit': 'Normal', - 'On': 'Base', - 'Off': 'Citation', + "to inherit": "Normal", + "On": "Base", + "Off": "Citation", }[setting] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.paragraph_format = document.styles[style_name].paragraph_format -@given('a paragraph format having {setting} line spacing') +@given("a paragraph format having {setting} line spacing") def given_a_paragraph_format_having_setting_line_spacing(context, setting): style_name = { - 'inherited': 'Normal', - '14 pt': 'Base', - 'double': 'Citation', + "inherited": "Normal", + "14 pt": "Base", + "double": "Citation", }[setting] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.paragraph_format = document.styles[style_name].paragraph_format -@given('a paragraph format having {setting} space {side}') +@given("a paragraph format having {setting} space {side}") def given_a_paragraph_format_having_setting_spacing(context, setting, side): - style_name = 'Normal' if setting == 'inherited' else 'Base' - document = Document(test_docx('sty-known-styles')) + style_name = "Normal" if setting == "inherited" else "Base" + document = Document(test_docx("sty-known-styles")) context.paragraph_format = document.styles[style_name].paragraph_format -@given('a paragraph format having {type} alignment') +@given("a paragraph format having {type} alignment") def given_a_paragraph_format_having_align_type_alignment(context, type): style_name = { - 'inherited': 'Normal', - 'center': 'Base', - 'right': 'Citation', + "inherited": "Normal", + "center": "Base", + "right": "Citation", }[type] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.paragraph_format = document.styles[style_name].paragraph_format -@given('a paragraph format having {type} indent of {value}') +@given("a paragraph format having {type} indent of {value}") def given_a_paragraph_format_having_type_indent_value(context, type, value): style_name = { - 'inherit': 'Normal', - '18 pt': 'Base', - '17.3 pt': 'Base', - '-17.3 pt': 'Citation', - '46.1 pt': 'Citation', + "inherit": "Normal", + "18 pt": "Base", + "17.3 pt": "Base", + "-17.3 pt": "Citation", + "46.1 pt": "Citation", }[value] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.paragraph_format = document.styles[style_name].paragraph_format # when ==================================================== -@when('I assign {value} to paragraph_format.line_spacing') + +@when("I assign {value} to paragraph_format.line_spacing") def when_I_assign_value_to_paragraph_format_line_spacing(context, value): new_value = { - 'Pt(14)': Pt(14), - '2': 2, + "Pt(14)": Pt(14), + "2": 2, }.get(value) new_value = float(value) if new_value is None else new_value context.paragraph_format.line_spacing = new_value -@when('I assign {value} to paragraph_format.line_spacing_rule') +@when("I assign {value} to paragraph_format.line_spacing_rule") def when_I_assign_value_to_paragraph_format_line_rule(context, value): new_value = { - 'None': None, - 'WD_LINE_SPACING.EXACTLY': WD_LINE_SPACING.EXACTLY, - 'WD_LINE_SPACING.MULTIPLE': WD_LINE_SPACING.MULTIPLE, - 'WD_LINE_SPACING.SINGLE': WD_LINE_SPACING.SINGLE, - 'WD_LINE_SPACING.DOUBLE': WD_LINE_SPACING.DOUBLE, - 'WD_LINE_SPACING.AT_LEAST': WD_LINE_SPACING.AT_LEAST, - 'WD_LINE_SPACING.ONE_POINT_FIVE': WD_LINE_SPACING.ONE_POINT_FIVE, + "None": None, + "WD_LINE_SPACING.EXACTLY": WD_LINE_SPACING.EXACTLY, + "WD_LINE_SPACING.MULTIPLE": WD_LINE_SPACING.MULTIPLE, + "WD_LINE_SPACING.SINGLE": WD_LINE_SPACING.SINGLE, + "WD_LINE_SPACING.DOUBLE": WD_LINE_SPACING.DOUBLE, + "WD_LINE_SPACING.AT_LEAST": WD_LINE_SPACING.AT_LEAST, + "WD_LINE_SPACING.ONE_POINT_FIVE": WD_LINE_SPACING.ONE_POINT_FIVE, }[value] paragraph_format = context.paragraph_format paragraph_format.line_spacing_rule = new_value -@when('I assign {value} to paragraph_format.alignment') +@when("I assign {value} to paragraph_format.alignment") def when_I_assign_value_to_paragraph_format_alignment(context, value): new_value = { - 'None': None, - 'WD_ALIGN_PARAGRAPH.CENTER': WD_ALIGN_PARAGRAPH.CENTER, - 'WD_ALIGN_PARAGRAPH.RIGHT': WD_ALIGN_PARAGRAPH.RIGHT, + "None": None, + "WD_ALIGN_PARAGRAPH.CENTER": WD_ALIGN_PARAGRAPH.CENTER, + "WD_ALIGN_PARAGRAPH.RIGHT": WD_ALIGN_PARAGRAPH.RIGHT, }[value] paragraph_format = context.paragraph_format paragraph_format.alignment = new_value -@when('I assign {value} to paragraph_format.space_{side}') +@when("I assign {value} to paragraph_format.space_{side}") def when_I_assign_value_to_paragraph_format_space(context, value, side): paragraph_format = context.paragraph_format - prop_name = 'space_%s' % side + prop_name = "space_%s" % side new_value = { - 'None': None, - 'Pt(12)': Pt(12), - 'Pt(18)': Pt(18), + "None": None, + "Pt(12)": Pt(12), + "Pt(18)": Pt(18), }[value] setattr(paragraph_format, prop_name, new_value) -@when('I assign {value} to paragraph_format.{type_}_indent') +@when("I assign {value} to paragraph_format.{type_}_indent") def when_I_assign_value_to_paragraph_format_indent(context, value, type_): paragraph_format = context.paragraph_format - prop_name = '%s_indent' % type_ - value = None if value == 'None' else Pt(float(value.split()[0])) + prop_name = "%s_indent" % type_ + value = None if value == "None" else Pt(float(value.split()[0])) setattr(paragraph_format, prop_name, value) -@when('I assign {value} to paragraph_format.{prop_name}') +@when("I assign {value} to paragraph_format.{prop_name}") def when_I_assign_value_to_paragraph_format_prop(context, value, prop_name): paragraph_format = context.paragraph_format - value = {'None': None, 'True': True, 'False': False}[value] + value = {"None": None, "True": True, "False": False}[value] setattr(paragraph_format, prop_name, value) # then ===================================================== -@then('paragraph_format.tab_stops is a TabStops object') + +@then("paragraph_format.tab_stops is a TabStops object") def then_paragraph_format_tab_stops_is_a_tabstops_object(context): tab_stops = context.paragraph_format.tab_stops assert isinstance(tab_stops, TabStops) -@then('paragraph_format.alignment is {value}') +@then("paragraph_format.alignment is {value}") def then_paragraph_format_alignment_is_value(context, value): expected_value = { - 'None': None, - 'WD_ALIGN_PARAGRAPH.LEFT': WD_ALIGN_PARAGRAPH.LEFT, - 'WD_ALIGN_PARAGRAPH.CENTER': WD_ALIGN_PARAGRAPH.CENTER, - 'WD_ALIGN_PARAGRAPH.RIGHT': WD_ALIGN_PARAGRAPH.RIGHT, + "None": None, + "WD_ALIGN_PARAGRAPH.LEFT": WD_ALIGN_PARAGRAPH.LEFT, + "WD_ALIGN_PARAGRAPH.CENTER": WD_ALIGN_PARAGRAPH.CENTER, + "WD_ALIGN_PARAGRAPH.RIGHT": WD_ALIGN_PARAGRAPH.RIGHT, }[value] paragraph_format = context.paragraph_format assert paragraph_format.alignment == expected_value -@then('paragraph_format.line_spacing is {value}') +@then("paragraph_format.line_spacing is {value}") def then_paragraph_format_line_spacing_is_value(context, value): expected_value = ( - None if value == 'None' else - float(value) if '.' in value else - int(value) + None if value == "None" else float(value) if "." in value else int(value) ) paragraph_format = context.paragraph_format @@ -179,42 +178,42 @@ def then_paragraph_format_line_spacing_is_value(context, value): assert abs(paragraph_format.line_spacing - expected_value) < 0.001 -@then('paragraph_format.line_spacing_rule is {value}') +@then("paragraph_format.line_spacing_rule is {value}") def then_paragraph_format_line_spacing_rule_is_value(context, value): expected_value = { - 'None': None, - 'WD_LINE_SPACING.EXACTLY': WD_LINE_SPACING.EXACTLY, - 'WD_LINE_SPACING.MULTIPLE': WD_LINE_SPACING.MULTIPLE, - 'WD_LINE_SPACING.SINGLE': WD_LINE_SPACING.SINGLE, - 'WD_LINE_SPACING.DOUBLE': WD_LINE_SPACING.DOUBLE, - 'WD_LINE_SPACING.AT_LEAST': WD_LINE_SPACING.AT_LEAST, - 'WD_LINE_SPACING.ONE_POINT_FIVE': WD_LINE_SPACING.ONE_POINT_FIVE, + "None": None, + "WD_LINE_SPACING.EXACTLY": WD_LINE_SPACING.EXACTLY, + "WD_LINE_SPACING.MULTIPLE": WD_LINE_SPACING.MULTIPLE, + "WD_LINE_SPACING.SINGLE": WD_LINE_SPACING.SINGLE, + "WD_LINE_SPACING.DOUBLE": WD_LINE_SPACING.DOUBLE, + "WD_LINE_SPACING.AT_LEAST": WD_LINE_SPACING.AT_LEAST, + "WD_LINE_SPACING.ONE_POINT_FIVE": WD_LINE_SPACING.ONE_POINT_FIVE, }[value] paragraph_format = context.paragraph_format assert paragraph_format.line_spacing_rule == expected_value -@then('paragraph_format.space_{side} is {value}') +@then("paragraph_format.space_{side} is {value}") def then_paragraph_format_space_side_is_value(context, side, value): - expected_value = None if value == 'None' else int(value) - prop_name = 'space_%s' % side + expected_value = None if value == "None" else int(value) + prop_name = "space_%s" % side paragraph_format = context.paragraph_format actual_value = getattr(paragraph_format, prop_name) assert actual_value == expected_value -@then('paragraph_format.{type_}_indent is {value}') +@then("paragraph_format.{type_}_indent is {value}") def then_paragraph_format_type_indent_is_value(context, type_, value): - expected_value = None if value == 'None' else int(value) - prop_name = '%s_indent' % type_ + expected_value = None if value == "None" else int(value) + prop_name = "%s_indent" % type_ paragraph_format = context.paragraph_format actual_value = getattr(paragraph_format, prop_name) assert actual_value == expected_value -@then('paragraph_format.{prop_name} is {value}') +@then("paragraph_format.{prop_name} is {value}") def then_paragraph_format_prop_name_is_value(context, prop_name, value): - expected_value = {'None': None, 'True': True, 'False': False}[value] + expected_value = {"None": None, "True": True, "False": False}[value] paragraph_format = context.paragraph_format actual_value = getattr(paragraph_format, prop_name) assert actual_value == expected_value diff --git a/features/steps/section.py b/features/steps/section.py index 91209eac1..1eae8017c 100644 --- a/features/steps/section.py +++ b/features/steps/section.py @@ -18,6 +18,7 @@ # given ==================================================== + @given("a Section object as section") def given_a_Section_object_as_section(context): context.section = Document(test_docx("sct-section-props")).sections[-1] @@ -29,105 +30,104 @@ def given_a_Section_object_with_or_without_first_page_header(context, with_or_wi context.section = Document(test_docx("sct-first-page-hdrftr")).sections[section_idx] -@given('a section collection containing 3 sections') +@given("a section collection containing 3 sections") def given_a_section_collection_containing_3_sections(context): - document = Document(test_docx('doc-access-sections')) + document = Document(test_docx("doc-access-sections")) context.sections = document.sections -@given('a section having known page dimension') +@given("a section having known page dimension") def given_a_section_having_known_page_dimension(context): - document = Document(test_docx('sct-section-props')) + document = Document(test_docx("sct-section-props")) context.section = document.sections[-1] -@given('a section having known page margins') +@given("a section having known page margins") def given_a_section_having_known_page_margins(context): - document = Document(test_docx('sct-section-props')) + document = Document(test_docx("sct-section-props")) context.section = document.sections[0] -@given('a section having start type {start_type}') +@given("a section having start type {start_type}") def given_a_section_having_start_type(context, start_type): section_idx = { - 'CONTINUOUS': 0, - 'NEW_PAGE': 1, - 'ODD_PAGE': 2, - 'EVEN_PAGE': 3, - 'NEW_COLUMN': 4, + "CONTINUOUS": 0, + "NEW_PAGE": 1, + "ODD_PAGE": 2, + "EVEN_PAGE": 3, + "NEW_COLUMN": 4, }[start_type] - document = Document(test_docx('sct-section-props')) + document = Document(test_docx("sct-section-props")) context.section = document.sections[section_idx] -@given('a section known to have {orientation} orientation') +@given("a section known to have {orientation} orientation") def given_a_section_having_known_orientation(context, orientation): - section_idx = { - 'landscape': 0, - 'portrait': 1 - }[orientation] - document = Document(test_docx('sct-section-props')) + section_idx = {"landscape": 0, "portrait": 1}[orientation] + document = Document(test_docx("sct-section-props")) context.section = document.sections[section_idx] # when ===================================================== + @when("I assign {bool_val} to section.different_first_page_header_footer") def when_I_assign_value_to_section_different_first_page_hdrftr(context, bool_val): context.section.different_first_page_header_footer = eval(bool_val) -@when('I set the {margin_side} margin to {inches} inches') +@when("I set the {margin_side} margin to {inches} inches") def when_I_set_the_margin_side_length(context, margin_side, inches): prop_name = { - 'left': 'left_margin', - 'right': 'right_margin', - 'top': 'top_margin', - 'bottom': 'bottom_margin', - 'gutter': 'gutter', - 'header': 'header_distance', - 'footer': 'footer_distance', + "left": "left_margin", + "right": "right_margin", + "top": "top_margin", + "bottom": "bottom_margin", + "gutter": "gutter", + "header": "header_distance", + "footer": "footer_distance", }[margin_side] new_value = Inches(float(inches)) setattr(context.section, prop_name, new_value) -@when('I set the section orientation to {orientation}') +@when("I set the section orientation to {orientation}") def when_I_set_the_section_orientation(context, orientation): new_orientation = { - 'WD_ORIENT.PORTRAIT': WD_ORIENT.PORTRAIT, - 'WD_ORIENT.LANDSCAPE': WD_ORIENT.LANDSCAPE, - 'None': None, + "WD_ORIENT.PORTRAIT": WD_ORIENT.PORTRAIT, + "WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE, + "None": None, }[orientation] context.section.orientation = new_orientation -@when('I set the section page height to {y} inches') +@when("I set the section page height to {y} inches") def when_I_set_the_section_page_height_to_y_inches(context, y): context.section.page_height = Inches(float(y)) -@when('I set the section page width to {x} inches') +@when("I set the section page width to {x} inches") def when_I_set_the_section_page_width_to_x_inches(context, x): context.section.page_width = Inches(float(x)) -@when('I set the section start type to {start_type}') +@when("I set the section start type to {start_type}") def when_I_set_the_section_start_type_to_start_type(context, start_type): new_start_type = { - 'None': None, - 'CONTINUOUS': WD_SECTION.CONTINUOUS, - 'EVEN_PAGE': WD_SECTION.EVEN_PAGE, - 'NEW_COLUMN': WD_SECTION.NEW_COLUMN, - 'NEW_PAGE': WD_SECTION.NEW_PAGE, - 'ODD_PAGE': WD_SECTION.ODD_PAGE, + "None": None, + "CONTINUOUS": WD_SECTION.CONTINUOUS, + "EVEN_PAGE": WD_SECTION.EVEN_PAGE, + "NEW_COLUMN": WD_SECTION.NEW_COLUMN, + "NEW_PAGE": WD_SECTION.NEW_PAGE, + "ODD_PAGE": WD_SECTION.ODD_PAGE, }[start_type] context.section.start_type = new_start_type # then ===================================================== -@then('I can access a section by index') + +@then("I can access a section by index") def then_I_can_access_a_section_by_index(context): sections = context.sections for idx in range(3): @@ -135,7 +135,7 @@ def then_I_can_access_a_section_by_index(context): assert isinstance(section, Section) -@then('I can iterate over the sections') +@then("I can iterate over the sections") def then_I_can_iterate_over_the_sections(context): sections = context.sections actual_count = 0 @@ -145,12 +145,10 @@ def then_I_can_iterate_over_the_sections(context): assert actual_count == 3 -@then('len(sections) is 3') +@then("len(sections) is 3") def then_len_sections_is_3(context): sections = context.sections - assert len(sections) == 3, ( - 'expected len(sections) of 3, got %s' % len(sections) - ) + assert len(sections) == 3, "expected len(sections) of 3, got %s" % len(sections) @then("section.different_first_page_header_footer is {bool_val}") @@ -208,53 +206,54 @@ def then_section_header_is_a_Header_object(context): def then_section_hdrftr_prop_is_linked_to_previous_is_True(context, propname): actual = getattr(context.section, propname).is_linked_to_previous expected = True - assert actual == expected, ( - "section.%s.is_linked_to_previous is %s" % (propname, actual) + assert actual == expected, "section.%s.is_linked_to_previous is %s" % ( + propname, + actual, ) -@then('the reported {margin_side} margin is {inches} inches') +@then("the reported {margin_side} margin is {inches} inches") def then_the_reported_margin_is_inches(context, margin_side, inches): prop_name = { - 'left': 'left_margin', - 'right': 'right_margin', - 'top': 'top_margin', - 'bottom': 'bottom_margin', - 'gutter': 'gutter', - 'header': 'header_distance', - 'footer': 'footer_distance', + "left": "left_margin", + "right": "right_margin", + "top": "top_margin", + "bottom": "bottom_margin", + "gutter": "gutter", + "header": "header_distance", + "footer": "footer_distance", }[margin_side] expected_value = Inches(float(inches)) actual_value = getattr(context.section, prop_name) assert actual_value == expected_value -@then('the reported page orientation is {orientation}') +@then("the reported page orientation is {orientation}") def then_the_reported_page_orientation_is_orientation(context, orientation): expected_value = { - 'WD_ORIENT.LANDSCAPE': WD_ORIENT.LANDSCAPE, - 'WD_ORIENT.PORTRAIT': WD_ORIENT.PORTRAIT, + "WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE, + "WD_ORIENT.PORTRAIT": WD_ORIENT.PORTRAIT, }[orientation] assert context.section.orientation == expected_value -@then('the reported page width is {x} inches') +@then("the reported page width is {x} inches") def then_the_reported_page_width_is_width(context, x): assert context.section.page_width == Inches(float(x)) -@then('the reported page height is {y} inches') +@then("the reported page height is {y} inches") def then_the_reported_page_height_is_11_inches(context, y): assert context.section.page_height == Inches(float(y)) -@then('the reported section start type is {start_type}') +@then("the reported section start type is {start_type}") def then_the_reported_section_start_type_is_type(context, start_type): expected_start_type = { - 'CONTINUOUS': WD_SECTION.CONTINUOUS, - 'EVEN_PAGE': WD_SECTION.EVEN_PAGE, - 'NEW_COLUMN': WD_SECTION.NEW_COLUMN, - 'NEW_PAGE': WD_SECTION.NEW_PAGE, - 'ODD_PAGE': WD_SECTION.ODD_PAGE, + "CONTINUOUS": WD_SECTION.CONTINUOUS, + "EVEN_PAGE": WD_SECTION.EVEN_PAGE, + "NEW_COLUMN": WD_SECTION.NEW_COLUMN, + "NEW_PAGE": WD_SECTION.NEW_PAGE, + "ODD_PAGE": WD_SECTION.ODD_PAGE, }[start_type] assert context.section.start_type == expected_start_type diff --git a/features/steps/settings.py b/features/steps/settings.py index f770f8c19..9c6d94e2d 100644 --- a/features/steps/settings.py +++ b/features/steps/settings.py @@ -14,26 +14,28 @@ # given ==================================================== -@given('a document having a settings part') + +@given("a document having a settings part") def given_a_document_having_a_settings_part(context): - context.document = Document(test_docx('doc-word-default-blank')) + context.document = Document(test_docx("doc-word-default-blank")) -@given('a document having no settings part') +@given("a document having no settings part") def given_a_document_having_no_settings_part(context): - context.document = Document(test_docx('set-no-settings-part')) + context.document = Document(test_docx("set-no-settings-part")) @given("a Settings object {with_or_without} odd and even page headers as settings") def given_a_Settings_object_with_or_without_odd_and_even_hdrs(context, with_or_without): - testfile_name = { - "with": "doc-odd-even-hdrs", "without": "sct-section-props" - }[with_or_without] + testfile_name = {"with": "doc-odd-even-hdrs", "without": "sct-section-props"}[ + with_or_without + ] context.settings = Document(test_docx(testfile_name)).settings # when ===================================================== + @when("I assign {bool_val} to settings.odd_and_even_pages_header_footer") def when_I_assign_value_to_settings_odd_and_even_pages_header_footer(context, bool_val): context.settings.odd_and_even_pages_header_footer = eval(bool_val) @@ -41,7 +43,8 @@ def when_I_assign_value_to_settings_odd_and_even_pages_header_footer(context, bo # then ===================================================== -@then('document.settings is a Settings object') + +@then("document.settings is a Settings object") def then_document_settings_is_a_Settings_object(context): document = context.document assert type(document.settings) is Settings diff --git a/features/steps/shape.py b/features/steps/shape.py index 1a8d0c60c..b35b385ad 100644 --- a/features/steps/shape.py +++ b/features/steps/shape.py @@ -20,36 +20,38 @@ # given =================================================== -@given('an inline shape collection containing five shapes') + +@given("an inline shape collection containing five shapes") def given_an_inline_shape_collection_containing_five_shapes(context): - docx_path = test_docx('shp-inline-shape-access') + docx_path = test_docx("shp-inline-shape-access") document = Document(docx_path) context.inline_shapes = document.inline_shapes -@given('an inline shape of known dimensions') +@given("an inline shape of known dimensions") def given_inline_shape_of_known_dimensions(context): - document = Document(test_docx('shp-inline-shape-access')) + document = Document(test_docx("shp-inline-shape-access")) context.inline_shape = document.inline_shapes[0] -@given('an inline shape known to be {shp_of_type}') +@given("an inline shape known to be {shp_of_type}") def given_inline_shape_known_to_be_shape_of_type(context, shp_of_type): inline_shape_idx = { - 'an embedded picture': 0, - 'a linked picture': 1, - 'a link+embed picture': 2, - 'a smart art diagram': 3, - 'a chart': 4, + "an embedded picture": 0, + "a linked picture": 1, + "a link+embed picture": 2, + "a smart art diagram": 3, + "a chart": 4, }[shp_of_type] - docx_path = test_docx('shp-inline-shape-access') + docx_path = test_docx("shp-inline-shape-access") document = Document(docx_path) context.inline_shape = document.inline_shapes[inline_shape_idx] # when ===================================================== -@when('I change the dimensions of the inline shape') + +@when("I change the dimensions of the inline shape") def when_change_dimensions_of_inline_shape(context): inline_shape = context.inline_shape inline_shape.width = Inches(1) @@ -58,7 +60,8 @@ def when_change_dimensions_of_inline_shape(context): # then ===================================================== -@then('I can access each inline shape by index') + +@then("I can access each inline shape by index") def then_can_access_each_inline_shape_by_index(context): inline_shapes = context.inline_shapes for idx in range(2): @@ -66,7 +69,7 @@ def then_can_access_each_inline_shape_by_index(context): assert isinstance(inline_shape, InlineShape) -@then('I can iterate over the inline shape collection') +@then("I can iterate over the inline shape collection") def then_can_iterate_over_inline_shape_collection(context): inline_shapes = context.inline_shapes shape_count = 0 @@ -74,38 +77,39 @@ def then_can_iterate_over_inline_shape_collection(context): shape_count += 1 assert isinstance(inline_shape, InlineShape) expected_count = 5 - assert shape_count == expected_count, ( - 'expected %d, got %d' % (expected_count, shape_count) + assert shape_count == expected_count, "expected %d, got %d" % ( + expected_count, + shape_count, ) -@then('its inline shape type is {shape_type}') +@then("its inline shape type is {shape_type}") def then_inline_shape_type_is_shape_type(context, shape_type): expected_value = { - 'WD_INLINE_SHAPE.CHART': WD_INLINE_SHAPE.CHART, - 'WD_INLINE_SHAPE.LINKED_PICTURE': WD_INLINE_SHAPE.LINKED_PICTURE, - 'WD_INLINE_SHAPE.PICTURE': WD_INLINE_SHAPE.PICTURE, - 'WD_INLINE_SHAPE.SMART_ART': WD_INLINE_SHAPE.SMART_ART, + "WD_INLINE_SHAPE.CHART": WD_INLINE_SHAPE.CHART, + "WD_INLINE_SHAPE.LINKED_PICTURE": WD_INLINE_SHAPE.LINKED_PICTURE, + "WD_INLINE_SHAPE.PICTURE": WD_INLINE_SHAPE.PICTURE, + "WD_INLINE_SHAPE.SMART_ART": WD_INLINE_SHAPE.SMART_ART, }[shape_type] inline_shape = context.inline_shape assert inline_shape.type == expected_value -@then('the dimensions of the inline shape match the known values') +@then("the dimensions of the inline shape match the known values") def then_dimensions_of_inline_shape_match_known_values(context): inline_shape = context.inline_shape - assert inline_shape.width == 1778000, 'got %s' % inline_shape.width - assert inline_shape.height == 711200, 'got %s' % inline_shape.height + assert inline_shape.width == 1778000, "got %s" % inline_shape.width + assert inline_shape.height == 711200, "got %s" % inline_shape.height -@then('the dimensions of the inline shape match the new values') +@then("the dimensions of the inline shape match the new values") def then_dimensions_of_inline_shape_match_new_values(context): inline_shape = context.inline_shape - assert inline_shape.width == 914400, 'got %s' % inline_shape.width - assert inline_shape.height == 457200, 'got %s' % inline_shape.height + assert inline_shape.width == 914400, "got %s" % inline_shape.width + assert inline_shape.height == 457200, "got %s" % inline_shape.height -@then('the document contains the inline picture') +@then("the document contains the inline picture") def then_the_document_contains_the_inline_picture(context): document = context.document picture_shape = document.inline_shapes[0] @@ -113,42 +117,41 @@ def then_the_document_contains_the_inline_picture(context): rId = blip.embed image_part = document.part.related_parts[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 length of the inline shape collection is 5') +@then("the length of the inline shape collection is 5") def then_len_of_inline_shape_collection_is_5(context): inline_shapes = context.inline_shapes shape_count = len(inline_shapes) - assert shape_count == 5, 'got %s' % shape_count + assert shape_count == 5, "got %s" % shape_count -@then('the picture has its native width and height') +@then("the picture has its native width and height") def then_picture_has_native_width_and_height(context): picture = context.picture - assert picture.width == 1905000, 'got %d' % picture.width - assert picture.height == 2717800, 'got %d' % picture.height + assert picture.width == 1905000, "got %d" % picture.width + assert picture.height == 2717800, "got %d" % picture.height -@then('picture.height is {inches} inches') +@then("picture.height is {inches} inches") def then_picture_height_is_value(context, inches): expected_value = { - '2.14': 1956816, - '2.5': 2286000, + "2.14": 1956816, + "2.5": 2286000, }[inches] picture = context.picture - assert picture.height == expected_value, 'got %d' % picture.height + assert picture.height == expected_value, "got %d" % picture.height -@then('picture.width is {inches} inches') +@then("picture.width is {inches} inches") def then_picture_width_is_value(context, inches): expected_value = { - '1.05': 961402, - '1.75': 1600200, + "1.05": 961402, + "1.75": 1600200, }[inches] picture = context.picture - assert picture.width == expected_value, 'got %d' % picture.width + assert picture.width == expected_value, "got %d" % picture.width diff --git a/features/steps/shared.py b/features/steps/shared.py index 5e82e24a6..c5e2881fe 100644 --- a/features/steps/shared.py +++ b/features/steps/shared.py @@ -15,14 +15,16 @@ # given =================================================== -@given('a document') + +@given("a document") def given_a_document(context): context.document = Document() # when ==================================================== -@when('I save the document') + +@when("I save the document") def when_save_document(context): if os.path.isfile(saved_docx_path): os.remove(saved_docx_path) diff --git a/features/steps/styles.py b/features/steps/styles.py index 01927bcd7..c4b34be85 100644 --- a/features/steps/styles.py +++ b/features/steps/styles.py @@ -16,266 +16,266 @@ from helpers import bool_vals, test_docx, tri_state_vals style_types = { - 'WD_STYLE_TYPE.CHARACTER': WD_STYLE_TYPE.CHARACTER, - 'WD_STYLE_TYPE.PARAGRAPH': WD_STYLE_TYPE.PARAGRAPH, - 'WD_STYLE_TYPE.LIST': WD_STYLE_TYPE.LIST, - 'WD_STYLE_TYPE.TABLE': WD_STYLE_TYPE.TABLE, + "WD_STYLE_TYPE.CHARACTER": WD_STYLE_TYPE.CHARACTER, + "WD_STYLE_TYPE.PARAGRAPH": WD_STYLE_TYPE.PARAGRAPH, + "WD_STYLE_TYPE.LIST": WD_STYLE_TYPE.LIST, + "WD_STYLE_TYPE.TABLE": WD_STYLE_TYPE.TABLE, } # given =================================================== -@given('a document having a styles part') + +@given("a document having a styles part") def given_a_document_having_a_styles_part(context): - docx_path = test_docx('sty-having-styles-part') + docx_path = test_docx("sty-having-styles-part") context.document = Document(docx_path) -@given('a document having known styles') +@given("a document having known styles") def given_a_document_having_known_styles(context): - docx_path = test_docx('sty-known-styles') + docx_path = test_docx("sty-known-styles") document = Document(docx_path) context.document = document context.style_count = len(document.styles) -@given('a document having no styles part') +@given("a document having no styles part") def given_a_document_having_no_styles_part(context): - docx_path = test_docx('sty-having-no-styles-part') + docx_path = test_docx("sty-having-no-styles-part") context.document = Document(docx_path) -@given('a latent style collection') +@given("a latent style collection") def given_a_latent_style_collection(context): - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.latent_styles = document.styles.latent_styles -@given('a latent style having a known name') +@given("a latent style having a known name") def given_a_latent_style_having_a_known_name(context): - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) latent_styles = list(document.styles.latent_styles) context.latent_style = latent_styles[0] # should be 'Normal' -@given('a latent style having priority of {setting}') +@given("a latent style having priority of {setting}") def given_a_latent_style_having_priority_of_setting(context, setting): latent_style_name = { - '42': 'Normal', - 'no setting': 'Subtitle', + "42": "Normal", + "no setting": "Subtitle", }[setting] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) latent_styles = document.styles.latent_styles context.latent_style = latent_styles[latent_style_name] -@given('a latent style having {prop_name} set {setting}') +@given("a latent style having {prop_name} set {setting}") def given_a_latent_style_having_prop_setting(context, prop_name, setting): latent_style_name = { - 'on': 'Normal', - 'off': 'Title', - 'no setting': 'Subtitle', + "on": "Normal", + "off": "Title", + "no setting": "Subtitle", }[setting] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) latent_styles = document.styles.latent_styles context.latent_style = latent_styles[latent_style_name] -@given('a latent styles object with known defaults') +@given("a latent styles object with known defaults") def given_a_latent_styles_object_with_known_defaults(context): - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.latent_styles = document.styles.latent_styles -@given('a style based on {base_style}') +@given("a style based on {base_style}") def given_a_style_based_on_setting(context, base_style): style_name = { - 'no style': 'Base', - 'Normal': 'Sub Normal', - 'Base': 'Citation', + "no style": "Base", + "Normal": "Sub Normal", + "Base": "Citation", }[base_style] - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.styles = document.styles context.style = document.styles[style_name] -@given('a style having a known {attr_name}') +@given("a style having a known {attr_name}") def given_a_style_having_a_known_attr_name(context, attr_name): - docx_path = test_docx('sty-having-styles-part') + docx_path = test_docx("sty-having-styles-part") document = Document(docx_path) - context.style = document.styles['Normal'] + context.style = document.styles["Normal"] -@given('a style having hidden set {setting}') +@given("a style having hidden set {setting}") def given_a_style_having_hidden_set_setting(context, setting): - document = Document(test_docx('sty-behav-props')) + document = Document(test_docx("sty-behav-props")) style_name = { - 'on': 'Foo', - 'off': 'Bar', - 'no setting': 'Baz', + "on": "Foo", + "off": "Bar", + "no setting": "Baz", }[setting] context.style = document.styles[style_name] -@given('a style having locked set {setting}') +@given("a style having locked set {setting}") def given_a_style_having_locked_setting(context, setting): - document = Document(test_docx('sty-behav-props')) + document = Document(test_docx("sty-behav-props")) style_name = { - 'on': 'Foo', - 'off': 'Bar', - 'no setting': 'Baz', + "on": "Foo", + "off": "Bar", + "no setting": "Baz", }[setting] context.style = document.styles[style_name] -@given('a style having next paragraph style set to {setting}') +@given("a style having next paragraph style set to {setting}") def given_a_style_having_next_paragraph_style_setting(context, setting): - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) style_name = { - 'Sub Normal': 'Citation', - 'Foobar': 'Sub Normal', - 'Base': 'Foo', - 'no setting': 'Base', + "Sub Normal": "Citation", + "Foobar": "Sub Normal", + "Base": "Foo", + "no setting": "Base", }[setting] context.styles = document.styles context.style = document.styles[style_name] -@given('a style having priority of {setting}') +@given("a style having priority of {setting}") def given_a_style_having_priority_of_setting(context, setting): - document = Document(test_docx('sty-behav-props')) + document = Document(test_docx("sty-behav-props")) style_name = { - 'no setting': 'Baz', - '42': 'Foo', + "no setting": "Baz", + "42": "Foo", }[setting] context.style = document.styles[style_name] -@given('a style having quick-style set {setting}') +@given("a style having quick-style set {setting}") def given_a_style_having_quick_style_setting(context, setting): - document = Document(test_docx('sty-behav-props')) + document = Document(test_docx("sty-behav-props")) style_name = { - 'on': 'Foo', - 'off': 'Bar', - 'no setting': 'Baz', + "on": "Foo", + "off": "Bar", + "no setting": "Baz", }[setting] context.style = document.styles[style_name] -@given('a style having unhide-when-used set {setting}') +@given("a style having unhide-when-used set {setting}") def given_a_style_having_unhide_when_used_setting(context, setting): - document = Document(test_docx('sty-behav-props')) + document = Document(test_docx("sty-behav-props")) style_name = { - 'on': 'Foo', - 'off': 'Bar', - 'no setting': 'Baz', + "on": "Foo", + "off": "Bar", + "no setting": "Baz", }[setting] context.style = document.styles[style_name] -@given('a style of type {style_type}') +@given("a style of type {style_type}") def given_a_style_of_type(context, style_type): - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) name = { - 'WD_STYLE_TYPE.CHARACTER': 'Default Paragraph Font', - 'WD_STYLE_TYPE.LIST': 'No List', - 'WD_STYLE_TYPE.PARAGRAPH': 'Normal', - 'WD_STYLE_TYPE.TABLE': 'Normal Table', + "WD_STYLE_TYPE.CHARACTER": "Default Paragraph Font", + "WD_STYLE_TYPE.LIST": "No List", + "WD_STYLE_TYPE.PARAGRAPH": "Normal", + "WD_STYLE_TYPE.TABLE": "Normal Table", }[style_type] context.style = document.styles[name] -@given('the style collection of a document') +@given("the style collection of a document") def given_the_style_collection_of_a_document(context): - document = Document(test_docx('sty-known-styles')) + document = Document(test_docx("sty-known-styles")) context.styles = document.styles # when ===================================================== -@when('I add a latent style named \'Foobar\'') + +@when("I add a latent style named 'Foobar'") def when_I_add_a_latent_style_named_Foobar(context): latent_styles = context.document.styles.latent_styles context.latent_styles = latent_styles context.latent_style_count = len(latent_styles) - latent_styles.add_latent_style('Foobar') + latent_styles.add_latent_style("Foobar") -@when('I assign a new name to the style') +@when("I assign a new name to the style") def when_I_assign_a_new_name_to_the_style(context): - context.style.name = 'Foobar' + context.style.name = "Foobar" -@when('I assign a new value to style.style_id') +@when("I assign a new value to style.style_id") def when_I_assign_a_new_value_to_style_style_id(context): - context.style.style_id = 'Foo42' + context.style.style_id = "Foo42" -@when('I assign {value} to latent_style.{prop_name}') +@when("I assign {value} to latent_style.{prop_name}") def when_I_assign_value_to_latent_style_prop(context, value, prop_name): latent_style = context.latent_style - new_value = ( - tri_state_vals[value] if value in tri_state_vals else int(value) - ) + new_value = tri_state_vals[value] if value in tri_state_vals else int(value) setattr(latent_style, prop_name, new_value) -@when('I assign {value} to latent_styles.{prop_name}') +@when("I assign {value} to latent_styles.{prop_name}") def when_I_assign_value_to_latent_styles_prop(context, value, prop_name): latent_styles = context.latent_styles new_value = bool_vals[value] if value in bool_vals else int(value) setattr(latent_styles, prop_name, new_value) -@when('I assign {value_key} to style.base_style') +@when("I assign {value_key} to style.base_style") def when_I_assign_value_to_style_base_style(context, value_key): value = { - 'None': None, - 'styles[\'Normal\']': context.styles['Normal'], - 'styles[\'Base\']': context.styles['Base'], + "None": None, + "styles['Normal']": context.styles["Normal"], + "styles['Base']": context.styles["Base"], }[value_key] context.style.base_style = value -@when('I assign {value} to style.hidden') +@when("I assign {value} to style.hidden") def when_I_assign_value_to_style_hidden(context, value): style, new_value = context.style, tri_state_vals[value] style.hidden = new_value -@when('I assign {value} to style.locked') +@when("I assign {value} to style.locked") def when_I_assign_value_to_style_locked(context, value): style, new_value = context.style, bool_vals[value] style.locked = new_value -@when('I assign {value} to style.next_paragraph_style') +@when("I assign {value} to style.next_paragraph_style") def when_I_assign_value_to_style_next_paragraph_style(context, value): styles, style = context.styles, context.style - new_value = None if value == 'None' else styles[value] + new_value = None if value == "None" else styles[value] style.next_paragraph_style = new_value -@when('I assign {value} to style.priority') +@when("I assign {value} to style.priority") def when_I_assign_value_to_style_priority(context, value): style = context.style - new_value = None if value == 'None' else int(value) + new_value = None if value == "None" else int(value) style.priority = new_value -@when('I assign {value} to style.quick_style') +@when("I assign {value} to style.quick_style") def when_I_assign_value_to_style_quick_style(context, value): style, new_value = context.style, bool_vals[value] style.quick_style = new_value -@when('I assign {value} to style.unhide_when_used') +@when("I assign {value} to style.unhide_when_used") def when_I_assign_value_to_style_unhide_when_used(context, value): style, new_value = context.style, bool_vals[value] style.unhide_when_used = new_value -@when('I call add_style(\'{name}\', {type_str}, builtin={builtin_str})') +@when("I call add_style('{name}', {type_str}, builtin={builtin_str})") def when_I_call_add_style(context, name, type_str, builtin_str): styles = context.document.styles type = style_types[type_str] @@ -283,70 +283,71 @@ def when_I_call_add_style(context, name, type_str, builtin_str): styles.add_style(name, type, builtin=builtin) -@when('I delete a latent style') +@when("I delete a latent style") def when_I_delete_a_latent_style(context): latent_styles = context.document.styles.latent_styles context.latent_styles = latent_styles context.latent_style_count = len(latent_styles) - latent_styles['Normal'].delete() + latent_styles["Normal"].delete() -@when('I delete a style') +@when("I delete a style") def when_I_delete_a_style(context): - context.document.styles['No List'].delete() + context.document.styles["No List"].delete() # then ===================================================== -@then('I can access a latent style by name') + +@then("I can access a latent style by name") def then_I_can_access_a_latent_style_by_name(context): latent_styles = context.latent_styles - latent_style = latent_styles['Colorful Shading'] + latent_style = latent_styles["Colorful Shading"] assert isinstance(latent_style, _LatentStyle) -@then('I can access a style by its UI name') +@then("I can access a style by its UI name") def then_I_can_access_a_style_by_its_UI_name(context): styles = context.document.styles - style = styles['Default Paragraph Font'] + style = styles["Default Paragraph Font"] assert isinstance(style, BaseStyle) -@then('I can access a style by style id') +@then("I can access a style by style id") def then_I_can_access_a_style_by_style_id(context): styles = context.document.styles - style = styles['DefaultParagraphFont'] + style = styles["DefaultParagraphFont"] assert isinstance(style, BaseStyle) -@then('I can iterate over its styles') +@then("I can iterate over its styles") def then_I_can_iterate_over_its_styles(context): styles = [s for s in context.document.styles] assert len(styles) > 0 assert all(isinstance(s, BaseStyle) for s in styles) -@then('I can iterate over the latent styles') +@then("I can iterate over the latent styles") def then_I_can_iterate_over_the_latent_styles(context): latent_styles = [ls for ls in context.latent_styles] assert len(latent_styles) == 137 assert all(isinstance(ls, _LatentStyle) for ls in latent_styles) -@then('latent_style.name is the known name') +@then("latent_style.name is the known name") def then_latent_style_name_is_the_known_name(context): latent_style = context.latent_style - assert latent_style.name == 'Normal' + assert latent_style.name == "Normal" -@then('latent_style.priority is {value}') +@then("latent_style.priority is {value}") def then_latent_style_priority_is_value(context, value): latent_style = context.latent_style - expected_value = None if value == 'None' else int(value) + expected_value = None if value == "None" else int(value) assert latent_style.priority == expected_value -@then('latent_style.{prop_name} is {value}') +@then("latent_style.{prop_name} is {value}") def then_latent_style_prop_name_is_value(context, prop_name, value): latent_style = context.latent_style actual_value = getattr(latent_style, prop_name) @@ -354,14 +355,14 @@ def then_latent_style_prop_name_is_value(context, prop_name, value): assert actual_value == expected_value -@then('latent_styles[\'Foobar\'] is a latent style') +@then("latent_styles['Foobar'] is a latent style") def then_latentStyles_Foobar_is_a_latent_style(context): latent_styles = context.latent_styles - latent_style = latent_styles['Foobar'] + latent_style = latent_styles["Foobar"] assert isinstance(latent_style, _LatentStyle) -@then('latent_styles.{prop_name} is {value}') +@then("latent_styles.{prop_name} is {value}") def then_latent_styles_prop_name_is_value(context, prop_name, value): latent_styles = context.latent_styles expected_value = bool_vals[value] if value in bool_vals else int(value) @@ -369,35 +370,35 @@ def then_latent_styles_prop_name_is_value(context, prop_name, value): assert actual_value == expected_value -@then('len(latent_styles) is 137') +@then("len(latent_styles) is 137") def then_len_latent_styles_is_137(context): assert len(context.latent_styles) == 137 -@then('len(styles) is {style_count_str}') +@then("len(styles) is {style_count_str}") def then_len_styles_is_style_count(context, style_count_str): assert len(context.document.styles) == int(style_count_str) -@then('style.base_style is {value_key}') +@then("style.base_style is {value_key}") def then_style_base_style_is_value(context, value_key): expected_value = { - 'None': None, - 'styles[\'Normal\']': context.styles['Normal'], - 'styles[\'Base\']': context.styles['Base'], + "None": None, + "styles['Normal']": context.styles["Normal"], + "styles['Base']": context.styles["Base"], }[value_key] style = context.style assert style.base_style == expected_value -@then('style.builtin is {builtin_str}') +@then("style.builtin is {builtin_str}") def then_style_builtin_is_builtin(context, builtin_str): style = context.style builtin = bool_vals[builtin_str] assert style.builtin == builtin -@then('style.font is the Font object for the style') +@then("style.font is the Font object for the style") def then_style_font_is_the_Font_object_for_the_style(context): style = context.style font = style.font @@ -405,37 +406,37 @@ def then_style_font_is_the_Font_object_for_the_style(context): assert font.element is style.element -@then('style.hidden is {value}') +@then("style.hidden is {value}") def then_style_hidden_is_value(context, value): style, expected_value = context.style, tri_state_vals[value] assert style.hidden is expected_value -@then('style.locked is {value}') +@then("style.locked is {value}") def then_style_locked_is_value(context, value): style, expected_value = context.style, bool_vals[value] assert style.locked is expected_value -@then('style.name is the {which} name') +@then("style.name is the {which} name") def then_style_name_is_the_which_name(context, which): expected_name = { - 'known': 'Normal', - 'new': 'Foobar', + "known": "Normal", + "new": "Foobar", }[which] style = context.style assert style.name == expected_name -@then('style.next_paragraph_style is {value}') +@then("style.next_paragraph_style is {value}") def then_style_next_paragraph_style_is_value(context, value): style, styles = context.style, context.styles actual_value = style.next_paragraph_style expected_value = styles[value] - assert actual_value == expected_value, 'got %s' % actual_value + assert actual_value == expected_value, "got %s" % actual_value -@then('style.paragraph_format is the ParagraphFormat object for the style') +@then("style.paragraph_format is the ParagraphFormat object for the style") def then_style_paragraph_format_is_the_ParagraphFormat_object(context): style = context.style paragraph_format = style.paragraph_format @@ -443,49 +444,49 @@ def then_style_paragraph_format_is_the_ParagraphFormat_object(context): assert paragraph_format.element is style.element -@then('style.priority is {value}') +@then("style.priority is {value}") def then_style_priority_is_value(context, value): style = context.style - expected_value = None if value == 'None' else int(value) + expected_value = None if value == "None" else int(value) assert style.priority == expected_value -@then('style.quick_style is {value}') +@then("style.quick_style is {value}") def then_style_quick_style_is_value(context, value): style, expected_value = context.style, bool_vals[value] assert style.quick_style is expected_value -@then('style.style_id is the {which} style id') +@then("style.style_id is the {which} style id") def then_style_style_id_is_the_which_style_id(context, which): expected_style_id = { - 'known': 'Normal', - 'new': 'Foo42', + "known": "Normal", + "new": "Foo42", }[which] style = context.style assert style.style_id == expected_style_id -@then('style.type is the known type') +@then("style.type is the known type") def then_style_type_is_the_known_type(context): style = context.style assert style.type == WD_STYLE_TYPE.PARAGRAPH -@then('style.type is {type_str}') +@then("style.type is {type_str}") def then_style_type_is_type(context, type_str): style = context.style style_type = style_types[type_str] assert style.type == style_type -@then('style.unhide_when_used is {value}') +@then("style.unhide_when_used is {value}") def then_style_unhide_when_used_is_value(context, value): style, expected_value = context.style, bool_vals[value] assert style.unhide_when_used is expected_value -@then('styles.latent_styles is the LatentStyles object for the document') +@then("styles.latent_styles is the LatentStyles object for the document") def then_styles_latent_styles_is_the_LatentStyles_object(context): styles = context.styles context.latent_styles = latent_styles = styles.latent_styles @@ -493,34 +494,34 @@ def then_styles_latent_styles_is_the_LatentStyles_object(context): assert latent_styles.element is styles.element.latentStyles -@then('styles[\'{name}\'] is a style') +@then("styles['{name}'] is a style") def then_styles_name_is_a_style(context, name): styles = context.document.styles style = context.style = styles[name] assert isinstance(style, BaseStyle) -@then('the deleted latent style is not in the latent styles collection') +@then("the deleted latent style is not in the latent styles collection") def then_the_deleted_latent_style_is_not_in_the_collection(context): latent_styles = context.latent_styles try: - latent_styles['Normal'] + latent_styles["Normal"] except KeyError: return - raise AssertionError('Latent style not deleted') + raise AssertionError("Latent style not deleted") -@then('the deleted style is not in the styles collection') +@then("the deleted style is not in the styles collection") def then_the_deleted_style_is_not_in_the_styles_collection(context): document = context.document try: - document.styles['No List'] + document.styles["No List"] except KeyError: return - raise AssertionError('Style not deleted') + raise AssertionError("Style not deleted") -@then('the document has one additional latent style') +@then("the document has one additional latent style") def then_the_document_has_one_additional_latent_style(context): latent_styles = context.document.styles.latent_styles latent_style_count = len(latent_styles) @@ -528,7 +529,7 @@ def then_the_document_has_one_additional_latent_style(context): assert latent_style_count == expected_count -@then('the document has one additional style') +@then("the document has one additional style") def then_the_document_has_one_additional_style(context): document = context.document style_count = len(document.styles) @@ -536,14 +537,14 @@ def then_the_document_has_one_additional_style(context): assert style_count == expected_style_count -@then('the document has one fewer latent styles') +@then("the document has one fewer latent styles") def then_the_document_has_one_fewer_latent_styles(context): latent_style_count = len(context.latent_styles) expected_count = context.latent_style_count - 1 assert latent_style_count == expected_count -@then('the document has one fewer styles') +@then("the document has one fewer styles") def then_the_document_has_one_fewer_styles(context): document = context.document style_count = len(document.styles) diff --git a/features/steps/table.py b/features/steps/table.py index dc6001941..83751d1f2 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -4,17 +4,13 @@ Step implementations for table-related features """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from behave import given, then, when from docx import Document from docx.enum.table import WD_ALIGN_VERTICAL # noqa -from docx.enum.table import ( - WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION -) +from docx.enum.table import WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION from docx.shared import Inches from docx.table import _Column, _Columns, _Row, _Rows @@ -23,269 +19,258 @@ # given =================================================== -@given('a 2 x 2 table') + +@given("a 2 x 2 table") def given_a_2x2_table(context): context.table_ = Document().add_table(rows=2, cols=2) -@given('a 3x3 table having {span_state}') +@given("a 3x3 table having {span_state}") def given_a_3x3_table_having_span_state(context, span_state): table_idx = { - 'only uniform cells': 0, - 'a horizontal span': 1, - 'a vertical span': 2, - 'a combined span': 3, + "only uniform cells": 0, + "a horizontal span": 1, + "a vertical span": 2, + "a combined span": 3, }[span_state] - document = Document(test_docx('tbl-cell-access')) + document = Document(test_docx("tbl-cell-access")) context.table_ = document.tables[table_idx] -@given('a _Cell object with {state} vertical alignment as cell') +@given("a _Cell object with {state} vertical alignment as cell") def given_a_Cell_object_with_vertical_alignment_as_cell(context, state): table_idx = { - 'inherited': 0, - 'bottom': 1, - 'center': 2, - 'top': 3, + "inherited": 0, + "bottom": 1, + "center": 2, + "top": 3, }[state] - document = Document(test_docx('tbl-props')) + document = Document(test_docx("tbl-props")) table = document.tables[table_idx] context.cell = table.cell(0, 0) -@given('a column collection having two columns') +@given("a column collection having two columns") def given_a_column_collection_having_two_columns(context): - docx_path = test_docx('blk-containing-table') + docx_path = test_docx("blk-containing-table") document = Document(docx_path) context.columns = document.tables[0].columns -@given('a row collection having two rows') +@given("a row collection having two rows") def given_a_row_collection_having_two_rows(context): - docx_path = test_docx('blk-containing-table') + docx_path = test_docx("blk-containing-table") document = Document(docx_path) context.rows = document.tables[0].rows -@given('a table') +@given("a table") def given_a_table(context): context.table_ = Document().add_table(rows=2, cols=2) -@given('a table cell having a width of {width}') +@given("a table cell having a width of {width}") def given_a_table_cell_having_a_width_of_width(context, width): - table_idx = {'no explicit setting': 0, '1 inch': 1, '2 inches': 2}[width] - document = Document(test_docx('tbl-props')) + table_idx = {"no explicit setting": 0, "1 inch": 1, "2 inches": 2}[width] + document = Document(test_docx("tbl-props")) table = document.tables[table_idx] cell = table.cell(0, 0) context.cell = cell -@given('a table column having a width of {width_desc}') +@given("a table column having a width of {width_desc}") def given_a_table_having_a_width_of_width_desc(context, width_desc): col_idx = { - 'no explicit setting': 0, - '1440': 1, + "no explicit setting": 0, + "1440": 1, }[width_desc] - docx_path = test_docx('tbl-col-props') + docx_path = test_docx("tbl-col-props") document = Document(docx_path) context.column = document.tables[0].columns[col_idx] -@given('a table having {alignment} alignment') +@given("a table having {alignment} alignment") def given_a_table_having_alignment_alignment(context, alignment): table_idx = { - 'inherited': 3, - 'left': 4, - 'right': 5, - 'center': 6, + "inherited": 3, + "left": 4, + "right": 5, + "center": 6, }[alignment] - docx_path = test_docx('tbl-props') + docx_path = test_docx("tbl-props") document = Document(docx_path) context.table_ = document.tables[table_idx] -@given('a table having an autofit layout of {autofit}') +@given("a table having an autofit layout of {autofit}") def given_a_table_having_an_autofit_layout_of_autofit(context, autofit): tbl_idx = { - 'no explicit setting': 0, - 'autofit': 1, - 'fixed': 2, + "no explicit setting": 0, + "autofit": 1, + "fixed": 2, }[autofit] - document = Document(test_docx('tbl-props')) + document = Document(test_docx("tbl-props")) context.table_ = document.tables[tbl_idx] -@given('a table having {style} style') +@given("a table having {style} style") def given_a_table_having_style(context, style): table_idx = { - 'no explicit': 0, - 'Table Grid': 1, - 'Light Shading - Accent 1': 2, + "no explicit": 0, + "Table Grid": 1, + "Light Shading - Accent 1": 2, }[style] - document = Document(test_docx('tbl-having-applied-style')) + document = Document(test_docx("tbl-having-applied-style")) context.document = document context.table_ = document.tables[table_idx] -@given('a table having table direction set {setting}') +@given("a table having table direction set {setting}") def given_a_table_having_table_direction_setting(context, setting): - table_idx = [ - 'to inherit', - 'right-to-left', - 'left-to-right' - ].index(setting) - document = Document(test_docx('tbl-on-off-props')) + table_idx = ["to inherit", "right-to-left", "left-to-right"].index(setting) + document = Document(test_docx("tbl-on-off-props")) context.table_ = document.tables[table_idx] -@given('a table having two columns') +@given("a table having two columns") def given_a_table_having_two_columns(context): - docx_path = test_docx('blk-containing-table') + docx_path = test_docx("blk-containing-table") document = Document(docx_path) # context.table is used internally by behave, underscore added # to distinguish this one context.table_ = document.tables[0] -@given('a table having two rows') +@given("a table having two rows") def given_a_table_having_two_rows(context): - docx_path = test_docx('blk-containing-table') + docx_path = test_docx("blk-containing-table") document = Document(docx_path) context.table_ = document.tables[0] -@given('a table row having height of {state}') +@given("a table row having height of {state}") def given_a_table_row_having_height_of_state(context, state): - table_idx = { - 'no explicit setting': 0, - '2 inches': 2, - '3 inches': 3 - }[state] - document = Document(test_docx('tbl-props')) + table_idx = {"no explicit setting": 0, "2 inches": 2, "3 inches": 3}[state] + document = Document(test_docx("tbl-props")) table = document.tables[table_idx] context.row = table.rows[0] -@given('a table row having height rule {state}') +@given("a table row having height rule {state}") def given_a_table_row_having_height_rule_state(context, state): - table_idx = { - 'no explicit setting': 0, - 'automatic': 1, - 'at least': 2, - 'exactly': 3 - }[state] - document = Document(test_docx('tbl-props')) + table_idx = {"no explicit setting": 0, "automatic": 1, "at least": 2, "exactly": 3}[ + state + ] + document = Document(test_docx("tbl-props")) table = document.tables[table_idx] context.row = table.rows[0] # when ===================================================== -@when('I add a 1.0 inch column to the table') + +@when("I add a 1.0 inch column to the table") def when_I_add_a_1_inch_column_to_table(context): context.column = context.table_.add_column(Inches(1.0)) -@when('I add a row to the table') +@when("I add a row to the table") def when_add_row_to_table(context): table = context.table_ context.row = table.add_row() -@when('I assign {value} to cell.vertical_alignment') +@when("I assign {value} to cell.vertical_alignment") def when_I_assign_value_to_cell_vertical_alignment(context, value): context.cell.vertical_alignment = eval(value) -@when('I assign {value} to row.height') +@when("I assign {value} to row.height") def when_I_assign_value_to_row_height(context, value): - new_value = None if value == 'None' else int(value) + new_value = None if value == "None" else int(value) context.row.height = new_value -@when('I assign {value} to row.height_rule') +@when("I assign {value} to row.height_rule") def when_I_assign_value_to_row_height_rule(context, value): - new_value = ( - None if value == 'None' else getattr(WD_ROW_HEIGHT_RULE, value) - ) + new_value = None if value == "None" else getattr(WD_ROW_HEIGHT_RULE, value) context.row.height_rule = new_value -@when('I assign {value_str} to table.alignment') +@when("I assign {value_str} to table.alignment") def when_I_assign_value_to_table_alignment(context, value_str): value = { - 'None': None, - 'WD_TABLE_ALIGNMENT.LEFT': WD_TABLE_ALIGNMENT.LEFT, - 'WD_TABLE_ALIGNMENT.RIGHT': WD_TABLE_ALIGNMENT.RIGHT, - 'WD_TABLE_ALIGNMENT.CENTER': WD_TABLE_ALIGNMENT.CENTER, + "None": None, + "WD_TABLE_ALIGNMENT.LEFT": WD_TABLE_ALIGNMENT.LEFT, + "WD_TABLE_ALIGNMENT.RIGHT": WD_TABLE_ALIGNMENT.RIGHT, + "WD_TABLE_ALIGNMENT.CENTER": WD_TABLE_ALIGNMENT.CENTER, }[value_str] table = context.table_ table.alignment = value -@when('I assign {value} to table.style') +@when("I assign {value} to table.style") def when_apply_value_to_table_style(context, value): table, styles = context.table_, context.document.styles - if value == 'None': + if value == "None": new_value = None - elif value.startswith('styles['): - new_value = styles[value.split('\'')[1]] + elif value.startswith("styles["): + new_value = styles[value.split("'")[1]] else: new_value = styles[value] table.style = new_value -@when('I assign {value} to table.table_direction') +@when("I assign {value} to table.table_direction") def when_assign_value_to_table_table_direction(context, value): - new_value = ( - None if value == 'None' else getattr(WD_TABLE_DIRECTION, value) - ) + new_value = None if value == "None" else getattr(WD_TABLE_DIRECTION, value) context.table_.table_direction = new_value -@when('I merge from cell {origin} to cell {other}') +@when("I merge from cell {origin} to cell {other}") def when_I_merge_from_cell_origin_to_cell_other(context, origin, other): def cell(table, idx): row, col = idx // 3, idx % 3 return table.cell(row, col) + a_idx, b_idx = int(origin) - 1, int(other) - 1 table = context.table_ a, b = cell(table, a_idx), cell(table, b_idx) a.merge(b) -@when('I set the cell width to {width}') +@when("I set the cell width to {width}") def when_I_set_the_cell_width_to_width(context, width): - new_value = {'1 inch': Inches(1)}[width] + new_value = {"1 inch": Inches(1)}[width] context.cell.width = new_value -@when('I set the column width to {width_emu}') +@when("I set the column width to {width_emu}") def when_I_set_the_column_width_to_width_emu(context, width_emu): - new_value = None if width_emu == 'None' else int(width_emu) + new_value = None if width_emu == "None" else int(width_emu) context.column.width = new_value -@when('I set the table autofit to {setting}') +@when("I set the table autofit to {setting}") def when_I_set_the_table_autofit_to_setting(context, setting): - new_value = {'autofit': True, 'fixed': False}[setting] + new_value = {"autofit": True, "fixed": False}[setting] table = context.table_ table.autofit = new_value # then ===================================================== -@then('cell.vertical_alignment is {value}') + +@then("cell.vertical_alignment is {value}") def then_cell_vertical_alignment_is_value(context, value): expected_value = eval(value) actual_value = context.cell.vertical_alignment assert actual_value is expected_value, ( - 'cell.vertical_alignment is %s' % actual_value + "cell.vertical_alignment is %s" % actual_value ) -@then('I can access a collection column by index') +@then("I can access a collection column by index") def then_can_access_collection_column_by_index(context): columns = context.columns for idx in range(2): @@ -293,7 +278,7 @@ def then_can_access_collection_column_by_index(context): assert isinstance(column, _Column) -@then('I can access a collection row by index') +@then("I can access a collection row by index") def then_can_access_collection_row_by_index(context): rows = context.rows for idx in range(2): @@ -301,21 +286,21 @@ def then_can_access_collection_row_by_index(context): assert isinstance(row, _Row) -@then('I can access the column collection of the table') +@then("I can access the column collection of the table") def then_can_access_column_collection_of_table(context): table = context.table_ columns = table.columns assert isinstance(columns, _Columns) -@then('I can access the row collection of the table') +@then("I can access the row collection of the table") def then_can_access_row_collection_of_table(context): table = context.table_ rows = table.rows assert isinstance(rows, _Rows) -@then('I can iterate over the column collection') +@then("I can iterate over the column collection") def then_can_iterate_over_column_collection(context): columns = context.columns actual_count = 0 @@ -325,7 +310,7 @@ def then_can_iterate_over_column_collection(context): assert actual_count == 2 -@then('I can iterate over the row collection') +@then("I can iterate over the row collection") def then_can_iterate_over_row_collection(context): rows = context.rows actual_count = 0 @@ -335,163 +320,161 @@ def then_can_iterate_over_row_collection(context): assert actual_count == 2 -@then('row.height is {value}') +@then("row.height is {value}") def then_row_height_is_value(context, value): - expected_height = None if value == 'None' else int(value) + expected_height = None if value == "None" else int(value) actual_height = context.row.height - assert actual_height == expected_height, ( - 'expected %s, got %s' % (expected_height, actual_height) + assert actual_height == expected_height, "expected %s, got %s" % ( + expected_height, + actual_height, ) -@then('row.height_rule is {value}') +@then("row.height_rule is {value}") def then_row_height_rule_is_value(context, value): - expected_rule = ( - None if value == 'None' else getattr(WD_ROW_HEIGHT_RULE, value) - ) + expected_rule = None if value == "None" else getattr(WD_ROW_HEIGHT_RULE, value) actual_rule = context.row.height_rule - assert actual_rule == expected_rule, ( - 'expected %s, got %s' % (expected_rule, actual_rule) + assert actual_rule == expected_rule, "expected %s, got %s" % ( + expected_rule, + actual_rule, ) -@then('table.alignment is {value_str}') +@then("table.alignment is {value_str}") def then_table_alignment_is_value(context, value_str): value = { - 'None': None, - 'WD_TABLE_ALIGNMENT.LEFT': WD_TABLE_ALIGNMENT.LEFT, - 'WD_TABLE_ALIGNMENT.RIGHT': WD_TABLE_ALIGNMENT.RIGHT, - 'WD_TABLE_ALIGNMENT.CENTER': WD_TABLE_ALIGNMENT.CENTER, + "None": None, + "WD_TABLE_ALIGNMENT.LEFT": WD_TABLE_ALIGNMENT.LEFT, + "WD_TABLE_ALIGNMENT.RIGHT": WD_TABLE_ALIGNMENT.RIGHT, + "WD_TABLE_ALIGNMENT.CENTER": WD_TABLE_ALIGNMENT.CENTER, }[value_str] table = context.table_ - assert table.alignment == value, 'got %s' % table.alignment + assert table.alignment == value, "got %s" % table.alignment -@then('table.cell({row}, {col}).text is {expected_text}') +@then("table.cell({row}, {col}).text is {expected_text}") def then_table_cell_row_col_text_is_text(context, row, col, expected_text): table = context.table_ row_idx, col_idx = int(row), int(col) cell_text = table.cell(row_idx, col_idx).text - assert cell_text == expected_text, 'got %s' % cell_text + assert cell_text == expected_text, "got %s" % cell_text -@then('table.style is styles[\'{style_name}\']') +@then("table.style is styles['{style_name}']") def then_table_style_is_styles_style_name(context, style_name): table, styles = context.table_, context.document.styles expected_style = styles[style_name] assert table.style == expected_style, "got '%s'" % table.style -@then('table.table_direction is {value}') +@then("table.table_direction is {value}") def then_table_table_direction_is_value(context, value): - expected_value = ( - None if value == 'None' else getattr(WD_TABLE_DIRECTION, value) - ) + expected_value = None if value == "None" else getattr(WD_TABLE_DIRECTION, value) actual_value = context.table_.table_direction assert actual_value == expected_value, "got '%s'" % actual_value -@then('the column cells text is {expected_text}') +@then("the column cells text is {expected_text}") def then_the_column_cells_text_is_expected_text(context, expected_text): table = context.table_ - cells_text = ' '.join(c.text for col in table.columns for c in col.cells) - assert cells_text == expected_text, 'got %s' % cells_text + cells_text = " ".join(c.text for col in table.columns for c in col.cells) + assert cells_text == expected_text, "got %s" % cells_text -@then('the length of the column collection is 2') +@then("the length of the column collection is 2") def then_len_of_column_collection_is_2(context): columns = context.table_.columns assert len(columns) == 2 -@then('the length of the row collection is 2') +@then("the length of the row collection is 2") def then_len_of_row_collection_is_2(context): rows = context.table_.rows assert len(rows) == 2 -@then('the new column has 2 cells') +@then("the new column has 2 cells") def then_new_column_has_2_cells(context): assert len(context.column.cells) == 2 -@then('the new column is 1.0 inches wide') +@then("the new column is 1.0 inches wide") def then_new_column_is_1_inches_wide(context): assert context.column.width == Inches(1) -@then('the new row has 2 cells') +@then("the new row has 2 cells") def then_new_row_has_2_cells(context): assert len(context.row.cells) == 2 -@then('the reported autofit setting is {autofit}') +@then("the reported autofit setting is {autofit}") def then_the_reported_autofit_setting_is_autofit(context, autofit): - expected_value = {'autofit': True, 'fixed': False}[autofit] + expected_value = {"autofit": True, "fixed": False}[autofit] table = context.table_ assert table.autofit is expected_value -@then('the reported column width is {width_emu}') +@then("the reported column width is {width_emu}") def then_the_reported_column_width_is_width_emu(context, width_emu): - expected_value = None if width_emu == 'None' else int(width_emu) - assert context.column.width == expected_value, ( - 'got %s' % context.column.width - ) + expected_value = None if width_emu == "None" else int(width_emu) + assert context.column.width == expected_value, "got %s" % context.column.width -@then('the reported width of the cell is {width}') +@then("the reported width of the cell is {width}") def then_the_reported_width_of_the_cell_is_width(context, width): - expected_width = {'None': None, '1 inch': Inches(1)}[width] + expected_width = {"None": None, "1 inch": Inches(1)}[width] actual_width = context.cell.width - assert actual_width == expected_width, ( - 'expected %s, got %s' % (expected_width, actual_width) + assert actual_width == expected_width, "expected %s, got %s" % ( + expected_width, + actual_width, ) -@then('the row cells text is {encoded_text}') +@then("the row cells text is {encoded_text}") def then_the_row_cells_text_is_expected_text(context, encoded_text): - expected_text = encoded_text.replace('\\', '\n') + expected_text = encoded_text.replace("\\", "\n") table = context.table_ - cells_text = ' '.join(c.text for row in table.rows for c in row.cells) - assert cells_text == expected_text, 'got %s' % cells_text + cells_text = " ".join(c.text for row in table.rows for c in row.cells) + assert cells_text == expected_text, "got %s" % cells_text -@then('the table has {count} columns') +@then("the table has {count} columns") def then_table_has_count_columns(context, count): column_count = int(count) columns = context.table_.columns assert len(columns) == column_count -@then('the table has {count} rows') +@then("the table has {count} rows") def then_table_has_count_rows(context, count): row_count = int(count) rows = context.table_.rows assert len(rows) == row_count -@then('the width of cell {n_str} is {inches_str} inches') +@then("the width of cell {n_str} is {inches_str} inches") def then_the_width_of_cell_n_is_x_inches(context, n_str, inches_str): def _cell(table, idx): row, col = idx // 3, idx % 3 return table.cell(row, col) + idx, inches = int(n_str) - 1, float(inches_str) cell = _cell(context.table_, idx) - assert cell.width == Inches(inches), 'got %s' % cell.width.inches + assert cell.width == Inches(inches), "got %s" % cell.width.inches -@then('the width of each cell is {inches} inches') +@then("the width of each cell is {inches} inches") def then_the_width_of_each_cell_is_inches(context, inches): table = context.table_ expected_width = Inches(float(inches)) for cell in table._cells: - assert cell.width == expected_width, 'got %s' % cell.width.inches + assert cell.width == expected_width, "got %s" % cell.width.inches -@then('the width of each column is {inches} inches') +@then("the width of each column is {inches} inches") def then_the_width_of_each_column_is_inches(context, inches): table = context.table_ expected_width = Inches(float(inches)) for column in table.columns: - assert column.width == expected_width, 'got %s' % column.width.inches + assert column.width == expected_width, "got %s" % column.width.inches diff --git a/features/steps/tabstops.py b/features/steps/tabstops.py index 4a6b442e0..b360d480e 100644 --- a/features/steps/tabstops.py +++ b/features/steps/tabstops.py @@ -16,71 +16,73 @@ # given =================================================== -@given('a tab_stops having {count} tab stops') + +@given("a tab_stops having {count} tab stops") def given_a_tab_stops_having_count_tab_stops(context, count): - paragraph_idx = {'0': 0, '3': 1}[count] - document = Document(test_docx('tab-stops')) + paragraph_idx = {"0": 0, "3": 1}[count] + document = Document(test_docx("tab-stops")) paragraph_format = document.paragraphs[paragraph_idx].paragraph_format context.tab_stops = paragraph_format.tab_stops -@given('a tab stop 0.5 inches {in_or_out} from the paragraph left edge') +@given("a tab stop 0.5 inches {in_or_out} from the paragraph left edge") def given_a_tab_stop_inches_from_paragraph_left_edge(context, in_or_out): - tab_idx = {'out': 0, 'in': 1}[in_or_out] - document = Document(test_docx('tab-stops')) + tab_idx = {"out": 0, "in": 1}[in_or_out] + document = Document(test_docx("tab-stops")) paragraph_format = document.paragraphs[2].paragraph_format context.tab_stops = paragraph_format.tab_stops context.tab_stop = paragraph_format.tab_stops[tab_idx] -@given('a tab stop having {alignment} alignment') +@given("a tab stop having {alignment} alignment") def given_a_tab_stop_having_alignment_alignment(context, alignment): - tab_idx = {'LEFT': 0, 'CENTER': 1, 'RIGHT': 2}[alignment] - document = Document(test_docx('tab-stops')) + tab_idx = {"LEFT": 0, "CENTER": 1, "RIGHT": 2}[alignment] + document = Document(test_docx("tab-stops")) paragraph_format = document.paragraphs[1].paragraph_format context.tab_stop = paragraph_format.tab_stops[tab_idx] -@given('a tab stop having {leader} leader') +@given("a tab stop having {leader} leader") def given_a_tab_stop_having_leader_leader(context, leader): - tab_idx = {'no specified': 0, 'a dotted': 2}[leader] - document = Document(test_docx('tab-stops')) + tab_idx = {"no specified": 0, "a dotted": 2}[leader] + document = Document(test_docx("tab-stops")) paragraph_format = document.paragraphs[1].paragraph_format context.tab_stop = paragraph_format.tab_stops[tab_idx] # when ==================================================== -@when('I add a tab stop') + +@when("I add a tab stop") def when_I_add_a_tab_stop(context): tab_stops = context.tab_stops tab_stops.add_tab_stop(Inches(1.75)) -@when('I assign {member} to tab_stop.alignment') +@when("I assign {member} to tab_stop.alignment") def when_I_assign_member_to_tab_stop_alignment(context, member): value = getattr(WD_TAB_ALIGNMENT, member) context.tab_stop.alignment = value -@when('I assign {member} to tab_stop.leader') +@when("I assign {member} to tab_stop.leader") def when_I_assign_member_to_tab_stop_leader(context, member): value = getattr(WD_TAB_LEADER, member) context.tab_stop.leader = value -@when('I assign {value} to tab_stop.position') +@when("I assign {value} to tab_stop.position") def when_I_assign_value_to_tab_stop_value(context, value): context.tab_stop.position = int(value) -@when('I call tab_stops.clear_all()') +@when("I call tab_stops.clear_all()") def when_I_call_tab_stops_clear_all(context): tab_stops = context.tab_stops tab_stops.clear_all() -@when('I remove a tab stop') +@when("I remove a tab stop") def when_I_remove_a_tab_stop(context): tab_stops = context.tab_stops del tab_stops[1] @@ -88,7 +90,8 @@ def when_I_remove_a_tab_stop(context): # then ===================================================== -@then('I can access a tab stop by index') + +@then("I can access a tab stop by index") def then_I_can_access_a_tab_stop_by_index(context): tab_stops = context.tab_stops for idx in range(3): @@ -96,48 +99,48 @@ def then_I_can_access_a_tab_stop_by_index(context): assert isinstance(tab_stop, TabStop) -@then('I can iterate the TabStops object') +@then("I can iterate the TabStops object") def then_I_can_iterate_the_TabStops_object(context): items = [ts for ts in context.tab_stops] assert len(items) == 3 assert all(isinstance(item, TabStop) for item in items) -@then('len(tab_stops) is {count}') +@then("len(tab_stops) is {count}") def then_len_tab_stops_is_count(context, count): tab_stops = context.tab_stops assert len(tab_stops) == int(count) -@then('tab_stop.alignment is {alignment}') +@then("tab_stop.alignment is {alignment}") def then_tab_stop_alignment_is_alignment(context, alignment): expected_value = getattr(WD_TAB_ALIGNMENT, alignment) tab_stop = context.tab_stop assert tab_stop.alignment == expected_value -@then('tab_stop.leader is {leader}') +@then("tab_stop.leader is {leader}") def then_tab_stop_leader_is_leader(context, leader): expected_value = getattr(WD_TAB_LEADER, leader) tab_stop = context.tab_stop assert tab_stop.leader == expected_value -@then('tab_stop.position is {position}') +@then("tab_stop.position is {position}") def then_tab_stop_position_is_position(context, position): tab_stop = context.tab_stop assert tab_stop.position == int(position) -@then('the removed tab stop is no longer present in tab_stops') +@then("the removed tab stop is no longer present in tab_stops") def then_the_removed_tab_stop_is_no_longer_present_in_tab_stops(context): tab_stops = context.tab_stops assert tab_stops[0].position == Inches(1) assert tab_stops[1].position == Inches(3) -@then('the tab stops are sequenced in position order') +@then("the tab stops are sequenced in position order") def then_the_tab_stops_are_sequenced_in_position_order(context): tab_stops = context.tab_stops for idx in range(len(tab_stops) - 1): - assert tab_stops[idx].position < tab_stops[idx+1].position + assert tab_stops[idx].position < tab_stops[idx + 1].position diff --git a/features/steps/text.py b/features/steps/text.py index cc56c930c..57451a1bb 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -22,7 +22,8 @@ # given =================================================== -@given('a run') + +@given("a run") def given_a_run(context): document = Document() run = document.add_paragraph().add_run() @@ -30,22 +31,22 @@ def given_a_run(context): context.run = run -@given('a run having {bool_prop_name} set on') +@given("a run having {bool_prop_name} set on") def given_a_run_having_bool_prop_set_on(context, bool_prop_name): run = Document().add_paragraph().add_run() setattr(run, bool_prop_name, True) context.run = run -@given('a run having known text and formatting') +@given("a run having known text and formatting") def given_a_run_having_known_text_and_formatting(context): - run = Document().add_paragraph().add_run('foobar') + run = Document().add_paragraph().add_run("foobar") run.bold = True run.italic = True context.run = run -@given('a run having mixed text content') +@given("a run having mixed text content") def given_a_run_having_mixed_text_content(context): """ Mixed here meaning it contains ````, ````, etc. elements. @@ -60,40 +61,40 @@ def given_a_run_having_mixed_text_content(context): jkl - """ % nsdecls('w') + """ % 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 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 +103,203 @@ 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\tdef\nghi\rjkl" -@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.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.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('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') +@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 - ) + assert context.run.text == "abc\tdef\nghi\njkl", "got '%s'" % context.run.text diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index 32091daba..421f72922 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -4,9 +4,7 @@ Test suite for docx.dml.color module. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from docx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR from docx.dml.color import ColorFormat @@ -18,7 +16,6 @@ class DescribeColorFormat(object): - def it_knows_its_color_type(self, type_fixture): color_format, expected_value = type_fixture assert color_format.type == expected_value @@ -43,86 +40,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..9eb371643 100644 --- a/tests/image/test_bmp.py +++ b/tests/image/test_bmp.py @@ -16,12 +16,11 @@ class DescribeBmp(object): - 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_) @@ -36,7 +35,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..fc00af690 100644 --- a/tests/image/test_gif.py +++ b/tests/image/test_gif.py @@ -14,10 +14,9 @@ class DescribeGif(object): - def it_can_construct_from_a_gif_stream(self, Gif__init__): cx, cy = 42, 24 - bytes_ = b'filler\x2A\x00\x18\x00' + bytes_ = b"filler\x2A\x00\x18\x00" stream = BytesIO(bytes_) gif = Gif.from_stream(stream) @@ -31,7 +30,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..8716257d7 100644 --- a/tests/image/test_helpers.py +++ b/tests/image/test_helpers.py @@ -14,12 +14,10 @@ class DescribeStreamReader(object): - - def it_can_read_a_string_of_specified_len_at_offset( - self, read_str_fixture): + 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,10 +31,12 @@ 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_) @@ -45,7 +45,7 @@ def read_long_fixture(self, request): @pytest.fixture def read_str_fixture(self): - stream = BytesIO(b'\x01\x02foobar\x03\x04') + stream = 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..5893f053c 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -30,7 +30,6 @@ class DescribeImage(object): - def it_can_construct_from_an_image_blob( self, blob_, BytesIO_, _from_stream_, stream_, image_ ): @@ -41,9 +40,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 +63,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 +100,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 +129,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 +149,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) 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 +226,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.BytesIO", return_value=stream_) @pytest.fixture def filename_(self, request): @@ -235,12 +235,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 +249,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): @@ -267,37 +266,37 @@ def stream_(self, request): @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): + 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 = 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.seek(666) @@ -305,7 +304,6 @@ def call_fixture(self, request): class DescribeBaseImageHeader(object): - 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..759bd94cc 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -35,22 +35,18 @@ class DescribeJpeg(object): - 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' + assert jpeg.default_ext == "jpg" class DescribeExif(object): - 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 ----------------------- @@ -62,11 +58,8 @@ def it_can_construct_from_an_exif_stream(self, from_exif_fixture): assert exif.vert_dpi == vert_dpi class DescribeJfif(object): - 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 +78,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 +88,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_ @@ -117,9 +106,8 @@ def stream_(self, request): class Describe_JfifMarkers(object): - 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 +151,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 +171,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 +185,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 +210,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,15 +220,11 @@ 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): @@ -256,7 +232,6 @@ def stream_(self, request): class Describe_Marker(object): - def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture): stream, marker_code, offset, _Marker__init_, length = from_stream_fixture @@ -267,13 +242,15 @@ 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' + bytes_ = b"\xFF\xD8\xFF\xE0\x00\x10" stream_reader = StreamReader(BytesIO(bytes_), BIG_ENDIAN) return stream_reader, marker_code, offset, _Marker__init_, length @@ -283,9 +260,8 @@ def _Marker__init_(self, request): class Describe_App0Marker(object): - 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) @@ -299,9 +275,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,24 +285,23 @@ 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): - 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) @@ -342,7 +315,7 @@ 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) @@ -353,8 +326,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:] @@ -377,18 +349,22 @@ def _App1Marker__init_(self, request): @pytest.fixture def BytesIO_(self, request, substream_): - return class_mock( - request, 'docx.image.jpeg.BytesIO', return_value=substream_ - ) + return class_mock(request, "docx.image.jpeg.BytesIO", return_value=substream_) @pytest.fixture def get_tiff_fixture(self, request, BytesIO_, substream_, Tiff_, tiff_): - bytes_ = b'xfillerxMM\x00*\x00\x00\x00\x42' + 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 @@ -397,7 +373,7 @@ def substream_(self, request): @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,15 +384,17 @@ 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): - 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) @@ -441,27 +419,33 @@ def _SofMarker__init_(self, request): class Describe_MarkerFactory(object): - 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 +459,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,7 +475,7 @@ 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): @@ -499,7 +483,6 @@ def stream_(self, request): class Describe_MarkerFinder(object): - def it_can_construct_from_a_stream(self, stream_, _MarkerFinder__init_): marker_finder = _MarkerFinder.from_stream(stream_) @@ -517,18 +500,20 @@ 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' + bytes_ = b"\xFF\xD8\xFF\xE0\x00\x01\xFF\x00\xFF\xFF\xFF\xD9" stream_reader = StreamReader(BytesIO(bytes_), BIG_ENDIAN) marker_finder = _MarkerFinder(stream_reader) expected_code_and_offset = (marker_code, segment_offset) @@ -540,7 +525,6 @@ def stream_(self, request): class Describe_MarkerParser(object): - def it_can_construct_from_a_jfif_stream( self, stream_, StreamReader_, _MarkerParser__init_, stream_reader_ ): @@ -550,16 +534,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 + 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()] _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 +567,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_ @@ -629,8 +631,7 @@ def stream_(self, request): @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..71b2c7e14 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -11,8 +11,14 @@ 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 + _Chunk, + _Chunks, + _ChunkFactory, + _ChunkParser, + _IHDRChunk, + _pHYsChunk, + Png, + _PngParser, ) from ..unitutil.mock import ( @@ -27,9 +33,8 @@ class DescribePng(object): - 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 +56,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 +66,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_ @@ -75,7 +80,6 @@ def stream_(self, request): class Describe_PngParser(object): - def it_can_parse_the_headers_of_a_PNG_stream( self, stream_, _Chunks_, _PngParser__init_, chunks_ ): @@ -104,7 +108,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 +134,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 @@ -158,7 +160,6 @@ def stream_(self, request): class Describe_Chunks(object): - def it_can_construct_from_a_stream( self, stream_, _ChunkParser_, chunk_parser_, _Chunks__init_ ): @@ -189,7 +190,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 +210,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 +220,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_): @@ -241,7 +238,6 @@ def stream_(self, request): class Describe_ChunkParser(object): - def it_can_construct_from_a_stream( self, stream_, StreamReader_, stream_rdr_, _ChunkParser__init_ ): @@ -267,8 +263,7 @@ 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()] assert chunk_offsets == expected_chunk_offsets @@ -286,8 +281,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,13 +299,15 @@ 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' + bytes_ = b"-filler-\x00\x00\x00\x00IHDRxxxx\x00\x00\x00\x00IEND" stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) chunk_parser = _ChunkParser(stream_rdr) expected_chunk_offsets = [ @@ -323,7 +319,7 @@ 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 @@ -336,24 +332,22 @@ def stream_rdr_(self, request): class Describe_ChunkFactory(object): - 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 +359,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 +369,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 +379,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_ @@ -399,16 +393,14 @@ def stream_rdr_(self, request): class Describe_Chunk(object): - 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): - 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 +412,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' + bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x18" stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) offset, px_width, px_height = 0, 42, 24 return stream_rdr, offset, px_width, px_height class Describe_pHYsChunk(object): - 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 +433,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' + 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 - ) + 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..a5f07acdb 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -35,7 +35,6 @@ class DescribeTiff(object): - def it_can_construct_from_a_tiff_stream( self, stream_, _TiffParser_, tiff_parser_, Tiff__init_ ): @@ -60,7 +59,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 +69,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_ @@ -84,7 +83,6 @@ def stream_(self, request): class Describe_TiffParser(object): - def it_can_parse_the_properties_from_a_tiff_stream( self, stream_, @@ -98,9 +96,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 +109,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 +124,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 +150,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,15 +167,17 @@ 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_) @@ -190,7 +190,7 @@ def stream_(self, request): @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 @@ -205,7 +205,6 @@ def _TiffParser__init_(self, request): class Describe_IfdEntries(object): - def it_can_construct_from_a_stream_and_offset( self, stream_, @@ -226,7 +225,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 +248,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 @@ -266,11 +265,14 @@ def stream_(self, request): 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 + 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()] assert _IfdEntryFactory_.call_args_list == [ call(stream_rdr, offsets[0]), @@ -291,24 +293,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(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): - 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,25 +316,34 @@ 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) offset = 0 @@ -347,35 +355,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_ @@ -385,11 +389,10 @@ def offset_(self, request): class Describe_IfdEntry(object): - 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' + bytes_ = b"\x00\x01\x66\x66\x00\x00\x00\x02\x00\x00\x00\x03" stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) offset, tag_code, value_count, value_offset = 0, 1, 2, 3 _parse_value_.return_value = value_ @@ -415,7 +418,7 @@ 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): @@ -423,36 +426,32 @@ def value_(self, request): class Describe_AsciiIfdEntry(object): - def it_can_parse_an_ascii_string_IFD_entry(self): - bytes_ = b'foobar\x00' + bytes_ = b"foobar\x00" stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) val = _AsciiIfdEntry._parse_value(stream_rdr, None, 7, 0) - assert val == 'foobar' + assert val == "foobar" class Describe_ShortIfdEntry(object): - def it_can_parse_a_short_int_IFD_entry(self): - bytes_ = b'foobaroo\x00\x2A' + bytes_ = b"foobaroo\x00\x2A" stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) val = _ShortIfdEntry._parse_value(stream_rdr, 0, 1, None) assert val == 42 class Describe_LongIfdEntry(object): - def it_can_parse_a_long_int_IFD_entry(self): - bytes_ = b'foobaroo\x00\x00\x00\x2A' + bytes_ = b"foobaroo\x00\x00\x00\x2A" stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) val = _LongIfdEntry._parse_value(stream_rdr, 0, 1, None) assert val == 42 class Describe_RationalIfdEntry(object): - def it_can_parse_a_rational_IFD_entry(self): - bytes_ = b'\x00\x00\x00\x2A\x00\x00\x00\x54' + bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x54" stream_rdr = StreamReader(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..66222cef7 100644 --- a/tests/opc/parts/test_coreprops.py +++ b/tests/opc/parts/test_coreprops.py @@ -4,9 +4,7 @@ Unit test suite for the docx.opc.parts.coreprops module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from datetime import datetime, timedelta @@ -20,7 +18,6 @@ class DescribeCorePropertiesPart(object): - 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 +28,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 +46,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..97da5dead 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -4,9 +4,7 @@ Unit test suite for the docx.opc.coreprops module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest @@ -17,7 +15,6 @@ class DescribeCoreProperties(object): - 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 +31,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 +46,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 +89,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 +154,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..c269bde35 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -6,55 +6,60 @@ 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, + an_Override, + a_Relationship, + a_Relationships, + a_Types, ) class DescribeCT_Default(object): - 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): - 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): - 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: @@ -69,7 +74,6 @@ def it_can_construct_from_attribute_values(self): class DescribeCT_Relationships(object): - def it_can_construct_a_new_relationships_element(self): rels = CT_Relationships.new() expected_xml = ( @@ -82,24 +86,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): - def it_provides_access_to_default_child_elements(self): types = a_Types().element assert len(types.defaults) == 2 @@ -124,10 +127,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..e2edb3ae6 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -29,22 +29,18 @@ class DescribeOpcPackage(object): - - def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, - Unmarshaller_): + 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 +52,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 +62,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,17 +77,13 @@ 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() @@ -121,15 +110,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 +123,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 +148,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 +175,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 +194,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 +206,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 +214,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,15 +226,15 @@ 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_): @@ -259,16 +243,15 @@ def parts(self, request, parts_): patch after each use. """ _patch = patch.object( - OpcPackage, 'parts', new_callable=PropertyMock, - return_value=parts_ + OpcPackage, "parts", new_callable=PropertyMock, return_value=parts_ ) request.addfinalizer(_patch.stop) return _patch.start() @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 +270,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 +289,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): - def it_can_unmarshal_from_a_pkg_reader( self, pkg_reader_, @@ -336,56 +318,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 +411,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 +429,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 +450,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 +465,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..a56badcda 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -10,16 +10,15 @@ class DescribePackURI(object): - def cases(self, expected_values): """ Return list of tuples zipped from uri_str cases and *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 +27,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') + 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 +58,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 +85,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..b32bf7f4f 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -26,7 +26,6 @@ class DescribePart(object): - def it_can_be_constructed_by_PartFactory( self, partname_, content_type_, blob_, package_, __init_ ): @@ -71,7 +70,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 +86,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 @@ -122,7 +121,6 @@ def partname_(self, request): class DescribePartRelationshipManagementInterface(object): - def it_provides_access_to_its_relationships(self, rels_fixture): part, Relationships_, partname_, rels_ = rels_fixture rels = part.rels @@ -132,19 +130,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 +162,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 +189,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 +235,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_): @@ -279,12 +268,14 @@ def url_(self, request): class DescribePartFactory(object): - - def it_constructs_part_from_selector_if_defined( - self, cls_selector_fixture): + 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 +288,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 +302,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,21 +324,29 @@ 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_): + 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 + 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_ + cls_selector_fn_, + part_load_params, + CustomPartClass_, + part_of_custom_type_, ) @pytest.fixture @@ -355,7 +356,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 +370,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 +389,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 +409,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 @@ -432,7 +428,6 @@ def reltype_2_(self, request): class DescribeXmlPart(object): - def it_can_be_constructed_by_PartFactory( self, partname_, content_type_, blob_, package_, element_, parse_xml_, __init_ ): @@ -489,9 +484,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 +492,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..5fa1e9e75 100644 --- a/tests/opc/test_phys_pkg.py +++ b/tests/opc/test_phys_pkg.py @@ -19,45 +19,47 @@ 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 + _DirPkgReader, + PhysPkgReader, + PhysPkgWriter, + _ZipPkgReader, + _ZipPkgWriter, ) from ..unitutil.file import absjoin, test_file_dir from ..unitutil.mock import class_mock, loose_mock, 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): - 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 +69,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): - def it_raises_when_pkg_path_is_not_a_package(self): with pytest.raises(PackageNotFoundError): - PhysPkgReader('foobar') + PhysPkgReader("foobar") class DescribeZipPkgReader(object): - 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,28 +104,28 @@ 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') + @pytest.fixture(scope="class") def phys_reader(self, request): phys_reader = _ZipPkgReader(zip_pkg_path) request.addfinalizer(phys_reader.close) @@ -137,17 +137,14 @@ def pkg_file_(self, request): class DescribeZipPkgWriter(object): - 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 +157,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() @@ -185,11 +182,12 @@ def pkg_file(self, request): # 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..ac75e8365 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -33,7 +33,6 @@ class DescribePackageReader(object): - def it_can_construct_from_pkg_file( self, _init_, PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts ): @@ -41,13 +40,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 +61,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()] # 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) # 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 +102,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 +123,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_1_blob, part_2_blob, part_3_blob = ( - '', '', '' + "/part/name1.xml", + "/part/name2.xml", + "/part/name3.xml", ) - 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 +174,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 +192,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 +212,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,138 +224,140 @@ 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 - ) + _patch = patch("docx.opc.pkgreader.PhysPkgReader", spec_set=_ZipPkgReader) request.addfinalizer(_patch.stop) return _patch.start() @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): - 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 @@ -354,13 +368,13 @@ def _xml_from(self, 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) @@ -370,14 +384,13 @@ def _xml_from(self, entries): class Describe_SerializedPart(object): - 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 ----------------------- @@ -389,42 +402,57 @@ def it_remembers_construction_values(self): class Describe_SerializedRelationship(object): - 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 +460,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) + srel = _SerializedRelationship("/", rel_elm) with pytest.raises(ValueError): srel.target_partname class Describe_SerializedRelationships(object): - 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 +506,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..25bcadb42 100644 --- a/tests/opc/test_pkgwriter.py +++ b/tests/opc/test_pkgwriter.py @@ -14,17 +14,22 @@ from .unitdata.types import a_Default, a_Types, an_Override from ..unitutil.mock import ( - call, class_mock, instance_mock, MagicMock, method_mock, Mock, patch + call, + class_mock, + instance_mock, + MagicMock, + method_mock, + Mock, + patch, ) class DescribePackageWriter(object): - 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 +44,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 +87,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_ @@ -99,7 +97,7 @@ def parts_(self, request): @pytest.fixture def PhysPkgWriter_(self, request): - _patch = patch('docx.opc.pkgwriter.PhysPkgWriter') + _patch = patch("docx.opc.pkgwriter.PhysPkgWriter") request.addfinalizer(_patch.stop) return _patch.start() @@ -108,20 +106,19 @@ 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): """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') + 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() @@ -133,11 +130,10 @@ def fin(): @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): - def it_can_compose_content_types_element(self, xml_for_fixture): cti, expected_xml = xml_for_fixture types_elm = cti._element @@ -148,41 +144,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..d94750027 100644 --- a/tests/opc/test_rel.py +++ b/tests/opc/test_rel.py @@ -4,9 +4,7 @@ Unit test suite for the docx.opc.rel module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest @@ -15,18 +13,15 @@ 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 ..unitutil.mock import call, class_mock, instance_mock, Mock, patch, PropertyMock class Describe_Relationship(object): - 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) @@ -42,8 +37,8 @@ def it_should_raise_on_target_part_access_on_external_rel(self): 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 +46,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): - 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 +76,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 +91,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 +105,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 +118,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 +139,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 +148,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,15 +165,16 @@ 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 @@ -192,20 +184,19 @@ def rels_elm(self, request): 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 @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 +208,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 +240,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 +271,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..73663d188 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -17,6 +17,7 @@ class BaseBuilder(object): """ Provides common behavior for all data builders. """ + @property def element(self): """Return element based on XML generated by builder""" @@ -30,9 +31,10 @@ def with_indent(self, indent): class RelationshipsBuilder(object): """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 +51,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,10 +72,11 @@ 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 @@ -89,16 +92,15 @@ def with_extension(self, extension): 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,12 +108,13 @@ 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*""" @@ -125,16 +128,15 @@ def with_partname(self, partname): 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,11 +144,12 @@ 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 @@ -168,27 +171,33 @@ def with_target(self, target): def with_target_mode(self, target_mode): """Set TargetMode attribute to *target_mode*""" - self._target_mode = None if target_mode == 'Internal' else 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 +205,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 +221,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 +240,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 +268,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..d5b742b10 100644 --- a/tests/opc/unitdata/types.py +++ b/tests/opc/unitdata/types.py @@ -12,24 +12,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..707a0dd71 100644 --- a/tests/oxml/parts/test_document.py +++ b/tests/oxml/parts/test_document.py @@ -12,7 +12,6 @@ class DescribeCT_Body(object): - def it_can_clear_all_its_content(self, clear_fixture): body, expected_xml = clear_fixture body.clear_content() @@ -26,13 +25,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 +42,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..6dd8efe79 100644 --- a/tests/oxml/parts/unitdata/document.py +++ b/tests/oxml/parts/unitdata/document.py @@ -8,14 +8,14 @@ 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..85302d64c 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -10,80 +10,71 @@ from lxml import etree -from docx.oxml import ( - OxmlElement, oxml_parser, parse_xml, register_element_cls -) +from docx.oxml import OxmlElement, oxml_parser, parse_xml, register_element_cls from docx.oxml.ns import qn from docx.oxml.shared import BaseOxmlElement class DescribeOxmlElement(object): - 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): - 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): - 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) + xml_text = "%s\n%s" % (enc_decl, xml_body) with pytest.raises(ValueError): 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 +85,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) + 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 +104,8 @@ def xml_text(self): return ( '\n' - ' foøbår\n' - '\n' + " foøbår\n" + "\n" ) @@ -124,5 +113,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..413d10bde 100644 --- a/tests/oxml/test_ns.py +++ b/tests/oxml/test_ns.py @@ -12,10 +12,9 @@ class DescribeNamespacePrefixedTag(object): - 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 +26,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 +40,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 +56,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..0c34f6637 100644 --- a/tests/oxml/test_styles.py +++ b/tests/oxml/test_styles.py @@ -4,9 +4,7 @@ Test suite for the docx.oxml.styles module. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest @@ -16,7 +14,6 @@ class DescribeCT_Styles(object): - 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 +22,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..bc40b5fba 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -16,7 +16,6 @@ class DescribeCT_Row(object): - def it_can_add_a_trPr(self, add_trPr_fixture): tr, expected_xml = add_trPr_fixture tr._add_trPr() @@ -29,12 +28,14 @@ def it_raises_on_tc_at_grid_col(self, tc_raise_fixture): # 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 +45,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): - 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 +92,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 @@ -131,25 +132,41 @@ def it_raises_on_tr_above(self, tr_above_raise_fixture): # 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 +174,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 +218,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 +266,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 +284,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 +326,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 +341,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 +349,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 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..15f68face 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -14,9 +14,17 @@ from docx.oxml.ns import qn 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, + serialize_for_reading, + OneOrMore, + OneAndOnlyOne, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, + ZeroOrOneChoice, + XmlString, ) from ..unitdata import BaseBuilder @@ -24,64 +32,71 @@ class DescribeBaseOxmlElement(object): - - def it_can_find_the_first_of_its_children_named_in_a_sequence( - self, first_fixture): + 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,16 +107,16 @@ 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) @@ -109,7 +124,6 @@ def rPr_bldr(self, children): class DescribeSerializeForReading(object): - def it_pretty_prints_an_lxml_element(self, pretty_fixture): element, expected_xml_text = pretty_fixture xml_text = serialize_for_reading(element) @@ -124,11 +138,7 @@ def it_returns_unicode_text(self, type_fixture): @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 +149,10 @@ def type_fixture(self, element): @pytest.fixture def element(self): - return parse_xml('text') + return parse_xml("text") class DescribeXmlString(object): - def it_parses_a_line_to_help_compare(self, parse_fixture): """ This internal function is important to test separately because if it @@ -167,54 +176,67 @@ 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] @@ -222,9 +244,7 @@ def xml_line_case(self, request): class DescribeChoice(object): - - def it_adds_a_getter_property_for_the_choice_element( - self, getter_fixture): + 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 +258,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 +267,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 +283,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 +334,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): - def it_adds_a_getter_property_for_the_child_element(self, getter_fixture): parent, oooChild = getter_fixture assert parent.oooChild is oooChild @@ -327,14 +351,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): + 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 +370,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 +379,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 +388,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 +402,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 @@ -417,7 +441,6 @@ def parent_bldr(self, oomChild_is_present): class DescribeOptionalAttribute(object): - 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 +459,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: @@ -451,7 +474,6 @@ def setter_fixture(self, request): class DescribeRequiredAttribute(object): - 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 +502,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 +519,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): + 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 +540,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 +549,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 +558,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 +575,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 @@ -588,7 +612,6 @@ def parent_bldr(self, zomChild_is_present): class DescribeZeroOrOne(object): - def it_adds_a_getter_property_for_the_child_element(self, getter_fixture): parent, zooChild = getter_fixture assert parent.zooChild is zooChild @@ -599,7 +622,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 +630,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 +656,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 +669,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 @@ -679,18 +703,17 @@ def parent_bldr(self, zooChild_is_present): class DescribeZeroOrOneChoice(object): - 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 +721,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 +732,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 +788,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_run.py b/tests/oxml/text/test_run.py index 57b8580fe..0dc091b26 100644 --- a/tests/oxml/text/test_run.py +++ b/tests/oxml/text/test_run.py @@ -4,9 +4,7 @@ Test suite for the docx.oxml.text.run module. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest @@ -14,7 +12,6 @@ class DescribeCT_R(object): - def it_can_add_a_t_preserving_edge_whitespace(self, add_t_fixture): r, text, expected_xml = add_t_fixture r.add_t(text) @@ -22,12 +19,17 @@ def it_can_add_a_t_preserving_edge_whitespace(self, add_t_fixture): # 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")'), - ]) + @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) diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index 84518f8b7..d2ba72ed7 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -8,50 +8,50 @@ 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',) + __tag__ = "pic:blipFill" + __nspfxs__ = ("pic",) __attrs__ = () class CT_DrawingBuilder(BaseBuilder): - __tag__ = 'w:drawing' - __nspfxs__ = ('w',) + __tag__ = "w:drawing" + __nspfxs__ = ("w",) __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',) + __tag__ = "a:graphicData" + __nspfxs__ = ("a",) + __attrs__ = ("uri",) class CT_GraphicalObjectFrameLockingBuilder(BaseBuilder): - __tag__ = 'a:graphicFrameLocks' - __nspfxs__ = ('a',) - __attrs__ = ('noChangeAspect',) + __tag__ = "a:graphicFrameLocks" + __nspfxs__ = ("a",) + __attrs__ = ("noChangeAspect",) class CT_InlineBuilder(BaseBuilder): - __tag__ = 'wp:inline' - __nspfxs__ = ('wp',) - __attrs__ = ('distT', 'distB', 'distL', 'distR') + __tag__ = "wp:inline" + __nspfxs__ = ("wp",) + __attrs__ = ("distT", "distB", "distL", "distR") class CT_NonVisualDrawingPropsBuilder(BaseBuilder): - __nspfxs__ = ('wp',) - __attrs__ = ('id', 'name', 'descr', 'hidden', 'title') + __nspfxs__ = ("wp",) + __attrs__ = ("id", "name", "descr", "hidden", "title") def __init__(self, tag): self.__tag__ = tag @@ -59,38 +59,38 @@ def __init__(self, tag): class CT_NonVisualGraphicFramePropertiesBuilder(BaseBuilder): - __tag__ = 'wp:cNvGraphicFramePr' - __nspfxs__ = ('wp',) + __tag__ = "wp:cNvGraphicFramePr" + __nspfxs__ = ("wp",) __attrs__ = () class CT_NonVisualPicturePropertiesBuilder(BaseBuilder): - __tag__ = 'pic:cNvPicPr' - __nspfxs__ = ('pic',) - __attrs__ = ('preferRelativeResize') + __tag__ = "pic:cNvPicPr" + __nspfxs__ = ("pic",) + __attrs__ = "preferRelativeResize" class CT_PictureBuilder(BaseBuilder): - __tag__ = 'pic:pic' - __nspfxs__ = ('pic',) + __tag__ = "pic:pic" + __nspfxs__ = ("pic",) __attrs__ = () class CT_PictureNonVisualBuilder(BaseBuilder): - __tag__ = 'pic:nvPicPr' - __nspfxs__ = ('pic',) + __tag__ = "pic:nvPicPr" + __nspfxs__ = ("pic",) __attrs__ = () class CT_Point2DBuilder(BaseBuilder): - __tag__ = 'a:off' - __nspfxs__ = ('a',) - __attrs__ = ('x', 'y') + __tag__ = "a:off" + __nspfxs__ = ("a",) + __attrs__ = ("x", "y") class CT_PositiveSize2DBuilder(BaseBuilder): __nspfxs__ = () - __attrs__ = ('cx', 'cy') + __attrs__ = ("cx", "cy") def __init__(self, tag): self.__tag__ = tag @@ -98,33 +98,33 @@ def __init__(self, tag): class CT_PresetGeometry2DBuilder(BaseBuilder): - __tag__ = 'a:prstGeom' - __nspfxs__ = ('a',) - __attrs__ = ('prst',) + __tag__ = "a:prstGeom" + __nspfxs__ = ("a",) + __attrs__ = ("prst",) class CT_RelativeRectBuilder(BaseBuilder): - __tag__ = 'a:fillRect' - __nspfxs__ = ('a',) - __attrs__ = ('l', 't', 'r', 'b') + __tag__ = "a:fillRect" + __nspfxs__ = ("a",) + __attrs__ = ("l", "t", "r", "b") class CT_ShapePropertiesBuilder(BaseBuilder): - __tag__ = 'pic:spPr' - __nspfxs__ = ('pic', 'a') - __attrs__ = ('bwMode',) + __tag__ = "pic:spPr" + __nspfxs__ = ("pic", "a") + __attrs__ = ("bwMode",) class CT_StretchInfoPropertiesBuilder(BaseBuilder): - __tag__ = 'a:stretch' - __nspfxs__ = ('a',) + __tag__ = "a:stretch" + __nspfxs__ = ("a",) __attrs__ = () class CT_Transform2DBuilder(BaseBuilder): - __tag__ = 'a:xfrm' - __nspfxs__ = ('a',) - __attrs__ = ('rot', 'flipH', 'flipV') + __tag__ = "a:xfrm" + __nspfxs__ = ("a",) + __attrs__ = ("rot", "flipH", "flipV") def a_blip(): @@ -144,11 +144,11 @@ def a_cNvPicPr(): def a_cNvPr(): - return CT_NonVisualDrawingPropsBuilder('pic:cNvPr') + return CT_NonVisualDrawingPropsBuilder("pic:cNvPr") def a_docPr(): - return CT_NonVisualDrawingPropsBuilder('wp:docPr') + return CT_NonVisualDrawingPropsBuilder("wp:docPr") def a_drawing(): @@ -184,11 +184,11 @@ def a_stretch(): def an_ext(): - return CT_PositiveSize2DBuilder('a:ext') + return CT_PositiveSize2DBuilder("a:ext") def an_extent(): - return CT_PositiveSize2DBuilder('wp:extent') + return CT_PositiveSize2DBuilder("wp:extent") def an_inline(): diff --git a/tests/oxml/unitdata/numbering.py b/tests/oxml/unitdata/numbering.py index 984667b32..92f943fdb 100644 --- a/tests/oxml/unitdata/numbering.py +++ b/tests/oxml/unitdata/numbering.py @@ -8,14 +8,14 @@ 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..13194f4c2 100644 --- a/tests/oxml/unitdata/section.py +++ b/tests/oxml/unitdata/section.py @@ -8,30 +8,35 @@ 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..a7007862e 100644 --- a/tests/oxml/unitdata/shared.py +++ b/tests/oxml/unitdata/shared.py @@ -8,20 +8,20 @@ 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 +29,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..0411c93e6 100644 --- a/tests/oxml/unitdata/styles.py +++ b/tests/oxml/unitdata/styles.py @@ -8,14 +8,14 @@ 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..45536d49a 100644 --- a/tests/oxml/unitdata/table.py +++ b/tests/oxml/unitdata/table.py @@ -9,50 +9,50 @@ 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 +73,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..de1d984d3 100644 --- a/tests/oxml/unitdata/text.py +++ b/tests/oxml/unitdata/text.py @@ -9,13 +9,13 @@ 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 +24,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 +88,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 +108,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 +152,7 @@ def a_pPr(): def a_pStyle(): - return CT_StringBuilder('w:pStyle') + return CT_StringBuilder("w:pStyle") def a_sectPr(): @@ -170,27 +168,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 +200,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..36616590b 100644 --- a/tests/parts/test_document.py +++ b/tests/parts/test_document.py @@ -24,7 +24,6 @@ class DescribeDocumentPart(object): - 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 +100,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 +209,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 +217,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 +246,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 +254,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 +262,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 +278,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 +290,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 +306,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 +318,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 +326,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..815b207cc 100644 --- a/tests/parts/test_hdrftr.py +++ b/tests/parts/test_hdrftr.py @@ -16,7 +16,6 @@ class DescribeFooterPart(object): - def it_is_used_by_loader_to_construct_footer_part( self, package_, FooterPart_load_, footer_part_ ): @@ -85,7 +84,6 @@ def parse_xml_(self, request): class DescribeHeaderPart(object): - 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..90a59c2fb 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -18,7 +18,6 @@ class DescribeImagePart(object): - def it_is_used_by_PartFactory_to_construct_image_part( self, image_part_load_, partname_, blob_, package_, image_part_ ): @@ -51,9 +50,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 +60,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 +103,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..44dedde78 100644 --- a/tests/parts/test_numbering.py +++ b/tests/parts/test_numbering.py @@ -16,11 +16,13 @@ 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 + 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 +31,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 +46,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 @@ -56,9 +61,7 @@ def numbering_elm_(self, request): class Describe_NumberingDefinitions(object): - - def it_knows_how_many_numbering_definitions_it_contains( - self, len_fixture): + 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..21ba2ec42 100644 --- a/tests/parts/test_settings.py +++ b/tests/parts/test_settings.py @@ -18,11 +18,10 @@ class DescribeSettingsPart(object): - 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 +40,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 +49,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 +57,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 +66,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..671547582 100644 --- a/tests/parts/test_story.py +++ b/tests/parts/test_story.py @@ -21,7 +21,6 @@ class DescribeBaseStoryPart(object): - 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" diff --git a/tests/parts/test_styles.py b/tests/parts/test_styles.py index 8ff600359..0f2a7b11d 100644 --- a/tests/parts/test_styles.py +++ b/tests/parts/test_styles.py @@ -18,7 +18,6 @@ class DescribeStylesPart(object): - def it_provides_access_to_its_styles(self, styles_fixture): styles_part, Styles_, styles_ = styles_fixture styles = styles_part.styles @@ -29,7 +28,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 +44,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..ee4bead87 100644 --- a/tests/styles/test_latent.py +++ b/tests/styles/test_latent.py @@ -4,9 +4,7 @@ Unit test suite for the docx.styles.latent module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest @@ -16,7 +14,6 @@ class DescribeLatentStyle(object): - def it_can_delete_itself(self, delete_fixture): latent_style, latent_styles, expected_xml = delete_fixture latent_style.delete() @@ -50,77 +47,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)) @@ -129,7 +136,6 @@ def priority_set_fixture(self, request): class DescribeLatentStyles(object): - def it_can_add_a_latent_style(self, add_fixture): latent_styles, name, expected_xml = add_fixture @@ -193,79 +199,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 +313,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..caa281db8 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -4,16 +4,18 @@ Test suite for the docx.styles.style module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest from docx.enum.style import WD_STYLE_TYPE from docx.styles.style import ( - BaseStyle, _CharacterStyle, _ParagraphStyle, _NumberingStyle, - StyleFactory, _TableStyle + BaseStyle, + _CharacterStyle, + _ParagraphStyle, + _NumberingStyle, + StyleFactory, + _TableStyle, ) from docx.text.font import Font from docx.text.parfmt import ParagraphFormat @@ -23,7 +25,6 @@ class DescribeStyleFactory(object): - def it_constructs_the_right_type_of_style(self, factory_fixture): style_elm, StyleCls_, style_ = factory_fixture style = StyleFactory(style_elm) @@ -32,19 +33,27 @@ 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 @@ -53,8 +62,7 @@ def factory_fixture( @pytest.fixture 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 @@ -64,8 +72,7 @@ def paragraph_style_(self, request): @pytest.fixture 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 @@ -75,8 +82,7 @@ def character_style_(self, request): @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 +92,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 @@ -96,7 +101,6 @@ def numbering_style_(self, request): class DescribeBaseStyle(object): - def it_knows_its_style_id(self, id_get_fixture): style, expected_value = id_get_fixture assert style.style_id == expected_value @@ -176,11 +180,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 +194,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)) @@ -376,11 +403,8 @@ def unhide_set_fixture(self, request): class Describe_CharacterStyle(object): - 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,14 +423,13 @@ 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) @@ -420,14 +443,13 @@ 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)) @@ -438,16 +460,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 +479,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): - 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 +500,61 @@ 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': + if style_name == "H1": next_style = _ParagraphStyle(next_style_elm) else: next_style = 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_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 +562,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..c01d2138a 100644 --- a/tests/styles/test_styles.py +++ b/tests/styles/test_styles.py @@ -13,13 +13,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): - 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 +26,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 +34,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] @@ -144,7 +139,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) @@ -176,37 +171,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 +225,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 +320,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 +351,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 +371,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 +405,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..23251cf34 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,9 +4,7 @@ Test suite for the docx.api module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals import pytest @@ -19,7 +17,6 @@ class DescribeDocument(object): - def it_opens_a_docx_file(self, open_fixture): docx, Package_, document_ = open_fixture document = Document(docx) @@ -41,7 +38,7 @@ def it_raises_on_not_a_Word_file(self, raise_fixture): @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 +47,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 +55,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 +71,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..3fe2d0ff5 100644 --- a/tests/test_blkcntnr.py +++ b/tests/test_blkcntnr.py @@ -17,7 +17,6 @@ class DescribeBlockItemContainer(object): - 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 +36,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 +69,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 +85,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 +129,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..27f17f978 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -25,7 +25,6 @@ class DescribeDocument(object): - def it_can_add_a_heading(self, add_heading_fixture, add_paragraph_, paragraph_): level, style = add_heading_fixture add_paragraph_.return_value = paragraph_ @@ -77,7 +76,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 +107,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 +147,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 +175,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 +219,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 +250,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 +275,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 +287,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 +323,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 +331,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 +339,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,7 +351,7 @@ 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): @@ -354,7 +359,6 @@ def tables_(self, request): class Describe_Body(object): - def it_can_clear_itself_of_all_content_it_holds(self, clear_fixture): body, expected_xml = clear_fixture _body = body.clear_content() @@ -363,12 +367,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..a07795ed5 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -12,55 +12,58 @@ import pytest from docx.enum.base import ( - alias, Enumeration, EnumMember, ReturnValueOnlyEnumMember, - XmlEnumeration, XmlMappedEnumMember + alias, + Enumeration, + EnumMember, + ReturnValueOnlyEnumMember, + XmlEnumeration, + XmlMappedEnumMember, ) -@alias('BARFOO') +@alias("BARFOO") class FOOBAR(Enumeration): """ Enumeration docstring """ - __ms_name__ = 'MsoFoobar' + __ms_name__ = "MsoFoobar" - __url__ = 'http://msdn.microsoft.com/foobar.aspx' + __url__ = "http://msdn.microsoft.com/foobar.aspx" __members__ = ( - EnumMember(None, None, 'No setting/remove setting'), - EnumMember('READ_WRITE', 1, 'Readable and settable'), - ReturnValueOnlyEnumMember('READ_ONLY', -2, 'Return value only'), + EnumMember(None, None, "No setting/remove setting"), + EnumMember("READ_WRITE", 1, "Readable and settable"), + ReturnValueOnlyEnumMember("READ_ONLY", -2, "Return value only"), ) -@alias('XML-FU') +@alias("XML-FU") class XMLFOO(XmlEnumeration): """ XmlEnumeration docstring """ - __ms_name__ = 'MsoXmlFoobar' + __ms_name__ = "MsoXmlFoobar" - __url__ = 'http://msdn.microsoft.com/msoxmlfoobar.aspx' + __url__ = "http://msdn.microsoft.com/msoxmlfoobar.aspx" __members__ = ( - XmlMappedEnumMember(None, None, None, 'No setting'), - XmlMappedEnumMember('XML_RW', 42, 'attrVal', 'Read/write setting'), - ReturnValueOnlyEnumMember('RO', -2, 'Return value only;'), + XmlMappedEnumMember(None, None, None, "No setting"), + XmlMappedEnumMember("XML_RW", 42, "attrVal", "Read/write setting"), + ReturnValueOnlyEnumMember("RO", -2, "Return value only;"), ) class DescribeEnumeration(object): - def it_has_the_right_metaclass(self): - assert type(FOOBAR).__name__ == 'MetaEnumeration' + assert type(FOOBAR).__name__ == "MetaEnumeration" def it_provides_an_EnumValue_instance_for_each_named_member(self): with pytest.raises(AttributeError): - getattr(FOOBAR, 'None') + getattr(FOOBAR, "None") for obj in (FOOBAR.READ_WRITE, FOOBAR.READ_ONLY): - assert type(obj).__name__ == 'EnumValue' + assert type(obj).__name__ == "EnumValue" def it_provides_the_enumeration_value_for_each_named_member(self): assert FOOBAR.READ_WRITE == 1 @@ -70,7 +73,7 @@ def it_knows_if_a_setting_is_valid(self): FOOBAR.validate(None) FOOBAR.validate(FOOBAR.READ_WRITE) with pytest.raises(ValueError): - FOOBAR.validate('foobar') + FOOBAR.validate("foobar") with pytest.raises(ValueError): FOOBAR.validate(FOOBAR.READ_ONLY) @@ -79,23 +82,21 @@ def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): class DescribeEnumValue(object): - def it_provides_its_symbolic_name_as_its_string_value(self): - assert ('%s' % FOOBAR.READ_WRITE) == 'READ_WRITE (1)' + assert ("%s" % FOOBAR.READ_WRITE) == "READ_WRITE (1)" def it_provides_its_description_as_its_docstring(self): - assert FOOBAR.READ_ONLY.__doc__ == 'Return value only' + assert FOOBAR.READ_ONLY.__doc__ == "Return value only" class DescribeXmlEnumeration(object): - 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' + 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_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)' + assert XMLFOO.from_xml("attrVal") == XMLFOO.XML_RW + assert str(XMLFOO.from_xml("attrVal")) == "XML_RW (42)" diff --git a/tests/test_package.py b/tests/test_package.py index c4df206ad..e94e5f318 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -16,7 +16,6 @@ class DescribePackage(object): - def it_can_get_or_add_an_image_part_containing_a_specified_image( self, image_parts_prop_, image_parts_, image_part_ ): @@ -30,7 +29,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: @@ -52,7 +51,6 @@ def image_parts_prop_(self, request): class DescribeImageParts(object): - def it_can_get_a_matching_image_part( self, Image_, image_, _get_by_sha1_, image_part_ ): @@ -104,20 +102,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 +122,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 +138,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 +146,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..ae887a960 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -17,7 +17,6 @@ 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 @@ -35,7 +34,8 @@ def it_can_iterate_over_its_Section_instances( section_lst = [s for s in 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_] @@ -71,7 +71,8 @@ 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_] @@ -91,7 +92,6 @@ def section_(self, request): class DescribeSection(object): - def it_knows_when_it_displays_a_distinct_first_page_header( self, diff_first_header_get_fixture ): @@ -115,7 +115,7 @@ def it_can_change_whether_the_document_has_distinct_odd_and_even_headers( def it_provides_access_to_its_even_page_footer( self, document_part_, _Footer_, footer_ ): - sectPr = element('w:sectPr') + sectPr = element("w:sectPr") _Footer_.return_value = footer_ section = Section(sectPr, document_part_) @@ -171,7 +171,7 @@ def it_provides_access_to_its_first_page_header( def it_provides_access_to_its_default_footer( self, document_part_, _Footer_, footer_ ): - sectPr = element('w:sectPr') + sectPr = element("w:sectPr") _Footer_.return_value = footer_ section = Section(sectPr, document_part_) @@ -185,7 +185,7 @@ def it_provides_access_to_its_default_footer( def it_provides_access_to_its_default_header( self, document_part_, _Header_, header_ ): - sectPr = element('w:sectPr') + sectPr = element("w:sectPr") _Header_.return_value = header_ section = Section(sectPr, document_part_) @@ -306,137 +306,171 @@ def diff_first_header_set_fixture(self, request): 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), - ]) + @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}'), - ]) + @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), - ]) + @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'), - ]) + @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') + 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), - ]) + @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}'), - ]) + @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') + 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), - ]) + @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}'), - ]) + @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') + 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), - ]) + @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}'), - ]) + @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) @@ -467,7 +501,6 @@ def header_(self, request): class Describe_BaseHeaderFooter(object): - def it_knows_when_its_linked_to_the_previous_header_or_footer( self, is_linked_get_fixture, _has_definition_prop_ ): @@ -553,7 +586,7 @@ def and_it_adds_a_definition_when_it_is_linked_and_the_first_section( _has_definition_prop_, _prior_headerfooter_prop_, _add_definition_, - header_part_ + header_part_, ): _has_definition_prop_.return_value = False _prior_headerfooter_prop_.return_value = None @@ -620,7 +653,6 @@ def _prior_headerfooter_prop_(self, request): class Describe_Footer(object): - def it_can_add_a_footer_part_to_help(self, document_part_, footer_part_): sectPr = element("w:sectPr{r:a=b}") document_part_.add_footer_part.return_value = footer_part_, "rId3" @@ -692,7 +724,8 @@ def but_it_returns_None_when_its_the_first_footer(self): @pytest.fixture( params=[ - ("w:sectPr", False), ("w:sectPr/w:footerReference{w:type=default}", True) + ("w:sectPr", False), + ("w:sectPr/w:footerReference{w:type=default}", True), ] ) def has_definition_fixture(self, request): @@ -716,7 +749,6 @@ def footer_part_(self, request): class Describe_Header(object): - def it_can_add_a_header_part_to_help(self, document_part_, header_part_): sectPr = element("w:sectPr{r:a=b}") document_part_.add_header_part.return_value = header_part_, "rId3" @@ -787,9 +819,7 @@ def but_it_returns_None_when_its_the_first_header(self): # fixtures ------------------------------------------------------- @pytest.fixture( - params=[ - ("w:sectPr", False), ("w:sectPr/w:headerReference{w:type=first}", True) - ] + params=[("w:sectPr", False), ("w:sectPr/w:headerReference{w:type=first}", True)] ) def has_definition_fixture(self, request): sectPr_cxml, expected_value = request.param diff --git a/tests/test_settings.py b/tests/test_settings.py index 5873ffa1d..5c07a6652 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -12,7 +12,6 @@ class DescribeSettings(object): - def it_knows_when_the_document_has_distinct_odd_and_even_headers( self, odd_and_even_get_fixture ): @@ -56,7 +55,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..5e08e9743 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -14,21 +14,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): + 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,15 +38,13 @@ 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 @@ -62,9 +62,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 +71,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): - 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 +101,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 +111,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 +162,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..7641c158d 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -4,23 +4,18 @@ Test suite for the docx.shared module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals 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 ElementProxy, Length, Cm, Emu, Inches, Mm, Pt, RGBColor, Twips from .unitutil.cxml import element from .unitutil.mock import instance_mock class DescribeElementProxy(object): - 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 +39,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 @@ -75,7 +70,6 @@ def part_(self, request): class DescribeLength(object): - def it_can_construct_from_convenient_units(self, construct_fixture): UnitCls, units_val, emu = construct_fixture length = UnitCls(units_val) @@ -91,50 +85,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): - def it_is_natively_constructed_using_three_ints_0_to_255(self): RGBColor(0x12, 0x34, 0x56) with pytest.raises(ValueError): - RGBColor('12', '34', '56') + RGBColor("12", "34", "56") with pytest.raises(ValueError): RGBColor(-1, 34, 56) with pytest.raises(ValueError): 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..b314b801e 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -8,7 +8,10 @@ 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.table import CT_Tc @@ -25,7 +28,6 @@ class DescribeTable(object): - def it_can_add_a_row(self, add_row_fixture): table, expected_xml = add_row_fixture row = table.add_row() @@ -103,17 +105,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 +133,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 +142,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 +244,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 +295,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 +329,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 +341,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): @@ -325,14 +351,12 @@ def table(self): class Describe_Cell(object): - 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 +381,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 +426,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 +441,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 +490,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) @@ -543,7 +590,6 @@ def tc_2_(self, request): class Describe_Column(object): - def it_provides_access_to_its_cells(self, cells_fixture): column, column_idx, expected_cells = cells_fixture cells = column.cells @@ -580,7 +626,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 +637,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 +670,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 +682,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): - def it_knows_how_many_columns_it_contains(self, columns_fixture): columns, column_count = columns_fixture assert len(columns) == column_count @@ -691,7 +740,6 @@ def table_(self, request): class Describe_Row(object): - def it_knows_its_height(self, height_get_fixture): row, expected_height = height_get_fixture assert row.height == expected_height @@ -734,74 +782,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 +874,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 +889,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 +901,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): - def it_knows_how_many_rows_it_contains(self, rows_fixture): rows, row_count = rows_fixture assert len(rows) == row_count @@ -891,10 +954,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 +981,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..5b4b15eb4 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -4,9 +4,7 @@ Test suite for the docx.text.run module """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from docx.dml.color import ColorFormat from docx.enum.text import WD_COLOR, WD_UNDERLINE @@ -20,7 +18,6 @@ class DescribeFont(object): - def it_provides_access_to_its_color_object(self, color_fixture): font, color_, ColorFormat_ = color_fixture color = font.color @@ -92,86 +89,99 @@ def it_can_change_its_highlight_color(self, highlight_set_fixture): # 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), - ]) + @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}'), - ]) + @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)) @@ -180,199 +190,240 @@ def bool_prop_set_fixture(self, request): @pytest.fixture def color_fixture(self, ColorFormat_, color_): - font = Font(element('w:r')) + 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), - ]) + @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'), - ]) + @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'), - ]) + @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}'), - ]) + @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)), - ]) + @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'), - ]) + @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), - ]) + @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}'), - ]) + @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), - ]) + @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}'), - ]) + @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)) 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), - ]) + @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}'), - ]) + @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) @@ -387,6 +438,4 @@ def color_(self, request): @pytest.fixture def ColorFormat_(self, request, color_): - return class_mock( - request, 'docx.text.font.ColorFormat', return_value=color_ - ) + return class_mock(request, "docx.text.font.ColorFormat", return_value=color_) diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index b8313e61e..e394334e5 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -16,13 +16,10 @@ import pytest from ..unitutil.cxml import element, xml -from ..unitutil.mock import ( - call, class_mock, instance_mock, method_mock, property_mock -) +from ..unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock class DescribeParagraph(object): - def it_knows_its_paragraph_style(self, style_get_fixture): paragraph, style_id_, style_ = style_get_fixture style = paragraph.style @@ -68,9 +65,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 +88,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 +102,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 +174,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 +184,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 +195,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,17 +227,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'), - ]) + @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) @@ -235,17 +247,17 @@ def text_get_fixture(self, request): @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 +265,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 +274,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 +285,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 +304,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..ce9927798 100644 --- a/tests/text/test_parfmt.py +++ b/tests/text/test_parfmt.py @@ -5,9 +5,7 @@ object. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.shared import Pt @@ -21,7 +19,6 @@ class DescribeParagraphFormat(object): - 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 +59,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 +108,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 +477,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 +486,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..02cb87412 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -18,7 +18,6 @@ class DescribeRun(object): - def it_knows_its_bool_prop_states(self, bool_prop_get_fixture): run, prop_name, expected_state = bool_prop_get_fixture assert getattr(run, prop_name) == expected_state @@ -31,17 +30,13 @@ def it_can_change_its_bool_prop_settings(self, bool_prop_set_fixture): 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,8 +48,7 @@ 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): + def it_raises_on_assign_invalid_underline_type(self, underline_raise_fixture): run, underline = underline_raise_fixture with pytest.raises(ValueError): run.underline = underline @@ -112,105 +106,111 @@ 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}'), - ]) + @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) + 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)'), - ]) + @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) @@ -219,29 +219,31 @@ def clear_fixture(self, request): @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 +251,27 @@ 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'), - ]) + @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,46 +279,53 @@ 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']) + @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) + run = Run(element("w:r/w:rPr"), None) return run, invalid_underline_setting # fixture components --------------------------------------------- @@ -323,7 +336,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 +344,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 +356,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..29627fcb9 100644 --- a/tests/text/test_tabstops.py +++ b/tests/text/test_tabstops.py @@ -5,9 +5,7 @@ TabStop objects. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER from docx.shared import Twips @@ -20,7 +18,6 @@ class DescribeTabStop(object): - def it_knows_its_position(self, position_get_fixture): tab_stop, expected_value = position_get_fixture assert tab_stop.position == expected_value @@ -51,20 +48,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 +73,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) @@ -132,15 +152,12 @@ def position_set_fixture(self, request): class DescribeTabStops(object): - 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 +172,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 +190,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 +199,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 +329,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..7fe0a7c12 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -14,17 +14,16 @@ class BaseBuilder(object): """ 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 +33,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'" @@ -85,45 +86,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 +132,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..3029eda77 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -32,6 +32,7 @@ # api functions # ==================================================================== + def element(cxel_str): """ Return an oxml element parsed from the XML generated from *cxel_str*. @@ -59,7 +60,7 @@ def nsdecls(*nspfxs): Return a string containing a namespace declaration for each of *nspfxs*, in the order they are specified. """ - nsdecls = '' + nsdecls = "" for nspfx in nspfxs: nsdecls += ' xmlns:%s="%s"' % (nspfx, nsmap[nspfx]) return nsdecls @@ -70,6 +71,7 @@ class Element(object): 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 @@ -122,10 +124,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 +146,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: @@ -168,10 +172,10 @@ def _xml(self, indent): Return a string containing the XML of this element and all its 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 +191,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 +211,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 +225,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 +238,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 +252,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 +261,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..f87def241 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -8,7 +8,7 @@ _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): @@ -24,7 +24,7 @@ def docx_path(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): @@ -33,11 +33,11 @@ def snippet_seq(name, offset=0, count=1024): 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 + 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]) @@ -47,11 +47,11 @@ def snippet_text(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): diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 828382e7e..4bd1fdccd 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -70,9 +70,7 @@ def instance_mock(request, cls, name=None, spec_set=True, **kwargs): 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): @@ -100,7 +98,7 @@ def open_mock(request, module_name, **kwargs): """ 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() From 80740f2c858da8f89fc4f2d699a2cfbdfe85d7e0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 24 Sep 2023 22:41:47 -0700 Subject: [PATCH 007/131] build: move docx package under src/ directory This improves packaging reliability because it prevents tests from running against the current directory instead of the installed version of the package. --- .gitignore | 3 ++- MANIFEST.in | 2 +- setup.py | 7 +++++-- {docx => src/docx}/__init__.py | 0 {docx => src/docx}/api.py | 0 {docx => src/docx}/blkcntnr.py | 0 {docx => src/docx}/compat.py | 0 {docx => src/docx}/dml/__init__.py | 0 {docx => src/docx}/dml/color.py | 0 {docx => src/docx}/document.py | 0 {docx => src/docx}/enum/__init__.py | 0 {docx => src/docx}/enum/base.py | 0 {docx => src/docx}/enum/dml.py | 0 {docx => src/docx}/enum/section.py | 0 {docx => src/docx}/enum/shape.py | 0 {docx => src/docx}/enum/style.py | 0 {docx => src/docx}/enum/table.py | 0 {docx => src/docx}/enum/text.py | 0 {docx => src/docx}/exceptions.py | 0 {docx => src/docx}/image/__init__.py | 0 {docx => src/docx}/image/bmp.py | 0 {docx => src/docx}/image/constants.py | 0 {docx => src/docx}/image/exceptions.py | 0 {docx => src/docx}/image/gif.py | 0 {docx => src/docx}/image/helpers.py | 0 {docx => src/docx}/image/image.py | 0 {docx => src/docx}/image/jpeg.py | 0 {docx => src/docx}/image/png.py | 0 {docx => src/docx}/image/tiff.py | 0 {docx => src/docx}/opc/__init__.py | 0 {docx => src/docx}/opc/compat.py | 0 {docx => src/docx}/opc/constants.py | 0 {docx => src/docx}/opc/coreprops.py | 0 {docx => src/docx}/opc/exceptions.py | 0 {docx => src/docx}/opc/oxml.py | 0 {docx => src/docx}/opc/package.py | 0 {docx => src/docx}/opc/packuri.py | 0 {docx => src/docx}/opc/part.py | 0 {docx => src/docx}/opc/parts/__init__.py | 0 {docx => src/docx}/opc/parts/coreprops.py | 0 {docx => src/docx}/opc/phys_pkg.py | 0 {docx => src/docx}/opc/pkgreader.py | 0 {docx => src/docx}/opc/pkgwriter.py | 0 {docx => src/docx}/opc/rel.py | 0 {docx => src/docx}/opc/shared.py | 0 {docx => src/docx}/opc/spec.py | 0 {docx => src/docx}/oxml/__init__.py | 0 {docx => src/docx}/oxml/coreprops.py | 0 {docx => src/docx}/oxml/document.py | 0 {docx => src/docx}/oxml/exceptions.py | 0 {docx => src/docx}/oxml/ns.py | 0 {docx => src/docx}/oxml/numbering.py | 0 {docx => src/docx}/oxml/section.py | 0 {docx => src/docx}/oxml/settings.py | 0 {docx => src/docx}/oxml/shape.py | 0 {docx => src/docx}/oxml/shared.py | 0 {docx => src/docx}/oxml/simpletypes.py | 0 {docx => src/docx}/oxml/styles.py | 0 {docx => src/docx}/oxml/table.py | 0 {docx => src/docx}/oxml/text/__init__.py | 0 {docx => src/docx}/oxml/text/font.py | 0 {docx => src/docx}/oxml/text/paragraph.py | 0 {docx => src/docx}/oxml/text/parfmt.py | 0 {docx => src/docx}/oxml/text/run.py | 0 {docx => src/docx}/oxml/xmlchemy.py | 0 {docx => src/docx}/package.py | 0 {docx => src/docx}/parts/__init__.py | 0 {docx => src/docx}/parts/document.py | 0 {docx => src/docx}/parts/hdrftr.py | 0 {docx => src/docx}/parts/image.py | 0 {docx => src/docx}/parts/numbering.py | 0 {docx => src/docx}/parts/settings.py | 0 {docx => src/docx}/parts/story.py | 0 {docx => src/docx}/parts/styles.py | 0 {docx => src/docx}/section.py | 0 {docx => src/docx}/settings.py | 0 {docx => src/docx}/shape.py | 0 {docx => src/docx}/shared.py | 0 {docx => src/docx}/styles/__init__.py | 0 {docx => src/docx}/styles/latent.py | 0 {docx => src/docx}/styles/style.py | 0 {docx => src/docx}/styles/styles.py | 0 {docx => src/docx}/table.py | 0 .../default-docx-template/[Content_Types].xml | 0 .../templates/default-docx-template/_rels/.rels | 0 .../customXml/_rels/item1.xml.rels | 0 .../default-docx-template/customXml/item1.xml | 0 .../default-docx-template/customXml/itemProps1.xml | 0 .../default-docx-template/docProps/app.xml | 0 .../default-docx-template/docProps/core.xml | 0 .../default-docx-template/docProps/thumbnail.jpeg | Bin .../word/_rels/document.xml.rels | 0 .../default-docx-template/word/document.xml | 0 .../default-docx-template/word/fontTable.xml | 0 .../default-docx-template/word/numbering.xml | 0 .../default-docx-template/word/settings.xml | 0 .../templates/default-docx-template/word/styles.xml | 0 .../word/stylesWithEffects.xml | 0 .../default-docx-template/word/theme/theme1.xml | 0 .../default-docx-template/word/webSettings.xml | 0 {docx => src/docx}/templates/default-footer.xml | 0 {docx => src/docx}/templates/default-header.xml | 0 {docx => src/docx}/templates/default-settings.xml | 0 {docx => src/docx}/templates/default-styles.xml | 0 {docx => src/docx}/templates/default.docx | Bin {docx => src/docx}/text/__init__.py | 0 {docx => src/docx}/text/font.py | 0 {docx => src/docx}/text/paragraph.py | 0 {docx => src/docx}/text/parfmt.py | 0 {docx => src/docx}/text/run.py | 0 {docx => src/docx}/text/tabstops.py | 0 111 files changed, 8 insertions(+), 4 deletions(-) rename {docx => src/docx}/__init__.py (100%) rename {docx => src/docx}/api.py (100%) rename {docx => src/docx}/blkcntnr.py (100%) rename {docx => src/docx}/compat.py (100%) rename {docx => src/docx}/dml/__init__.py (100%) rename {docx => src/docx}/dml/color.py (100%) rename {docx => src/docx}/document.py (100%) rename {docx => src/docx}/enum/__init__.py (100%) rename {docx => src/docx}/enum/base.py (100%) rename {docx => src/docx}/enum/dml.py (100%) rename {docx => src/docx}/enum/section.py (100%) rename {docx => src/docx}/enum/shape.py (100%) rename {docx => src/docx}/enum/style.py (100%) rename {docx => src/docx}/enum/table.py (100%) rename {docx => src/docx}/enum/text.py (100%) rename {docx => src/docx}/exceptions.py (100%) rename {docx => src/docx}/image/__init__.py (100%) rename {docx => src/docx}/image/bmp.py (100%) rename {docx => src/docx}/image/constants.py (100%) rename {docx => src/docx}/image/exceptions.py (100%) rename {docx => src/docx}/image/gif.py (100%) rename {docx => src/docx}/image/helpers.py (100%) rename {docx => src/docx}/image/image.py (100%) rename {docx => src/docx}/image/jpeg.py (100%) rename {docx => src/docx}/image/png.py (100%) rename {docx => src/docx}/image/tiff.py (100%) rename {docx => src/docx}/opc/__init__.py (100%) rename {docx => src/docx}/opc/compat.py (100%) rename {docx => src/docx}/opc/constants.py (100%) rename {docx => src/docx}/opc/coreprops.py (100%) rename {docx => src/docx}/opc/exceptions.py (100%) rename {docx => src/docx}/opc/oxml.py (100%) rename {docx => src/docx}/opc/package.py (100%) rename {docx => src/docx}/opc/packuri.py (100%) rename {docx => src/docx}/opc/part.py (100%) rename {docx => src/docx}/opc/parts/__init__.py (100%) rename {docx => src/docx}/opc/parts/coreprops.py (100%) rename {docx => src/docx}/opc/phys_pkg.py (100%) rename {docx => src/docx}/opc/pkgreader.py (100%) rename {docx => src/docx}/opc/pkgwriter.py (100%) rename {docx => src/docx}/opc/rel.py (100%) rename {docx => src/docx}/opc/shared.py (100%) rename {docx => src/docx}/opc/spec.py (100%) rename {docx => src/docx}/oxml/__init__.py (100%) rename {docx => src/docx}/oxml/coreprops.py (100%) rename {docx => src/docx}/oxml/document.py (100%) rename {docx => src/docx}/oxml/exceptions.py (100%) rename {docx => src/docx}/oxml/ns.py (100%) rename {docx => src/docx}/oxml/numbering.py (100%) rename {docx => src/docx}/oxml/section.py (100%) rename {docx => src/docx}/oxml/settings.py (100%) rename {docx => src/docx}/oxml/shape.py (100%) rename {docx => src/docx}/oxml/shared.py (100%) rename {docx => src/docx}/oxml/simpletypes.py (100%) rename {docx => src/docx}/oxml/styles.py (100%) rename {docx => src/docx}/oxml/table.py (100%) rename {docx => src/docx}/oxml/text/__init__.py (100%) rename {docx => src/docx}/oxml/text/font.py (100%) rename {docx => src/docx}/oxml/text/paragraph.py (100%) rename {docx => src/docx}/oxml/text/parfmt.py (100%) rename {docx => src/docx}/oxml/text/run.py (100%) rename {docx => src/docx}/oxml/xmlchemy.py (100%) rename {docx => src/docx}/package.py (100%) rename {docx => src/docx}/parts/__init__.py (100%) rename {docx => src/docx}/parts/document.py (100%) rename {docx => src/docx}/parts/hdrftr.py (100%) rename {docx => src/docx}/parts/image.py (100%) rename {docx => src/docx}/parts/numbering.py (100%) rename {docx => src/docx}/parts/settings.py (100%) rename {docx => src/docx}/parts/story.py (100%) rename {docx => src/docx}/parts/styles.py (100%) rename {docx => src/docx}/section.py (100%) rename {docx => src/docx}/settings.py (100%) rename {docx => src/docx}/shape.py (100%) rename {docx => src/docx}/shared.py (100%) rename {docx => src/docx}/styles/__init__.py (100%) rename {docx => src/docx}/styles/latent.py (100%) rename {docx => src/docx}/styles/style.py (100%) rename {docx => src/docx}/styles/styles.py (100%) rename {docx => src/docx}/table.py (100%) rename {docx => src/docx}/templates/default-docx-template/[Content_Types].xml (100%) rename {docx => src/docx}/templates/default-docx-template/_rels/.rels (100%) rename {docx => src/docx}/templates/default-docx-template/customXml/_rels/item1.xml.rels (100%) rename {docx => src/docx}/templates/default-docx-template/customXml/item1.xml (100%) rename {docx => src/docx}/templates/default-docx-template/customXml/itemProps1.xml (100%) rename {docx => src/docx}/templates/default-docx-template/docProps/app.xml (100%) rename {docx => src/docx}/templates/default-docx-template/docProps/core.xml (100%) rename {docx => src/docx}/templates/default-docx-template/docProps/thumbnail.jpeg (100%) rename {docx => src/docx}/templates/default-docx-template/word/_rels/document.xml.rels (100%) rename {docx => src/docx}/templates/default-docx-template/word/document.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/fontTable.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/numbering.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/settings.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/styles.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/stylesWithEffects.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/theme/theme1.xml (100%) rename {docx => src/docx}/templates/default-docx-template/word/webSettings.xml (100%) rename {docx => src/docx}/templates/default-footer.xml (100%) rename {docx => src/docx}/templates/default-header.xml (100%) rename {docx => src/docx}/templates/default-settings.xml (100%) rename {docx => src/docx}/templates/default-styles.xml (100%) rename {docx => src/docx}/templates/default.docx (100%) rename {docx => src/docx}/text/__init__.py (100%) rename {docx => src/docx}/text/font.py (100%) rename {docx => src/docx}/text/paragraph.py (100%) rename {docx => src/docx}/text/parfmt.py (100%) rename {docx => src/docx}/text/run.py (100%) rename {docx => src/docx}/text/tabstops.py (100%) 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/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/setup.py b/setup.py index 7c34edcca..d99f736b5 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,9 @@ def text_of(relpath): # 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) +version = re.search(r'__version__ = "([^"]+)"', text_of("src/docx/__init__.py")).group( + 1 +) NAME = "python-docx" @@ -32,7 +34,7 @@ def text_of(relpath): AUTHOR_EMAIL = "python-docx@googlegroups.com" URL = "https://github.com/python-openxml/python-docx" LICENSE = text_of("LICENSE") -PACKAGES = find_packages(exclude=["tests", "tests.*"]) +PACKAGES = find_packages(where="src") PACKAGE_DATA = {"docx": ["templates/*.xml", "templates/*.docx"]} INSTALL_REQUIRES = ["lxml>=2.3.2"] @@ -72,6 +74,7 @@ def text_of(relpath): "license": LICENSE, "packages": PACKAGES, "package_data": PACKAGE_DATA, + "package_dir": {"": "src"}, "install_requires": INSTALL_REQUIRES, "tests_require": TESTS_REQUIRE, "test_suite": TEST_SUITE, diff --git a/docx/__init__.py b/src/docx/__init__.py similarity index 100% rename from docx/__init__.py rename to src/docx/__init__.py diff --git a/docx/api.py b/src/docx/api.py similarity index 100% rename from docx/api.py rename to src/docx/api.py diff --git a/docx/blkcntnr.py b/src/docx/blkcntnr.py similarity index 100% rename from docx/blkcntnr.py rename to src/docx/blkcntnr.py diff --git a/docx/compat.py b/src/docx/compat.py similarity index 100% rename from docx/compat.py rename to src/docx/compat.py 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/docx/dml/color.py b/src/docx/dml/color.py similarity index 100% rename from docx/dml/color.py rename to src/docx/dml/color.py diff --git a/docx/document.py b/src/docx/document.py similarity index 100% rename from docx/document.py rename to src/docx/document.py diff --git a/docx/enum/__init__.py b/src/docx/enum/__init__.py similarity index 100% rename from docx/enum/__init__.py rename to src/docx/enum/__init__.py diff --git a/docx/enum/base.py b/src/docx/enum/base.py similarity index 100% rename from docx/enum/base.py rename to src/docx/enum/base.py diff --git a/docx/enum/dml.py b/src/docx/enum/dml.py similarity index 100% rename from docx/enum/dml.py rename to src/docx/enum/dml.py diff --git a/docx/enum/section.py b/src/docx/enum/section.py similarity index 100% rename from docx/enum/section.py rename to src/docx/enum/section.py diff --git a/docx/enum/shape.py b/src/docx/enum/shape.py similarity index 100% rename from docx/enum/shape.py rename to src/docx/enum/shape.py diff --git a/docx/enum/style.py b/src/docx/enum/style.py similarity index 100% rename from docx/enum/style.py rename to src/docx/enum/style.py diff --git a/docx/enum/table.py b/src/docx/enum/table.py similarity index 100% rename from docx/enum/table.py rename to src/docx/enum/table.py diff --git a/docx/enum/text.py b/src/docx/enum/text.py similarity index 100% rename from docx/enum/text.py rename to src/docx/enum/text.py diff --git a/docx/exceptions.py b/src/docx/exceptions.py similarity index 100% rename from docx/exceptions.py rename to src/docx/exceptions.py diff --git a/docx/image/__init__.py b/src/docx/image/__init__.py similarity index 100% rename from docx/image/__init__.py rename to src/docx/image/__init__.py diff --git a/docx/image/bmp.py b/src/docx/image/bmp.py similarity index 100% rename from docx/image/bmp.py rename to src/docx/image/bmp.py diff --git a/docx/image/constants.py b/src/docx/image/constants.py similarity index 100% rename from docx/image/constants.py rename to src/docx/image/constants.py diff --git a/docx/image/exceptions.py b/src/docx/image/exceptions.py similarity index 100% rename from docx/image/exceptions.py rename to src/docx/image/exceptions.py diff --git a/docx/image/gif.py b/src/docx/image/gif.py similarity index 100% rename from docx/image/gif.py rename to src/docx/image/gif.py diff --git a/docx/image/helpers.py b/src/docx/image/helpers.py similarity index 100% rename from docx/image/helpers.py rename to src/docx/image/helpers.py diff --git a/docx/image/image.py b/src/docx/image/image.py similarity index 100% rename from docx/image/image.py rename to src/docx/image/image.py diff --git a/docx/image/jpeg.py b/src/docx/image/jpeg.py similarity index 100% rename from docx/image/jpeg.py rename to src/docx/image/jpeg.py diff --git a/docx/image/png.py b/src/docx/image/png.py similarity index 100% rename from docx/image/png.py rename to src/docx/image/png.py diff --git a/docx/image/tiff.py b/src/docx/image/tiff.py similarity index 100% rename from docx/image/tiff.py rename to src/docx/image/tiff.py 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/docx/opc/compat.py b/src/docx/opc/compat.py similarity index 100% rename from docx/opc/compat.py rename to src/docx/opc/compat.py diff --git a/docx/opc/constants.py b/src/docx/opc/constants.py similarity index 100% rename from docx/opc/constants.py rename to src/docx/opc/constants.py diff --git a/docx/opc/coreprops.py b/src/docx/opc/coreprops.py similarity index 100% rename from docx/opc/coreprops.py rename to src/docx/opc/coreprops.py diff --git a/docx/opc/exceptions.py b/src/docx/opc/exceptions.py similarity index 100% rename from docx/opc/exceptions.py rename to src/docx/opc/exceptions.py diff --git a/docx/opc/oxml.py b/src/docx/opc/oxml.py similarity index 100% rename from docx/opc/oxml.py rename to src/docx/opc/oxml.py diff --git a/docx/opc/package.py b/src/docx/opc/package.py similarity index 100% rename from docx/opc/package.py rename to src/docx/opc/package.py diff --git a/docx/opc/packuri.py b/src/docx/opc/packuri.py similarity index 100% rename from docx/opc/packuri.py rename to src/docx/opc/packuri.py diff --git a/docx/opc/part.py b/src/docx/opc/part.py similarity index 100% rename from docx/opc/part.py rename to src/docx/opc/part.py 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/docx/opc/parts/coreprops.py b/src/docx/opc/parts/coreprops.py similarity index 100% rename from docx/opc/parts/coreprops.py rename to src/docx/opc/parts/coreprops.py diff --git a/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py similarity index 100% rename from docx/opc/phys_pkg.py rename to src/docx/opc/phys_pkg.py diff --git a/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py similarity index 100% rename from docx/opc/pkgreader.py rename to src/docx/opc/pkgreader.py diff --git a/docx/opc/pkgwriter.py b/src/docx/opc/pkgwriter.py similarity index 100% rename from docx/opc/pkgwriter.py rename to src/docx/opc/pkgwriter.py diff --git a/docx/opc/rel.py b/src/docx/opc/rel.py similarity index 100% rename from docx/opc/rel.py rename to src/docx/opc/rel.py diff --git a/docx/opc/shared.py b/src/docx/opc/shared.py similarity index 100% rename from docx/opc/shared.py rename to src/docx/opc/shared.py diff --git a/docx/opc/spec.py b/src/docx/opc/spec.py similarity index 100% rename from docx/opc/spec.py rename to src/docx/opc/spec.py diff --git a/docx/oxml/__init__.py b/src/docx/oxml/__init__.py similarity index 100% rename from docx/oxml/__init__.py rename to src/docx/oxml/__init__.py diff --git a/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py similarity index 100% rename from docx/oxml/coreprops.py rename to src/docx/oxml/coreprops.py diff --git a/docx/oxml/document.py b/src/docx/oxml/document.py similarity index 100% rename from docx/oxml/document.py rename to src/docx/oxml/document.py diff --git a/docx/oxml/exceptions.py b/src/docx/oxml/exceptions.py similarity index 100% rename from docx/oxml/exceptions.py rename to src/docx/oxml/exceptions.py diff --git a/docx/oxml/ns.py b/src/docx/oxml/ns.py similarity index 100% rename from docx/oxml/ns.py rename to src/docx/oxml/ns.py diff --git a/docx/oxml/numbering.py b/src/docx/oxml/numbering.py similarity index 100% rename from docx/oxml/numbering.py rename to src/docx/oxml/numbering.py diff --git a/docx/oxml/section.py b/src/docx/oxml/section.py similarity index 100% rename from docx/oxml/section.py rename to src/docx/oxml/section.py diff --git a/docx/oxml/settings.py b/src/docx/oxml/settings.py similarity index 100% rename from docx/oxml/settings.py rename to src/docx/oxml/settings.py diff --git a/docx/oxml/shape.py b/src/docx/oxml/shape.py similarity index 100% rename from docx/oxml/shape.py rename to src/docx/oxml/shape.py diff --git a/docx/oxml/shared.py b/src/docx/oxml/shared.py similarity index 100% rename from docx/oxml/shared.py rename to src/docx/oxml/shared.py diff --git a/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py similarity index 100% rename from docx/oxml/simpletypes.py rename to src/docx/oxml/simpletypes.py diff --git a/docx/oxml/styles.py b/src/docx/oxml/styles.py similarity index 100% rename from docx/oxml/styles.py rename to src/docx/oxml/styles.py diff --git a/docx/oxml/table.py b/src/docx/oxml/table.py similarity index 100% rename from docx/oxml/table.py rename to src/docx/oxml/table.py 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/docx/oxml/text/font.py b/src/docx/oxml/text/font.py similarity index 100% rename from docx/oxml/text/font.py rename to src/docx/oxml/text/font.py diff --git a/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py similarity index 100% rename from docx/oxml/text/paragraph.py rename to src/docx/oxml/text/paragraph.py diff --git a/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py similarity index 100% rename from docx/oxml/text/parfmt.py rename to src/docx/oxml/text/parfmt.py diff --git a/docx/oxml/text/run.py b/src/docx/oxml/text/run.py similarity index 100% rename from docx/oxml/text/run.py rename to src/docx/oxml/text/run.py diff --git a/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py similarity index 100% rename from docx/oxml/xmlchemy.py rename to src/docx/oxml/xmlchemy.py diff --git a/docx/package.py b/src/docx/package.py similarity index 100% rename from docx/package.py rename to src/docx/package.py 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 100% rename from docx/parts/document.py rename to src/docx/parts/document.py diff --git a/docx/parts/hdrftr.py b/src/docx/parts/hdrftr.py similarity index 100% rename from docx/parts/hdrftr.py rename to src/docx/parts/hdrftr.py diff --git a/docx/parts/image.py b/src/docx/parts/image.py similarity index 100% rename from docx/parts/image.py rename to src/docx/parts/image.py diff --git a/docx/parts/numbering.py b/src/docx/parts/numbering.py similarity index 100% rename from docx/parts/numbering.py rename to src/docx/parts/numbering.py diff --git a/docx/parts/settings.py b/src/docx/parts/settings.py similarity index 100% rename from docx/parts/settings.py rename to src/docx/parts/settings.py diff --git a/docx/parts/story.py b/src/docx/parts/story.py similarity index 100% rename from docx/parts/story.py rename to src/docx/parts/story.py diff --git a/docx/parts/styles.py b/src/docx/parts/styles.py similarity index 100% rename from docx/parts/styles.py rename to src/docx/parts/styles.py diff --git a/docx/section.py b/src/docx/section.py similarity index 100% rename from docx/section.py rename to src/docx/section.py diff --git a/docx/settings.py b/src/docx/settings.py similarity index 100% rename from docx/settings.py rename to src/docx/settings.py diff --git a/docx/shape.py b/src/docx/shape.py similarity index 100% rename from docx/shape.py rename to src/docx/shape.py diff --git a/docx/shared.py b/src/docx/shared.py similarity index 100% rename from docx/shared.py rename to src/docx/shared.py diff --git a/docx/styles/__init__.py b/src/docx/styles/__init__.py similarity index 100% rename from docx/styles/__init__.py rename to src/docx/styles/__init__.py diff --git a/docx/styles/latent.py b/src/docx/styles/latent.py similarity index 100% rename from docx/styles/latent.py rename to src/docx/styles/latent.py diff --git a/docx/styles/style.py b/src/docx/styles/style.py similarity index 100% rename from docx/styles/style.py rename to src/docx/styles/style.py diff --git a/docx/styles/styles.py b/src/docx/styles/styles.py similarity index 100% rename from docx/styles/styles.py rename to src/docx/styles/styles.py diff --git a/docx/table.py b/src/docx/table.py similarity index 100% rename from docx/table.py rename to src/docx/table.py 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/docx/text/font.py b/src/docx/text/font.py similarity index 100% rename from docx/text/font.py rename to src/docx/text/font.py diff --git a/docx/text/paragraph.py b/src/docx/text/paragraph.py similarity index 100% rename from docx/text/paragraph.py rename to src/docx/text/paragraph.py diff --git a/docx/text/parfmt.py b/src/docx/text/parfmt.py similarity index 100% rename from docx/text/parfmt.py rename to src/docx/text/parfmt.py diff --git a/docx/text/run.py b/src/docx/text/run.py similarity index 100% rename from docx/text/run.py rename to src/docx/text/run.py diff --git a/docx/text/tabstops.py b/src/docx/text/tabstops.py similarity index 100% rename from docx/text/tabstops.py rename to src/docx/text/tabstops.py From 6ac2c6909b3720abdcb8bab0088659306336a63e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 24 Sep 2023 22:13:46 -0700 Subject: [PATCH 008/131] build: modernize packaging --- Makefile | 52 +++++++++++++++++--------- README.rst | 2 +- pyproject.toml | 79 ++++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 5 +++ requirements-test.txt | 5 +++ requirements.txt | 5 --- setup.py | 85 ------------------------------------------- src/docx/__init__.py | 2 +- tox.ini | 32 +--------------- 9 files changed, 127 insertions(+), 140 deletions(-) create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 requirements-test.txt delete mode 100644 setup.py diff --git a/Makefile b/Makefile index f335818fe..6d5dd85b8 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: sdist wheel + $(TWINE) upload dist/* -upload: - $(SETUP) sdist upload +wheel: + $(BUILD) --wheel . diff --git a/README.rst b/README.rst index 82d1f0bd7..afc1dadeb 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ .. 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 +*python-docx* is a Python library for reading, creating, and updating Microsoft Word (.docx) files. More information is available in the `python-docx documentation`_. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..251d4e955 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,79 @@ +[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>=2.3.2", +] +description = "Create, read, and update Microsoft Word .docx files." +dynamic = ["version"] +keywords = ["docx", "office", "openxml", "word"] +license = { text = "MIT" } +readme = "README.rst" +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_", "they_", "and_it_", "but_it_"] + +[tool.ruff] +exclude = [] +ignore = [ + "COM812", # -- over-aggressively insists on trailing commas where not desired -- +] +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.setuptools.dynamic] +version = {attr = "docx.__version__"} 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-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..17b9696d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1 @@ -behave>=1.2.3 -flake8>=2.0 lxml>=3.1.0 -mock>=1.0.1 -pyparsing>=2.0.1 -pytest>=2.5 diff --git a/setup.py b/setup.py deleted file mode 100644 index d99f736b5..000000000 --- a/setup.py +++ /dev/null @@ -1,85 +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("src/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(where="src") -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, - "package_dir": {"": "src"}, - "install_requires": INSTALL_REQUIRES, - "tests_require": TESTS_REQUIRE, - "test_suite": TEST_SUITE, - "classifiers": CLASSIFIERS, - "zip_safe": ZIP_SAFE, -} - -setup(**params) diff --git a/src/docx/__init__.py b/src/docx/__init__.py index 59756c021..c3543088b 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -2,7 +2,7 @@ from docx.api import Document # noqa -__version__ = "0.8.11" +__version__ = "1.0.0rc1" # register custom Part classes with opc package reader diff --git a/tox.ini b/tox.ini index 6ced79b71..f8c0ce994 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,5 @@ -# -# 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 = @@ -24,20 +11,3 @@ deps = 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 From 163cfc1bf7f23914742678310c15db4ece19d418 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 25 Sep 2023 00:07:59 -0700 Subject: [PATCH 009/131] lint: add isort linting in ruff This required a large number of adjustments. I took the opportunity to make some further clean-up of top-matter. --- features/steps/api.py | 8 +------- features/steps/block.py | 7 +------ features/steps/cell.py | 9 +-------- features/steps/coreprops.py | 9 +-------- features/steps/document.py | 11 ++--------- features/steps/font.py | 9 +-------- features/steps/hdrftr.py | 7 +------ features/steps/helpers.py | 6 +----- features/steps/image.py | 9 +-------- features/steps/numbering.py | 7 +------ features/steps/paragraph.py | 7 +------ features/steps/parfmt.py | 9 +-------- features/steps/section.py | 9 +-------- features/steps/settings.py | 7 +------ features/steps/shape.py | 9 +-------- features/steps/shared.py | 7 +------ features/steps/styles.py | 8 ++------ features/steps/table.py | 23 ++++++++++++----------- features/steps/tabstops.py | 7 +------ features/steps/text.py | 9 +-------- pyproject.toml | 6 +++++- src/docx/__init__.py | 6 ++---- src/docx/enum/dml.py | 10 ++-------- src/docx/enum/section.py | 10 ++-------- src/docx/enum/style.py | 10 ++-------- src/docx/enum/table.py | 10 ++-------- src/docx/enum/text.py | 10 ++-------- src/docx/image/__init__.py | 10 +++------- src/docx/image/helpers.py | 5 ----- src/docx/image/image.py | 15 ++++++--------- src/docx/opc/oxml.py | 16 ++++++---------- src/docx/opc/part.py | 22 ++++++++-------------- src/docx/opc/parts/coreprops.py | 18 ++++++------------ src/docx/opc/phys_pkg.py | 17 +++++------------ src/docx/opc/spec.py | 9 ++------- src/docx/oxml/__init__.py | 11 +++-------- src/docx/oxml/coreprops.py | 7 +------ src/docx/oxml/document.py | 9 ++------- src/docx/oxml/ns.py | 9 +-------- src/docx/oxml/table.py | 22 +++++++++------------- src/docx/oxml/text/font.py | 30 ++++++++++++++++++------------ src/docx/oxml/xmlchemy.py | 11 ++--------- src/docx/styles/latent.py | 12 +++--------- src/docx/styles/style.py | 18 ++++++------------ src/docx/table.py | 16 +++++----------- src/docx/text/paragraph.py | 16 +++++----------- src/docx/text/parfmt.py | 14 ++++---------- src/docx/text/run.py | 18 ++++++------------ src/docx/text/tabstops.py | 10 ++-------- tests/dml/test_color.py | 12 +++--------- tests/image/test_bmp.py | 10 ++-------- tests/image/test_gif.py | 6 +----- tests/image/test_helpers.py | 8 +------- tests/image/test_image.py | 4 ---- tests/image/test_jpeg.py | 10 +++------- tests/image/test_png.py | 10 +++------- tests/image/test_tiff.py | 6 +----- tests/opc/parts/test_coreprops.py | 8 +------- tests/opc/test_coreprops.py | 10 ++-------- tests/opc/test_oxml.py | 8 ++------ tests/opc/test_package.py | 10 +++------- tests/opc/test_packuri.py | 6 +----- tests/opc/test_part.py | 8 ++------ tests/opc/test_phys_pkg.py | 17 +++++------------ tests/opc/test_pkgreader.py | 15 ++++++--------- tests/opc/test_pkgwriter.py | 14 +++++--------- tests/opc/test_rel.py | 12 +++--------- tests/opc/unitdata/rels.py | 13 +++---------- tests/oxml/test__init__.py | 9 +-------- tests/oxml/test_ns.py | 8 +------- tests/oxml/test_styles.py | 8 +------- tests/oxml/test_table.py | 6 +----- tests/oxml/test_xmlchemy.py | 14 ++++---------- tests/parts/test_document.py | 6 +----- tests/parts/test_hdrftr.py | 9 +++------ tests/parts/test_image.py | 9 +++------ tests/parts/test_numbering.py | 8 +------- tests/parts/test_settings.py | 4 ---- tests/parts/test_story.py | 6 +----- tests/parts/test_styles.py | 8 +------- tests/styles/test_latent.py | 10 ++-------- tests/styles/test_style.py | 12 +++--------- tests/styles/test_styles.py | 6 +----- tests/test_api.py | 11 ++--------- tests/test_blkcntnr.py | 6 +----- tests/test_document.py | 8 ++------ tests/test_enum.py | 12 ++++-------- tests/test_package.py | 6 +----- tests/test_section.py | 8 ++------ tests/test_settings.py | 6 +----- tests/test_shape.py | 8 +------- tests/test_shared.py | 10 ++-------- tests/test_table.py | 8 ++------ tests/text/test_font.py | 10 ++-------- tests/text/test_paragraph.py | 8 ++------ tests/text/test_parfmt.py | 11 ++--------- tests/text/test_run.py | 8 ++------ tests/text/test_tabstops.py | 11 ++--------- tests/unitutil/cxml.py | 17 ++++++----------- tests/unitutil/file.py | 7 +------ 100 files changed, 259 insertions(+), 760 deletions(-) diff --git a/features/steps/api.py b/features/steps/api.py index 1c66c4c89..16038ffe7 100644 --- a/features/steps/api.py +++ b/features/steps/api.py @@ -1,18 +1,12 @@ -# 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 ==================================================== diff --git a/features/steps/block.py b/features/steps/block.py index 686da2c99..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,7 +7,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/cell.py b/features/steps/cell.py index 4ec633ae3..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,7 +6,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/coreprops.py b/features/steps/coreprops.py index e13c5f8fb..0f6b6a854 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -1,10 +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 @@ -15,7 +9,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/document.py b/features/steps/document.py index 2aae3e191..49165efc3 100644 --- a/features/steps/document.py +++ b/features/steps/document.py @@ -1,25 +1,18 @@ -# 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 =================================================== diff --git a/features/steps/font.py b/features/steps/font.py index ed6e51f25..63ca6b48e 100644 --- a/features/steps/font.py +++ b/features/steps/font.py @@ -1,10 +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 @@ -16,7 +10,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/hdrftr.py b/features/steps/hdrftr.py index 4f0e3b915..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,7 +6,6 @@ from helpers import test_docx, test_file - # given ==================================================== diff --git a/features/steps/helpers.py b/features/steps/helpers.py index cd64a7861..6b4300e94 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Helper methods and variables for acceptance tests. -""" +"""Helper methods and variables for acceptance tests.""" import os diff --git a/features/steps/image.py b/features/steps/image.py index 7f80baa59..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,7 +6,6 @@ from helpers import test_file - # given =================================================== diff --git a/features/steps/numbering.py b/features/steps/numbering.py index 2a20fd087..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,7 +6,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/paragraph.py b/features/steps/paragraph.py index d372983a0..ae827a1ab 100644 --- a/features/steps/paragraph.py +++ b/features/steps/paragraph.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for paragraph-related features -""" +"""Step implementations for paragraph-related features.""" from behave import given, then, when @@ -12,7 +8,6 @@ from helpers import saved_docx_path, test_docx, test_text - # given =================================================== diff --git a/features/steps/parfmt.py b/features/steps/parfmt.py index 197cf1626..ca39c8575 100644 --- a/features/steps/parfmt.py +++ b/features/steps/parfmt.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for paragraph format-related features. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Step implementations for paragraph format-related features.""" from behave import given, then, when @@ -15,7 +9,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/section.py b/features/steps/section.py index 1eae8017c..4e0621056 100644 --- a/features/steps/section.py +++ b/features/steps/section.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for section-related features -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Step implementations for section-related features.""" from behave import given, then, when @@ -15,7 +9,6 @@ from helpers import test_docx - # given ==================================================== diff --git a/features/steps/settings.py b/features/steps/settings.py index 9c6d94e2d..286ec6c2d 100644 --- a/features/steps/settings.py +++ b/features/steps/settings.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Step implementations for document settings-related features""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Step implementations for document settings-related features.""" from behave import given, then, when @@ -11,7 +7,6 @@ from helpers import test_docx - # given ==================================================== diff --git a/features/steps/shape.py b/features/steps/shape.py index b35b385ad..d0acb7fd5 100644 --- a/features/steps/shape.py +++ b/features/steps/shape.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for graphical object (shape) related features -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Step implementations for graphical object (shape) related features.""" import hashlib @@ -17,7 +11,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/shared.py b/features/steps/shared.py index c5e2881fe..987721648 100644 --- a/features/steps/shared.py +++ b/features/steps/shared.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -General-purpose step implementations -""" +"""General-purpose step implementations.""" import os @@ -12,7 +8,6 @@ from helpers import saved_docx_path - # given =================================================== diff --git a/features/steps/styles.py b/features/steps/styles.py index c4b34be85..ad8730965 100644 --- a/features/steps/styles.py +++ b/features/steps/styles.py @@ -1,14 +1,10 @@ -# encoding: utf-8 - -""" -Step implementations for styles-related features -""" +"""Step implementations for styles-related features.""" from behave import given, then, when from docx import Document from docx.enum.style import WD_STYLE_TYPE -from docx.styles.latent import _LatentStyle, LatentStyles +from docx.styles.latent import LatentStyles, _LatentStyle from docx.styles.style import BaseStyle from docx.text.font import Font from docx.text.parfmt import ParagraphFormat diff --git a/features/steps/table.py b/features/steps/table.py index 83751d1f2..95f2fab75 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -1,22 +1,19 @@ -# encoding: utf-8 - -""" -Step implementations for table-related features -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Step implementations for table-related features.""" from behave import given, then, when from docx import Document -from docx.enum.table import WD_ALIGN_VERTICAL # noqa -from docx.enum.table import WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION +from docx.enum.table import ( + WD_ALIGN_VERTICAL, + WD_ROW_HEIGHT_RULE, + WD_TABLE_ALIGNMENT, + WD_TABLE_DIRECTION, +) from docx.shared import Inches from docx.table import _Column, _Columns, _Row, _Rows from helpers import test_docx - # given =================================================== @@ -263,7 +260,11 @@ def when_I_set_the_table_autofit_to_setting(context, setting): @then("cell.vertical_alignment is {value}") def then_cell_vertical_alignment_is_value(context, value): - expected_value = eval(value) + expected_value = { + "None": None, + "WD_ALIGN_VERTICAL.BOTTOM": WD_ALIGN_VERTICAL.BOTTOM, + "WD_ALIGN_VERTICAL.CENTER": WD_ALIGN_VERTICAL.CENTER, + }[value] actual_value = context.cell.vertical_alignment assert actual_value is expected_value, ( "cell.vertical_alignment is %s" % actual_value diff --git a/features/steps/tabstops.py b/features/steps/tabstops.py index b360d480e..03f6f203c 100644 --- a/features/steps/tabstops.py +++ b/features/steps/tabstops.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for paragraph-related features -""" +"""Step implementations for paragraph-related features.""" from behave import given, then, when @@ -13,7 +9,6 @@ from helpers import test_docx - # given =================================================== diff --git a/features/steps/text.py b/features/steps/text.py index 57451a1bb..3eb5b0fee 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for text-related features -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Step implementations for text-related features.""" import hashlib @@ -19,7 +13,6 @@ from helpers import test_docx, test_file, test_text - # given =================================================== diff --git a/pyproject.toml b/pyproject.toml index 251d4e955..d64b2d130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ select = [ "COM", # -- flake8-commas -- "E", # -- pycodestyle errors -- "F", # -- pyflakes -- - # "I", # -- isort (imports) -- + "I", # -- isort (imports) -- "PLR0402", # -- Name compared with itself like `foo == foo` -- # "PT", # -- flake8-pytest-style -- # "SIM", # -- flake8-simplify -- @@ -75,5 +75,9 @@ select = [ ] target-version = "py37" +[tool.ruff.isort] +known-first-party = ["docx"] +known-local-folder = ["helpers"] + [tool.setuptools.dynamic] version = {attr = "docx.__version__"} diff --git a/src/docx/__init__.py b/src/docx/__init__.py index c3543088b..19fea99eb 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - from docx.api import Document # noqa __version__ = "1.0.0rc1" @@ -7,10 +5,10 @@ # 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/src/docx/enum/dml.py b/src/docx/enum/dml.py index 133d824d6..f85923182 100644 --- a/src/docx/enum/dml.py +++ b/src/docx/enum/dml.py @@ -1,12 +1,6 @@ -# encoding: utf-8 +"""Enumerations used by DrawingML objects.""" -""" -Enumerations used by DrawingML objects -""" - -from __future__ import absolute_import - -from .base import alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember +from .base import Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember, alias class MSO_COLOR_TYPE(Enumeration): diff --git a/src/docx/enum/section.py b/src/docx/enum/section.py index 814e4cfba..df0cd5414 100644 --- a/src/docx/enum/section.py +++ b/src/docx/enum/section.py @@ -1,12 +1,6 @@ -# encoding: utf-8 +"""Enumerations related to the main document in WordprocessingML files.""" -""" -Enumerations related to the main document in WordprocessingML files -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from .base import alias, XmlEnumeration, XmlMappedEnumMember +from .base import XmlEnumeration, XmlMappedEnumMember, alias @alias("WD_HEADER_FOOTER") diff --git a/src/docx/enum/style.py b/src/docx/enum/style.py index 1915429c0..3330f0a0e 100644 --- a/src/docx/enum/style.py +++ b/src/docx/enum/style.py @@ -1,12 +1,6 @@ -# encoding: utf-8 +"""Enumerations related to styles.""" -""" -Enumerations related to styles -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from .base import alias, EnumMember, XmlEnumeration, XmlMappedEnumMember +from .base import EnumMember, XmlEnumeration, XmlMappedEnumMember, alias @alias("WD_STYLE") diff --git a/src/docx/enum/table.py b/src/docx/enum/table.py index f66fdccd5..30982a83c 100644 --- a/src/docx/enum/table.py +++ b/src/docx/enum/table.py @@ -1,12 +1,6 @@ -# encoding: utf-8 +"""Enumerations related to tables in WordprocessingML files.""" -""" -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 +from .base import Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember, alias @alias("WD_ALIGN_VERTICAL") diff --git a/src/docx/enum/text.py b/src/docx/enum/text.py index 1de05326f..8015187e4 100644 --- a/src/docx/enum/text.py +++ b/src/docx/enum/text.py @@ -1,12 +1,6 @@ -# encoding: utf-8 +"""Enumerations related to text in WordprocessingML files.""" -""" -Enumerations related to text in WordprocessingML files -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from .base import alias, EnumMember, XmlEnumeration, XmlMappedEnumMember +from .base import EnumMember, XmlEnumeration, XmlMappedEnumMember, alias @alias("WD_ALIGN_PARAGRAPH") diff --git a/src/docx/image/__init__.py b/src/docx/image/__init__.py index e85234545..d28033ef1 100644 --- a/src/docx/image/__init__.py +++ b/src/docx/image/__init__.py @@ -1,11 +1,8 @@ -# encoding: utf-8 +"""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. """ -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 @@ -13,7 +10,6 @@ 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"), diff --git a/src/docx/image/helpers.py b/src/docx/image/helpers.py index 83b593bf3..c98795657 100644 --- a/src/docx/image/helpers.py +++ b/src/docx/image/helpers.py @@ -1,12 +1,7 @@ -# encoding: utf-8 - -from __future__ import absolute_import, division, print_function - from struct import Struct from .exceptions import UnexpectedEndOfFileError - BIG_ENDIAN = ">" LITTLE_ENDIAN = "<" diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 8fddaabc4..fe31b7111 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -1,18 +1,15 @@ -# encoding: utf-8 +"""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. """ -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 +from docx.compat import BytesIO, is_string +from docx.image.exceptions import UnrecognizedImageError +from docx.shared import Emu, Inches, lazyproperty class Image(object): diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index 432dcf9f7..b09a6d7a4 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -1,18 +1,14 @@ -# encoding: utf-8 +"""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. """ -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 - +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() diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index df20253e7..048a06753 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -1,17 +1,11 @@ -# 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 +"""Open Packaging Convention (OPC) objects related to package parts.""" + +from docx.opc.compat import cls_method_fn +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 lazyproperty +from docx.oxml import parse_xml class Part(object): diff --git a/src/docx/opc/parts/coreprops.py b/src/docx/opc/parts/coreprops.py index 7e5b8c17b..2bb43c9ce 100644 --- a/src/docx/opc/parts/coreprops.py +++ b/src/docx/opc/parts/coreprops.py @@ -1,18 +1,12 @@ -# encoding: utf-8 - -""" -Core properties part, corresponds to ``/docProps/core.xml`` part in package. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" 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 +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): diff --git a/src/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py index 6d68198df..3d2f768ab 100644 --- a/src/docx/opc/phys_pkg.py +++ b/src/docx/opc/phys_pkg.py @@ -1,18 +1,11 @@ -# encoding: utf-8 - -""" -Provides a general interface to a *physical* OPC package, such as a zip file. -""" - -from __future__ import absolute_import +"""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 zipfile import ZipFile, is_zipfile, ZIP_DEFLATED - -from .compat import is_string -from .exceptions import PackageNotFoundError -from .packuri import CONTENT_TYPES_URI +from docx.opc.compat import is_string +from docx.opc.exceptions import PackageNotFoundError +from docx.opc.packuri import CONTENT_TYPES_URI class PhysPkgReader(object): diff --git a/src/docx/opc/spec.py b/src/docx/opc/spec.py index 87489fcfb..011a4825d 100644 --- a/src/docx/opc/spec.py +++ b/src/docx/opc/spec.py @@ -1,11 +1,6 @@ -# encoding: utf-8 - -""" -Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500. -""" - -from .constants import CONTENT_TYPE as CT +"""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), diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 36539340c..80327205c 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -1,16 +1,11 @@ -# encoding: utf-8 +"""Initializes oxml sub-package. +This including registering custom element classes corresponding to Open XML elements. """ -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 - +from docx.oxml.ns import NamespacePrefixedTag, nsmap # configure XML parser element_class_lookup = etree.ElementNamespaceClassLookup() diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 3d4f64e75..8c0dd414e 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -1,11 +1,6 @@ -# encoding: utf-8 - -"""Custom element classes for core properties-related XML elements""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Custom element classes for core properties-related XML elements.""" import re - from datetime import datetime, timedelta from docx.compat import is_string diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index 5c3e6f2be..b8de0221f 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -1,11 +1,6 @@ -# encoding: utf-8 +"""Custom element classes that correspond to the document part, e.g. .""" -""" -Custom element classes that correspond to the document part, e.g. -. -""" - -from .xmlchemy import BaseOxmlElement, ZeroOrOne, ZeroOrMore +from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne class CT_Document(BaseOxmlElement): diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py index 192f73ce0..a94333752 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -1,11 +1,4 @@ -# encoding: utf-8 - -""" -Namespace-related objects. -""" - -from __future__ import absolute_import, print_function, unicode_literals - +"""Namespace-related objects.""" nsmap = { "a": "http://schemas.openxmlformats.org/drawingml/2006/main", diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index f9423da6b..063105505 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -1,30 +1,26 @@ -# encoding: utf-8 +"""Custom element classes for tables.""" -"""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 ( +from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE +from docx.exceptions import InvalidSpanError +from docx.oxml import parse_xml +from docx.oxml.ns import nsdecls, qn +from docx.oxml.simpletypes import ( ST_Merge, ST_TblLayoutType, ST_TblWidth, ST_TwipsMeasure, XsdInt, ) -from .xmlchemy import ( +from docx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OneOrMore, OptionalAttribute, RequiredAttribute, - ZeroOrOne, ZeroOrMore, + ZeroOrOne, ) +from docx.shared import Emu, Twips class CT_Height(BaseOxmlElement): diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 73913af9a..1c8ddd986 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -1,15 +1,21 @@ -# 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 +"""Custom element classes related to run properties (font).""" + +from docx.enum.dml import MSO_THEME_COLOR +from docx.enum.text import WD_COLOR, WD_UNDERLINE +from docx.oxml import parse_xml +from docx.oxml.ns import nsdecls, qn +from docx.oxml.simpletypes import ( + ST_HexColor, + ST_HpsMeasure, + ST_String, + ST_VerticalAlignRun, +) +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, +) class CT_Color(BaseOxmlElement): diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 0fff364dc..f913bb792 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -1,16 +1,9 @@ -# 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 absolute_import +import re from lxml import etree -import re - from docx.compat import Unicode from docx.oxml import OxmlElement from docx.oxml.exceptions import InvalidXmlError diff --git a/src/docx/styles/latent.py b/src/docx/styles/latent.py index 6f723692b..f790fdb3c 100644 --- a/src/docx/styles/latent.py +++ b/src/docx/styles/latent.py @@ -1,13 +1,7 @@ -# encoding: utf-8 +"""Latent style-related objects.""" -""" -Latent style-related objects. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from . import BabelFish -from ..shared import ElementProxy +from docx.shared import ElementProxy +from docx.styles import BabelFish class LatentStyles(ElementProxy): diff --git a/src/docx/styles/style.py b/src/docx/styles/style.py index ff63a819b..89096a592 100644 --- a/src/docx/styles/style.py +++ b/src/docx/styles/style.py @@ -1,16 +1,10 @@ -# encoding: utf-8 +"""Style object hierarchy.""" -""" -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 +from docx.enum.style import WD_STYLE_TYPE +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): diff --git a/src/docx/table.py b/src/docx/table.py index bd4e1e277..3191ec1ea 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -1,15 +1,9 @@ -# encoding: utf-8 +"""The |Table| object and related proxy classes.""" -""" -The |Table| object and related proxy classes. -""" - -from __future__ import absolute_import, print_function, unicode_literals - -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): diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index d349e5378..59dab690c 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -1,15 +1,9 @@ -# encoding: utf-8 +"""Paragraph-related proxy types.""" -""" -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 +from docx.enum.style import WD_STYLE_TYPE +from docx.shared import Parented +from docx.text.parfmt import ParagraphFormat +from docx.text.run import Run class Paragraph(Parented): diff --git a/src/docx/text/parfmt.py b/src/docx/text/parfmt.py index 6d9215549..4a99b0b59 100644 --- a/src/docx/text/parfmt.py +++ b/src/docx/text/parfmt.py @@ -1,14 +1,8 @@ -# 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): diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 88c37fa3c..09d95f206 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -1,16 +1,10 @@ -# encoding: utf-8 +"""Run-related proxy objects for python-docx, Run in particular.""" -""" -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 +from docx.enum.style import WD_STYLE_TYPE +from docx.enum.text import WD_BREAK +from docx.shape import InlineShape +from docx.shared import Parented +from docx.text.font import Font class Run(Parented): diff --git a/src/docx/text/tabstops.py b/src/docx/text/tabstops.py index 6915285cf..c6b899c05 100644 --- a/src/docx/text/tabstops.py +++ b/src/docx/text/tabstops.py @@ -1,13 +1,7 @@ -# encoding: utf-8 +"""Tabstop-related proxy types.""" -""" -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 +from docx.shared import ElementProxy class TabStops(ElementProxy): diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index 421f72922..a461650f5 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -1,19 +1,13 @@ -# 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): def it_knows_its_color_type(self, type_fixture): diff --git a/tests/image/test_bmp.py b/tests/image/test_bmp.py index 9eb371643..bdd2ff4e7 100644 --- a/tests/image/test_bmp.py +++ b/tests/image/test_bmp.py @@ -1,16 +1,10 @@ -# encoding: utf-8 - -""" -Test suite for docx.image.bmp module -""" - -from __future__ import absolute_import, print_function +"""Test suite for docx.image.bmp module.""" 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 diff --git a/tests/image/test_gif.py b/tests/image/test_gif.py index fc00af690..fd9eac2ea 100644 --- a/tests/image/test_gif.py +++ b/tests/image/test_gif.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for docx.image.gif module""" - -from __future__ import absolute_import, print_function +"""Unit test suite for docx.image.gif module.""" import pytest diff --git a/tests/image/test_helpers.py b/tests/image/test_helpers.py index 8716257d7..e98ac6ca7 100644 --- a/tests/image/test_helpers.py +++ b/tests/image/test_helpers.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for docx.image.helpers module -""" - -from __future__ import absolute_import, print_function +"""Test suite for docx.image.helpers module.""" import pytest diff --git a/tests/image/test_image.py b/tests/image/test_image.py index 5893f053c..791831077 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -1,9 +1,5 @@ -# encoding: utf-8 - """Unit test suite for docx.image package""" -from __future__ import absolute_import, print_function, unicode_literals - import pytest from docx.compat import BytesIO diff --git a/tests/image/test_jpeg.py b/tests/image/test_jpeg.py index 759bd94cc..53ed9b6cc 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - """Unit test suite for docx.image.jpeg module""" -from __future__ import absolute_import, print_function - 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, diff --git a/tests/image/test_png.py b/tests/image/test_png.py index 71b2c7e14..d86d43d84 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for docx.image.png module""" - -from __future__ import absolute_import, print_function +"""Unit test suite for docx.image.png module.""" import pytest @@ -11,13 +7,13 @@ from docx.image.exceptions import InvalidImageStreamError from docx.image.helpers import BIG_ENDIAN, StreamReader from docx.image.png import ( + Png, _Chunk, - _Chunks, _ChunkFactory, _ChunkParser, + _Chunks, _IHDRChunk, _pHYsChunk, - Png, _PngParser, ) diff --git a/tests/image/test_tiff.py b/tests/image/test_tiff.py index a5f07acdb..f073bafac 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -1,15 +1,12 @@ -# encoding: utf-8 - """Unit test suite for docx.image.tiff module""" -from __future__ import absolute_import, print_function - 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 +15,6 @@ _LongIfdEntry, _RationalIfdEntry, _ShortIfdEntry, - Tiff, _TiffParser, ) diff --git a/tests/opc/parts/test_coreprops.py b/tests/opc/parts/test_coreprops.py index 66222cef7..0f528829a 100644 --- a/tests/opc/parts/test_coreprops.py +++ b/tests/opc/parts/test_coreprops.py @@ -1,10 +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 diff --git a/tests/opc/test_coreprops.py b/tests/opc/test_coreprops.py index 97da5dead..a5e08bed4 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -1,15 +1,9 @@ -# 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 diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index c269bde35..2fc2a22db 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -1,8 +1,4 @@ -# 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 ( @@ -16,10 +12,10 @@ from .unitdata.rels import ( a_Default, - an_Override, a_Relationship, a_Relationships, a_Types, + an_Override, ) diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index e2edb3ae6..790d6fbc9 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,17 +9,17 @@ 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, ) diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index a56badcda..71c8722c9 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for the docx.opc.packuri module -""" +"""Test suite for the docx.opc.packuri module.""" import pytest diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index b32bf7f4f..df0b6eb12 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -1,27 +1,23 @@ -# 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, ) diff --git a/tests/opc/test_phys_pkg.py b/tests/opc/test_phys_pkg.py index 5fa1e9e75..800819f80 100644 --- a/tests/opc/test_phys_pkg.py +++ b/tests/opc/test_phys_pkg.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for docx.opc.phys_pkg module -""" - -from __future__ import absolute_import +"""Test suite for docx.opc.phys_pkg module.""" try: from io import BytesIO # Python 3 @@ -12,23 +6,22 @@ from StringIO import StringIO as BytesIO import hashlib -import pytest - 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, + _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") diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py index ac75e8365..ad63aaf2e 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,9 +24,9 @@ instance_mock, loose_mock, method_mock, - Mock, patch, ) +from .unitdata.types import a_Default, a_Types, an_Override class DescribePackageReader(object): diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py index 25bcadb42..7b0286ce8 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,18 +6,18 @@ 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 ( + MagicMock, + Mock, call, class_mock, instance_mock, - MagicMock, method_mock, - Mock, patch, ) +from .unitdata.types import a_Default, a_Types, an_Override class DescribePackageWriter(object): diff --git a/tests/opc/test_rel.py b/tests/opc/test_rel.py index d94750027..45a218bb7 100644 --- a/tests/opc/test_rel.py +++ b/tests/opc/test_rel.py @@ -1,19 +1,13 @@ -# encoding: utf-8 - -""" -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 docx.opc.rel import Relationships, _Relationship -from ..unitutil.mock import call, class_mock, instance_mock, Mock, patch, PropertyMock +from ..unitutil.mock import Mock, PropertyMock, call, class_mock, instance_mock, patch class Describe_Relationship(object): diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index 73663d188..c0b6024fa 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -1,16 +1,9 @@ -# 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): diff --git a/tests/oxml/test__init__.py b/tests/oxml/test__init__.py index 85302d64c..51889b97e 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -1,13 +1,6 @@ -# 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 diff --git a/tests/oxml/test_ns.py b/tests/oxml/test_ns.py index 413d10bde..7e1d659f9 100644 --- a/tests/oxml/test_ns.py +++ b/tests/oxml/test_ns.py @@ -1,10 +1,4 @@ -# 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 diff --git a/tests/oxml/test_styles.py b/tests/oxml/test_styles.py index 0c34f6637..a6748b4f0 100644 --- a/tests/oxml/test_styles.py +++ b/tests/oxml/test_styles.py @@ -1,10 +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 diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index bc40b5fba..0e1eeb9c1 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -1,8 +1,4 @@ -# 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 diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index 15f68face..c81a8de52 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -1,10 +1,4 @@ -# 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 @@ -16,15 +10,15 @@ from docx.oxml.xmlchemy import ( BaseOxmlElement, Choice, - serialize_for_reading, - OneOrMore, OneAndOnlyOne, + OneOrMore, OptionalAttribute, RequiredAttribute, + XmlString, ZeroOrMore, ZeroOrOne, ZeroOrOneChoice, - XmlString, + serialize_for_reading, ) from ..unitdata import BaseBuilder diff --git a/tests/parts/test_document.py b/tests/parts/test_document.py index 36616590b..b9b50676a 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 diff --git a/tests/parts/test_hdrftr.py b/tests/parts/test_hdrftr.py index 815b207cc..d153ad348 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 diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 90a59c2fb..3b6424fe4 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 diff --git a/tests/parts/test_numbering.py b/tests/parts/test_numbering.py index 44dedde78..2783b9eea 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 diff --git a/tests/parts/test_settings.py b/tests/parts/test_settings.py index 21ba2ec42..20ec8cdff 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 diff --git a/tests/parts/test_story.py b/tests/parts/test_story.py index 671547582..5fb461fe1 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 diff --git a/tests/parts/test_styles.py b/tests/parts/test_styles.py index 0f2a7b11d..faad075b5 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 diff --git a/tests/styles/test_latent.py b/tests/styles/test_latent.py index ee4bead87..4faf63bb4 100644 --- a/tests/styles/test_latent.py +++ b/tests/styles/test_latent.py @@ -1,14 +1,8 @@ -# 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 diff --git a/tests/styles/test_style.py b/tests/styles/test_style.py index caa281db8..7d028ea0d 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -1,20 +1,14 @@ -# 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, + StyleFactory, _CharacterStyle, - _ParagraphStyle, _NumberingStyle, - StyleFactory, + _ParagraphStyle, _TableStyle, ) from docx.text.font import Font diff --git a/tests/styles/test_styles.py b/tests/styles/test_styles.py index c01d2138a..419db3570 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 diff --git a/tests/test_api.py b/tests/test_api.py index 23251cf34..6e8baf5ca 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,19 +1,12 @@ -# 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): diff --git a/tests/test_blkcntnr.py b/tests/test_blkcntnr.py index 3fe2d0ff5..9da168e1d 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 diff --git a/tests/test_document.py b/tests/test_document.py index 27f17f978..79dbc3d60 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 diff --git a/tests/test_enum.py b/tests/test_enum.py index a07795ed5..72c74f82a 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -1,23 +1,19 @@ -# 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 pytest from docx.enum.base import ( - alias, Enumeration, EnumMember, ReturnValueOnlyEnumMember, XmlEnumeration, XmlMappedEnumMember, + alias, ) diff --git a/tests/test_package.py b/tests/test_package.py index e94e5f318..978670b1d 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 diff --git a/tests/test_section.py b/tests/test_section.py index ae887a960..a5997d581 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.section module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.section module.""" import pytest from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENT, WD_SECTION from docx.parts.document import DocumentPart from docx.parts.hdrftr import FooterPart, HeaderPart -from docx.section import _BaseHeaderFooter, _Footer, _Header, Section, Sections +from docx.section import Section, Sections, _BaseHeaderFooter, _Footer, _Header from docx.shared import Inches from .unitutil.cxml import element, xml diff --git a/tests/test_settings.py b/tests/test_settings.py index 5c07a6652..a4dc5d786 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 diff --git a/tests/test_shape.py b/tests/test_shape.py index 5e08e9743..e0f73bcb6 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 diff --git a/tests/test_shared.py b/tests/test_shared.py index 7641c158d..ac1c01fcd 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,15 +1,9 @@ -# 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 diff --git a/tests/test_table.py b/tests/test_table.py index b314b801e..87c7f07c4 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,8 +1,4 @@ -# 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 @@ -17,7 +13,7 @@ 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 diff --git a/tests/text/test_font.py b/tests/text/test_font.py index 5b4b15eb4..d37c4252c 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -1,18 +1,12 @@ -# encoding: utf-8 +"""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 +import pytest from docx.dml.color import ColorFormat from docx.enum.text import WD_COLOR, WD_UNDERLINE from docx.shared import Pt from docx.text.font import Font -import pytest - from ..unitutil.cxml import element, xml from ..unitutil.mock import class_mock, instance_mock diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index e394334e5..29778c284 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Unit test suite for the docx.text.paragraph module.""" -"""Unit test suite for the docx.text.paragraph module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +import pytest from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_ALIGN_PARAGRAPH @@ -13,8 +11,6 @@ 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 diff --git a/tests/text/test_parfmt.py b/tests/text/test_parfmt.py index ce9927798..5f9da996b 100644 --- a/tests/text/test_parfmt.py +++ b/tests/text/test_parfmt.py @@ -1,19 +1,12 @@ -# 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 diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 02cb87412..ef1b79bf1 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""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 +import pytest from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_BREAK, WD_UNDERLINE @@ -11,8 +9,6 @@ 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 diff --git a/tests/text/test_tabstops.py b/tests/text/test_tabstops.py index 29627fcb9..ca2122206 100644 --- a/tests/text/test_tabstops.py +++ b/tests/text/test_tabstops.py @@ -1,18 +1,11 @@ -# 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 diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index 3029eda77..f212a4c07 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -1,33 +1,28 @@ -# 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 - # ==================================================================== # api functions # ==================================================================== diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index f87def241..0d1562942 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,12 +1,7 @@ -# encoding: utf-8 - -""" -Utility functions for loading files for unit testing -""" +"""Utility functions for loading files for unit testing.""" import os - _thisdir = os.path.split(__file__)[0] test_file_dir = os.path.abspath(os.path.join(_thisdir, "..", "test_files")) From d5097841991ad00a9a4f3bc9a0cc9f510f3a6c7f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 25 Sep 2023 17:25:34 -0700 Subject: [PATCH 010/131] lint: fix C4xx comprehension errors Also any Python 2 clean-up that was handy. --- features/steps/styles.py | 4 ++-- features/steps/tabstops.py | 2 +- pyproject.toml | 2 +- src/docx/document.py | 4 ---- src/docx/enum/base.py | 12 +++--------- src/docx/image/png.py | 6 +----- src/docx/image/tiff.py | 6 +----- src/docx/opc/package.py | 8 ++------ src/docx/opc/pkgwriter.py | 23 ++++++++++------------- src/docx/oxml/ns.py | 4 ++-- src/docx/shape.py | 14 +++++--------- src/docx/styles/__init__.py | 10 ++-------- tests/image/test_jpeg.py | 2 +- tests/image/test_png.py | 4 ++-- tests/image/test_tiff.py | 2 +- tests/opc/test_package.py | 2 +- tests/opc/test_pkgreader.py | 4 ++-- tests/styles/test_latent.py | 2 +- tests/test_section.py | 2 +- 19 files changed, 39 insertions(+), 74 deletions(-) diff --git a/features/steps/styles.py b/features/steps/styles.py index ad8730965..a40f2dc6a 100644 --- a/features/steps/styles.py +++ b/features/steps/styles.py @@ -318,14 +318,14 @@ def then_I_can_access_a_style_by_style_id(context): @then("I can iterate over its styles") def then_I_can_iterate_over_its_styles(context): - styles = [s for s in context.document.styles] + styles = list(context.document.styles) assert len(styles) > 0 assert all(isinstance(s, BaseStyle) for s in styles) @then("I can iterate over the latent styles") def then_I_can_iterate_over_the_latent_styles(context): - latent_styles = [ls for ls in context.latent_styles] + latent_styles = list(context.latent_styles) assert len(latent_styles) == 137 assert all(isinstance(ls, _LatentStyle) for ls in latent_styles) diff --git a/features/steps/tabstops.py b/features/steps/tabstops.py index 03f6f203c..ceb9d8f9b 100644 --- a/features/steps/tabstops.py +++ b/features/steps/tabstops.py @@ -96,7 +96,7 @@ def then_I_can_access_a_tab_stop_by_index(context): @then("I can iterate the TabStops object") def then_I_can_iterate_the_TabStops_object(context): - items = [ts for ts in context.tab_stops] + items = list(context.tab_stops) assert len(items) == 3 assert all(isinstance(item, TabStop) for item in items) diff --git a/pyproject.toml b/pyproject.toml index d64b2d130..a7310da4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ ignore = [ "COM812", # -- over-aggressively insists on trailing commas where not desired -- ] select = [ - # "C4", # -- flake8-comprehensions -- + "C4", # -- flake8-comprehensions -- "COM", # -- flake8-commas -- "E", # -- pycodestyle errors -- "F", # -- pyflakes -- diff --git a/src/docx/document.py b/src/docx/document.py index 6acdb839a..e47bfc816 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -1,9 +1,5 @@ -# 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 diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index 0bcdbd6dd..fae7c9aff 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Base classes and other objects used by enumerations -""" - -from __future__ import absolute_import, print_function +"""Base classes and other objects used by enumerations.""" import sys import textwrap @@ -358,7 +352,7 @@ 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() + clsdict["_member_to_xml"] = {} return clsdict["_member_to_xml"] @staticmethod @@ -367,5 +361,5 @@ 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() + clsdict["_xml_to_member"] = {} return clsdict["_xml_to_member"] diff --git a/src/docx/image/png.py b/src/docx/image/png.py index c2e4ae820..72f2ec83a 100644 --- a/src/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 @@ -127,7 +123,7 @@ def from_stream(cls, 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 diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py index 5b5e8b748..05b1addf8 100644 --- a/src/docx/image/tiff.py +++ b/src/docx/image/tiff.py @@ -1,7 +1,3 @@ -# 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 @@ -177,7 +173,7 @@ def from_stream(cls, stream, offset): *offset*. """ ifd_parser = _IfdParser(stream, offset) - entries = dict((e.tag, e.value) for e in ifd_parser.iter_entries()) + entries = {e.tag: e.value for e in ifd_parser.iter_entries()} return cls(entries) def get(self, tag_code, default=None): diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index 2e81d93c6..014c11f10 100644 --- a/src/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 @@ -70,7 +66,7 @@ def iter_parts(self): performing a depth-first traversal of the rels graph. """ - def walk_parts(source, visited=list()): + def walk_parts(source, visited=[]): for rel in source.rels.values(): if rel.is_external: continue @@ -146,7 +142,7 @@ 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 list(self.iter_parts()) def relate_to(self, part, reltype): """ diff --git a/src/docx/opc/pkgwriter.py b/src/docx/opc/pkgwriter.py index 1610cbb06..1f8901eea 100644 --- a/src/docx/opc/pkgwriter.py +++ b/src/docx/opc/pkgwriter.py @@ -1,18 +1,15 @@ -# encoding: utf-8 +"""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(). """ -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 +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(object): @@ -75,7 +72,7 @@ class _ContentTypesItem(object): def __init__(self): self._defaults = CaseInsensitiveDict() - self._overrides = dict() + self._overrides = {} @property def blob(self): diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py index a94333752..868a1356e 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -19,7 +19,7 @@ "xsi": "http://www.w3.org/2001/XMLSchema-instance", } -pfxmap = dict((value, key) for key, value in nsmap.items()) +pfxmap = {value: key for key, value in nsmap.items()} class NamespacePrefixedTag(str): @@ -94,7 +94,7 @@ def nspfxmap(*nspfxs): *nspfxs*. Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p'). """ - return dict((pfx, nsmap[pfx]) for pfx in nspfxs) + return {pfx: nsmap[pfx] for pfx in nspfxs} def qn(tag): diff --git a/src/docx/shape.py b/src/docx/shape.py index 036118d46..1f7a938a3 100644 --- a/src/docx/shape.py +++ b/src/docx/shape.py @@ -1,15 +1,11 @@ -# 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): diff --git a/src/docx/styles/__init__.py b/src/docx/styles/__init__.py index 61d011d38..ff2e71ce7 100644 --- a/src/docx/styles/__init__.py +++ b/src/docx/styles/__init__.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Sub-package module for docx.styles sub-package. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Sub-package module for docx.styles sub-package.""" class BabelFish(object): @@ -29,7 +23,7 @@ class BabelFish(object): ) internal_style_names = dict(style_aliases) - ui_style_names = dict((item[1], item[0]) for item in style_aliases) + ui_style_names = {item[1]: item[0] for item in style_aliases} @classmethod def ui2internal(cls, ui_style_name): diff --git a/tests/image/test_jpeg.py b/tests/image/test_jpeg.py index 53ed9b6cc..bd77bb3a1 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -541,7 +541,7 @@ def it_can_iterate_over_the_jfif_markers_in_its_stream(self, iter_markers_fixtur offsets, marker_lst, ) = iter_markers_fixture - markers = [marker for marker in marker_parser.iter_markers()] + 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 _MarkerFactory_.call_args_list == [ diff --git a/tests/image/test_png.py b/tests/image/test_png.py index d86d43d84..38a7ab935 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -250,7 +250,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 == [ @@ -261,7 +261,7 @@ def it_can_iterate_over_the_chunks_in_its_png_stream( 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 ------------------------------------------------------- diff --git a/tests/image/test_tiff.py b/tests/image/test_tiff.py index f073bafac..6768f6bd9 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -269,7 +269,7 @@ def it_can_iterate_through_the_directory_entries_in_an_IFD(self, iter_fixture): offsets, expected_entries, ) = iter_fixture - entries = [e for e in ifd_parser.iter_entries()] + entries = list(ifd_parser.iter_entries()) assert _IfdEntryFactory_.call_args_list == [ call(stream_rdr, offsets[0]), call(stream_rdr, offsets[1]), diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 790d6fbc9..1e88a1660 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -84,7 +84,7 @@ def it_can_iterate_over_parts_by_walking_rels_graph(self): # 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_ diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py index ad63aaf2e..3d83f1d63 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -65,7 +65,7 @@ def it_can_iterate_over_all_the_srels(self): ] 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"), @@ -84,7 +84,7 @@ def it_can_load_serialized_parts(self, _SerializedPart_, _walk_phys_parts): ("/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") diff --git a/tests/styles/test_latent.py b/tests/styles/test_latent.py index 4faf63bb4..5ab235445 100644 --- a/tests/styles/test_latent.py +++ b/tests/styles/test_latent.py @@ -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) diff --git a/tests/test_section.py b/tests/test_section.py index a5997d581..a810eb418 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -27,7 +27,7 @@ def it_can_iterate_over_its_Section_instances( 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_), From 878f75854ec80f96263eee6dbdfa329e0ce6dd27 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 25 Sep 2023 20:08:00 -0700 Subject: [PATCH 011/131] lint: fix flake8-pytest-style diagnostics --- pyproject.toml | 4 +++- tests/opc/test_package.py | 8 ++++---- tests/opc/test_packuri.py | 2 +- tests/opc/test_part.py | 8 ++------ tests/opc/test_phys_pkg.py | 20 ++++++++------------ tests/opc/test_pkgreader.py | 10 +++++----- tests/opc/test_pkgwriter.py | 20 +++++++++----------- tests/opc/test_rel.py | 10 ++++++---- tests/oxml/test__init__.py | 2 +- tests/oxml/test_table.py | 4 ++-- tests/styles/test_styles.py | 4 ++-- tests/test_api.py | 2 +- tests/test_document.py | 4 ++-- tests/test_enum.py | 6 +++--- tests/test_shape.py | 8 ++++---- tests/test_shared.py | 6 +++--- tests/test_table.py | 9 +++++---- tests/text/test_run.py | 2 +- 18 files changed, 62 insertions(+), 67 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a7310da4e..60b7e3ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,8 @@ python_functions = ["it_", "they_", "and_it_", "but_it_"] 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 -- @@ -66,7 +68,7 @@ select = [ "F", # -- pyflakes -- "I", # -- isort (imports) -- "PLR0402", # -- Name compared with itself like `foo == foo` -- - # "PT", # -- flake8-pytest-style -- + "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) -- diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 1e88a1660..7bb6ae62e 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -233,16 +233,16 @@ def part_related_by_(self, request): 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( + 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): diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index 71c8722c9..1d5732a85 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -29,7 +29,7 @@ def it_can_construct_from_relative_ref(self): assert pack_uri == "/ppt/slideLayouts/slideLayout1.xml" def it_should_raise_on_construct_with_bad_pack_uri_str(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="PackURI must begin with slash"): PackURI("foobar") def it_can_calculate_baseURI(self): diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index df0b6eb12..f9b5f8732 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -326,24 +326,20 @@ def cls_method_fn_(self, request, 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 - original_part_class_selector = PartFactory.part_class_selector - request.addfinalizer(reset_part_class_selector) - return ( + 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_): diff --git a/tests/opc/test_phys_pkg.py b/tests/opc/test_phys_pkg.py index 800819f80..e33d5cf5a 100644 --- a/tests/opc/test_phys_pkg.py +++ b/tests/opc/test_phys_pkg.py @@ -1,11 +1,7 @@ """Test suite for docx.opc.phys_pkg module.""" -try: - from io import BytesIO # Python 3 -except ImportError: - from StringIO import StringIO as BytesIO - import hashlib +import io from zipfile import ZIP_DEFLATED, ZipFile import pytest @@ -119,10 +115,10 @@ def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): # fixtures --------------------------------------------- @pytest.fixture(scope="class") - def phys_reader(self, request): + 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): @@ -167,10 +163,10 @@ 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 ------------------------------------------------- diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py index 3d83f1d63..cd010e396 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -231,10 +231,10 @@ def partnames_(self, request): 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): @@ -464,7 +464,7 @@ def it_raises_on_target_partname_when_external(self): target_mode=RTM.EXTERNAL, ) srel = _SerializedRelationship("/", rel_elm) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="target_partname attribute on Relat"): srel.target_partname diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py index 7b0286ce8..77d44ea63 100644 --- a/tests/opc/test_pkgwriter.py +++ b/tests/opc/test_pkgwriter.py @@ -92,10 +92,10 @@ 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): @@ -106,7 +106,7 @@ 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") @@ -116,13 +116,11 @@ def _write_methods(self, request): 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() + yield root_mock - request.addfinalizer(fin) - return root_mock + patch1.stop() + patch2.stop() + patch3.stop() @pytest.fixture def xml_for_(self, request): diff --git a/tests/opc/test_rel.py b/tests/opc/test_rel.py index 45a218bb7..65d756ec0 100644 --- a/tests/opc/test_rel.py +++ b/tests/opc/test_rel.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for the docx.opc.rel module.""" import pytest @@ -27,7 +29,7 @@ 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): @@ -172,7 +174,7 @@ def rels(self): 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() @@ -186,8 +188,8 @@ def rels_elm(self, request): # patch CT_Relationships to return that 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): diff --git a/tests/oxml/test__init__.py b/tests/oxml/test__init__.py index 51889b97e..6a5a2f901 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -63,7 +63,7 @@ def it_accepts_unicode_providing_there_is_no_encoding_declaration(self): 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): + with pytest.raises(ValueError, match="Unicode strings with encoding declara"): parse_xml(xml_text) def it_uses_registered_element_classes(self, xml_bytes): diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 0e1eeb9c1..b24c4abf3 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -19,7 +19,7 @@ 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 ------------------------------------------------------- @@ -123,7 +123,7 @@ 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 ------------------------------------------------------- diff --git a/tests/styles/test_styles.py b/tests/styles/test_styles.py index 419db3570..bdee6d2b5 100644 --- a/tests/styles/test_styles.py +++ b/tests/styles/test_styles.py @@ -60,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): @@ -156,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): diff --git a/tests/test_api.py b/tests/test_api.py index 6e8baf5ca..acd3606d5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -24,7 +24,7 @@ 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 ------------------------------------------------------- diff --git a/tests/test_document.py b/tests/test_document.py index 79dbc3d60..b5d58aefd 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -33,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_): diff --git a/tests/test_enum.py b/tests/test_enum.py index 72c74f82a..51f95f2d8 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -68,9 +68,9 @@ def it_provides_the_enumeration_value_for_each_named_member(self): def it_knows_if_a_setting_is_valid(self): FOOBAR.validate(None) FOOBAR.validate(FOOBAR.READ_WRITE) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="foobar not a member of FOOBAR enumerat"): FOOBAR.validate("foobar") - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"READ_ONLY \(-2\) not a member of FOOB"): FOOBAR.validate(FOOBAR.READ_ONLY) def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): @@ -89,7 +89,7 @@ class DescribeXmlEnumeration(object): 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): + with pytest.raises(ValueError, match=r"value 'RO \(-2\)' not in enumeration "): XMLFOO.to_xml(XMLFOO.RO) def it_can_map_each_of_its_xml_members_from_the_XML_value(self): diff --git a/tests/test_shape.py b/tests/test_shape.py index e0f73bcb6..647ae683c 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -40,11 +40,11 @@ def it_provides_indexed_access_to_inline_shapes(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_): diff --git a/tests/test_shared.py b/tests/test_shared.py index ac1c01fcd..7a026c456 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -112,11 +112,11 @@ def units_fixture(self, request): class DescribeRGBColor(object): def it_is_natively_constructed_using_three_ints_0_to_255(self): RGBColor(0x12, 0x34, 0x56) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"RGBColor\(\) takes three integer valu"): RGBColor("12", "34", "56") - with pytest.raises(ValueError): + 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): diff --git a/tests/test_table.py b/tests/test_table.py index 87c7f07c4..11823117f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -930,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): diff --git a/tests/text/test_run.py b/tests/text/test_run.py index ef1b79bf1..dd546f005 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -46,7 +46,7 @@ def it_can_change_its_underline_type(self, underline_set_fixture): def it_raises_on_assign_invalid_underline_type(self, underline_raise_fixture): run, underline = underline_raise_fixture - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="' not in enumeration WD_UNDERLINE"): run.underline = underline def it_provides_access_to_its_font(self, font_fixture): From c8aad1c7f603d37a0c32bd9067a5164fc8df239c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 25 Sep 2023 20:25:49 -0700 Subject: [PATCH 012/131] lint: fix flake8-simplify diagnostics --- pyproject.toml | 2 +- src/docx/image/tiff.py | 7 ++----- src/docx/opc/packuri.py | 14 ++++---------- src/docx/oxml/document.py | 11 ++++------- src/docx/oxml/table.py | 10 ++++------ src/docx/oxml/xmlchemy.py | 9 ++------- src/docx/section.py | 8 ++++---- src/docx/styles/styles.py | 5 +---- tests/image/test_png.py | 2 +- tests/styles/test_style.py | 5 +---- 10 files changed, 24 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 60b7e3ed2..72591d894 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ select = [ "I", # -- isort (imports) -- "PLR0402", # -- Name compared with itself like `foo == foo` -- "PT", # -- flake8-pytest-style -- - # "SIM", # -- flake8-simplify -- + "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 -- diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py index 05b1addf8..1aca7a0e8 100644 --- a/src/docx/image/tiff.py +++ b/src/docx/image/tiff.py @@ -225,11 +225,8 @@ def _IfdEntryFactory(stream_rdr, offset): 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) + EntryCls = ifd_entry_classes.get(field_type, _IfdEntry) + return EntryCls.from_stream(stream_rdr, offset) class _IfdEntry(object): diff --git a/src/docx/opc/packuri.py b/src/docx/opc/packuri.py index f20af84f8..50fa6e214 100644 --- a/src/docx/opc/packuri.py +++ b/src/docx/opc/packuri.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Provides the PackURI value type. -""" -Provides the PackURI value type along with some useful known pack URI strings -such as PACKAGE_URI. +Also some useful known pack URI strings such as PACKAGE_URI. """ import posixpath @@ -18,7 +16,7 @@ class PackURI(str): _filename_re = re.compile("([a-zA-Z]+)([1-9][0-9]*)?") def __new__(cls, pack_uri_str): - if not pack_uri_str[0] == "/": + 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) @@ -96,11 +94,7 @@ def relative_ref(self, baseURI): """ # 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 + return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) @property def rels_uri(self): diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index b8de0221f..a97f31990 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -52,13 +52,10 @@ def add_section_break(self): return sentinel_sectPr def clear_content(self): + """Remove all content child elements from this element. + + Leave the element if it is present. """ - 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[:] + 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/table.py b/src/docx/oxml/table.py index 063105505..fc131f88e 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -327,14 +327,12 @@ 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): diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index f913bb792..4b994b773 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -482,13 +482,8 @@ def get_or_change_to_child(obj): @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 diff --git a/src/docx/section.py b/src/docx/section.py index 32ceec7da..c230eefd7 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -383,10 +383,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): @@ -427,10 +427,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/src/docx/styles/styles.py b/src/docx/styles/styles.py index d09fbd137..e19e97c1c 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -26,10 +26,7 @@ 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 + return any(style.name_val == internal_name for style in self._element.style_lst) def __getitem__(self, key): """ diff --git a/tests/image/test_png.py b/tests/image/test_png.py index 38a7ab935..07b50eda0 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -171,7 +171,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 diff --git a/tests/styles/test_style.py b/tests/styles/test_style.py index 7d028ea0d..113da3339 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -516,10 +516,7 @@ def next_get_fixture(self, request): 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 + next_style = _ParagraphStyle(next_style_elm) if style_name == "H1" else style return style, next_style @pytest.fixture( From 5cb952f560eae017244e570b9f098da87116eb26 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 25 Sep 2023 21:41:17 -0700 Subject: [PATCH 013/131] rfctr: remove Python 2 compatibility layer --- ref/xsd/vml-wordprocessingDrawing.xsd | 9 +++-- src/docx/compat.py | 37 -------------------- src/docx/image/image.py | 8 ++--- src/docx/image/jpeg.py | 19 +++++------ src/docx/opc/compat.py | 49 --------------------------- src/docx/opc/part.py | 3 +- src/docx/opc/phys_pkg.py | 3 +- src/docx/opc/shared.py | 13 ++++--- src/docx/oxml/coreprops.py | 3 +- src/docx/oxml/settings.py | 4 --- src/docx/oxml/xmlchemy.py | 3 +- src/docx/section.py | 7 ++-- tests/image/test_bmp.py | 5 +-- tests/image/test_gif.py | 5 +-- tests/image/test_helpers.py | 7 ++-- tests/image/test_image.py | 13 +++---- tests/image/test_jpeg.py | 38 ++++++++++----------- tests/image/test_png.py | 17 +++++----- tests/image/test_tiff.py | 25 +++++++------- tests/oxml/test_xmlchemy.py | 3 +- 20 files changed, 89 insertions(+), 182 deletions(-) delete mode 100644 src/docx/compat.py delete mode 100644 src/docx/opc/compat.py 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/src/docx/compat.py b/src/docx/compat.py deleted file mode 100644 index dfa8ea054..000000000 --- a/src/docx/compat.py +++ /dev/null @@ -1,37 +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/src/docx/image/image.py b/src/docx/image/image.py index fe31b7111..530a90f8f 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -5,9 +5,9 @@ """ import hashlib +import io import os -from docx.compat import BytesIO, is_string from docx.image.exceptions import UnrecognizedImageError from docx.shared import Emu, Inches, lazyproperty @@ -30,7 +30,7 @@ def from_blob(cls, blob): Return a new |Image| subclass instance parsed from the image binary contained in *blob*. """ - stream = BytesIO(blob) + stream = io.BytesIO(blob) return cls._from_stream(stream, blob) @classmethod @@ -39,11 +39,11 @@ 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): + if isinstance(image_descriptor, str): path = image_descriptor with open(path, "rb") as f: blob = f.read() - stream = BytesIO(blob) + stream = io.BytesIO(blob) filename = os.path.basename(path) else: stream = image_descriptor diff --git a/src/docx/image/jpeg.py b/src/docx/image/jpeg.py index adba5c1ad..6a9db9438 100644 --- a/src/docx/image/jpeg.py +++ b/src/docx/image/jpeg.py @@ -1,17 +1,14 @@ -# 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): @@ -460,7 +457,7 @@ def _tiff_from_exif_segment(cls, stream, offset, segment_length): # 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) + substream = io.BytesIO(segment_bytes) return Tiff.from_stream(substream) diff --git a/src/docx/opc/compat.py b/src/docx/opc/compat.py deleted file mode 100644 index d5f63ac19..000000000 --- a/src/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/src/docx/opc/part.py b/src/docx/opc/part.py index 048a06753..e9fab7973 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -1,10 +1,9 @@ """Open Packaging Convention (OPC) objects related to package parts.""" -from docx.opc.compat import cls_method_fn 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 lazyproperty +from docx.opc.shared import cls_method_fn, lazyproperty from docx.oxml import parse_xml diff --git a/src/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py index 3d2f768ab..ae38a647b 100644 --- a/src/docx/opc/phys_pkg.py +++ b/src/docx/opc/phys_pkg.py @@ -3,7 +3,6 @@ import os from zipfile import ZIP_DEFLATED, ZipFile, is_zipfile -from docx.opc.compat import is_string from docx.opc.exceptions import PackageNotFoundError from docx.opc.packuri import CONTENT_TYPES_URI @@ -15,7 +14,7 @@ class PhysPkgReader(object): def __new__(cls, pkg_file): # if *pkg_file* is a string, treat it as a path - if is_string(pkg_file): + if isinstance(pkg_file, str): if os.path.isdir(pkg_file): reader_cls = _DirPkgReader elif is_zipfile(pkg_file): diff --git a/src/docx/opc/shared.py b/src/docx/opc/shared.py index 15eed35d9..370780e6b 100644 --- a/src/docx/opc/shared.py +++ b/src/docx/opc/shared.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Objects shared by opc modules. -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Objects shared by opc modules.""" class CaseInsensitiveDict(dict): @@ -26,6 +20,11 @@ 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 diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 8c0dd414e..7c43995fb 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -3,7 +3,6 @@ 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 @@ -281,7 +280,7 @@ def _set_element_datetime(self, prop_name, value): def _set_element_text(self, prop_name, value): """Set string value of *name* property to *value*.""" - if not is_string(value): + if not isinstance(value, str): value = str(value) if len(value) > 255: diff --git a/src/docx/oxml/settings.py b/src/docx/oxml/settings.py index 3fc72e27f..dc899cbc0 100644 --- a/src/docx/oxml/settings.py +++ b/src/docx/oxml/settings.py @@ -1,9 +1,5 @@ -# 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 diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 4b994b773..11a47beae 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -4,7 +4,6 @@ from lxml import etree -from docx.compat import Unicode from docx.oxml import OxmlElement from docx.oxml.exceptions import InvalidXmlError from docx.oxml.ns import NamespacePrefixedTag, nsmap, qn @@ -20,7 +19,7 @@ def serialize_for_reading(element): return XmlString(xml) -class XmlString(Unicode): +class XmlString(str): """ Provides string comparison override suitable for serialized XML that is useful for tests. diff --git a/src/docx/section.py b/src/docx/section.py index c230eefd7..f69f547f2 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -1,11 +1,8 @@ -# encoding: utf-8 +"""The |Section| object and related proxy classes.""" -"""The |Section| object and related proxy classes""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from collections.abc import Sequence from docx.blkcntnr import BlockItemContainer -from docx.compat import Sequence from docx.enum.section import WD_HEADER_FOOTER from docx.shared import lazyproperty diff --git a/tests/image/test_bmp.py b/tests/image/test_bmp.py index bdd2ff4e7..ed49db386 100644 --- a/tests/image/test_bmp.py +++ b/tests/image/test_bmp.py @@ -1,8 +1,9 @@ """Test suite for docx.image.bmp module.""" +import io + import pytest -from docx.compat import BytesIO from docx.image.bmp import Bmp from docx.image.constants import MIME_TYPE @@ -16,7 +17,7 @@ def it_can_construct_from_a_bmp_stream(self, Bmp__init__): 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) diff --git a/tests/image/test_gif.py b/tests/image/test_gif.py index fd9eac2ea..3af8cfd9f 100644 --- a/tests/image/test_gif.py +++ b/tests/image/test_gif.py @@ -1,8 +1,9 @@ """Unit test suite for docx.image.gif module.""" +import io + import pytest -from docx.compat import BytesIO from docx.image.constants import MIME_TYPE from docx.image.gif import Gif @@ -13,7 +14,7 @@ class DescribeGif(object): 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_) + stream = io.BytesIO(bytes_) gif = Gif.from_stream(stream) diff --git a/tests/image/test_helpers.py b/tests/image/test_helpers.py index e98ac6ca7..8d3db4760 100644 --- a/tests/image/test_helpers.py +++ b/tests/image/test_helpers.py @@ -1,8 +1,9 @@ """Test suite for docx.image.helpers module.""" +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 @@ -33,13 +34,13 @@ def it_can_read_a_long(self, read_long_fixture): ) 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" return stream_rdr, expected_string diff --git a/tests/image/test_image.py b/tests/image/test_image.py index 791831077..addbe61fc 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -1,8 +1,9 @@ """Unit test suite for docx.image package""" +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 @@ -148,7 +149,7 @@ def from_filelike_fixture(self, _from_stream_, image_): 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 @@ -222,7 +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): @@ -258,7 +259,7 @@ 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): @@ -272,7 +273,7 @@ def it_constructs_the_right_class_for_a_given_image_stream(self, call_fixture): 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) @@ -294,7 +295,7 @@ def call_fixture(self, request): image_path = test_file(image_filename) 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 diff --git a/tests/image/test_jpeg.py b/tests/image/test_jpeg.py index bd77bb3a1..d18fbc234 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -1,8 +1,9 @@ """Unit test suite for docx.image.jpeg module""" +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 ( @@ -98,7 +99,7 @@ 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): @@ -224,7 +225,7 @@ def sos_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) class Describe_Marker(object): @@ -247,7 +248,7 @@ def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture): 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) + stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) return stream_reader, marker_code, offset, _Marker__init_, length @pytest.fixture @@ -260,7 +261,7 @@ def it_can_construct_from_a_stream_and_offset(self, _App0Marker__init_): 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) @@ -300,7 +301,7 @@ def it_can_construct_from_a_stream_and_offset( 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) @@ -313,7 +314,7 @@ def it_can_construct_from_a_stream_and_offset( def it_can_construct_from_non_Exif_APP1_segment(self, _App1Marker__init_): 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) @@ -344,13 +345,12 @@ 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_) - - @pytest.fixture - def get_tiff_fixture(self, request, BytesIO_, substream_, Tiff_, tiff_): + def get_tiff_fixture(self, request, substream_, Tiff_, tiff_): bytes_ = b"xfillerxMM\x00*\x00\x00\x00\x42" - stream_reader = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) + BytesIO_ = class_mock( + request, "docx.image.jpeg.io.BytesIO", return_value=substream_ + ) offset, segment_length, segment_bytes = 0, 16, bytes_[8:] return ( stream_reader, @@ -365,7 +365,7 @@ def get_tiff_fixture(self, request, BytesIO_, 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_): @@ -393,7 +393,7 @@ def it_can_construct_from_a_stream_and_offset(self, request, _SofMarker__init_): 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) @@ -475,7 +475,7 @@ def _SofMarker_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) class Describe_MarkerFinder(object): @@ -510,14 +510,14 @@ def _MarkerFinder__init_(self, request): 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) + 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): @@ -622,7 +622,7 @@ 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_): diff --git a/tests/image/test_png.py b/tests/image/test_png.py index 07b50eda0..dce3aec95 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -1,8 +1,9 @@ """Unit test suite for docx.image.png module.""" +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 @@ -72,7 +73,7 @@ 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): @@ -152,7 +153,7 @@ 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): @@ -230,7 +231,7 @@ 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): @@ -304,7 +305,7 @@ def _iter_chunk_offsets_(self, request): @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) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) chunk_parser = _ChunkParser(stream_rdr) expected_chunk_offsets = [ (PNG_CHUNK_TYPE.IHDR, 16), @@ -320,7 +321,7 @@ def StreamReader_(self, request, 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): @@ -409,7 +410,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" - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) offset, px_width, px_height = 0, 42, 24 return stream_rdr, offset, px_width, px_height @@ -430,6 +431,6 @@ 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) + 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 6768f6bd9..08027ac1d 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -1,8 +1,9 @@ """Unit test suite for docx.image.tiff module""" +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 ( @@ -75,7 +76,7 @@ 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): @@ -176,12 +177,12 @@ def _make_stream_reader_(self, request, stream_rdr_): ) 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_): @@ -257,7 +258,7 @@ def offset_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) class Describe_IfdParser(object): @@ -296,7 +297,7 @@ def _IfdEntryFactory_(self, request, 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_] @@ -341,7 +342,7 @@ def fixture( "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_ @@ -389,7 +390,7 @@ 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) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) offset, tag_code, value_count, value_offset = 0, 1, 2, 3 _parse_value_.return_value = value_ @@ -424,7 +425,7 @@ def value_(self, request): class Describe_AsciiIfdEntry(object): def it_can_parse_an_ascii_string_IFD_entry(self): bytes_ = b"foobar\x00" - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _AsciiIfdEntry._parse_value(stream_rdr, None, 7, 0) assert val == "foobar" @@ -432,7 +433,7 @@ def it_can_parse_an_ascii_string_IFD_entry(self): class Describe_ShortIfdEntry(object): def it_can_parse_a_short_int_IFD_entry(self): bytes_ = b"foobaroo\x00\x2A" - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _ShortIfdEntry._parse_value(stream_rdr, 0, 1, None) assert val == 42 @@ -440,7 +441,7 @@ def it_can_parse_a_short_int_IFD_entry(self): class Describe_LongIfdEntry(object): def it_can_parse_a_long_int_IFD_entry(self): bytes_ = b"foobaroo\x00\x00\x00\x2A" - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _LongIfdEntry._parse_value(stream_rdr, 0, 1, None) assert val == 42 @@ -448,6 +449,6 @@ def it_can_parse_a_long_int_IFD_entry(self): class Describe_RationalIfdEntry(object): 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) + 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/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index c81a8de52..0cb769551 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -2,7 +2,6 @@ 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 @@ -126,7 +125,7 @@ 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 --------------------------------------------- From cbf940c66b6b7c1adaab08dec6606f52bf42a7f0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 26 Sep 2023 14:52:27 -0700 Subject: [PATCH 014/131] rfctr: bulk Python 2 formatting updates * encoding header is no longer needed * from __future__ imports no longer needed * make module docstrings PEP 257 compliant. --- features/environment.py | 7 +- src/docx/api.py | 6 +- src/docx/blkcntnr.py | 4 - src/docx/dml/color.py | 8 +- src/docx/enum/__init__.py | 8 +- src/docx/enum/shape.py | 8 +- src/docx/enum/text.py | 49 ++++----- src/docx/exceptions.py | 5 +- src/docx/image/bmp.py | 4 - src/docx/image/constants.py | 6 +- src/docx/image/exceptions.py | 6 +- src/docx/image/gif.py | 4 - src/docx/opc/constants.py | 144 +++++++++++-------------- src/docx/opc/coreprops.py | 8 +- src/docx/opc/exceptions.py | 5 +- src/docx/opc/pkgreader.py | 19 ++-- src/docx/opc/rel.py | 8 +- src/docx/oxml/coreprops.py | 50 ++++----- src/docx/oxml/exceptions.py | 6 +- src/docx/oxml/numbering.py | 14 +-- src/docx/oxml/section.py | 6 +- src/docx/oxml/shape.py | 14 +-- src/docx/oxml/shared.py | 16 +-- src/docx/oxml/simpletypes.py | 11 +- src/docx/oxml/styles.py | 12 +-- src/docx/oxml/text/paragraph.py | 38 ++----- src/docx/oxml/text/parfmt.py | 10 +- src/docx/oxml/text/run.py | 118 +++++++++----------- src/docx/oxml/xmlchemy.py | 33 +++--- src/docx/package.py | 6 +- src/docx/parts/document.py | 6 +- src/docx/parts/hdrftr.py | 6 +- src/docx/parts/image.py | 8 +- src/docx/parts/numbering.py | 8 +- src/docx/parts/settings.py | 18 ++-- src/docx/parts/story.py | 6 +- src/docx/parts/styles.py | 18 ++-- src/docx/settings.py | 6 +- src/docx/shared.py | 8 +- src/docx/styles/styles.py | 6 +- src/docx/text/font.py | 8 +- src/docx/text/paragraph.py | 27 +++-- tests/opc/unitdata/types.py | 8 +- tests/oxml/parts/test_document.py | 8 +- tests/oxml/parts/unitdata/document.py | 6 +- tests/oxml/text/test_run.py | 8 +- tests/oxml/unitdata/dml.py | 150 +------------------------- tests/oxml/unitdata/numbering.py | 6 +- tests/oxml/unitdata/section.py | 6 +- tests/oxml/unitdata/shared.py | 6 +- tests/oxml/unitdata/styles.py | 6 +- tests/oxml/unitdata/table.py | 6 +- tests/oxml/unitdata/text.py | 6 +- tests/unitdata.py | 8 +- tests/unitutil/cxml.py | 13 +-- tests/unitutil/mock.py | 6 +- 56 files changed, 273 insertions(+), 728 deletions(-) diff --git a/features/environment.py b/features/environment.py index f180da73c..dfd2028a3 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,9 +1,4 @@ -# 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 diff --git a/src/docx/api.py b/src/docx/api.py index 4cf6acd1b..9b496c461 100644 --- a/src/docx/api.py +++ b/src/docx/api.py @@ -1,13 +1,9 @@ -# 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. """ -from __future__ import absolute_import, division, print_function - import os from docx.opc.constants import CONTENT_TYPE as CT diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index b4d4d8b04..9c6c506a8 100644 --- a/src/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 diff --git a/src/docx/dml/color.py b/src/docx/dml/color.py index cfdf097a4..6f00b4f99 100644 --- a/src/docx/dml/color.py +++ b/src/docx/dml/color.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -DrawingML objects related to color, ColorFormat being the most prominent. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""DrawingML objects related to color, ColorFormat being the most prominent.""" from ..enum.dml import MSO_COLOR_TYPE from ..oxml.simpletypes import ST_HexColorAuto diff --git a/src/docx/enum/__init__.py b/src/docx/enum/__init__.py index e1bbd47f2..c0ba1a5a3 100644 --- a/src/docx/enum/__init__.py +++ b/src/docx/enum/__init__.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Enumerations used in python-docx -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Enumerations used in python-docx.""" class Enumeration(object): diff --git a/src/docx/enum/shape.py b/src/docx/enum/shape.py index b785ab9c1..64221dc73 100644 --- a/src/docx/enum/shape.py +++ b/src/docx/enum/shape.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Enumerations related to DrawingML shapes in WordprocessingML files -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Enumerations related to DrawingML shapes in WordprocessingML files.""" class WD_INLINE_SHAPE_TYPE(object): diff --git a/src/docx/enum/text.py b/src/docx/enum/text.py index 8015187e4..2ccbdc522 100644 --- a/src/docx/enum/text.py +++ b/src/docx/enum/text.py @@ -5,17 +5,15 @@ @alias("WD_ALIGN_PARAGRAPH") class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): - """ - alias: **WD_ALIGN_PARAGRAPH** + """Alias: **WD_ALIGN_PARAGRAPH** Specifies paragraph justification type. Example:: - from docx.enum.text import WD_ALIGN_PARAGRAPH + from docx.enum.text import WD_ALIGN_PARAGRAPH - paragraph = document.add_paragraph() - paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + paragraph = document.add_paragraph() paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER """ __ms_name__ = "WdParagraphAlignment" @@ -38,34 +36,32 @@ class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): "JUSTIFY_MED", 5, "mediumKashida", - "Justified with a medium char" "acter compression ratio.", + "Justified with a medium character compression ratio.", ), XmlMappedEnumMember( "JUSTIFY_HI", 7, "highKashida", - "Justified with a high character" " compression ratio.", + "Justified with a high character compression ratio.", ), XmlMappedEnumMember( "JUSTIFY_LOW", 8, "lowKashida", - "Justified with a low character " "compression ratio.", + "Justified with a low character compression ratio.", ), XmlMappedEnumMember( "THAI_JUSTIFY", 9, "thaiDistribute", - "Justified according to Tha" "i formatting layout.", + "Justified according to Thai formatting layout.", ), ) class WD_BREAK_TYPE(object): - """ - Corresponds to WdBreakType enumeration - http://msdn.microsoft.com/en-us/library/office/ff195905.aspx - """ + """Corresponds to WdBreakType enumeration http://msdn.microsoft.com/en- + us/library/office/ff195905.aspx.""" COLUMN = 8 LINE = 6 @@ -85,9 +81,9 @@ class WD_BREAK_TYPE(object): @alias("WD_COLOR") class WD_COLOR_INDEX(XmlEnumeration): - """ - Specifies a standard preset color to apply. Used for font highlighting and - perhaps other applications. + """Specifies a standard preset color to apply. + + Used for font highlighting and perhaps other applications. """ __ms_name__ = "WdColorIndex" @@ -121,15 +117,14 @@ class WD_COLOR_INDEX(XmlEnumeration): class WD_LINE_SPACING(XmlEnumeration): - """ - Specifies a line spacing format to be applied to a paragraph. + """Specifies a line spacing format to be applied to a paragraph. Example:: - from docx.enum.text import WD_LINE_SPACING + from docx.enum.text import WD_LINE_SPACING - paragraph = document.add_paragraph() - paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY + paragraph = document.add_paragraph() paragraph.line_spacing_rule = + WD_LINE_SPACING.EXACTLY """ __ms_name__ = "WdLineSpacing" @@ -166,9 +161,7 @@ class WD_LINE_SPACING(XmlEnumeration): class WD_TAB_ALIGNMENT(XmlEnumeration): - """ - Specifies the tab stop alignment to apply. - """ + """Specifies the tab stop alignment to apply.""" __ms_name__ = "WdTabAlignment" @@ -189,9 +182,7 @@ class WD_TAB_ALIGNMENT(XmlEnumeration): class WD_TAB_LEADER(XmlEnumeration): - """ - Specifies the character to use as the leader with formatted tabs. - """ + """Specifies the character to use as the leader with formatted tabs.""" __ms_name__ = "WdTabLeader" @@ -208,9 +199,7 @@ class WD_TAB_LEADER(XmlEnumeration): class WD_UNDERLINE(XmlEnumeration): - """ - Specifies the style of underline applied to a run of characters. - """ + """Specifies the style of underline applied to a run of characters.""" __ms_name__ = "WdUnderline" diff --git a/src/docx/exceptions.py b/src/docx/exceptions.py index 7a8b99c81..8507a1ded 100644 --- a/src/docx/exceptions.py +++ b/src/docx/exceptions.py @@ -1,7 +1,4 @@ -# encoding: utf-8 - -""" -Exceptions used with python-docx. +"""Exceptions used with python-docx. The base exception class is PythonDocxError. """ diff --git a/src/docx/image/bmp.py b/src/docx/image/bmp.py index aebc6b9cc..e4332b637 100644 --- a/src/docx/image/bmp.py +++ b/src/docx/image/bmp.py @@ -1,7 +1,3 @@ -# 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 diff --git a/src/docx/image/constants.py b/src/docx/image/constants.py index e4fa17fb3..5caf64eb2 100644 --- a/src/docx/image/constants.py +++ b/src/docx/image/constants.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Constants specific the the image sub-package -""" +"""Constants specific the the image sub-package.""" class JPEG_MARKER_CODE(object): diff --git a/src/docx/image/exceptions.py b/src/docx/image/exceptions.py index f233edc4e..9e3fab6bf 100644 --- a/src/docx/image/exceptions.py +++ b/src/docx/image/exceptions.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Exceptions specific the the image sub-package -""" +"""Exceptions specific the the image sub-package.""" class InvalidImageStreamError(Exception): diff --git a/src/docx/image/gif.py b/src/docx/image/gif.py index 07f2b1c77..b6b396be4 100644 --- a/src/docx/image/gif.py +++ b/src/docx/image/gif.py @@ -1,7 +1,3 @@ -# encoding: utf-8 - -from __future__ import absolute_import, division, print_function - from struct import Struct from .constants import MIME_TYPE diff --git a/src/docx/opc/constants.py b/src/docx/opc/constants.py index 425435d92..48e32e0ce 100644 --- a/src/docx/opc/constants.py +++ b/src/docx/opc/constants.py @@ -1,32 +1,28 @@ -# encoding: utf-8 +"""Constant values related to the Open Packaging Convention. -""" -Constant values related to the Open Packaging Convention, in particular, -content types and relationship types. +In particular it includes content types and relationship types. """ class CONTENT_TYPE(object): - """ - Content type URIs (like MIME-types) that specify a part's format - """ + """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" + "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml" ) DML_DIAGRAM_COLORS = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramColo" "rs+xml" + "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml" ) DML_DIAGRAM_DATA = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramData" "+xml" + "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml" ) DML_DIAGRAM_LAYOUT = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramLayo" "ut+xml" + "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" ) DML_DIAGRAM_STYLE = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramStyl" "e+xml" + "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" ) GIF = "image/gif" JPEG = "image/jpeg" @@ -35,11 +31,11 @@ class CONTENT_TYPE(object): "application/vnd.openxmlformats-officedocument.custom-properties+xml" ) OFC_CUSTOM_XML_PROPERTIES = ( - "application/vnd.openxmlformats-officedocument.customXmlProperties+x" "ml" + "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" ) OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml" OFC_EXTENDED_PROPERTIES = ( - "application/vnd.openxmlformats-officedocument.extended-properties+x" "ml" + "application/vnd.openxmlformats-officedocument.extended-properties+xml" ) OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" @@ -50,17 +46,17 @@ class CONTENT_TYPE(object): 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" + "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-xmlsignatu" "re+xml" + "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" ) OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" PML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.presentationml.commen" "ts+xml" + "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" ) PML_COMMENT_AUTHORS = ( "application/vnd.openxmlformats-officedocument.presentationml.commen" @@ -75,22 +71,20 @@ class CONTENT_TYPE(object): "aster+xml" ) PML_NOTES_SLIDE = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesS" "lide+xml" + "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.presPr" "ops+xml" + "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_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" PML_SLIDESHOW_MAIN = ( "application/vnd.openxmlformats-officedocument.presentationml.slides" "how.main+xml" @@ -111,34 +105,32 @@ class CONTENT_TYPE(object): "application/vnd.openxmlformats-officedocument.presentationml.tableS" "tyles+xml" ) - PML_TAGS = ( - "application/vnd.openxmlformats-officedocument.presentationml.tags+x" "ml" - ) + 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.viewPr" "ops+xml" + "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" ) PNG = "image/png" SML_CALC_CHAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.calcCha" "in+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" ) SML_CHARTSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsh" "eet+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" ) SML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.comment" "s+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" ) SML_CONNECTIONS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.connect" "ions+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" ) SML_CUSTOM_PROPERTY = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.customP" "roperty" + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" ) SML_DIALOGSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogs" "heet+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" ) SML_EXTERNAL_LINK = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.externa" @@ -153,20 +145,20 @@ class CONTENT_TYPE(object): "cheRecords+xml" ) SML_PIVOT_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTa" "ble+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" ) SML_PRINTER_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.printer" "Settings" + "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" ) SML_QUERY_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTa" "ble+xml" + "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.revisio" "nLog+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" ) SML_SHARED_STRINGS = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedS" @@ -174,18 +166,16 @@ class CONTENT_TYPE(object): ) SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" SML_SHEET_MAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.m" "ain+xml" + "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+x" "ml" + "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" @@ -195,21 +185,21 @@ class CONTENT_TYPE(object): "e.main+xml" ) SML_USER_NAMES = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.userNam" "es+xml" + "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.workshe" "et+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" ) TIFF = "image/tiff" WML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.comm" "ents+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" ) WML_DOCUMENT = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" "ment" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) WML_DOCUMENT_GLOSSARY = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" @@ -220,21 +210,21 @@ class CONTENT_TYPE(object): "ment.main+xml" ) WML_ENDNOTES = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.endn" "otes+xml" + "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.foot" "er+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" ) WML_FOOTNOTES = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.foot" "notes+xml" ) WML_HEADER = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.head" "er+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" ) WML_NUMBERING = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.numb" @@ -245,10 +235,10 @@ class CONTENT_TYPE(object): "terSettings" ) WML_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.sett" "ings+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" ) WML_STYLES = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.styl" "es+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" ) WML_WEB_SETTINGS = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.webS" @@ -262,10 +252,10 @@ class CONTENT_TYPE(object): class NAMESPACE(object): - """Constant values for OPC XML namespaces""" + """Constant values for OPC XML namespaces.""" DML_WORDPROCESSING_DRAWING = ( - "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDraw" "ing" + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ) OFC_RELATIONSHIPS = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" @@ -276,18 +266,16 @@ class NAMESPACE(object): class RELATIONSHIP_TARGET_MODE(object): - """Open XML relationship target modes""" + """Open XML relationship target modes.""" EXTERNAL = "External" INTERNAL = "Internal" class RELATIONSHIP_TYPE(object): - AUDIO = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/audio" - ) + AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" A_F_CHUNK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/aFChunk" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" ) CALC_CHAIN = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" @@ -297,9 +285,7 @@ class RELATIONSHIP_TYPE(object): "http://schemas.openxmlformats.org/package/2006/relationships/digita" "l-signature/certificate" ) - CHART = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/chart" - ) + CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" CHARTSHEET = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/chartsheet" @@ -321,7 +307,7 @@ class RELATIONSHIP_TYPE(object): "/connections" ) CONTROL = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/control" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" ) CORE_PROPERTIES = ( "http://schemas.openxmlformats.org/package/2006/relationships/metada" @@ -364,7 +350,7 @@ class RELATIONSHIP_TYPE(object): "/dialogsheet" ) DRAWING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/drawing" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" ) ENDNOTES = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" @@ -378,13 +364,13 @@ class RELATIONSHIP_TYPE(object): "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/externalLink" ) - FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/font" + 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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" ) FOOTNOTES = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" @@ -399,15 +385,13 @@ class RELATIONSHIP_TYPE(object): "/handoutMaster" ) HEADER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/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" - ) + IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" NOTES_MASTER = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/notesMaster" @@ -433,7 +417,7 @@ class RELATIONSHIP_TYPE(object): "l-signature/origin" ) PACKAGE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/package" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" ) PIVOT_CACHE_DEFINITION = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" @@ -483,9 +467,7 @@ class RELATIONSHIP_TYPE(object): "http://schemas.openxmlformats.org/package/2006/relationships/digita" "l-signature/signature" ) - SLIDE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/slide" - ) + SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" SLIDE_LAYOUT = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/slideLayout" @@ -499,11 +481,9 @@ class RELATIONSHIP_TYPE(object): "/slideUpdateInfo" ) STYLES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/styles" - ) - TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/table" + "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" @@ -512,10 +492,8 @@ class RELATIONSHIP_TYPE(object): "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" - ) + 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" @@ -528,9 +506,7 @@ class RELATIONSHIP_TYPE(object): "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/usernames" ) - VIDEO = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/video" - ) + VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video" VIEW_PROPS = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/viewProps" @@ -552,5 +528,5 @@ class RELATIONSHIP_TYPE(object): "/worksheetSource" ) XML_MAPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "/xmlMaps" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" ) diff --git a/src/docx/opc/coreprops.py b/src/docx/opc/coreprops.py index 186e851df..c0434730a 100644 --- a/src/docx/opc/coreprops.py +++ b/src/docx/opc/coreprops.py @@ -1,11 +1,7 @@ -# encoding: utf-8 +"""Provides CoreProperties, Dublin-Core attributes of the document. +These are broadly-standardized attributes like author, last-modified, etc. """ -The :mod:`pptx.packaging` module coheres around the concerns of reading and -writing presentations to and from a .pptx file. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals class CoreProperties(object): diff --git a/src/docx/opc/exceptions.py b/src/docx/opc/exceptions.py index b8e6de43f..3225d6943 100644 --- a/src/docx/opc/exceptions.py +++ b/src/docx/opc/exceptions.py @@ -1,7 +1,4 @@ -# encoding: utf-8 - -""" -Exceptions specific to python-opc +"""Exceptions specific to python-opc. The base exception class is OpcError. """ diff --git a/src/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py index b5180192e..ca6849e26 100644 --- a/src/docx/opc/pkgreader.py +++ b/src/docx/opc/pkgreader.py @@ -1,17 +1,10 @@ -# 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 __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 +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 class PackageReader(object): diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index a57d02f79..97be3f6bf 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Relationship-related objects. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Relationship-related objects.""" from .oxml import CT_Relationships diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 7c43995fb..854b83309 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -9,12 +9,11 @@ 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. + """`` 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=()) @@ -37,9 +36,7 @@ class CT_CoreProperties(BaseOxmlElement): @classmethod def new(cls): - """ - Return a new ```` element - """ + """Return a new `` element.""" xml = cls._coreProperties_tmpl coreProperties = parse_xml(xml) return coreProperties @@ -157,7 +154,7 @@ def revision_number(self): @revision_number.setter def revision_number(self, value): """ - Set revision property to string value of integer *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'" @@ -202,7 +199,7 @@ def _datetime_of_element(self, property_name): def _get_or_add(self, prop_name): """ - Return element returned by 'get_or_add_' method for *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) @@ -211,10 +208,9 @@ def _get_or_add(self, prop_name): @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'``. + """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: @@ -231,11 +227,11 @@ def _offset_dt(cls, dt, offset_str): @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' + # 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", @@ -243,7 +239,7 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): "%Y", ) # strptime isn't smart enough to parse literal timezone offsets like - # '-07:30', so we have to do it ourselves + # "-07:30", so we have to do it ourselves parseable_part = w3cdtf_str[:19] offset_str = w3cdtf_str[19:] dt = None @@ -261,7 +257,7 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): def _set_element_datetime(self, prop_name, value): """ - Set date/time value of child element having *prop_name* to *value*. + Set date/time value of child element having `prop_name` to `value`. """ if not isinstance(value, datetime): tmpl = "property requires object, got %s" @@ -270,7 +266,7 @@ def _set_element_datetime(self, prop_name, value): 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"' + # 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 @@ -279,7 +275,7 @@ def _set_element_datetime(self, prop_name, value): del self.attrib[qn("xsi:foo")] def _set_element_text(self, prop_name, value): - """Set string value of *name* property to *value*.""" + """Set string value of `name` property to `value`.""" if not isinstance(value, str): value = str(value) @@ -290,9 +286,9 @@ def _set_element_text(self, prop_name, value): 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. + """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: diff --git a/src/docx/oxml/exceptions.py b/src/docx/oxml/exceptions.py index 4696f1e93..ee0cff832 100644 --- a/src/docx/oxml/exceptions.py +++ b/src/docx/oxml/exceptions.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Exceptions for oxml sub-package -""" +"""Exceptions for oxml sub-package.""" class XmlchemyError(Exception): diff --git a/src/docx/oxml/numbering.py b/src/docx/oxml/numbering.py index d5297b2a5..6fb7e1b01 100644 --- a/src/docx/oxml/numbering.py +++ b/src/docx/oxml/numbering.py @@ -1,13 +1,9 @@ -# encoding: utf-8 +"""Custom element classes related to the numbering part.""" -""" -Custom element classes related to the numbering part -""" - -from . import OxmlElement -from .shared import CT_DecimalNumber -from .simpletypes import ST_DecimalNumber -from .xmlchemy import ( +from docx.oxml import OxmlElement +from docx.oxml.shared import CT_DecimalNumber +from docx.oxml.simpletypes import ST_DecimalNumber +from docx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index e71936774..73a928e07 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Section-related custom element classes""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Section-related custom element classes.""" from copy import deepcopy diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index 885535c71..a66d251bf 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -1,12 +1,8 @@ -# encoding: utf-8 +"""Custom element classes for shape-related elements like ``.""" -""" -Custom element classes for shape-related elements like ```` -""" - -from . import parse_xml -from .ns import nsdecls -from .simpletypes import ( +from docx.oxml import parse_xml +from docx.oxml.ns import nsdecls +from docx.oxml.simpletypes import ( ST_Coordinate, ST_DrawingElementId, ST_PositiveCoordinate, @@ -14,7 +10,7 @@ XsdString, XsdToken, ) -from .xmlchemy import ( +from docx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index 4981e9200..9ef42a023 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -1,15 +1,9 @@ -# encoding: utf-8 +"""Objects shared by modules in the docx.oxml subpackage.""" -""" -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 +from docx.oxml import OxmlElement +from docx.oxml.ns import qn +from docx.oxml.simpletypes import ST_DecimalNumber, ST_OnOff, ST_String +from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute class CT_DecimalNumber(BaseOxmlElement): diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index 8fb6cb89d..56226b6da 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -1,12 +1,9 @@ -# 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 ..exceptions import InvalidXmlError from ..shared import Emu, Pt, RGBColor, Twips diff --git a/src/docx/oxml/styles.py b/src/docx/oxml/styles.py index 96b390baf..3cbb005e1 100644 --- a/src/docx/oxml/styles.py +++ b/src/docx/oxml/styles.py @@ -1,12 +1,8 @@ -# encoding: utf-8 +"""Custom element classes related to the styles part.""" -""" -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 ( +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, diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index 8386420f6..e50914c2c 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -1,17 +1,11 @@ -# encoding: utf-8 +"""Custom element classes related to paragraphs (CT_P).""" -""" -Custom element classes related to paragraphs (CT_P). -""" - -from ..ns import qn -from ..xmlchemy import BaseOxmlElement, OxmlElement, ZeroOrMore, ZeroOrOne +from docx.oxml.ns import qn +from docx.oxml.xmlchemy import BaseOxmlElement, OxmlElement, ZeroOrMore, ZeroOrOne class CT_P(BaseOxmlElement): - """ - ```` element, containing the properties and text for a paragraph. - """ + """`` element, containing the properties and text for a paragraph.""" pPr = ZeroOrOne("w:pPr") r = ZeroOrMore("w:r") @@ -21,19 +15,14 @@ def _insert_pPr(self, pPr): return pPr def add_p_before(self): - """ - Return a new ```` element inserted directly prior to this one. - """ + """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. - """ + """The value of the `` grandchild element or |None| if not present.""" pPr = self.pPr if pPr is None: return None @@ -45,28 +34,23 @@ def alignment(self, value): pPr.jc_val = value def clear_content(self): - """ - Remove all child elements, except the ```` element if present. - """ + """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. - """ + """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): - """ - String contained in w:val attribute of ./w:pPr/w:pStyle grandchild, - or |None| if not present. + """String contained in `w:val` attribute of `./w:pPr/w:pStyle` grandchild. + + |None| if not present. """ pPr = self.pPr if pPr is None: diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index cae729d57..90f879340 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Custom element classes related to paragraph properties (CT_PPr). -""" +"""Custom element classes related to paragraph properties (CT_PPr).""" from ...enum.text import ( WD_ALIGN_PARAGRAPH, @@ -355,9 +351,7 @@ class CT_Spacing(BaseOxmlElement): class CT_TabStop(BaseOxmlElement): - """ - ```` element, representing an individual tab stop. - """ + """`` 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) diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 3d3d3cdf6..9d930aad0 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -1,27 +1,15 @@ -# encoding: utf-8 +"""Custom element classes related to text runs (CT_R).""" -""" -Custom element classes related to text runs (CT_R). -""" +from docx.oxml.ns import qn +from docx.oxml.simpletypes import ST_BrClear, ST_BrType +from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne -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) +# ------------------------------------------------------------------------------------ +# Run-level elements class CT_R(BaseOxmlElement): - """ - ```` element, containing the properties and text for a run. - """ + """`` element, containing the properties and text for a run.""" rPr = ZeroOrOne("w:rPr") t = ZeroOrMore("w:t") @@ -30,40 +18,32 @@ class CT_R(BaseOxmlElement): 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*. - """ + """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. + """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): - """ - Remove all child elements except the ```` element if present. - """ + """Remove all child elements except a `w:rPr` 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 + """String contained in `w:val` attribute of `w:rStyle` grandchild. + |None| if that element is not present. """ rPr = self.rPr @@ -73,18 +53,18 @@ def style(self): @style.setter def style(self, style): - """ - Set the character style of this element to *style*. If *style* - is None, remove the style element. + """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): - """ - A string representing the textual content of this run, with content - child elements like ```` translated to their Python + """The textual content of this run. + + Inner-content child elements like `w:tab` are translated to their text equivalent. """ text = "" @@ -103,21 +83,37 @@ def text(self, text): self.clear_content() _RunContentAppender.append_to_run_from_text(self, text) + def _insert_rPr(self, 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 = OptionalAttribute("w:type", ST_BrType) + clear = OptionalAttribute("w:clear", ST_BrClear) + class CT_Text(BaseOxmlElement): - """ - ```` element, containing a sequence of characters within a run. - """ + """`` element, containing a sequence of characters within a run.""" + + +# ------------------------------------------------------------------------------------ +# Utility 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. + """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): @@ -126,30 +122,22 @@ def __init__(self, r): @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*. - """ + """Append inner-content elements for `text` to `r` element.""" 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. - """ + """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): - """ - 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. + """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() diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 11a47beae..887ffda86 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -84,9 +84,7 @@ def _parse_line(cls, line): class MetaOxmlElement(type): - """ - Metaclass for BaseOxmlElement - """ + """Metaclass for BaseOxmlElement.""" def __init__(cls, clsname, bases, clsdict): dispatchable = ( @@ -698,9 +696,9 @@ def _remove_choice_group_method_name(self): 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 + """Effective base class for all custom element classes. + + Adds standardized behavior to all classes in one place. Actual inheritance is from BaseOxmlElement below, needed to manage Python 2-3 metaclass declaration compatibility. """ @@ -715,10 +713,7 @@ def __repr__(self): ) def first_child_found_in(self, *tagnames): - """ - Return the first child found with tag in *tagnames*, or None if - not found. - """ + """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: @@ -734,10 +729,7 @@ def insert_element_before(self, elm, *tagnames): return elm def remove_all(self, *tagnames): - """ - Remove all child elements whose tagname (e.g. 'a:p') appears in - *tagnames*. - """ + """Remove child elements with tagname (e.g. "a:p") in `tagnames`.""" for tagname in tagnames: matching = self.findall(qn(tagname)) for child in matching: @@ -745,17 +737,16 @@ def remove_all(self, *tagnames): @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. + """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. + """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) diff --git a/src/docx/package.py b/src/docx/package.py index 07f0718a2..8baa6f1f1 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""WordprocessingML Package class and related objects""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""WordprocessingML Package class and related objects.""" from docx.image.image import Image from docx.opc.constants import RELATIONSHIP_TYPE as RT diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 59d0b7a71..0631dc36a 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""|DocumentPart| and closely related objects""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""|DocumentPart| and closely related objects.""" from docx.document import Document from docx.opc.constants import RELATIONSHIP_TYPE as RT diff --git a/src/docx/parts/hdrftr.py b/src/docx/parts/hdrftr.py index 22ea874a0..1a4522dcf 100644 --- a/src/docx/parts/hdrftr.py +++ b/src/docx/parts/hdrftr.py @@ -1,8 +1,4 @@ -# 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 diff --git a/src/docx/parts/image.py b/src/docx/parts/image.py index a235b465f..919548a8a 100644 --- a/src/docx/parts/image.py +++ b/src/docx/parts/image.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -The proxy class for an image part, and related objects. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""The proxy class for an image part, and related objects.""" import hashlib diff --git a/src/docx/parts/numbering.py b/src/docx/parts/numbering.py index 8bcd271a3..3eae202ab 100644 --- a/src/docx/parts/numbering.py +++ b/src/docx/parts/numbering.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -|NumberingPart| and closely related objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""|NumberingPart| and closely related objects.""" from ..opc.part import XmlPart from ..shared import lazyproperty diff --git a/src/docx/parts/settings.py b/src/docx/parts/settings.py index e0f751b34..f23bf459a 100644 --- a/src/docx/parts/settings.py +++ b/src/docx/parts/settings.py @@ -1,18 +1,12 @@ -# encoding: utf-8 - -""" -|SettingsPart| and closely related objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""|SettingsPart| and closely related objects.""" 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 +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.opc.part import XmlPart +from docx.oxml import parse_xml +from docx.settings import Settings class SettingsPart(XmlPart): diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py index 49cfa396b..11ac3c60a 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""|BaseStoryPart| and related objects""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""|BaseStoryPart| and related objects.""" from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.part import XmlPart diff --git a/src/docx/parts/styles.py b/src/docx/parts/styles.py index a16b4188f..c23637e54 100644 --- a/src/docx/parts/styles.py +++ b/src/docx/parts/styles.py @@ -1,18 +1,12 @@ -# encoding: utf-8 - -""" -Provides StylesPart and related objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Provides StylesPart and related objects.""" 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 +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.opc.part import XmlPart +from docx.oxml import parse_xml +from docx.styles.styles import Styles class StylesPart(XmlPart): diff --git a/src/docx/settings.py b/src/docx/settings.py index 502c9d4db..3485e76d3 100644 --- a/src/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 diff --git a/src/docx/shared.py b/src/docx/shared.py index ea855a21c..61d21aee8 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Objects shared by docx modules. -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Objects shared by docx modules.""" class Length(int): diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py index e19e97c1c..766645a83 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Styles object, container for all objects in the styles part""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Styles object, container for all objects in the styles part.""" from warnings import warn diff --git a/src/docx/text/font.py b/src/docx/text/font.py index 5a88e8af5..ecb824151 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Font-related proxy objects. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Font-related proxy objects.""" from ..dml.color import ColorFormat from ..shared import ElementProxy diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 59dab690c..a0ff17a93 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -7,9 +7,7 @@ class Paragraph(Parented): - """ - Proxy object wrapping ```` element. - """ + """Proxy object wrapping a `` element.""" def __init__(self, p, parent): super(Paragraph, self).__init__(parent) @@ -104,18 +102,17 @@ def style(self, style_or_name): 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. + def text(self) -> str: + """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: diff --git a/tests/opc/unitdata/types.py b/tests/opc/unitdata/types.py index d5b742b10..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 diff --git a/tests/oxml/parts/test_document.py b/tests/oxml/parts/test_document.py index 707a0dd71..ec275941c 100644 --- a/tests/oxml/parts/test_document.py +++ b/tests/oxml/parts/test_document.py @@ -1,10 +1,4 @@ -# 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 diff --git a/tests/oxml/parts/unitdata/document.py b/tests/oxml/parts/unitdata/document.py index 6dd8efe79..36d7738b8 100644 --- a/tests/oxml/parts/unitdata/document.py +++ b/tests/oxml/parts/unitdata/document.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test data builders for parts XML. -""" +"""Test data builders for parts XML.""" from ....unitdata import BaseBuilder diff --git a/tests/oxml/text/test_run.py b/tests/oxml/text/test_run.py index 0dc091b26..db5ae727a 100644 --- a/tests/oxml/text/test_run.py +++ b/tests/oxml/text/test_run.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for the docx.oxml.text.run module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Test suite for the docx.oxml.text.run module.""" import pytest diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index d2ba72ed7..325a3f690 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test data builders for DrawingML XML elements -""" +"""Test data builders for DrawingML XML elements.""" from ...unitdata import BaseBuilder @@ -19,12 +15,6 @@ class CT_BlipFillPropertiesBuilder(BaseBuilder): __attrs__ = () -class CT_DrawingBuilder(BaseBuilder): - __tag__ = "w:drawing" - __nspfxs__ = ("w",) - __attrs__ = () - - class CT_GraphicalObjectBuilder(BaseBuilder): __tag__ = "a:graphic" __nspfxs__ = ("a",) @@ -37,96 +27,18 @@ class CT_GraphicalObjectDataBuilder(BaseBuilder): __attrs__ = ("uri",) -class CT_GraphicalObjectFrameLockingBuilder(BaseBuilder): - __tag__ = "a:graphicFrameLocks" - __nspfxs__ = ("a",) - __attrs__ = ("noChangeAspect",) - - 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" - - 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",) - __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 92f943fdb..386ec6117 100644 --- a/tests/oxml/unitdata/numbering.py +++ b/tests/oxml/unitdata/numbering.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test data builders for numbering part XML elements -""" +"""Test data builders for numbering part XML elements.""" from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/section.py b/tests/oxml/unitdata/section.py index 13194f4c2..397ade7db 100644 --- a/tests/oxml/unitdata/section.py +++ b/tests/oxml/unitdata/section.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test data builders for section-related XML elements -""" +"""Test data builders for section-related XML elements.""" from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/shared.py b/tests/oxml/unitdata/shared.py index a7007862e..bf67463e0 100644 --- a/tests/oxml/unitdata/shared.py +++ b/tests/oxml/unitdata/shared.py @@ -1,8 +1,4 @@ -# 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 diff --git a/tests/oxml/unitdata/styles.py b/tests/oxml/unitdata/styles.py index 0411c93e6..24acd5ed2 100644 --- a/tests/oxml/unitdata/styles.py +++ b/tests/oxml/unitdata/styles.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test data builders for styles part XML elements -""" +"""Test data builders for styles part XML elements.""" from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/table.py b/tests/oxml/unitdata/table.py index 45536d49a..4f760c1a8 100644 --- a/tests/oxml/unitdata/table.py +++ b/tests/oxml/unitdata/table.py @@ -1,8 +1,4 @@ -# 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 diff --git a/tests/oxml/unitdata/text.py b/tests/oxml/unitdata/text.py index de1d984d3..8bf60bbe3 100644 --- a/tests/oxml/unitdata/text.py +++ b/tests/oxml/unitdata/text.py @@ -1,8 +1,4 @@ -# 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 diff --git a/tests/unitdata.py b/tests/unitdata.py index 7fe0a7c12..d5939899e 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Shared code for unit test data builders -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Shared code for unit test data builders.""" from docx.oxml import parse_xml from docx.oxml.ns import nsdecls diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index f212a4c07..cc52ecdd1 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -29,17 +29,13 @@ def element(cxel_str): - """ - Return an oxml element parsed from the XML generated from *cxel_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*. - """ + """Return the XML generated from *cxel_str*.""" root_token = root_node.parseString(cxel_str) xml = root_token.element.xml return xml @@ -51,10 +47,7 @@ def xml(cxel_str): def nsdecls(*nspfxs): - """ - Return a string containing a namespace declaration for each of *nspfxs*, - in the order they are specified. - """ + """Namespace-declaration including each of *nspfxs*, in the order specified.""" nsdecls = "" for nspfx in nspfxs: nsdecls += ' xmlns:%s="%s"' % (nspfx, nsmap[nspfx]) diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 4bd1fdccd..c47c22890 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Utility functions wrapping the excellent *mock* library""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Utility functions wrapping the excellent *mock* library.""" import sys From e88e1d549e092afeb50cfbc49fd0bcd09b9e836e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 27 Sep 2023 20:18:28 -0700 Subject: [PATCH 015/131] rfctr: change param ref from asterisk to backticks Sphinx handling is actually better, not only using italic but also a mono-width font. --- docs/api/document.rst | 30 ++++++------ docs/api/shared.rst | 2 +- docs/dev/analysis/features/header.rst | 8 ++-- docs/dev/analysis/features/sections.rst | 2 +- docs/dev/analysis/features/shapes/index.rst | 4 +- .../features/styles/character-style.rst | 2 +- docs/dev/analysis/features/styles/index.rst | 2 +- .../features/styles/latent-styles.rst | 6 +-- docs/dev/analysis/features/styles/style.rst | 6 +-- .../analysis/features/table/table-props.rst | 2 +- docs/dev/analysis/features/text/font.rst | 8 ++-- .../features/text/paragraph-format.rst | 4 +- docs/user/hdrftr.rst | 10 ++-- docs/user/quickstart.rst | 6 +-- docs/user/sections.rst | 4 +- docs/user/shapes.rst | 6 +-- docs/user/styles-understanding.rst | 6 +-- docs/user/text.rst | 6 +-- features/steps/helpers.py | 4 +- requirements-docs.txt | 3 ++ src/docx/api.py | 17 +++---- src/docx/blkcntnr.py | 8 ++-- src/docx/document.py | 22 ++++----- src/docx/enum/base.py | 10 ++-- src/docx/image/bmp.py | 6 +-- src/docx/image/gif.py | 2 +- src/docx/image/helpers.py | 12 ++--- src/docx/image/image.py | 16 +++---- src/docx/image/jpeg.py | 48 +++++++++---------- src/docx/image/png.py | 22 ++++----- src/docx/image/tiff.py | 36 +++++++------- src/docx/opc/oxml.py | 4 +- src/docx/opc/package.py | 30 ++++++------ src/docx/opc/packuri.py | 6 +-- src/docx/opc/part.py | 20 ++++---- src/docx/opc/phys_pkg.py | 18 +++---- src/docx/opc/pkgreader.py | 22 ++++----- src/docx/opc/pkgwriter.py | 16 +++---- src/docx/opc/rel.py | 12 ++--- src/docx/oxml/__init__.py | 20 ++++---- src/docx/oxml/document.py | 2 +- src/docx/oxml/ns.py | 4 +- src/docx/oxml/numbering.py | 16 +++---- src/docx/oxml/section.py | 12 ++--- src/docx/oxml/shared.py | 8 ++-- src/docx/oxml/styles.py | 22 ++++----- src/docx/oxml/table.py | 36 +++++++------- src/docx/oxml/text/font.py | 6 +-- src/docx/oxml/text/parfmt.py | 6 +-- src/docx/oxml/xmlchemy.py | 28 +++++------ src/docx/package.py | 8 ++-- src/docx/parts/document.py | 22 ++++----- src/docx/parts/image.py | 4 +- src/docx/parts/story.py | 22 ++++----- src/docx/section.py | 2 +- src/docx/shared.py | 2 +- src/docx/styles/__init__.py | 4 +- src/docx/styles/latent.py | 2 +- src/docx/styles/style.py | 4 +- src/docx/styles/styles.py | 40 ++++++++-------- src/docx/table.py | 24 +++++----- src/docx/text/font.py | 4 +- src/docx/text/paragraph.py | 10 ++-- src/docx/text/parfmt.py | 8 ++-- src/docx/text/run.py | 10 ++-- src/docx/text/tabstops.py | 10 ++-- tests/opc/test_packuri.py | 2 +- tests/opc/test_pkgreader.py | 2 +- tests/opc/unitdata/rels.py | 18 +++---- tests/oxml/test_table.py | 2 +- tests/unitdata.py | 4 +- tests/unitutil/cxml.py | 10 ++-- tests/unitutil/file.py | 10 ++-- tests/unitutil/mock.py | 24 +++++----- 74 files changed, 428 insertions(+), 428 deletions(-) create mode 100644 requirements-docs.txt 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/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/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..502ea584a 100644 --- a/docs/user/sections.rst +++ b/docs/user/sections.rst @@ -3,14 +3,14 @@ Working with Sections ===================== -Word supports the notion of a *section*, a division of a document having the +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 +page layout. But when you `do` need to change the page layout, you'll need to understand sections to get it done. 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/features/steps/helpers.py b/features/steps/helpers.py index 6b4300e94..59495681f 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -26,13 +26,13 @@ def absjoin(*paths): def test_docx(name): """ - 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(thisdir, "test_files", "%s.docx" % name) def test_file(name): """ - Return the absolute path to file with *name* in test_files directory + Return the absolute path to file with `name` in test_files directory """ return absjoin(thisdir, "test_files", "%s" % name) 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/src/docx/api.py b/src/docx/api.py index 9b496c461..ab37ed608 100644 --- a/src/docx/api.py +++ b/src/docx/api.py @@ -1,7 +1,6 @@ """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. """ import os @@ -11,11 +10,11 @@ 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. + """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 @@ -26,8 +25,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") diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index 9c6c506a8..f0000d226 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -24,8 +24,8 @@ def __init__(self, element, parent): 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 + 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. """ paragraph = self._add_paragraph() @@ -37,8 +37,8 @@ def add_paragraph(self, text="", style=None): def add_table(self, rows, cols, width): """ - Return a table of *width* having *rows* rows and *cols* columns, - newly appended to the content in this container. *width* is evenly + 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 diff --git a/src/docx/document.py b/src/docx/document.py index e47bfc816..327edbe25 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -24,10 +24,10 @@ def __init__(self, element, part): 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* + 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. + {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) @@ -43,9 +43,9 @@ def add_page_break(self): 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 + 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 + form for a tab. `text` can also include newline (``\\n``) or carriage return (``\\r``) characters, each of which is converted to a line break. """ @@ -55,7 +55,7 @@ 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 + `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, @@ -70,7 +70,7 @@ def add_picture(self, image_path_or_stream, width=None, height=None): 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 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. """ @@ -80,9 +80,9 @@ def add_section(self, start_type=WD_SECTION.NEW_PAGE): 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 + 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) @@ -125,7 +125,7 @@ def part(self): def save(self, path_or_stream): """ - Save this document to *path_or_stream*, which can be either a path to + 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) diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index fae7c9aff..6192f3cb4 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -163,7 +163,7 @@ class EnumerationBase(object): @classmethod def validate(cls, value): """ - Raise |ValueError| if *value* is not an assignable value. + Raise |ValueError| if `value` is not an assignable value. """ if value not in cls._valid_settings: raise ValueError( @@ -187,7 +187,7 @@ class XmlEnumeration(Enumeration): def from_xml(cls, xml_val): """ Return the enumeration member corresponding to the XML value - *xml_val*. + `xml_val`. """ if xml_val not in cls._xml_to_member: raise InvalidXmlError( @@ -198,7 +198,7 @@ def from_xml(cls, xml_val): @classmethod def to_xml(cls, enum_val): """ - Return the XML value of the enumeration value *enum_val*. + Return the XML value of the enumeration value `enum_val`. """ if enum_val not in cls._member_to_xml: raise ValueError( @@ -222,7 +222,7 @@ def __init__(self, name, value, docstring): def add_to_enum(self, clsdict): """ - Add a name to *clsdict* for this member. + Add a name to `clsdict` for this member. """ self.register_name(clsdict) @@ -245,7 +245,7 @@ def name(self): def register_name(self, clsdict): """ - Add a member name to the class dict *clsdict* containing the value of + 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. diff --git a/src/docx/image/bmp.py b/src/docx/image/bmp.py index e4332b637..c420b1898 100644 --- a/src/docx/image/bmp.py +++ b/src/docx/image/bmp.py @@ -12,7 +12,7 @@ class Bmp(BaseImageHeader): def from_stream(cls, stream): """ Return |Bmp| instance having header properties parsed from the BMP - image in *stream*. + image in `stream`. """ stream_rdr = StreamReader(stream, LITTLE_ENDIAN) @@ -45,8 +45,8 @@ def default_ext(self): @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 diff --git a/src/docx/image/gif.py b/src/docx/image/gif.py index b6b396be4..7ff53d306 100644 --- a/src/docx/image/gif.py +++ b/src/docx/image/gif.py @@ -15,7 +15,7 @@ class Gif(BaseImageHeader): def from_stream(cls, stream): """ Return |Gif| instance having header properties parsed from GIF image - in *stream*. + in `stream`. """ px_width, px_height = cls._dimensions_from_stream(stream) return cls(px_width, px_height, 72, 72) diff --git a/src/docx/image/helpers.py b/src/docx/image/helpers.py index c98795657..df0946d02 100644 --- a/src/docx/image/helpers.py +++ b/src/docx/image/helpers.py @@ -9,7 +9,7 @@ class StreamReader(object): """ Wraps a file-like object to provide access to structured data from a - binary file. Byte-order is configurable. *base_offset* is added to any + binary file. Byte-order is configurable. `base_offset` is added to any base value provided to calculate actual location for reads. """ @@ -28,7 +28,7 @@ def read(self, count): def read_byte(self, base, offset=0): """ Return the int value of the byte at the file position defined by - self._base_offset + *base* + *offset*. If *base* is None, the byte is + self._base_offset + `base` + `offset`. If `base` is None, the byte is read from the current position in the stream. """ fmt = "B" @@ -37,7 +37,7 @@ def read_byte(self, base, offset=0): def read_long(self, base, offset=0): """ Return the int value of the four bytes at the file position defined by - self._base_offset + *base* + *offset*. If *base* is None, the long is + self._base_offset + `base` + `offset`. If `base` is None, the long is read from the current position in the stream. The endian setting of this instance is used to interpret the byte layout of the long. """ @@ -47,15 +47,15 @@ def read_long(self, base, offset=0): 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. + 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*. + Return a string containing the `char_count` bytes at the file + position determined by self._base_offset + `base` + `offset`. """ def str_struct(char_count): diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 530a90f8f..7f5911240 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -28,7 +28,7 @@ def __init__(self, blob, filename, image_header): def from_blob(cls, blob): """ Return a new |Image| subclass instance parsed from the image binary - contained in *blob*. + contained in `blob`. """ stream = io.BytesIO(blob) return cls._from_stream(stream, blob) @@ -37,7 +37,7 @@ def from_blob(cls, blob): 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. + identified by `image_descriptor`, a path or file-like object. """ if isinstance(image_descriptor, str): path = image_descriptor @@ -134,12 +134,12 @@ def height(self): 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 + 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 + 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 @@ -169,7 +169,7 @@ def sha1(self): def _from_stream(cls, stream, blob, filename=None): """ Return an instance of the |Image| subclass corresponding to the - format of the image in *stream*. + format of the image in `stream`. """ image_header = _ImageHeaderFactory(stream) if filename is None: @@ -180,7 +180,7 @@ def _from_stream(cls, stream, blob, filename=None): def _ImageHeaderFactory(stream): """ Return a |BaseImageHeader| subclass instance that knows how to parse the - headers of the image in *stream*. + headers of the image in `stream`. """ from docx.image import SIGNATURES diff --git a/src/docx/image/jpeg.py b/src/docx/image/jpeg.py index 6a9db9438..23f90c5bc 100644 --- a/src/docx/image/jpeg.py +++ b/src/docx/image/jpeg.py @@ -41,7 +41,7 @@ class Exif(Jpeg): def from_stream(cls, stream): """ Return |Exif| instance having header properties parsed from Exif - image in *stream*. + image in `stream`. """ markers = _JfifMarkers.from_stream(stream) # print('\n%s' % markers) @@ -63,7 +63,7 @@ class Jfif(Jpeg): def from_stream(cls, stream): """ Return a |Jfif| instance having header properties parsed from image - in *stream*. + in `stream`. """ markers = _JfifMarkers.from_stream(stream) @@ -110,7 +110,7 @@ def __str__(self): # pragma: no cover def from_stream(cls, stream): """ Return a |_JfifMarkers| instance containing a |_JfifMarker| subclass - instance for each marker in *stream*. + instance for each marker in `stream`. """ marker_parser = _MarkerParser.from_stream(stream) markers = [] @@ -165,7 +165,7 @@ def __init__(self, stream_reader): def from_stream(cls, stream): """ Return a |_MarkerParser| instance to parse JFIF markers from - *stream*. + `stream`. """ stream_reader = StreamReader(stream, BIG_ENDIAN) return cls(stream_reader) @@ -173,7 +173,7 @@ def from_stream(cls, stream): 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. + the JPEG `stream`, in the order they occur in the stream. """ marker_finder = _MarkerFinder.from_stream(self._stream) start = 0 @@ -197,15 +197,15 @@ def __init__(self, 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 + 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. """ @@ -225,9 +225,9 @@ def next(self, start): 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 + 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) @@ -239,8 +239,8 @@ def _next_non_ff_byte(self, start): 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 + 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) @@ -263,8 +263,8 @@ def _read_byte(self): 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 @@ -292,8 +292,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 @@ -356,7 +356,7 @@ def vert_dpi(self): 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 @@ -369,8 +369,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 # ------------------ --- ----- ------------------- @@ -405,7 +405,7 @@ def __init__(self, marker_code, offset, length, horz_dpi, vert_dpi): 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*. + header at `offset` in `stream`. """ # field off len type notes # -------------------- --- --- ----- ---------------------------- @@ -440,7 +440,7 @@ def vert_dpi(self): @classmethod def _is_non_Exif_APP1_segment(cls, stream, offset): """ - Return True if the APP1 segment at *offset* in *stream* is NOT an + 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. """ @@ -452,7 +452,7 @@ def _is_non_Exif_APP1_segment(cls, stream, offset): 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*. + `segment_length` at `offset` in `stream`. """ # wrap full segment in its own stream and feed to Tiff() stream.seek(offset + 8) @@ -474,7 +474,7 @@ def __init__(self, marker_code, offset, segment_length, px_width, px_height): @classmethod def from_stream(cls, stream, marker_code, offset): """ - Return an |_SofMarker| instance for the SOFn marker at *offset* in + Return an |_SofMarker| instance for the SOFn marker at `offset` in stream. """ # field off type notes diff --git a/src/docx/image/png.py b/src/docx/image/png.py index 72f2ec83a..842a0752f 100644 --- a/src/docx/image/png.py +++ b/src/docx/image/png.py @@ -28,7 +28,7 @@ def default_ext(self): def from_stream(cls, stream): """ Return a |Png| instance having header properties parsed from image in - *stream*. + `stream`. """ parser = _PngParser.parse(stream) @@ -54,7 +54,7 @@ def __init__(self, chunks): def parse(cls, stream): """ Return a |_PngParser| instance containing the header properties - parsed from the PNG image in *stream*. + parsed from the PNG image in `stream`. """ chunks = _Chunks.from_stream(stream) return cls(chunks) @@ -100,8 +100,8 @@ 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)) @@ -120,7 +120,7 @@ def __init__(self, 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 = list(chunk_parser.iter_chunks()) @@ -148,7 +148,7 @@ def pHYs(self): def _find_first(self, match): """ Return first chunk in stream order returning True for function - *match*. + `match`. """ for chunk in self._chunks: if match(chunk): @@ -169,7 +169,7 @@ def __init__(self, stream_rdr): def from_stream(cls, stream): """ Return a |_ChunkParser| instance that can extract the chunks from the - PNG image in *stream*. + PNG image in `stream`. """ stream_rdr = StreamReader(stream, BIG_ENDIAN) return cls(stream_rdr) @@ -203,8 +203,8 @@ def _iter_chunk_offsets(self): 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, @@ -253,7 +253,7 @@ def __init__(self, chunk_type, px_width, px_height): 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*. + extracted from the IHDR chunk in `stream` at `offset`. """ px_width = stream_rdr.read_long(offset) px_height = stream_rdr.read_long(offset, 4) @@ -283,7 +283,7 @@ def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifi 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*. + 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) diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py index 1aca7a0e8..42d3fb11d 100644 --- a/src/docx/image/tiff.py +++ b/src/docx/image/tiff.py @@ -28,7 +28,7 @@ def default_ext(self): def from_stream(cls, stream): """ Return a |Tiff| instance containing the properties of the TIFF image - in *stream*. + in `stream`. """ parser = _TiffParser.parse(stream) @@ -54,7 +54,7 @@ def __init__(self, ifd_entries): def parse(cls, stream): """ Return an instance of |_TiffParser| containing the properties parsed - from the TIFF image in *stream*. + from the TIFF image in `stream`. """ stream_rdr = cls._make_stream_reader(stream) ifd0_offset = stream_rdr.read_long(4) @@ -101,7 +101,7 @@ def px_width(self): 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'. + indicator found in the TIFF `stream` header, either 'MM' or 'II'. """ stream.seek(0) endian_str = stream.read(2) @@ -109,7 +109,7 @@ def _detect_endian(cls, stream): def _dpi(self, resolution_tag): """ - Return the dpi value calculated for *resolution_tag*, which can be + 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. @@ -136,7 +136,7 @@ def _dpi(self, resolution_tag): @classmethod def _make_stream_reader(cls, stream): """ - Return a |StreamReader| instance with wrapping *stream* and having + Return a |StreamReader| instance with wrapping `stream` and having "endian-ness" determined by the 'MM' or 'II' indicator in the TIFF stream header. """ @@ -169,8 +169,8 @@ def __getitem__(self, key): @classmethod def from_stream(cls, stream, offset): """ - Return a new |_IfdEntries| instance parsed from *stream* starting at - *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()} @@ -178,8 +178,8 @@ def from_stream(cls, stream, offset): 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 value of IFD entry having tag matching `tag_code`, or + `default` if no matching tag found. """ return self._entries.get(tag_code, default) @@ -216,7 +216,7 @@ def _entry_count(self): def _IfdEntryFactory(stream_rdr, offset): """ Return an |_IfdEntry| subclass instance containing the value of the - directory entry at *offset* in *stream_rdr*. + directory entry at `offset` in `stream_rdr`. """ ifd_entry_classes = { TIFF_FLD.ASCII: _AsciiIfdEntry, @@ -244,7 +244,7 @@ def __init__(self, tag_code, value): 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 + 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. """ @@ -257,7 +257,7 @@ def from_stream(cls, stream_rdr, offset): @classmethod def _parse_value(cls, stream_rdr, offset, value_count, value_offset): """ - Return the value of this field parsed from *stream_rdr* at *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 @@ -285,9 +285,9 @@ class _AsciiIfdEntry(_IfdEntry): @classmethod def _parse_value(cls, stream_rdr, offset, value_count, value_offset): """ - Return the ASCII string parsed from *stream_rdr* at *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*. + character, is in `value_count`. """ return stream_rdr.read_str(value_count - 1, value_offset) @@ -300,7 +300,7 @@ class _ShortIfdEntry(_IfdEntry): @classmethod def _parse_value(cls, stream_rdr, offset, value_count, value_offset): """ - Return the short int value contained in the *value_offset* field of + Return the short int value contained in the `value_offset` field of this entry. Only supports single values at present. """ if value_count == 1: @@ -317,7 +317,7 @@ class _LongIfdEntry(_IfdEntry): @classmethod def _parse_value(cls, stream_rdr, offset, value_count, value_offset): """ - Return the long int value contained in the *value_offset* field of + Return the long int value contained in the `value_offset` field of this entry. Only supports single values at present. """ if value_count == 1: @@ -334,8 +334,8 @@ class _RationalIfdEntry(_IfdEntry): @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 + 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: diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index b09a6d7a4..408fe5af6 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -47,7 +47,7 @@ def qn(tag): def serialize_part_xml(part_elm): """ - Serialize *part_elm* etree element to XML suitable for storage as an XML + 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. """ @@ -56,7 +56,7 @@ def serialize_part_xml(part_elm): def serialize_for_reading(element): """ - Serialize *element* to human-readable XML suitable for tests. No XML + Serialize `element` to human-readable XML suitable for tests. No XML declaration. """ return etree.tostring(element, encoding="unicode", pretty_print=True) diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index 014c11f10..d4882c8ce 100644 --- a/src/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -84,9 +84,9 @@ def walk_parts(source, visited=[]): 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 + 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. @@ -104,10 +104,10 @@ def main_document_part(self): 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,7 +121,7 @@ def next_partname(self, template): def open(cls, pkg_file): """ Return an |OpcPackage| instance loaded with the contents of - *pkg_file*. + `pkg_file`. """ pkg_reader = PackageReader.from_file(pkg_file) package = cls() @@ -130,7 +130,7 @@ def open(cls, pkg_file): def part_related_by(self, reltype): """ - Return part to which this package has a relationship of *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. """ @@ -146,7 +146,7 @@ def parts(self): def relate_to(self, part, reltype): """ - Return rId key of relationship to *part*, from the existing + 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) @@ -162,7 +162,7 @@ def rels(self): def save(self, pkg_file): """ - Save this package to *pkg_file*, where *file* can be either a path to + 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: @@ -190,8 +190,8 @@ class Unmarshaller(object): 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*. + 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) Unmarshaller._unmarshal_relationships(pkg_reader, package, parts) @@ -203,8 +203,8 @@ def unmarshal(pkg_reader, package, part_factory): 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*. + `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(): @@ -217,8 +217,8 @@ def _unmarshal_parts(pkg_reader, package, part_factory): 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*. + 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] diff --git a/src/docx/opc/packuri.py b/src/docx/opc/packuri.py index 50fa6e214..f86e83635 100644 --- a/src/docx/opc/packuri.py +++ b/src/docx/opc/packuri.py @@ -25,7 +25,7 @@ def __new__(cls, pack_uri_str): def from_rel_ref(baseURI, relative_ref): """ Return a |PackURI| instance containing the absolute pack URI formed by - translating *relative_ref* onto *baseURI*. + translating `relative_ref` onto `baseURI`. """ joined_uri = posixpath.join(baseURI, relative_ref) abs_uri = posixpath.abspath(joined_uri) @@ -89,11 +89,11 @@ def membername(self): def relative_ref(self, baseURI): """ Return string containing relative reference to package item from - *baseURI*. E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would + `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 ('/') + # relative path when `start` (second) parameter is root ('/') return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) @property diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index e9fab7973..874fed21f 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -59,7 +59,7 @@ def content_type(self): def drop_rel(self, rId): """ - Remove the relationship identified by *rId* if its reference count + Remove the relationship identified by `rId` if its reference count is less than 2. Relationships with a reference count of 0 are implicit relationships. """ @@ -72,9 +72,9 @@ def load(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 + 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. @@ -105,7 +105,7 @@ def partname(self, partname): def part_related_by(self, reltype): """ - Return part to which this part has a relationship of *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. @@ -114,7 +114,7 @@ def part_related_by(self, reltype): def relate_to(self, target, reltype, is_external=False): """ - Return rId key of relationship of *reltype* to *target*, from an + 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: @@ -142,7 +142,7 @@ def rels(self): def target_ref(self, rId): """ Return URL contained in target ref of relationship identified by - *rId*. + `rId`. """ rel = self.rels[rId] return rel.target_ref @@ -150,7 +150,7 @@ def target_ref(self, rId): def _rel_ref_count(self, rId): """ Return the count of references in this part's XML to the relationship - identified by *rId*. + identified by `rId`. """ rIds = self._element.xpath("//@r:id") return len([_rId for _rId in rIds if _rId == rId]) @@ -186,9 +186,9 @@ def __new__(cls, partname, content_type, reltype, blob, package): @classmethod def _part_cls_for(cls, content_type): """ - Return the custom part class registered for *content_type*, or the + Return the custom part class registered for `content_type`, or the default part class if no custom class is registered for - *content_type*. + `content_type`. """ if content_type in cls.part_type_for: return cls.part_type_for[content_type] diff --git a/src/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py index ae38a647b..9c8a18993 100644 --- a/src/docx/opc/phys_pkg.py +++ b/src/docx/opc/phys_pkg.py @@ -1,4 +1,4 @@ -"""Provides a general interface to a *physical* OPC package, such as a zip file.""" +"""Provides a general interface to a `physical` OPC package, such as a zip file.""" import os from zipfile import ZIP_DEFLATED, ZipFile, is_zipfile @@ -13,7 +13,7 @@ class PhysPkgReader(object): """ def __new__(cls, pkg_file): - # if *pkg_file* is a string, treat it as a path + # 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 @@ -44,14 +44,14 @@ class _DirPkgReader(PhysPkgReader): def __init__(self, path): """ - *path* is the path to a directory containing an expanded package. + `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 + Return contents of file corresponding to `pack_uri` in package directory. """ path = os.path.join(self._path, pack_uri.membername) @@ -75,7 +75,7 @@ def content_types_xml(self): def rels_xml_for(self, source_uri): """ - Return rels item XML for source with *source_uri*, or None if the + Return rels item XML for source with `source_uri`, or None if the item has no rels item. """ try: @@ -96,7 +96,7 @@ def __init__(self, pkg_file): def blob_for(self, pack_uri): """ - Return blob corresponding to *pack_uri*. Raises |ValueError| if no + Return blob corresponding to `pack_uri`. Raises |ValueError| if no matching member is present in zip archive. """ return self._zipf.read(pack_uri.membername) @@ -116,7 +116,7 @@ def content_types_xml(self): def rels_xml_for(self, source_uri): """ - Return rels item XML for source with *source_uri* or None if no rels + Return rels item XML for source with `source_uri` or None if no rels item is present. """ try: @@ -144,7 +144,7 @@ def close(self): def write(self, pack_uri, blob): """ - Write *blob* to this zip package with the membername corresponding to - *pack_uri*. + Write `blob` to this zip package with the membername corresponding to + `pack_uri`. """ self._zipf.writestr(pack_uri.membername, blob) diff --git a/src/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py index ca6849e26..defa60ec6 100644 --- a/src/docx/opc/pkgreader.py +++ b/src/docx/opc/pkgreader.py @@ -21,7 +21,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) @@ -55,8 +55,8 @@ def iter_srels(self): 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*. + 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) @@ -70,7 +70,7 @@ def _load_serialized_parts(phys_reader, pkg_srels, content_types): def _srels_for(phys_reader, source_uri): """ Return |_SerializedRelationships| instance populated with - relationships for source identified by *source_uri*. + 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) @@ -79,7 +79,7 @@ def _srels_for(phys_reader, source_uri): 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 + parts in `phys_reader` by walking the relationship graph rooted at srels. """ if visited_partnames is None: @@ -115,7 +115,7 @@ def __init__(self): 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" @@ -131,7 +131,7 @@ def __getitem__(self, partname): def from_xml(content_types_xml): """ Return a new |_ContentTypeMap| instance populated with the contents - of *content_types_xml*. + of `content_types_xml`. """ types_elm = parse_xml(content_types_xml) ct_map = _ContentTypeMap() @@ -143,14 +143,14 @@ def from_xml(content_types_xml): def _add_default(self, extension, content_type): """ - Add the default mapping of *extension* to *content_type* to this + 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 + Add the default mapping of `partname` to `content_type` to this content type mapping. """ self._overrides[partname] = content_type @@ -283,8 +283,8 @@ def __iter__(self): 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|. + 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 index 1f8901eea..9a3e56fdb 100644 --- a/src/docx/opc/pkgwriter.py +++ b/src/docx/opc/pkgwriter.py @@ -14,7 +14,7 @@ class PackageWriter(object): """ - Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be + 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. @@ -23,8 +23,8 @@ class PackageWriter(object): @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 + 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) @@ -37,7 +37,7 @@ def write(pkg_file, pkg_rels, parts): 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*. + appropriate content type lookup target for each part in `parts`. """ cti = _ContentTypesItem.from_parts(parts) phys_writer.write(CONTENT_TYPES_URI, cti.blob) @@ -45,7 +45,7 @@ def _write_content_types_stream(phys_writer, parts): @staticmethod def _write_parts(phys_writer, parts): """ - Write the blob of each part in *parts* to the package, along with a + 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: @@ -56,7 +56,7 @@ def _write_parts(phys_writer, parts): @staticmethod def _write_pkg_rels(phys_writer, pkg_rels): """ - Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the + Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package. """ phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) @@ -85,7 +85,7 @@ def blob(self): @classmethod def from_parts(cls, parts): """ - Return content types XML mapping each part in *parts* to the + 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. """ @@ -98,7 +98,7 @@ def from_parts(cls, parts): def _add_content_type(self, partname, content_type): """ - Add a content type for the part with *partname* and *content_type*, + Add a content type for the part with `partname` and `content_type`, using a default or override as appropriate. """ ext = partname.ext diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index 97be3f6bf..a137d4590 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -25,7 +25,7 @@ def add_relationship(self, reltype, target, rId, is_external=False): def get_or_add(self, reltype, target_part): """ - Return relationship of *reltype* to *target_part*, newly added if not + Return relationship of `reltype` to `target_part`, newly added if not already present in collection. """ rel = self._get_matching(reltype, target_part) @@ -36,7 +36,7 @@ def get_or_add(self, reltype, target_part): def get_or_add_ext_rel(self, reltype, target_ref): """ - Return rId of external relationship of *reltype* to *target_ref*, + 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) @@ -47,7 +47,7 @@ def get_or_add_ext_rel(self, reltype, target_ref): def part_with_reltype(self, reltype): """ - Return target part of rel with matching *reltype*, raising |KeyError| + Return target part of rel with matching `reltype`, raising |KeyError| if not found and |ValueError| if more than one matching relationship is found. """ @@ -75,8 +75,8 @@ def xml(self): 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): @@ -96,7 +96,7 @@ def matches(rel, reltype, target, is_external): def _get_rel_of_type(self, reltype): """ - Return single relationship of type *reltype* from the collection. + 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. """ diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 80327205c..ab3995105 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -16,9 +16,9 @@ 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 + `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. + `xml` that have them. """ root_element = etree.fromstring(xml, oxml_parser) return root_element @@ -26,8 +26,8 @@ def parse_xml(xml): 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 + 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(":") @@ -37,15 +37,15 @@ def register_element_cls(tag, 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'. + 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, + 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_str`. """ nsptag = NamespacePrefixedTag(nsptag_str) if nsdecls is None: diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index a97f31990..22ee9e187 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -38,7 +38,7 @@ def add_section_break(self): 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 + 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])--- diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py index 868a1356e..d3558cbf1 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -83,7 +83,7 @@ def nsuri(self): def nsdecls(*prefixes): """ Return a string containing a namespace declaration for each of the - namespace prefix strings, e.g. 'p', 'ct', passed as *prefixes*. + namespace prefix strings, e.g. 'p', 'ct', passed as `prefixes`. """ return " ".join(['xmlns:%s="%s"' % (pfx, nsmap[pfx]) for pfx in prefixes]) @@ -91,7 +91,7 @@ def nsdecls(*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. + `nspfxs`. Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p'). """ return {pfx: nsmap[pfx] for pfx in nspfxs} diff --git a/src/docx/oxml/numbering.py b/src/docx/oxml/numbering.py index 6fb7e1b01..c386e8c3a 100644 --- a/src/docx/oxml/numbering.py +++ b/src/docx/oxml/numbering.py @@ -26,16 +26,16 @@ class CT_Num(BaseOxmlElement): def add_lvlOverride(self, ilvl): """ Return a newly added CT_NumLvl () element having its - ``ilvl`` attribute set to *ilvl*. + ``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 + Return a new ```` element having numId of `num_id` and having a ```` child with val attribute set to - *abstractNum_id*. + `abstractNum_id`. """ num = OxmlElement("w:num") num.numId = num_id @@ -56,7 +56,7 @@ class CT_NumLvl(BaseOxmlElement): def add_startOverride(self, val): """ Return a newly added CT_DecimalNumber element having tagname - ``w:startOverride`` and ``val`` attribute set to *val*. + ``w:startOverride`` and ``val`` attribute set to `val`. """ return self._add_startOverride(val=val) @@ -73,7 +73,7 @@ class CT_NumPr(BaseOxmlElement): # @ilvl.setter # def _set_ilvl(self, val): # """ - # Get or add a child and set its ``w:val`` attribute to *val*. + # Get or add a child and set its ``w:val`` attribute to `val`. # """ # ilvl = self.get_or_add_ilvl() # ilvl.val = val @@ -82,7 +82,7 @@ class CT_NumPr(BaseOxmlElement): # def numId(self, val): # """ # Get or add a child and set its ``w:val`` attribute to - # *val*. + # `val`. # """ # numId = self.get_or_add_numId() # numId.val = val @@ -99,7 +99,7 @@ class CT_Numbering(BaseOxmlElement): def add_num(self, abstractNum_id): """ Return a newly added CT_Num () element referencing the - abstract numbering definition identified by *abstractNum_id*. + abstract numbering definition identified by `abstractNum_id`. """ next_num_id = self._next_numId num = CT_Num.new(next_num_id, abstractNum_id) @@ -108,7 +108,7 @@ def add_num(self, abstractNum_id): def num_having_numId(self, numId): """ Return the ```` child element having ``numId`` attribute - matching *numId*. + matching `numId`. """ xpath = './w:num[@w:numId="%d"]' % numId try: diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index 73a928e07..72ab5c6f4 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -87,7 +87,7 @@ class CT_SectPr(BaseOxmlElement): del _tag_seq def add_footerReference(self, type_, rId): - """Return newly added CT_HdrFtrRef element of *type_* with *rId*. + """Return newly added CT_HdrFtrRef element of `type_` with `rId`. The element tag is `w:footerReference`. """ @@ -97,7 +97,7 @@ def add_footerReference(self, type_, rId): return footerReference def add_headerReference(self, type_, rId): - """Return newly added CT_HdrFtrRef element of *type_* with *rId*. + """Return newly added CT_HdrFtrRef element of `type_` with `rId`. The element tag is `w:headerReference`. """ @@ -151,7 +151,7 @@ def footer(self, value): pgMar.footer = value def get_footerReference(self, type_): - """Return footerReference element of *type_* or None if not present.""" + """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: @@ -159,7 +159,7 @@ def get_footerReference(self, type_): return footerReferences[0] def get_headerReference(self, type_): - """Return headerReference element of *type_* or None if not present.""" + """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_) ) @@ -275,14 +275,14 @@ def preceding_sectPr(self): 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.""" + """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.""" + """Return rId of w:headerReference child of `type_` after removing it.""" headerReference = self.get_headerReference(type_) rId = headerReference.rId self.remove(headerReference) diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index 9ef42a023..eb939291d 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -18,8 +18,8 @@ class CT_DecimalNumber(BaseOxmlElement): @classmethod def new(cls, nsptagname, val): """ - Return a new ``CT_DecimalNumber`` element having tagname *nsptagname* - and ``val`` attribute set to *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)}) @@ -44,8 +44,8 @@ class CT_String(BaseOxmlElement): @classmethod def new(cls, nsptagname, val): """ - Return a new ``CT_String`` element with tagname *nsptagname* and - ``val`` attribute set to *val*. + Return a new ``CT_String`` element with tagname `nsptagname` and + ``val`` attribute set to `val`. """ elm = OxmlElement(nsptagname) elm.val = val diff --git a/src/docx/oxml/styles.py b/src/docx/oxml/styles.py index 3cbb005e1..8de7a2290 100644 --- a/src/docx/oxml/styles.py +++ b/src/docx/oxml/styles.py @@ -13,7 +13,7 @@ def styleId_from_name(name): """ - Return the style id corresponding to *name*, taking into account + Return the style id corresponding to `name`, taking into account special-case names such as 'Heading 1'. """ return { @@ -48,7 +48,7 @@ class CT_LatentStyles(BaseOxmlElement): def bool_prop(self, attr_name): """ - Return the boolean value of the attribute having *attr_name*, or + Return the boolean value of the attribute having `attr_name`, or |False| if not present. """ value = getattr(self, attr_name) @@ -58,7 +58,7 @@ def bool_prop(self, attr_name): def get_by_name(self, name): """ - Return the `w:lsdException` child having *name*, or |None| if not + Return the `w:lsdException` child having `name`, or |None| if not found. """ found = self.xpath('w:lsdException[@w:name="%s"]' % name) @@ -68,7 +68,7 @@ def get_by_name(self, name): def set_bool_prop(self, attr_name, value): """ - Set the on/off attribute having *attr_name* to *value*. + Set the on/off attribute having `attr_name` to `value`. """ setattr(self, attr_name, bool(value)) @@ -94,14 +94,14 @@ def delete(self): def on_off_prop(self, attr_name): """ - Return the boolean value of the attribute having *attr_name*, or + 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*. + Set the on/off attribute having `attr_name` to `value`. """ setattr(self, attr_name, value) @@ -318,9 +318,9 @@ class CT_Styles(BaseOxmlElement): 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*. + 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 @@ -344,7 +344,7 @@ def default_for(self, style_type): def get_by_id(self, styleId): """ Return the ```` child element having ``styleId`` attribute - matching *styleId*, or |None| if not found. + matching `styleId`, or |None| if not found. """ xpath = 'w:style[@w:styleId="%s"]' % styleId try: @@ -355,7 +355,7 @@ def get_by_id(self, styleId): def get_by_name(self, name): """ Return the ```` child element having ```` child - element with value *name*, or |None| if not found. + element with value `name`, or |None| if not found. """ xpath = 'w:style[w:name/@w:val="%s"]' % name try: diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index fc131f88e..ef5e44702 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -43,7 +43,7 @@ class CT_Row(BaseOxmlElement): def tc_at_grid_col(self, idx): """ - The ```` element appearing at grid column *idx*. Raises + The ```` element appearing at grid column `idx`. Raises |ValueError| if no ``w:tc`` element begins at that grid column. """ grid_col = 0 @@ -158,8 +158,8 @@ 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)) @@ -178,7 +178,7 @@ def tblStyle_val(self): 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. + `styleId`. If `styleId` is None, remove the `w:tblStyle` element. """ tblPr = self.tblPr tblPr._remove_tblStyle() @@ -458,7 +458,7 @@ 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. + `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) @@ -526,8 +526,8 @@ def width(self, 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 @@ -544,7 +544,7 @@ def _grid_col(self): def _grow_to(self, width, height, top_tc=None): """ - Grow this cell to *width* grid columns and *height* rows by expanding + Grow this cell to `width` grid columns and `height` rows by expanding horizontal spans and creating continuation cells to form vertical spans. """ @@ -585,7 +585,7 @@ def _is_empty(self): def _move_content_to(self, other_tc): """ - Append the content of this cell to *other_tc*, leaving this cell with + Append the content of this cell to `other_tc`, leaving this cell with a single empty ```` element. """ if other_tc is self: @@ -634,7 +634,7 @@ def _remove_trailing_empty_p(self): 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 + the merged cell formed by using this tc and `other_tc` as opposite corner extents. """ @@ -666,13 +666,13 @@ def raise_on_tee_shaped(a, b): 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 + 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. + 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: @@ -684,10 +684,10 @@ 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 + 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. + `grid_width` or if there is no next `` element in the row. """ def raise_on_invalid_swallow(next_tc): diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 1c8ddd986..75c7f1b71 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -208,8 +208,8 @@ def style(self): @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 + 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: @@ -305,7 +305,7 @@ def u_val(self, value): def _get_bool_val(self, name): """ - Return the value of the boolean child element having *name*, e.g. + Return the value of the boolean child element having `name`, e.g. 'b', 'i', and 'smallCaps'. """ element = getattr(self, name) diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index 90f879340..e459d4dfa 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -310,8 +310,8 @@ def style(self): @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 + 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: @@ -367,7 +367,7 @@ class CT_TabStops(BaseOxmlElement): def insert_tab_in_order(self, pos, align, leader): """ - Insert a newly created `w:tab` child element in *pos* order. + 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 diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 887ffda86..f4504a1cd 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -12,7 +12,7 @@ def serialize_for_reading(element): """ - Serialize *element* to human-readable XML suitable for tests. No XML + Serialize `element` to human-readable XML suitable for tests. No XML declaration. """ xml = etree.tostring(element, encoding="unicode", pretty_print=True) @@ -48,7 +48,7 @@ def __ne__(self, other): def _attr_seq(self, attrs): """ - Return a sequence of attribute strings parsed from *attrs*. Each + Return a sequence of attribute strings parsed from `attrs`. Each attribute string is stripped of whitespace on both ends. """ attrs = attrs.strip() @@ -57,8 +57,8 @@ def _attr_seq(self, attrs): 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) @@ -76,7 +76,7 @@ def _eq_elm_strs(self, line, line_2): def _parse_line(cls, line): """ Return front, attrs, close, text 4-tuple result of parsing XML element - string *line*. + string `line`. """ match = cls._xml_elm_line_patt.match(line) front, attrs, close, text = [match.group(n) for n in range(1, 5)] @@ -114,7 +114,7 @@ def __init__(self, attr_name, simple_type): def populate_class_members(self, element_cls, prop_name): """ - Add the appropriate methods to *element_cls*. + Add the appropriate methods to `element_cls`. """ self._element_cls = element_cls self._prop_name = prop_name @@ -264,7 +264,7 @@ def __init__(self, nsptagname, successors=()): def populate_class_members(self, element_cls, prop_name): """ Baseline behavior for adding the appropriate methods to - *element_cls*. + `element_cls`. """ self._element_cls = element_cls self._prop_name = prop_name @@ -358,7 +358,7 @@ def add_child(obj): def _add_to_class(self, name, method): """ - Add *method* to the target class as *name*, unless *name* is already + Add `method` to the target class as `name`, unless `name` is already defined on the class. """ if hasattr(self._element_cls, name): @@ -444,7 +444,7 @@ def nsptagname(self): def populate_class_members(self, element_cls, group_prop_name, successors): """ - Add the appropriate methods to *element_cls*. + Add the appropriate methods to `element_cls`. """ self._element_cls = element_cls self._group_prop_name = group_prop_name @@ -502,7 +502,7 @@ def __init__(self, nsptagname): def populate_class_members(self, element_cls, prop_name): """ - Add the appropriate methods to *element_cls*. + Add the appropriate methods to `element_cls`. """ super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @@ -536,7 +536,7 @@ class OneOrMore(_BaseChildElement): def populate_class_members(self, element_cls, prop_name): """ - Add the appropriate methods to *element_cls*. + Add the appropriate methods to `element_cls`. """ super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() @@ -554,7 +554,7 @@ class ZeroOrMore(_BaseChildElement): def populate_class_members(self, element_cls, prop_name): """ - Add the appropriate methods to *element_cls*. + Add the appropriate methods to `element_cls`. """ super(ZeroOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() @@ -572,7 +572,7 @@ class ZeroOrOne(_BaseChildElement): 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() @@ -631,7 +631,7 @@ def __init__(self, choices, successors=()): def populate_class_members(self, element_cls, prop_name): """ - Add the appropriate methods to *element_cls*. + Add the appropriate methods to `element_cls`. """ super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name) self._add_choice_getter() diff --git a/src/docx/package.py b/src/docx/package.py index 8baa6f1f1..e4e9433e9 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -19,7 +19,7 @@ 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*. + """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. @@ -62,7 +62,7 @@ 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*. + """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. @@ -86,7 +86,7 @@ def _add_image_part(self, image): def _get_by_sha1(self, sha1): """ Return the image part in this collection having a SHA1 hash matching - *sha1*, or |None| if not found. + `sha1`, or |None| if not found. """ for image_part in self._image_parts: if image_part.sha1 == sha1: @@ -97,7 +97,7 @@ 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* + partname is unique by number, without regard to the extension. `ext` does not include the leading period. """ diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 0631dc36a..1d02033f1 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -48,33 +48,33 @@ def document(self): return Document(self._element, self) def drop_header_part(self, rId): - """Remove related header part identified by *rId*.""" + """Remove related header part identified by `rId`.""" self.drop_rel(rId) def footer_part(self, rId): - """Return |FooterPart| related by *rId*.""" + """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*. + 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 + 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*.""" + """Return |HeaderPart| related by `rId`.""" return self.related_parts[rId] @lazyproperty @@ -101,7 +101,7 @@ def numbering_part(self): def save(self, path_or_stream): """ - Save this document to *path_or_stream*, which can be either a path to + 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) diff --git a/src/docx/parts/image.py b/src/docx/parts/image.py index 919548a8a..deb099289 100644 --- a/src/docx/parts/image.py +++ b/src/docx/parts/image.py @@ -55,8 +55,8 @@ def filename(self): @classmethod def from_image(cls, image, partname): """ - Return an |ImagePart| instance newly created from *image* and - assigned *partname*. + Return an |ImagePart| instance newly created from `image` and + assigned `partname`. """ return ImagePart(partname, image.content_type, image.blob, image) diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py index 11ac3c60a..fa3cacf0a 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -15,11 +15,11 @@ class BaseStoryPart(XmlPart): """ def get_or_add_image(self, image_descriptor): - """Return (rId, image) pair for image identified by *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 + `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, + `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) @@ -27,18 +27,18 @@ def get_or_add_image(self, image_descriptor): return rId, image_part.image def get_style(self, style_id, style_type): - """Return the style in this document matching *style_id*. + """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*. + 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*. + """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 + 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) @@ -46,8 +46,8 @@ def get_style_id(self, 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*. + 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) diff --git a/src/docx/section.py b/src/docx/section.py index f69f547f2..4b81b368d 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -133,7 +133,7 @@ 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 + added to the `inner` margin to ensure even margins after page binding. """ return self._sectPr.gutter diff --git a/src/docx/shared.py b/src/docx/shared.py index 61d21aee8..63f3969fa 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -204,7 +204,7 @@ def __init__(self, element, parent=None): 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 + 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. diff --git a/src/docx/styles/__init__.py b/src/docx/styles/__init__.py index ff2e71ce7..bcd84ac9b 100644 --- a/src/docx/styles/__init__.py +++ b/src/docx/styles/__init__.py @@ -28,7 +28,7 @@ class BabelFish(object): @classmethod def ui2internal(cls, ui_style_name): """ - Return the internal style name corresponding to *ui_style_name*, such + 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) @@ -37,6 +37,6 @@ def ui2internal(cls, ui_style_name): def internal2ui(cls, internal_style_name): """ Return the user interface style name corresponding to - *internal_style_name*, such as 'Heading 1' for 'heading 1'. + `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 index f790fdb3c..0b9121332 100644 --- a/src/docx/styles/latent.py +++ b/src/docx/styles/latent.py @@ -33,7 +33,7 @@ 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*. + having `name`. """ lsdException = self._element.add_lsdException() lsdException.name = BabelFish.ui2internal(name) diff --git a/src/docx/styles/style.py b/src/docx/styles/style.py index 89096a592..8d5917b23 100644 --- a/src/docx/styles/style.py +++ b/src/docx/styles/style.py @@ -10,7 +10,7 @@ def StyleFactory(style_elm): """ Return a style object of the appropriate |BaseStyle| subclass, according - to the type of *style_elm*. + to the type of `style_elm`. """ style_cls = { WD_STYLE_TYPE.PARAGRAPH: _ParagraphStyle, @@ -211,7 +211,7 @@ 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 + |None| or `self` removes the setting such that new paragraphs are created using this same style. """ next_style_elm = self._element.next_style diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py index 766645a83..8e9cffffb 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -53,9 +53,9 @@ def __len__(self): 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. + 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: @@ -65,7 +65,7 @@ def add_style(self, name, style_type, builtin=False): def default(self, style_type): """ - Return the default style for *style_type* or |None| if no default is + 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) @@ -74,10 +74,10 @@ def default(self, style_type): return StyleFactory(style) def get_by_id(self, style_id, style_type): - """Return the style of *style_type* matching *style_id*. + """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*. + 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) @@ -85,12 +85,12 @@ def get_by_id(self, 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 + 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*. + `style_type`. """ if style_or_name is None: return None @@ -111,9 +111,9 @@ def latent_styles(self): 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*. + 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: @@ -122,17 +122,17 @@ def _get_by_id(self, style_id, style_type): 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 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*. + 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( diff --git a/src/docx/table.py b/src/docx/table.py index 3191ec1ea..43482ae96 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -17,7 +17,7 @@ def __init__(self, tbl, parent): def add_column(self, width): """ - Return a |_Column| object of *width*, newly added rightmost to the + Return a |_Column| object of `width`, newly added rightmost to the table. """ tblGrid = self._tbl.tblGrid @@ -69,15 +69,15 @@ def autofit(self, 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) @@ -93,7 +93,7 @@ def columns(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 @@ -192,13 +192,13 @@ def __init__(self, tc, parent): 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 + 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``) + 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 + 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) @@ -206,7 +206,7 @@ def add_paragraph(self, text="", style=None): 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 + 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. """ @@ -218,7 +218,7 @@ def add_table(self, rows, cols): 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 + 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 @@ -253,7 +253,7 @@ def text(self): @text.setter def text(self, text): """ - Write-only. Set entire contents of cell to the string *text*. Any + Write-only. Set entire contents of cell to the string `text`. Any existing content or revisions are replaced. """ tc = self._tc diff --git a/src/docx/text/font.py b/src/docx/text/font.py index ecb824151..80d0b7dad 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -388,7 +388,7 @@ def web_hidden(self, value): def _get_bool_prop(self, name): """ - Return the value of boolean child of `w:rPr` having *name*. + Return the value of boolean child of `w:rPr` having `name`. """ rPr = self._element.rPr if rPr is None: @@ -397,7 +397,7 @@ def _get_bool_prop(self, name): def _set_bool_prop(self, name, value): """ - Assign *value* to the boolean child *name* of `w:rPr`. + 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/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index a0ff17a93..c397bb328 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -15,10 +15,10 @@ def __init__(self, p, parent): 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 + 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 + for a tab. `text` can also include newline (``\\n``) or carriage return (``\\r``) characters, each of which is converted to a line break. """ @@ -56,8 +56,8 @@ def clear(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 + 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() diff --git a/src/docx/text/parfmt.py b/src/docx/text/parfmt.py index 4a99b0b59..328eb7def 100644 --- a/src/docx/text/parfmt.py +++ b/src/docx/text/parfmt.py @@ -264,10 +264,10 @@ def widow_control(self, value): 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``, + `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|. + Returns |None| when `spacing_line` is |None|. """ if spacing_line is None: return None @@ -279,7 +279,7 @@ def _line_spacing(spacing_line, spacing_lineRule): 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 + `line` and `lineRule`. Returns special members of the :ref:`WdLineSpacing` enumeration when line spacing is single, double, or 1.5 lines. """ diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 09d95f206..72f49a069 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -22,10 +22,10 @@ def __init__(self, r, parent): def add_break(self, break_type=WD_BREAK.LINE): """ - Add a break element of *break_type* to this run. *break_type* can + 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`. + `break_type` defaults to `WD_BREAK.LINE`. """ type_, clear = { WD_BREAK.LINE: (None, None), @@ -44,8 +44,8 @@ def add_break(self, break_type=WD_BREAK.LINE): 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 + `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 @@ -68,7 +68,7 @@ def add_tab(self): def add_text(self, text): """ Returns a newly appended |_Text| object (corresponding to a new - ```` child element) to the run, containing *text*. Compare with + ```` child element) to the run, containing `text`. Compare with the possibly more friendly approach of assigning text to the :attr:`Run.text` property. """ diff --git a/src/docx/text/tabstops.py b/src/docx/text/tabstops.py index c6b899c05..96cf44751 100644 --- a/src/docx/text/tabstops.py +++ b/src/docx/text/tabstops.py @@ -21,7 +21,7 @@ def __init__(self, element): def __delitem__(self, idx): """ - Remove the tab at offset *idx* in this sequence. + Remove the tab at offset `idx` in this sequence. """ tabs = self._pPr.tabs try: @@ -62,13 +62,13 @@ 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 + 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 + `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 + 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*. + :ref:`WdTabLeader` enumeration as `leader`. """ tabs = self._pPr.get_or_add_tabs() tab = tabs.insert_tab_in_order(position, alignment, leader) diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index 1d5732a85..272175795 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -9,7 +9,7 @@ class DescribePackURI(object): 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 = [ "/", diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py index cd010e396..10f3cf0b1 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -361,7 +361,7 @@ def match_override_fixture(self, request): 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: diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index c0b6024fa..3b4e8fa4d 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -17,7 +17,7 @@ def element(self): 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 @@ -74,12 +74,12 @@ def __init__(self): 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 @@ -110,12 +110,12 @@ def __init__(self): 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 @@ -148,22 +148,22 @@ def __init__(self): 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*""" + """Set TargetMode attribute to `target_mode`""" self._target_mode = None if target_mode == "Internal" else target_mode return self diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index b24c4abf3..519151be4 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -365,7 +365,7 @@ def _span_to_width_(self, request): 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]) diff --git a/tests/unitdata.py b/tests/unitdata.py index d5939899e..057b54490 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -56,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) @@ -64,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. """ diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index cc52ecdd1..9c687df67 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -29,13 +29,13 @@ def element(cxel_str): - """Return an oxml element parsed from the XML generated from *cxel_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*.""" + """Return the XML generated from `cxel_str`.""" root_token = root_node.parseString(cxel_str) xml = root_token.element.xml return xml @@ -47,7 +47,7 @@ def xml(cxel_str): def nsdecls(*nspfxs): - """Namespace-declaration including each of *nspfxs*, in the order specified.""" + """Namespace-declaration including each of `nspfxs`, in the order specified.""" nsdecls = "" for nspfx in nspfxs: nsdecls += ' xmlns:%s="%s"' % (nspfx, nsmap[nspfx]) @@ -76,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: @@ -158,7 +158,7 @@ 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 xml = self._start_tag diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 0d1562942..432c8635e 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -17,7 +17,7 @@ def absjoin(*paths): def docx_path(name): """ - 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) @@ -25,8 +25,8 @@ def docx_path(name): def snippet_seq(name, offset=0, count=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. + 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: @@ -39,7 +39,7 @@ def snippet_seq(name, offset=0, count=1024): def snippet_text(snippet_file_name): """ 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 @@ -51,6 +51,6 @@ def snippet_text(snippet_file_name): def test_file(name): """ - 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 c47c22890..c2c15bd62 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,4 +1,4 @@ -"""Utility functions wrapping the excellent *mock* library.""" +"""Utility functions wrapping the excellent `mock` library.""" import sys @@ -13,10 +13,10 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): - """Return mock patching class with qualified name *q_class_name*. + """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) @@ -26,7 +26,7 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): """ - Return a mock for attribute *attr_name* on *cls* where the patch is + Return a mock for attribute `attr_name` on `cls` where the patch is reversed after pytest uses it. """ name = request.fixturename if name is None else name @@ -36,7 +36,7 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): def function_mock(request, q_function_name, autospec=True, **kwargs): - """Return mock patching function with qualified name *q_function_name*. + """Return mock patching function with qualified name `q_function_name`. Patch is reversed after calling test returns. """ @@ -46,7 +46,7 @@ def function_mock(request, q_function_name, autospec=True, **kwargs): def initializer_mock(request, cls, autospec=True, **kwargs): - """Return mock for __init__() method on *cls*. + """Return mock for __init__() method on `cls`. The patch is reversed after pytest uses it. """ @@ -59,8 +59,8 @@ def initializer_mock(request, cls, autospec=True, **kwargs): def instance_mock(request, cls, name=None, spec_set=True, **kwargs): """ - 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. @@ -81,7 +81,7 @@ def loose_mock(request, name=None, **kwargs): def method_mock(request, cls, method_name, autospec=True, **kwargs): - """Return mock for method *method_name* on *cls*. + """Return mock for method `method_name` on `cls`. The patch is reversed after pytest uses it. """ @@ -92,7 +92,7 @@ def method_mock(request, cls, method_name, autospec=True, **kwargs): def open_mock(request, module_name, **kwargs): """ - 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 _patch = patch(target, mock_open(), create=True, **kwargs) @@ -102,7 +102,7 @@ def open_mock(request, module_name, **kwargs): def property_mock(request, cls, prop_name, **kwargs): """ - 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) @@ -112,7 +112,7 @@ def property_mock(request, cls, prop_name, **kwargs): def var_mock(request, q_var_name, **kwargs): """ - 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) From 1c5bb2817e5354771f3609c62496179708451de3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 27 Sep 2023 21:02:16 -0700 Subject: [PATCH 016/131] rfctr: bulk best-efforts PEP 257 docstrings Done by `docformatter`, it can't get the summary line right if it's too long, but adjusts the description block width and gets the shorter docstrings right, so a lot better than doing them all by hand. The rest will have to wait for hand curation as we go. --- src/docx/blkcntnr.py | 42 ++-- src/docx/dml/color.py | 76 +++---- src/docx/document.py | 133 +++++------- src/docx/enum/base.py | 188 ++++++---------- src/docx/enum/dml.py | 16 +- src/docx/enum/section.py | 19 +- src/docx/enum/shape.py | 6 +- src/docx/enum/style.py | 20 +- src/docx/enum/table.py | 36 ++-- src/docx/exceptions.py | 16 +- src/docx/image/bmp.py | 26 +-- src/docx/image/constants.py | 20 +- src/docx/image/exceptions.py | 12 +- src/docx/image/gif.py | 24 +-- src/docx/image/helpers.py | 47 ++-- src/docx/image/image.py | 148 +++++-------- src/docx/image/jpeg.py | 233 +++++++------------- src/docx/image/png.py | 142 ++++-------- src/docx/image/tiff.py | 207 +++++++----------- src/docx/opc/coreprops.py | 6 +- src/docx/opc/exceptions.py | 8 +- src/docx/opc/oxml.py | 161 +++++--------- src/docx/opc/package.py | 114 ++++------ src/docx/opc/packuri.py | 68 +++--- src/docx/opc/part.py | 155 ++++++------- src/docx/opc/parts/coreprops.py | 18 +- src/docx/opc/phys_pkg.py | 79 +++---- src/docx/opc/pkgreader.py | 138 +++++------- src/docx/opc/pkgwriter.py | 74 +++---- src/docx/opc/rel.py | 63 ++---- src/docx/opc/shared.py | 20 +- src/docx/oxml/__init__.py | 33 ++- src/docx/oxml/coreprops.py | 20 +- src/docx/oxml/document.py | 16 +- src/docx/oxml/exceptions.py | 6 +- src/docx/oxml/ns.py | 53 ++--- src/docx/oxml/numbering.py | 64 ++---- src/docx/oxml/section.py | 114 ++++------ src/docx/oxml/settings.py | 6 +- src/docx/oxml/shape.py | 107 +++------ src/docx/oxml/shared.py | 32 +-- src/docx/oxml/simpletypes.py | 44 ++-- src/docx/oxml/styles.py | 130 ++++------- src/docx/oxml/table.py | 370 ++++++++++++-------------------- src/docx/oxml/text/font.py | 101 +++------ src/docx/oxml/text/parfmt.py | 101 +++------ src/docx/oxml/xmlchemy.py | 288 +++++++++---------------- src/docx/package.py | 26 +-- src/docx/parts/document.py | 73 +++---- src/docx/parts/image.py | 45 ++-- src/docx/parts/numbering.py | 24 +-- src/docx/parts/settings.py | 20 +- src/docx/parts/styles.py | 21 +- src/docx/section.py | 87 ++++---- src/docx/shape.py | 32 ++- src/docx/shared.py | 134 +++++------- src/docx/styles/__init__.py | 18 +- src/docx/styles/latent.py | 140 ++++++------ src/docx/styles/style.py | 159 +++++++------- src/docx/styles/styles.py | 71 +++--- src/docx/table.py | 260 +++++++++------------- src/docx/text/font.py | 235 ++++++++++---------- src/docx/text/paragraph.py | 66 +++--- src/docx/text/parfmt.py | 171 +++++++-------- src/docx/text/run.py | 133 ++++++------ src/docx/text/tabstops.py | 75 +++---- 66 files changed, 2166 insertions(+), 3424 deletions(-) diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index f0000d226..81166556a 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -13,8 +13,8 @@ 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): @@ -22,11 +22,13 @@ def __init__(self, element, 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. + """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: @@ -36,12 +38,13 @@ 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) @@ -49,16 +52,16 @@ def add_table(self, rows, cols, width): @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 @@ -66,8 +69,5 @@ def tables(self): 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/src/docx/dml/color.py b/src/docx/dml/color.py index 6f00b4f99..ec0a0f3e0 100644 --- a/src/docx/dml/color.py +++ b/src/docx/dml/color.py @@ -6,10 +6,8 @@ class ColorFormat(ElementProxy): - """ - Provides access to color settings such as RGB color, theme color, and - luminance adjustments. - """ + """Provides access to color settings such as RGB color, theme color, and luminance + adjustments.""" __slots__ = () @@ -18,22 +16,19 @@ def __init__(self, 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. + """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: @@ -53,18 +48,16 @@ def rgb(self, 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. + """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: @@ -80,12 +73,13 @@ def theme_color(self, value): 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. + 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: @@ -98,9 +92,9 @@ def type(self): @property def _color(self): - """ - Return `w:rPr/w:color` or |None| if not present. Helper to factor out - repetitive element access. + """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: diff --git a/src/docx/document.py b/src/docx/document.py index 327edbe25..1baca5643 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -1,10 +1,11 @@ -"""|Document| and closely related objects""" +"""|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): @@ -40,50 +41,47 @@ def add_page_break(self): 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 + 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 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. + """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 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. + """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 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. + """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 @@ -91,42 +89,38 @@ def add_table(self, rows, cols, style=None): @property def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties of this document. - """ + """A |CoreProperties| object providing Dublin Core properties of 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. + """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): - """ - 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. + """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. - """ + """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. + """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) @@ -137,53 +131,43 @@ def sections(self): @property def settings(self): - """ - A |Settings| object providing access to the document-level settings - for this document. - """ + """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. - """ + """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. + """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): - """ - Return a |Length| object specifying the width of available "writing" - space between the margins of the last section of this document. - """ + """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. - """ + """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. + """Proxy for `` element in this document. + + It's primary role is a container for document content. """ def __init__(self, body_elm, parent): @@ -191,10 +175,9 @@ def __init__(self, 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. + """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/enum/base.py b/src/docx/enum/base.py index 6192f3cb4..054e1e3b4 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -7,10 +7,8 @@ def alias(*aliases): - """ - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to - be referenced by each of the names provided as arguments. - """ + """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 @@ -26,9 +24,8 @@ def decorator(cls): 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. + 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): @@ -37,9 +34,9 @@ def __init__(self, clsname, clsdict): @property def page_str(self): - """ - The RestructuredText documentation page for the enumeration. This is - the only API member for the class. + """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 = ( @@ -64,10 +61,8 @@ def _intro_text(self): 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. - """ + """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, @@ -79,10 +74,8 @@ def _member_def(self, member): @property def _member_defs(self): - """ - A single string containing the aggregated member definitions section - of the documentation page - """ + """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 @@ -91,26 +84,22 @@ def _member_defs(self): @property def _ms_name(self): - """ - The Microsoft API name for this enumeration - """ + """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 - """ + """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 + """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): @@ -121,10 +110,10 @@ def __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. + """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: @@ -132,9 +121,10 @@ def _add_enum_members(meta, 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. + """Return a sequence containing the enumeration values that are valid assignment + values. + + Return-only values are excluded. """ enum_members = clsdict["__members__"] valid_settings = [] @@ -144,17 +134,15 @@ def _collect_valid_settings(meta, clsdict): @classmethod def _generate_docs_page(meta, clsname, clsdict): - """ - Return the RST documentation page for the enumeration. - """ + """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. + """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__ = () @@ -162,9 +150,7 @@ class EnumerationBase(object): @classmethod def validate(cls, value): - """ - Raise |ValueError| if `value` is not an assignable 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__) @@ -175,20 +161,15 @@ def validate(cls, value): class XmlEnumeration(Enumeration): - """ - Provides ``to_xml()`` and ``from_xml()`` methods in addition to base - enumeration features - """ + """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`. - """ + """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 @@ -197,9 +178,7 @@ def from_xml(cls, xml_val): @classmethod def to_xml(cls, enum_val): - """ - Return the XML value of the enumeration value `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__) @@ -208,10 +187,8 @@ def to_xml(cls, enum_val): class EnumMember(object): - """ - Used in the enumeration class definition to define a member value and its - mappings - """ + """Used in the enumeration class definition to define a member value and its + mappings.""" def __init__(self, name, value, docstring): self._name = name @@ -221,34 +198,29 @@ def __init__(self, name, value, docstring): self._docstring = docstring def add_to_enum(self, clsdict): - """ - Add a name to `clsdict` for this member. - """ + """Add a name to `clsdict` for this member.""" self.register_name(clsdict) @property def docstring(self): - """ - The description of this member - """ + """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|. + """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. + """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 @@ -256,26 +228,24 @@ def register_name(self, clsdict): @property def valid_settings(self): - """ - A sequence containing the values valid for assignment for this - member. May be zero, one, or more in number. + """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|. - """ + """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. + """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): @@ -288,52 +258,38 @@ def __init__(self, member_name, int_value, docstring): @property def __doc__(self): - """ - The description of this enumeration member - """ + """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)' - """ + """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) - """ + """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. - """ + """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. - """ + """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. - """ + """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. - """ + """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) @@ -341,25 +297,19 @@ def register_xml_mapping(self, clsdict): @property def xml_value(self): - """ - The XML attribute value that corresponds to this enumeration value - """ + """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 - """ + """Add the enum -> xml value mapping to the enumeration class state.""" if "_member_to_xml" not in clsdict: clsdict["_member_to_xml"] = {} 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 - """ + """Add the xml -> enum value mapping to the enumeration class state.""" if "_xml_to_member" not in clsdict: clsdict["_xml_to_member"] = {} return clsdict["_xml_to_member"] diff --git a/src/docx/enum/dml.py b/src/docx/enum/dml.py index f85923182..16237e70b 100644 --- a/src/docx/enum/dml.py +++ b/src/docx/enum/dml.py @@ -4,14 +4,13 @@ class MSO_COLOR_TYPE(Enumeration): - """ - Specifies the color specification scheme + """Specifies the color specification scheme. Example:: - from docx.enum.dml import MSO_COLOR_TYPE + from docx.enum.dml import MSO_COLOR_TYPE - assert font.color.type == MSO_COLOR_TYPE.SCHEME + assert font.color.type == MSO_COLOR_TYPE.SCHEME """ __ms_name__ = "MsoColorType" @@ -31,17 +30,16 @@ class MSO_COLOR_TYPE(Enumeration): @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. + """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 + from docx.enum.dml import MSO_THEME_COLOR - font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 + font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 """ __ms_name__ = "MsoThemeColorIndex" diff --git a/src/docx/enum/section.py b/src/docx/enum/section.py index df0cd5414..583ceb999 100644 --- a/src/docx/enum/section.py +++ b/src/docx/enum/section.py @@ -5,8 +5,7 @@ @alias("WD_HEADER_FOOTER") class WD_HEADER_FOOTER_INDEX(XmlEnumeration): - """ - alias: **WD_HEADER_FOOTER** + """Alias: **WD_HEADER_FOOTER** Specifies one of the three possible header/footer definitions for a section. @@ -32,17 +31,15 @@ class WD_HEADER_FOOTER_INDEX(XmlEnumeration): @alias("WD_ORIENT") class WD_ORIENTATION(XmlEnumeration): - """ - alias: **WD_ORIENT** + """Alias: **WD_ORIENT** Specifies the page layout orientation. Example:: - from docx.enum.section import WD_ORIENT + from docx.enum.section import WD_ORIENT - section = document.sections[-1] - section.orientation = WD_ORIENT.LANDSCAPE + section = document.sections[-1] section.orientation = WD_ORIENT.LANDSCAPE """ __ms_name__ = "WdOrientation" @@ -57,17 +54,15 @@ class WD_ORIENTATION(XmlEnumeration): @alias("WD_SECTION") class WD_SECTION_START(XmlEnumeration): - """ - alias: **WD_SECTION** + """Alias: **WD_SECTION** Specifies the start type of a section break. Example:: - from docx.enum.section import WD_SECTION + from docx.enum.section import WD_SECTION - section = document.sections[0] - section.start_type = WD_SECTION.NEW_PAGE + section = document.sections[0] section.start_type = WD_SECTION.NEW_PAGE """ __ms_name__ = "WdSectionStart" diff --git a/src/docx/enum/shape.py b/src/docx/enum/shape.py index 64221dc73..6b49ee8f0 100644 --- a/src/docx/enum/shape.py +++ b/src/docx/enum/shape.py @@ -2,10 +2,8 @@ class WD_INLINE_SHAPE_TYPE(object): - """ - Corresponds to WdInlineShapeType enumeration - http://msdn.microsoft.com/en-us/library/office/ff192587.aspx - """ + """Corresponds to WdInlineShapeType enumeration http://msdn.microsoft.com/en- + us/library/office/ff192587.aspx.""" CHART = 12 LINKED_PICTURE = 4 diff --git a/src/docx/enum/style.py b/src/docx/enum/style.py index 3330f0a0e..f7a8a16e0 100644 --- a/src/docx/enum/style.py +++ b/src/docx/enum/style.py @@ -5,19 +5,15 @@ @alias("WD_STYLE") class WD_BUILTIN_STYLE(XmlEnumeration): - """ - alias: **WD_STYLE** + """Alias: **WD_STYLE** Specifies a built-in Microsoft Word style. Example:: - from docx import Document - from docx.enum.style import WD_STYLE + from docx import Document from docx.enum.style import WD_STYLE - document = Document() - styles = document.styles - style = styles[WD_STYLE.BODY_TEXT] + document = Document() styles = document.styles style = styles[WD_STYLE.BODY_TEXT] """ __ms_name__ = "WdBuiltinStyle" @@ -165,17 +161,13 @@ class WD_BUILTIN_STYLE(XmlEnumeration): class WD_STYLE_TYPE(XmlEnumeration): - """ - Specifies one of the four style types: paragraph, character, list, or - table. + """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 + from docx import Document from docx.enum.style import WD_STYLE_TYPE - styles = Document().styles - assert styles[0].type == WD_STYLE_TYPE.PARAGRAPH + styles = Document().styles assert styles[0].type == WD_STYLE_TYPE.PARAGRAPH """ __ms_name__ = "WdStyleType" diff --git a/src/docx/enum/table.py b/src/docx/enum/table.py index 30982a83c..be8fe7050 100644 --- a/src/docx/enum/table.py +++ b/src/docx/enum/table.py @@ -5,17 +5,16 @@ @alias("WD_ALIGN_VERTICAL") class WD_CELL_VERTICAL_ALIGNMENT(XmlEnumeration): - """ - alias: **WD_ALIGN_VERTICAL** + """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 + from docx.enum.table import WD_ALIGN_VERTICAL - table = document.add_table(3, 3) - table.cell(0, 0).vertical_alignment = WD_ALIGN_VERTICAL.BOTTOM + table = document.add_table(3, 3) table.cell(0, 0).vertical_alignment = + WD_ALIGN_VERTICAL.BOTTOM """ __ms_name__ = "WdCellVerticalAlignment" @@ -50,17 +49,16 @@ class WD_CELL_VERTICAL_ALIGNMENT(XmlEnumeration): @alias("WD_ROW_HEIGHT") class WD_ROW_HEIGHT_RULE(XmlEnumeration): - """ - alias: **WD_ROW_HEIGHT** + """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 + 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 + table = document.add_table(3, 3) table.rows[0].height_rule = + WD_ROW_HEIGHT_RULE.EXACTLY """ __ms_name__ = "WdRowHeightRule" @@ -86,15 +84,13 @@ class WD_ROW_HEIGHT_RULE(XmlEnumeration): class WD_TABLE_ALIGNMENT(XmlEnumeration): - """ - Specifies table justification type. + """Specifies table justification type. Example:: - from docx.enum.table import WD_TABLE_ALIGNMENT + from docx.enum.table import WD_TABLE_ALIGNMENT - table = document.add_table(3, 3) - table.alignment = WD_TABLE_ALIGNMENT.CENTER + table = document.add_table(3, 3) table.alignment = WD_TABLE_ALIGNMENT.CENTER """ __ms_name__ = "WdRowAlignment" @@ -109,16 +105,14 @@ class WD_TABLE_ALIGNMENT(XmlEnumeration): class WD_TABLE_DIRECTION(Enumeration): - """ - Specifies the direction in which an application orders cells in the - specified table or row. + """Specifies the direction in which an application orders cells in the specified + table or row. Example:: - from docx.enum.table import WD_TABLE_DIRECTION + from docx.enum.table import WD_TABLE_DIRECTION - table = document.add_table(3, 3) - table.direction = WD_TABLE_DIRECTION.RTL + table = document.add_table(3, 3) table.direction = WD_TABLE_DIRECTION.RTL """ __ms_name__ = "WdTableDirection" diff --git a/src/docx/exceptions.py b/src/docx/exceptions.py index 8507a1ded..e26f4c3bf 100644 --- a/src/docx/exceptions.py +++ b/src/docx/exceptions.py @@ -5,20 +5,14 @@ class PythonDocxError(Exception): - """ - Generic error class. - """ + """Generic error class.""" class InvalidSpanError(PythonDocxError): - """ - Raised when an invalid merge region is specified in a request to merge - table cells. - """ + """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 - """ + """Raised when invalid XML is encountered, such as on attempt to access a missing + required child element.""" diff --git a/src/docx/image/bmp.py b/src/docx/image/bmp.py index c420b1898..115b01d51 100644 --- a/src/docx/image/bmp.py +++ b/src/docx/image/bmp.py @@ -4,16 +4,12 @@ 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) @@ -29,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. - """ + """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 index 5caf64eb2..65286c647 100644 --- a/src/docx/image/constants.py +++ b/src/docx/image/constants.py @@ -2,9 +2,7 @@ class JPEG_MARKER_CODE(object): - """ - JPEG marker codes - """ + """JPEG marker codes.""" TEM = b"\x01" DHT = b"\xC4" @@ -100,9 +98,7 @@ def is_standalone(cls, marker_code): class MIME_TYPE(object): - """ - Image content types - """ + """Image content types.""" BMP = "image/bmp" GIF = "image/gif" @@ -112,9 +108,7 @@ class MIME_TYPE(object): class PNG_CHUNK_TYPE(object): - """ - PNG chunk type names - """ + """PNG chunk type names.""" IHDR = "IHDR" pHYs = "pHYs" @@ -122,9 +116,7 @@ class PNG_CHUNK_TYPE(object): class TIFF_FLD_TYPE(object): - """ - Tag codes for TIFF Image File Directory (IFD) entries. - """ + """Tag codes for TIFF Image File Directory (IFD) entries.""" BYTE = 1 ASCII = 2 @@ -145,9 +137,7 @@ class TIFF_FLD_TYPE(object): class TIFF_TAG(object): - """ - Tag codes for TIFF Image File Directory (IFD) entries. - """ + """Tag codes for TIFF Image File Directory (IFD) entries.""" IMAGE_WIDTH = 0x0100 IMAGE_LENGTH = 0x0101 diff --git a/src/docx/image/exceptions.py b/src/docx/image/exceptions.py index 9e3fab6bf..2b35187d1 100644 --- a/src/docx/image/exceptions.py +++ b/src/docx/image/exceptions.py @@ -2,18 +2,12 @@ class InvalidImageStreamError(Exception): - """ - The recognized image stream appears to be corrupted - """ + """The recognized image stream appears to be corrupted.""" class UnexpectedEndOfFileError(Exception): - """ - EOF was unexpectedly encountered while reading an image stream. - """ + """EOF was unexpectedly encountered while reading an image stream.""" class UnrecognizedImageError(Exception): - """ - The provided image stream could not be recognized. - """ + """The provided image stream could not be recognized.""" diff --git a/src/docx/image/gif.py b/src/docx/image/gif.py index 7ff53d306..e16487264 100644 --- a/src/docx/image/gif.py +++ b/src/docx/image/gif.py @@ -5,34 +5,28 @@ 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. + """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`. - """ + """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. - """ + """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. - """ + """Default filename extension, always 'gif' for GIF images.""" return "gif" @classmethod diff --git a/src/docx/image/helpers.py b/src/docx/image/helpers.py index df0946d02..8532a2e58 100644 --- a/src/docx/image/helpers.py +++ b/src/docx/image/helpers.py @@ -7,10 +7,10 @@ class StreamReader(object): - """ - Wraps a file-like object to provide access to structured data from a - binary file. Byte-order is configurable. `base_offset` is added to any - base value provided to calculate actual location for reads. + """Wraps a file-like object to provide access to structured data from a binary file. + + Byte-order is configurable. `base_offset` is added to any base value provided to + calculate actual location for reads. """ def __init__(self, stream, byte_order, base_offset=0): @@ -20,43 +20,38 @@ def __init__(self, stream, byte_order, base_offset=0): self._base_offset = base_offset def read(self, count): - """ - Allow pass-through read() call - """ + """Allow pass-through read() call.""" return self._stream.read(count) def read_byte(self, base, offset=0): - """ - Return the int value of the byte at the file position defined by - self._base_offset + `base` + `offset`. If `base` is None, the byte is - read from the current position in the stream. + """Return the int value of the byte at the file position defined by + self._base_offset + `base` + `offset`. + + If `base` is None, the byte is read from the current position in the stream. """ fmt = "B" return self._read_int(fmt, base, offset) def read_long(self, base, offset=0): - """ - Return the int value of the four bytes at the file position defined by - self._base_offset + `base` + `offset`. If `base` is None, the long is - read from the current position in the stream. The endian setting of - this instance is used to interpret the byte layout of the long. + """Return the int value of the four bytes at the file position defined by + self._base_offset + `base` + `offset`. + + If `base` is None, the long is read from the current position in the stream. The + endian setting of this instance is used to interpret the byte layout of the + long. """ fmt = "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. - """ + """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`. - """ + """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 @@ -72,9 +67,7 @@ def seek(self, base, offset=0): self._stream.seek(location) def tell(self): - """ - Allow pass-through tell() call - """ + """Allow pass-through tell() call.""" return self._stream.tell() def _read_bytes(self, byte_count, base, offset): diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 7f5911240..2e5286f6f 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -13,10 +13,8 @@ class Image(object): - """ - Graphical image stream such as JPEG, PNG, or GIF with properties and - methods required by ImagePart. - """ + """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__() @@ -26,19 +24,15 @@ def __init__(self, blob, filename, image_header): @classmethod def from_blob(cls, blob): - """ - Return a new |Image| subclass instance parsed from the image binary - contained in `blob`. - """ + """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. - """ + """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: @@ -54,96 +48,80 @@ def from_file(cls, image_descriptor): @property def blob(self): - """ - The bytes of the image 'file' - """ + """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 - """ + """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'. + """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. - """ + """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 - """ + """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 - """ + """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. + """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. + """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`. - """ + """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`. - """ + """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. + """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 @@ -160,17 +138,13 @@ def scaled_dimensions(self, width=None, height=None): @lazyproperty def sha1(self): - """ - SHA1 hash digest of the image blob - """ + """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`. - """ + """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 @@ -178,10 +152,8 @@ def _from_stream(cls, stream, blob, filename=None): def _ImageHeaderFactory(stream): - """ - Return a |BaseImageHeader| subclass instance that knows how to parse the - headers of the image in `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): @@ -198,9 +170,7 @@ def read_32(stream): class BaseImageHeader(object): - """ - Base class for image header subclasses like |Jpeg| and |Tiff|. - """ + """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 @@ -210,9 +180,7 @@ def __init__(self, px_width, px_height, horz_dpi, vert_dpi): @property def content_type(self): - """ - Abstract property definition, must be implemented by all subclasses. - """ + """Abstract property definition, must be implemented by all subclasses.""" msg = ( "content_type property must be implemented by all subclasses of " "BaseImageHeader" @@ -221,9 +189,9 @@ def content_type(self): @property def default_ext(self): - """ - Default filename extension for images of this type. An abstract - property definition, must be implemented by all subclasses. + """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 " @@ -233,30 +201,26 @@ def default_ext(self): @property def px_width(self): - """ - The horizontal pixel dimension of the image - """ + """The horizontal pixel dimension of the image.""" return self._px_width @property def px_height(self): - """ - The vertical pixel dimension of the image - """ + """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. + """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. + """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/src/docx/image/jpeg.py b/src/docx/image/jpeg.py index 23f90c5bc..92770e948 100644 --- a/src/docx/image/jpeg.py +++ b/src/docx/image/jpeg.py @@ -12,37 +12,27 @@ 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. - """ + """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) @@ -55,16 +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 @@ -76,20 +62,16 @@ def from_stream(cls, stream): class _JfifMarkers(object): - """ - Sequence of markers in a JPEG file, perhaps truncated at first SOS marker - for performance reasons. - """ + """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. - """ + """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 = [] @@ -108,10 +90,8 @@ def __str__(self): # pragma: no cover @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(): @@ -122,9 +102,7 @@ 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 @@ -132,9 +110,7 @@ def app0(self): @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 @@ -142,9 +118,7 @@ def app1(self): @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 @@ -152,10 +126,8 @@ def sof(self): class _MarkerParser(object): - """ - Service class that knows how to parse a JFIF stream and iterate over its - markers. - """ + """Service class that knows how to parse a JFIF stream and iterate over its + markers.""" def __init__(self, stream_reader): super(_MarkerParser, self).__init__() @@ -163,18 +135,13 @@ def __init__(self, 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 @@ -186,9 +153,7 @@ def iter_markers(self): class _MarkerFinder(object): - """ - Service class that knows how to find the next JFIF marker in a stream. - """ + """Service class that knows how to find the next JFIF marker in a stream.""" def __init__(self, stream): super(_MarkerFinder, self).__init__() @@ -196,18 +161,16 @@ def __init__(self, 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: @@ -224,11 +187,11 @@ def next(self, start): 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() @@ -238,10 +201,11 @@ def _next_non_ff_byte(self, start): 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() @@ -251,9 +215,9 @@ def _offset_of_next_ff_byte(self, start): 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 @@ -262,10 +226,8 @@ def _read_byte(self): 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: @@ -278,9 +240,9 @@ def _MarkerFactory(marker_code, stream, offset): class _Marker(object): - """ - Base class for JFIF marker classes. Represents a marker and its segment - occuring in a JPEG byte stream. + """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): @@ -291,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: @@ -303,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 @@ -319,16 +277,12 @@ 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 @@ -340,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: @@ -368,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 @@ -392,9 +338,7 @@ def from_stream(cls, stream, marker_code, offset): 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) @@ -403,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 @@ -423,37 +365,29 @@ def from_stream(cls, stream, marker_code, offset): @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. - """ + """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" @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) @@ -462,9 +396,7 @@ def _tiff_from_exif_segment(cls, stream, offset, segment_length): class _SofMarker(_Marker): - """ - Represents a JFIF start of frame (SOFx) marker segment. - """ + """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) @@ -473,10 +405,7 @@ def __init__(self, marker_code, offset, segment_length, px_width, 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 @@ -491,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/src/docx/image/png.py b/src/docx/image/png.py index 842a0752f..bfbbaf30d 100644 --- a/src/docx/image/png.py +++ b/src/docx/image/png.py @@ -5,31 +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. - """ + """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 @@ -41,10 +33,7 @@ def from_stream(cls, stream): class _PngParser(object): - """ - Parses a PNG image stream to extract the image properties found in its - chunks. - """ + """Parses a PNG image stream to extract the image properties found in its chunks.""" def __init__(self, chunks): super(_PngParser, self).__init__() @@ -52,34 +41,28 @@ def __init__(self, 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: @@ -88,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: @@ -99,19 +82,15 @@ 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 - """ + """Collection of the chunks parsed from a PNG image stream.""" def __init__(self, chunk_iterable): super(_Chunks, self).__init__() @@ -119,18 +98,14 @@ def __init__(self, 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 = 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: @@ -139,17 +114,12 @@ def IHDR(self): @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 @@ -157,9 +127,7 @@ def _find_first(self, match): class _ChunkParser(object): - """ - Extracts chunks from a PNG image stream - """ + """Extracts chunks from a PNG image stream.""" def __init__(self, stream_rdr): super(_ChunkParser, self).__init__() @@ -167,27 +135,23 @@ def __init__(self, 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: @@ -202,10 +166,8 @@ def _iter_chunk_offsets(self): 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, @@ -215,9 +177,9 @@ def _ChunkFactory(chunk_type, stream_rdr, offset): class _Chunk(object): - """ - Base class for specific chunk types. Also serves as the default chunk - type. + """Base class for specific chunk types. + + Also serves as the default chunk type. """ def __init__(self, chunk_type): @@ -226,23 +188,17 @@ def __init__(self, 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) @@ -251,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) @@ -269,9 +223,7 @@ def px_height(self): class _pHYsChunk(_Chunk): - """ - pYHs chunk, contains the image dpi information - """ + """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) @@ -281,10 +233,8 @@ def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifi @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) diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py index 42d3fb11d..4e09db15d 100644 --- a/src/docx/image/tiff.py +++ b/src/docx/image/tiff.py @@ -4,32 +4,26 @@ class Tiff(BaseImageHeader): - """ - Image header parser for TIFF images. Handles both big and little endian - byte ordering. + """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 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. - """ + """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`. - """ + """Return a |Tiff| instance containing the properties of the TIFF image in + `stream`.""" parser = _TiffParser.parse(stream) px_width = parser.px_width @@ -41,10 +35,8 @@ def from_stream(cls, stream): class _TiffParser(object): - """ - Parses a TIFF image stream to extract the image properties found in its - main image file directory (IFD) - """ + """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__() @@ -52,10 +44,8 @@ def __init__(self, ifd_entries): @classmethod def parse(cls, stream): - """ - Return an instance of |_TiffParser| containing the properties parsed - from the TIFF image in `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) @@ -63,55 +53,43 @@ def parse(cls, stream): @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. - """ + """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. - """ + """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. - """ + """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. - """ + """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'. - """ + """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 + """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 @@ -135,60 +113,45 @@ def _dpi(self, resolution_tag): @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. - """ + """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. - """ + """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`` - """ + """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]`` - """ + """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`. - """ + """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 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) - """ + """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__() @@ -196,10 +159,8 @@ def __init__(self, stream_rdr, offset): self._offset = offset def iter_entries(self): - """ - Generate an |_IfdEntry| instance corresponding to each entry in the - directory. - """ + """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) @@ -207,17 +168,13 @@ def iter_entries(self): @property def _entry_count(self): - """ - The count of directory entries, read from the top of the IFD header - """ + """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`. - """ + """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, @@ -230,9 +187,9 @@ def _IfdEntryFactory(stream_rdr, offset): class _IfdEntry(object): - """ - Base class for IFD entry classes. Subclasses are differentiated by value - type, e.g. ASCII, long int, etc. + """Base class for IFD entry classes. + + Subclasses are differentiated by value type, e.g. ASCII, long int, etc. """ def __init__(self, tag_code, value): @@ -242,11 +199,11 @@ def __init__(self, tag_code, 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. + """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) @@ -256,52 +213,45 @@ def from_stream(cls, stream_rdr, offset): @classmethod def _parse_value(cls, stream_rdr, offset, value_count, value_offset): - """ - Return the value of this field parsed from `stream_rdr` at `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 - """ + """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. - """ + """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 - """ + """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 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 - """ + """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. + """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) @@ -310,15 +260,14 @@ def _parse_value(cls, stream_rdr, offset, value_count, value_offset): class _LongIfdEntry(_IfdEntry): - """ - IFD entry expressed as a long (4-byte) integer - """ + """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. + """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) @@ -327,16 +276,14 @@ def _parse_value(cls, stream_rdr, offset, value_count, value_offset): class _RationalIfdEntry(_IfdEntry): - """ - IFD entry expressed as a numerator, denominator pair - """ + """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. + """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) diff --git a/src/docx/opc/coreprops.py b/src/docx/opc/coreprops.py index c0434730a..26e73fef6 100644 --- a/src/docx/opc/coreprops.py +++ b/src/docx/opc/coreprops.py @@ -5,10 +5,8 @@ class CoreProperties(object): - """ - Corresponds to part named ``/docProps/core.xml``, containing the core - document properties for this document package. - """ + """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 index 3225d6943..c5583d301 100644 --- a/src/docx/opc/exceptions.py +++ b/src/docx/opc/exceptions.py @@ -5,12 +5,8 @@ class OpcError(Exception): - """ - Base error class for python-opc - """ + """Base error class for python-opc.""" class PackageNotFoundError(OpcError): - """ - Raised when a package cannot be found at the specified path. - """ + """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 index 408fe5af6..dfbad8d06 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -28,16 +28,15 @@ def parse_xml(text): - """ - ``etree.fromstring()`` replacement that uses oxml parser - """ + """``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 + """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(":") @@ -46,18 +45,18 @@ def qn(tag): 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. + """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. + """Serialize `element` to human-readable XML suitable for tests. + + No XML declaration. """ return etree.tostring(element, encoding="unicode", pretty_print=True) @@ -68,49 +67,37 @@ def serialize_for_reading(element): class BaseOxmlElement(etree.ElementBase): - """ - Base class for all custom element classes, to add standardized behavior - to all classes in one place. - """ + """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 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. - """ + """```` 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. - """ + """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. - """ + """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. - """ + """Return a new ```` element with attributes set to parameter + values.""" xml = '' % nsmap["ct"] default = parse_xml(xml) default.set("Extension", ext) @@ -119,25 +106,19 @@ def new(ext, content_type): class CT_Override(BaseOxmlElement): - """ - ```` element, specifying the content type to be applied for a - part with the specified partname. - """ + """```` 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. - """ + """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. - """ + """Return a new ```` element with attributes set to parameter + values.""" xml = '' % nsmap["ct"] override = parse_xml(xml) override.set("PartName", partname) @@ -146,24 +127,17 @@ def new(partname, content_type): @property def partname(self): - """ - String held in the ``PartName`` attribute of this ```` - element. - """ + """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. - """ + """```` 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. - """ + """Return a new ```` element.""" xml = '' % nsmap["pr"] relationship = parse_xml(xml) relationship.set("Id", rId) @@ -175,96 +149,71 @@ def new(rId, reltype, target, target_mode=RTM.INTERNAL): @property def rId(self): - """ - String held in the ``Id`` attribute of this ```` - element. - """ + """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. - """ + """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. - """ + """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``. + """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. - """ + """```` 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. - """ + """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. - """ + """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 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 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. - """ + """```` 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. - """ + """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. - """ + """Add a child ```` element with attributes set to parameter + values.""" override = CT_Override.new(partname, content_type) self.append(override) @@ -274,9 +223,7 @@ def defaults(self): @staticmethod def new(): - """ - Return a new ```` element. - """ + """Return a new ```` element.""" xml = '' % nsmap["ct"] types = parse_xml(xml) return types diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index d4882c8ce..671c585de 100644 --- a/src/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -21,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 @@ -31,17 +31,13 @@ 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 @@ -61,10 +57,8 @@ 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. - """ + """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(): @@ -83,23 +77,22 @@ def walk_parts(source, visited=[]): 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) @@ -119,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 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) @@ -188,10 +169,10 @@ class Unmarshaller(object): @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) Unmarshaller._unmarshal_relationships(pkg_reader, package, parts) @@ -201,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(): @@ -215,11 +197,9 @@ 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 = ( diff --git a/src/docx/opc/packuri.py b/src/docx/opc/packuri.py index f86e83635..fe330d89b 100644 --- a/src/docx/opc/packuri.py +++ b/src/docx/opc/packuri.py @@ -8,9 +8,10 @@ class PackURI(str): - """ - Provides access to pack URI components such as the baseURI and the - filename slice. Behaves as |str| otherwise. + """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]*)?") @@ -23,28 +24,27 @@ def __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`. - """ + """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 '/'. + """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. + """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] @@ -52,20 +52,18 @@ def ext(self): @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 ''. + """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'``. - """ + """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 @@ -79,18 +77,18 @@ def idx(self): @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 '/'. + """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'. + """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 ('/') @@ -98,10 +96,10 @@ def relative_ref(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 '/'. + """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) diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index 874fed21f..1dd2c4b29 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -8,10 +8,10 @@ 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. + """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): @@ -22,20 +22,20 @@ def __init__(self, partname, content_type, blob=None, package=None): 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. + """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. + """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 @@ -43,25 +43,23 @@ def before_marshal(self): @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. + """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. - """ + """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. + """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] @@ -71,29 +69,24 @@ 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 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. - """ + """|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' - """ + """|PackURI| instance holding partname of this part, e.g. + '/ppt/slides/slide1.xml'.""" return self._partname @partname.setter @@ -104,19 +97,17 @@ def partname(self, partname): 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 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. - """ + """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: @@ -125,49 +116,39 @@ def relate_to(self, target, reltype, is_external=False): @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. - """ + """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. - """ + """|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`. - """ + """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`. - """ + """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``. + """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 @@ -185,22 +166,18 @@ def __new__(cls, partname, content_type, reltype, 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`. - """ + """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. + """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): @@ -213,9 +190,7 @@ def blob(self): @property def element(self): - """ - The root XML element of this XML part. - """ + """The root XML element of this XML part.""" return self._element @classmethod @@ -225,9 +200,9 @@ def load(cls, partname, content_type, blob, 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. + """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/src/docx/opc/parts/coreprops.py b/src/docx/opc/parts/coreprops.py index 2bb43c9ce..6e26e1d05 100644 --- a/src/docx/opc/parts/coreprops.py +++ b/src/docx/opc/parts/coreprops.py @@ -10,17 +10,13 @@ class CorePropertiesPart(XmlPart): - """ - Corresponds to part named ``/docProps/core.xml``, containing the core - document properties for this document package. - """ + """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. - """ + """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" @@ -31,10 +27,8 @@ def default(cls, package): @property def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties contained in this core properties part. - """ + """A |CoreProperties| object providing read/write access to the core properties + contained in this core properties part.""" return CoreProperties(self.element) @classmethod diff --git a/src/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py index 9c8a18993..71c096278 100644 --- a/src/docx/opc/phys_pkg.py +++ b/src/docx/opc/phys_pkg.py @@ -8,9 +8,7 @@ class PhysPkgReader(object): - """ - Factory for physical package reader objects. - """ + """Factory for physical package reader objects.""" def __new__(cls, pkg_file): # if `pkg_file` is a string, treat it as a path @@ -28,56 +26,41 @@ def __new__(cls, pkg_file): class PhysPkgWriter(object): - """ - Factory for physical package writer objects. - """ + """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. - """ + """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. - """ + """`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. - """ + """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. - """ + """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 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. - """ + """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: @@ -86,39 +69,31 @@ def rels_xml_for(self, source_uri): class _ZipPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for a zip file OPC package. - """ + """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 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. - """ + """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 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. - """ + """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: @@ -127,24 +102,18 @@ def rels_xml_for(self, source_uri): class _ZipPkgWriter(PhysPkgWriter): - """ - Implements |PhysPkgWriter| interface for a zip file OPC package. - """ + """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. - """ + """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`. - """ + """Write `blob` to this zip package with the membername corresponding to + `pack_uri`.""" self._zipf.writestr(pack_uri.membername, blob) diff --git a/src/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py index defa60ec6..b5459a61e 100644 --- a/src/docx/opc/pkgreader.py +++ b/src/docx/opc/pkgreader.py @@ -8,10 +8,8 @@ class PackageReader(object): - """ - Provides access to the contents of a zip-format OPC package via its - :attr:`serialized_parts` and :attr:`pkg_srels` attributes. - """ + """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__() @@ -20,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) @@ -33,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: @@ -53,11 +45,9 @@ 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: @@ -68,20 +58,15 @@ def _load_serialized_parts(phys_reader, pkg_srels, content_types): @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) @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: @@ -103,10 +88,8 @@ def _walk_phys_parts(phys_reader, srels, visited_partnames=None): class _ContentTypeMap(object): - """ - Value type providing dictionary semantics for looking up content type by - part name, e.g. ``content_type = cti['/ppt/presentation.xml']``. - """ + """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__() @@ -114,9 +97,7 @@ def __init__(self): 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)) @@ -129,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: @@ -142,24 +121,21 @@ 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. + """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): @@ -184,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 @@ -195,10 +169,10 @@ def srels(self): 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. + """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): @@ -211,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 @@ -223,35 +195,29 @@ 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 = ( @@ -266,25 +232,23 @@ def target_partname(self): class _SerializedRelationships(object): - """ - Read-only sequence of |_SerializedRelationship| instances corresponding - to the relationships item XML passed to constructor. - """ + """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 index 9a3e56fdb..5506373be 100644 --- a/src/docx/opc/pkgwriter.py +++ b/src/docx/opc/pkgwriter.py @@ -13,20 +13,17 @@ 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. + """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. - """ + """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) @@ -35,19 +32,15 @@ def write(pkg_file, pkg_rels, parts): @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`. - """ + """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. - """ + """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): @@ -55,19 +48,16 @@ def _write_parts(phys_writer, parts): @staticmethod def _write_pkg_rels(phys_writer, pkg_rels): - """ - Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the - package. - """ + """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)``. + """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): @@ -76,19 +66,15 @@ def __init__(self): @property def blob(self): - """ - Return XML form of this content types item, suitable for storage as - ``[Content_Types].xml`` in an OPC package. - """ + """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. - """ + """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 @@ -97,10 +83,8 @@ def from_parts(cls, parts): 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. - """ + """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 @@ -109,11 +93,11 @@ def _add_content_type(self, 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 + """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() diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index a137d4590..58acfc6e3 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -4,9 +4,7 @@ class Relationships(dict): - """ - Collection object for |_Relationship| instances, having list semantics. - """ + """Collection object for |_Relationship| instances, having list semantics.""" def __init__(self, baseURI): super(Relationships, self).__init__() @@ -14,9 +12,7 @@ def __init__(self, baseURI): self._target_parts_by_rId = {} def add_relationship(self, reltype, target, rId, is_external=False): - """ - Return a newly added |_Relationship| instance. - """ + """Return a newly added |_Relationship| instance.""" rel = _Relationship(rId, reltype, target, self._baseURI, is_external) self[rId] = rel if not is_external: @@ -24,10 +20,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 @@ -35,10 +29,8 @@ 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 @@ -46,38 +38,29 @@ def get_or_add_ext_rel(self, reltype, target_ref): 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) 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: @@ -95,10 +78,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: @@ -111,10 +94,8 @@ 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']. - """ + """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: @@ -122,9 +103,7 @@ def _next_rId(self): class _Relationship(object): - """ - Value object for relationship to part. - """ + """Value object for relationship to part.""" def __init__(self, rId, reltype, target, baseURI, external=False): super(_Relationship, self).__init__() diff --git a/src/docx/opc/shared.py b/src/docx/opc/shared.py index 370780e6b..1862f66db 100644 --- a/src/docx/opc/shared.py +++ b/src/docx/opc/shared.py @@ -2,12 +2,12 @@ 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 + """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): @@ -26,10 +26,10 @@ def cls_method_fn(cls: type, method_name: str): 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. + """@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__ diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index ab3995105..b8278b3e4 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -14,10 +14,10 @@ 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 + """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) @@ -25,10 +25,10 @@ def parse_xml(xml): 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'``. + """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]) @@ -36,15 +36,14 @@ def register_element_cls(tag, 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 + """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) diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 854b83309..ddb9f703c 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -43,9 +43,7 @@ def new(cls): @property def author_text(self): - """ - The text in the `dc:creator` child element. - """ + """The text in the `dc:creator` child element.""" return self._text_of_element("creator") @author_text.setter @@ -134,9 +132,7 @@ def modified_datetime(self, value): @property def revision_number(self): - """ - Integer value of revision property. - """ + """Integer value of revision property.""" revision = self.revision if revision is None: return 0 @@ -153,9 +149,7 @@ def revision_number(self): @revision_number.setter def revision_number(self, value): - """ - Set revision property to string value of integer `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) @@ -198,9 +192,7 @@ def _datetime_of_element(self, property_name): return None def _get_or_add(self, prop_name): - """ - Return element returned by "get_or_add_" method for `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() @@ -256,9 +248,7 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): return dt def _set_element_datetime(self, prop_name, value): - """ - Set date/time value of child element having `prop_name` to `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)) diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index 22ee9e187..f84c0f7cc 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -4,26 +4,20 @@ class CT_Document(BaseOxmlElement): - """ - ```` element, the root element of a document.xml file. - """ + """```` 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 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``. - """ + """````, 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",)) diff --git a/src/docx/oxml/exceptions.py b/src/docx/oxml/exceptions.py index ee0cff832..8919239a2 100644 --- a/src/docx/oxml/exceptions.py +++ b/src/docx/oxml/exceptions.py @@ -6,7 +6,5 @@ class XmlchemyError(Exception): class InvalidXmlError(XmlchemyError): - """ - Raised when invalid XML is encountered, such as on attempt to access a - missing required child element - """ + """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 index d3558cbf1..dd8745634 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -23,10 +23,7 @@ class NamespacePrefixedTag(str): - """ - Value object that knows the semantics of an XML tag having a namespace - prefix. - """ + """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) @@ -47,60 +44,54 @@ def from_clark_name(cls, clark_name): @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 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 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 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 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 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 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 {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 + """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(":") diff --git a/src/docx/oxml/numbering.py b/src/docx/oxml/numbering.py index c386e8c3a..99ca7a74d 100644 --- a/src/docx/oxml/numbering.py +++ b/src/docx/oxml/numbering.py @@ -13,30 +13,23 @@ 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. - """ + """```` 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 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`. - """ + """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) @@ -45,27 +38,21 @@ def new(cls, num_id, abstractNum_id): class CT_NumLvl(BaseOxmlElement): - """ - ```` element, which identifies a level in a list - definition to override with settings it contains. - """ + """```` 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 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. - """ + """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")) @@ -89,27 +76,21 @@ class CT_NumPr(BaseOxmlElement): class CT_Numbering(BaseOxmlElement): - """ - ```` element, the root element of a numbering part, i.e. - numbering.xml - """ + """```` 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`. - """ + """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`. - """ + """Return the ```` child element having ``numId`` attribute matching + `numId`.""" xpath = './w:num[@w:numId="%d"]' % numId try: return self.xpath(xpath)[0] @@ -118,11 +99,8 @@ def num_having_numId(self, 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. - """ + """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): diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index 72ab5c6f4..75d80eeb1 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -14,23 +14,21 @@ class CT_HdrFtr(BaseOxmlElement): - """`w:hdr` and `w:ftr`, the root element for header and footer part respectively""" + """`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""" + """`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. - """ + """```` element, defining page margins.""" top = OptionalAttribute("w:top", ST_SignedTwipsMeasure) right = OptionalAttribute("w:right", ST_TwipsMeasure) @@ -42,9 +40,7 @@ class CT_PageMar(BaseOxmlElement): class CT_PageSz(BaseOxmlElement): - """ - ```` element, defining page dimensions and orientation. - """ + """```` element, defining page dimensions and orientation.""" w = OptionalAttribute("w:w", ST_TwipsMeasure) h = OptionalAttribute("w:h", ST_TwipsMeasure) @@ -54,7 +50,7 @@ class CT_PageSz(BaseOxmlElement): class CT_SectPr(BaseOxmlElement): - """`w:sectPr` element, the container element for section properties""" + """`w:sectPr` element, the container element for section properties.""" _tag_seq = ( "w:footnotePr", @@ -108,11 +104,9 @@ def add_headerReference(self, type_, rId): @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. - """ + """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 @@ -124,10 +118,10 @@ def bottom_margin(self, value): 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. + """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() @@ -135,11 +129,9 @@ def clone(self): @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. - """ + """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 @@ -169,11 +161,9 @@ def get_headerReference(self, type_): @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. - """ + """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 @@ -186,11 +176,9 @@ def gutter(self, 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. - """ + """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 @@ -203,11 +191,9 @@ def header(self, 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. - """ + """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 @@ -220,11 +206,9 @@ def left_margin(self, 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. - """ + """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 @@ -237,10 +221,8 @@ def orientation(self, value): @property def page_height(self): - """ - Value in EMU of the ``h`` attribute of the ```` child - element, or |None| if not present. - """ + """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 @@ -253,10 +235,8 @@ def page_height(self, value): @property def page_width(self): - """ - Value in EMU of the ``w`` attribute of the ```` child - element, or |None| if not present. - """ + """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 @@ -269,7 +249,7 @@ def page_width(self, value): @property def preceding_sectPr(self): - """sectPr immediately preceding this one or None if this is the first.""" + """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 @@ -290,11 +270,9 @@ def remove_headerReference(self, type_): @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. - """ + """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 @@ -307,11 +285,9 @@ def right_margin(self, 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. - """ + """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 @@ -327,7 +303,7 @@ def start_type(self, value): @property def titlePg_val(self): - """Value of `w:titlePg/@val` or |None| if not present""" + """Value of `w:titlePg/@val` or |None| if not present.""" titlePg = self.titlePg if titlePg is None: return False @@ -342,11 +318,9 @@ def titlePg_val(self, 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. - """ + """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 @@ -359,8 +333,6 @@ def top_margin(self, value): class CT_SectType(BaseOxmlElement): - """ - ```` element, defining the section start type. - """ + """```` element, defining the section start type.""" val = OptionalAttribute("w:val", WD_SECTION_START) diff --git a/src/docx/oxml/settings.py b/src/docx/oxml/settings.py index dc899cbc0..fd39fbd99 100644 --- a/src/docx/oxml/settings.py +++ b/src/docx/oxml/settings.py @@ -1,10 +1,10 @@ -"""Custom element classes related to document settings""" +"""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""" + """`w:settings` element, root element for the settings part.""" _tag_seq = ( "w:writeProtection", @@ -111,7 +111,7 @@ class CT_Settings(BaseOxmlElement): @property def evenAndOddHeaders_val(self): - """value of `w:evenAndOddHeaders/@w:val` or |None| if not present.""" + """Value of `w:evenAndOddHeaders/@w:val` or |None| if not present.""" evenAndOddHeaders = self.evenAndOddHeaders if evenAndOddHeaders is None: return False diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index a66d251bf..f26179fbb 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -20,44 +20,34 @@ class CT_Blip(BaseOxmlElement): - """ - ```` element, specifies image source and adjustments such as - alpha and tint. - """ + """```` 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 - """ + """```` 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 - """ + """```` element, container for a DrawingML object.""" graphicData = OneAndOnlyOne("a:graphicData") class CT_GraphicalObjectData(BaseOxmlElement): - """ - ```` element, container for the XML of a DrawingML object - """ + """```` 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. - """ + """```` element, container for an inline shape.""" extent = OneAndOnlyOne("wp:extent") docPr = OneAndOnlyOne("wp:docPr") @@ -65,10 +55,8 @@ class CT_Inline(BaseOxmlElement): @classmethod def new(cls, cx, cy, shape_id, pic): - """ - Return a new ```` element populated with the values passed - as parameters. - """ + """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 @@ -82,10 +70,8 @@ def new(cls, cx, cy, shape_id, pic): @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. - """ + """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) @@ -109,9 +95,9 @@ def _inline_xml(cls): class CT_NonVisualDrawingProps(BaseOxmlElement): - """ - Used for ```` element, and perhaps others. Specifies the id and - name of a DrawingML drawing. + """Used for ```` element, and perhaps others. + + Specifies the id and name of a DrawingML drawing. """ id = RequiredAttribute("id", ST_DrawingElementId) @@ -119,16 +105,11 @@ class CT_NonVisualDrawingProps(BaseOxmlElement): class CT_NonVisualPictureProperties(BaseOxmlElement): - """ - ```` element, specifies picture locking and resize - behaviors. - """ + """```` element, specifies picture locking and resize behaviors.""" class CT_Picture(BaseOxmlElement): - """ - ```` element, a DrawingML picture - """ + """```` element, a DrawingML picture.""" nvPicPr = OneAndOnlyOne("pic:nvPicPr") blipFill = OneAndOnlyOne("pic:blipFill") @@ -136,11 +117,9 @@ class CT_Picture(BaseOxmlElement): @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. - """ + """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 @@ -175,17 +154,15 @@ def _pic_xml(cls): class CT_PictureNonVisual(BaseOxmlElement): - """ - ```` element, non-visual picture properties - """ + """```` 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). + """Used for ```` element, and perhaps others. + + Specifies an x, y coordinate (point). """ x = RequiredAttribute("x", ST_Coordinate) @@ -193,9 +170,9 @@ class CT_Point2D(BaseOxmlElement): class CT_PositiveSize2D(BaseOxmlElement): - """ - Used for ```` element, and perhaps others later. Specifies the - size of a DrawingML drawing. + """Used for ```` element, and perhaps others later. + + Specifies the size of a DrawingML drawing. """ cx = RequiredAttribute("cx", ST_PositiveCoordinate) @@ -203,23 +180,17 @@ class CT_PositiveSize2D(BaseOxmlElement): class CT_PresetGeometry2D(BaseOxmlElement): - """ - ```` element, specifies an preset autoshape geometry, such - as ``rect``. - """ + """```` element, specifies an preset autoshape geometry, such as + ``rect``.""" class CT_RelativeRect(BaseOxmlElement): - """ - ```` element, specifying picture should fill containing - rectangle shape. - """ + """```` element, specifying picture should fill containing rectangle + shape.""" class CT_ShapeProperties(BaseOxmlElement): - """ - ```` element, specifies size and shape of picture container. - """ + """```` element, specifies size and shape of picture container.""" xfrm = ZeroOrOne( "a:xfrm", @@ -237,9 +208,7 @@ class CT_ShapeProperties(BaseOxmlElement): @property def cx(self): - """ - Shape width as an instance of Emu, or None if not present. - """ + """Shape width as an instance of Emu, or None if not present.""" xfrm = self.xfrm if xfrm is None: return None @@ -252,9 +221,7 @@ def cx(self, value): @property def cy(self): - """ - Shape height as an instance of Emu, or None if not present. - """ + """Shape height as an instance of Emu, or None if not present.""" xfrm = self.xfrm if xfrm is None: return None @@ -267,16 +234,12 @@ def cy(self, value): class CT_StretchInfoProperties(BaseOxmlElement): - """ - ```` element, specifies how picture should fill its containing - shape. - """ + """```` element, specifies how picture should fill its containing + shape.""" class CT_Transform2D(BaseOxmlElement): - """ - ```` element, specifies size and shape of picture container. - """ + """```` element, specifies size and shape of picture container.""" off = ZeroOrOne("a:off", successors=("a:ext",)) ext = ZeroOrOne("a:ext", successors=()) diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index eb939291d..b8a79550c 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -7,46 +7,36 @@ class CT_DecimalNumber(BaseOxmlElement): - """ - Used for ````, ````, ```` and several - others, containing a text representation of a decimal number (e.g. 42) in - its ``val`` attribute. - """ + """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 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'. - """ + """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. - """ + """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`. - """ + """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/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index 56226b6da..e0c609959 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -83,11 +83,9 @@ def validate(cls, 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): @@ -113,9 +111,9 @@ 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 @@ -138,16 +136,12 @@ 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 @@ -217,9 +211,7 @@ def convert_from_xml(cls, 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 @@ -234,9 +226,7 @@ def validate(cls, value): class ST_HexColorAuto(XsdStringEnumeration): - """ - Value for `w:color/[@val="auto"] attribute setting - """ + """Value for `w:color/[@val="auto"] attribute setting.""" AUTO = "auto" @@ -244,9 +234,7 @@ class ST_HexColorAuto(XsdStringEnumeration): 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): @@ -262,9 +250,7 @@ def convert_to_xml(cls, value): class ST_Merge(XsdStringEnumeration): - """ - Valid values for attribute - """ + """Valid values for attribute.""" CONTINUE = "continue" RESTART = "restart" @@ -365,9 +351,7 @@ def convert_from_xml(cls, str_value): class ST_VerticalAlignRun(XsdStringEnumeration): - """ - Valid values for `w:vertAlign/@val`. - """ + """Valid values for `w:vertAlign/@val`.""" BASELINE = "baseline" SUPERSCRIPT = "superscript" diff --git a/src/docx/oxml/styles.py b/src/docx/oxml/styles.py index 8de7a2290..486110848 100644 --- a/src/docx/oxml/styles.py +++ b/src/docx/oxml/styles.py @@ -12,10 +12,8 @@ def styleId_from_name(name): - """ - Return the style id corresponding to `name`, taking into account - special-case names such as 'Heading 1'. - """ + """Return the style id corresponding to `name`, taking into account special-case + names such as 'Heading 1'.""" return { "caption": "Caption", "heading 1": "Heading1", @@ -31,11 +29,9 @@ def styleId_from_name(name): 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. - """ + """`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=()) @@ -47,37 +43,28 @@ class CT_LatentStyles(BaseOxmlElement): 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. - """ + """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. - """ + """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`. - """ + """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. - """ + """```` element, defining override visibility behaviors for a named + latent style.""" locked = OptionalAttribute("w:locked", ST_OnOff) name = RequiredAttribute("w:name", ST_String) @@ -87,29 +74,21 @@ class CT_LsdException(BaseOxmlElement): unhideWhenUsed = OptionalAttribute("w:unhideWhenUsed", ST_OnOff) def delete(self): - """ - Remove this `w:lsdException` element from the XML document. - """ + """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 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`. - """ + """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 - """ + """A ```` element, representing a style definition.""" _tag_seq = ( "w:name", @@ -154,9 +133,7 @@ class CT_Style(BaseOxmlElement): @property def basedOn_val(self): - """ - Value of `w:basedOn/@w:val` or |None| if not present. - """ + """Value of `w:basedOn/@w:val` or |None| if not present.""" basedOn = self.basedOn if basedOn is None: return None @@ -171,10 +148,8 @@ def basedOn_val(self, 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. - """ + """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 @@ -185,16 +160,12 @@ def base_style(self): return base_style def delete(self): - """ - Remove this `w:style` element from its parent `w:styles` element. - """ + """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. - """ + """Value of `w:locked/@w:val` or |False| if not present.""" locked = self.locked if locked is None: return False @@ -209,9 +180,7 @@ def locked_val(self, value): @property def name_val(self): - """ - Value of ```` child or |None| if not present. - """ + """Value of ```` child or |None| if not present.""" name = self.name if name is None: return None @@ -226,11 +195,8 @@ def name_val(self, 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. - """ + """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 @@ -239,9 +205,7 @@ def next_style(self): @property def qFormat_val(self): - """ - Value of `w:qFormat/@w:val` or |False| if not present. - """ + """Value of `w:qFormat/@w:val` or |False| if not present.""" qFormat = self.qFormat if qFormat is None: return False @@ -255,9 +219,7 @@ def qFormat_val(self, value): @property def semiHidden_val(self): - """ - Value of ```` child or |False| if not present. - """ + """Value of ```` child or |False| if not present.""" semiHidden = self.semiHidden if semiHidden is None: return False @@ -272,9 +234,7 @@ def semiHidden_val(self, value): @property def uiPriority_val(self): - """ - Value of ```` child or |None| if not present. - """ + """Value of ```` child or |None| if not present.""" uiPriority = self.uiPriority if uiPriority is None: return None @@ -289,9 +249,7 @@ def uiPriority_val(self, value): @property def unhideWhenUsed_val(self): - """ - Value of `w:unhideWhenUsed/@w:val` or |False| if not present. - """ + """Value of `w:unhideWhenUsed/@w:val` or |False| if not present.""" unhideWhenUsed = self.unhideWhenUsed if unhideWhenUsed is None: return False @@ -306,10 +264,7 @@ def unhideWhenUsed_val(self, value): class CT_Styles(BaseOxmlElement): - """ - ```` element, the root element of a styles part, i.e. - styles.xml - """ + """```` 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:]) @@ -317,10 +272,9 @@ class CT_Styles(BaseOxmlElement): 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`. + """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 @@ -330,9 +284,7 @@ def add_style_of_type(self, name, style_type, builtin): return style def default_for(self, style_type): - """ - Return `w:style[@w:type="*{style_type}*][-1]` or |None| if not found. - """ + """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 ] @@ -342,10 +294,8 @@ def default_for(self, style_type): 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. - """ + """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] @@ -353,10 +303,8 @@ def get_by_id(self, styleId): return None def get_by_name(self, name): - """ - Return the ```` child element having ```` child - element with value `name`, or |None| if not found. - """ + """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] @@ -364,7 +312,5 @@ def get_by_name(self, name): return None def _iter_styles(self): - """ - Generate each of the `w:style` child elements in document order. - """ + """Generate each of the `w:style` child elements in document order.""" return (style for style in self.xpath("w:style")) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index ef5e44702..e86214a62 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -24,27 +24,23 @@ class CT_Height(BaseOxmlElement): - """ - Used for ```` to specify a row height and 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 - """ + """```` 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: @@ -57,18 +53,13 @@ def tc_at_grid_col(self, idx): @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 @@ -81,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 @@ -110,9 +98,7 @@ def _new_tc(self): class CT_Tbl(BaseOxmlElement): - """ - ```` element - """ + """```` element.""" tblPr = OneAndOnlyOne("w:tblPr") tblGrid = OneAndOnlyOne("w:tblGrid") @@ -120,10 +106,9 @@ class CT_Tbl(BaseOxmlElement): @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: @@ -140,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: @@ -157,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 @@ -176,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() @@ -239,45 +219,34 @@ def _tcs_xml(cls, col_count, col_width): class CT_TblGrid(BaseOxmlElement): - """ - ```` element, child of ````, holds ```` - elements that define column count, width, etc. - """ + """```` 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. - """ + """```` 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. - """ + """```` 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", @@ -307,10 +276,10 @@ class CT_TblPr(BaseOxmlElement): @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: @@ -341,10 +310,8 @@ def autofit(self, value): @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 @@ -359,10 +326,8 @@ 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 @@ -372,10 +337,8 @@ class CT_TblWidth(BaseOxmlElement): @property def width(self): - """ - Return the EMU length value represented by the combined ``w:w`` and - ``w:type`` attributes. - """ + """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) @@ -387,7 +350,7 @@ def width(self, value): 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") @@ -395,11 +358,10 @@ class CT_Tc(BaseOxmlElement): @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 @@ -408,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 @@ -423,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: @@ -438,10 +400,8 @@ 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. - """ + """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: @@ -449,17 +409,13 @@ def iter_block_items(self): @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) @@ -467,37 +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 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 @@ -510,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 @@ -525,29 +473,24 @@ 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: @@ -562,19 +505,14 @@ def vMerge_val(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 @@ -584,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: @@ -604,24 +540,18 @@ 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. - """ + """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"): @@ -632,11 +562,9 @@ 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: @@ -664,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: @@ -680,14 +608,14 @@ 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): @@ -705,23 +633,17 @@ def raise_on_invalid_swallow(next_tc): @property def _tbl(self): - """ - The tbl element this tc element appears in. - """ + """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 @@ -729,15 +651,13 @@ def _tc_below(self): @property def _tr(self): - """ - The tr element this tc element appears in. - """ + """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 @@ -748,10 +668,8 @@ def _tr_above(self): @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: @@ -761,16 +679,12 @@ def _tr_below(self): @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", @@ -800,9 +714,9 @@ class CT_TcPr(BaseOxmlElement): @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: @@ -819,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: @@ -836,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 @@ -853,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 @@ -869,9 +779,7 @@ def width(self, value): class CT_TrPr(BaseOxmlElement): - """ - ```` element, defining table row properties - """ + """```` element, defining table row properties.""" _tag_seq = ( "w:cnfStyle", @@ -895,9 +803,7 @@ class CT_TrPr(BaseOxmlElement): @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 @@ -912,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 @@ -935,8 +839,6 @@ class CT_VerticalJc(BaseOxmlElement): class CT_VMerge(BaseOxmlElement): - """ - ```` element, specifying vertical merging behavior of a cell. - """ + """```` element, specifying vertical merging behavior of a cell.""" val = OptionalAttribute("w:val", ST_Merge, default=ST_Merge.CONTINUE) diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 75c7f1b71..45b2335e0 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -19,46 +19,34 @@ class CT_Color(BaseOxmlElement): - """ - `w:color` element, specifying the color of a font and perhaps other - objects. - """ + """`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. - """ + """```` 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. - """ + """`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. - """ + """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. - """ + """```` element, containing the properties for a run.""" _tag_seq = ( "w:rStyle", @@ -131,18 +119,13 @@ class CT_RPr(BaseOxmlElement): del _tag_seq def _new_color(self): - """ - Override metaclass method to set `w:color/@val` to RGB black on - create. - """ + """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. - """ + """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 @@ -158,11 +141,11 @@ def highlight_val(self, 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. + """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: @@ -179,9 +162,7 @@ def rFonts_ascii(self, value): @property def rFonts_hAnsi(self): - """ - The value of `w:rFonts/@w:hAnsi` or |None| if not present. - """ + """The value of `w:rFonts/@w:hAnsi` or |None| if not present.""" rFonts = self.rFonts if rFonts is None: return None @@ -196,10 +177,8 @@ def rFonts_hAnsi(self, value): @property def style(self): - """ - String contained in child, or None if that element is not - present. - """ + """String contained in child, or None if that element is not + present.""" rStyle = self.rStyle if rStyle is None: return None @@ -207,10 +186,10 @@ def style(self): @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. + """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() @@ -221,9 +200,9 @@ def style(self, 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 + """|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 @@ -246,9 +225,9 @@ def subscript(self, value): @property def superscript(self): - """ - |True| if `w:vertAlign/@w:val` is 'superscript'. |False| if - `w:vertAlign/@w:val` contains any other value. |None| if + """|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 @@ -271,9 +250,7 @@ def superscript(self, value): @property def sz_val(self): - """ - The value of `w:sz/@w:val` or |None| if not present. - """ + """The value of `w:sz/@w:val` or |None| if not present.""" sz = self.sz if sz is None: return None @@ -289,9 +266,7 @@ def sz_val(self, value): @property def u_val(self): - """ - Value of `w:u/@val`, or None if not present. - """ + """Value of `w:u/@val`, or None if not present.""" u = self.u if u is None: return None @@ -304,10 +279,8 @@ def u_val(self, value): 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'. - """ + """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 @@ -322,15 +295,11 @@ def _set_bool_val(self, name, value): class CT_Underline(BaseOxmlElement): - """ - ```` element, specifying the underlining style for a run. - """ + """```` element, specifying the underlining style for a run.""" @property def val(self): - """ - The underline type corresponding to the ``w:val`` attribute value. - """ + """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: @@ -354,8 +323,6 @@ def val(self, value): class CT_VerticalAlignRun(BaseOxmlElement): - """ - ```` element, specifying subscript or superscript. - """ + """```` element, specifying subscript or superscript.""" val = RequiredAttribute("w:val", ST_VerticalAlignRun) diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index e459d4dfa..255a71f36 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -18,9 +18,7 @@ class CT_Ind(BaseOxmlElement): - """ - ```` element, specifying paragraph indentation. - """ + """```` element, specifying paragraph indentation.""" left = OptionalAttribute("w:left", ST_SignedTwipsMeasure) right = OptionalAttribute("w:right", ST_SignedTwipsMeasure) @@ -29,17 +27,13 @@ class CT_Ind(BaseOxmlElement): class CT_Jc(BaseOxmlElement): - """ - ```` element, specifying paragraph justification. - """ + """```` element, specifying paragraph justification.""" val = RequiredAttribute("w:val", WD_ALIGN_PARAGRAPH) class CT_PPr(BaseOxmlElement): - """ - ```` element, containing the properties for a paragraph. - """ + """```` element, containing the properties for a paragraph.""" _tag_seq = ( "w:pStyle", @@ -94,10 +88,10 @@ class CT_PPr(BaseOxmlElement): @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. + """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: @@ -125,9 +119,7 @@ def first_line_indent(self, value): @property def ind_left(self): - """ - The value of `w:ind/@w:left` or |None| if not present. - """ + """The value of `w:ind/@w:left` or |None| if not present.""" ind = self.ind if ind is None: return None @@ -142,9 +134,7 @@ def ind_left(self, value): @property def ind_right(self): - """ - The value of `w:ind/@w:right` or |None| if not present. - """ + """The value of `w:ind/@w:right` or |None| if not present.""" ind = self.ind if ind is None: return None @@ -159,9 +149,7 @@ def ind_right(self, value): @property def jc_val(self): - """ - The value of the ```` child element or |None| if not present. - """ + """The value of the ```` child element or |None| if not present.""" jc = self.jc if jc is None: return None @@ -176,9 +164,7 @@ def jc_val(self, value): @property def keepLines_val(self): - """ - The value of `keepLines/@val` or |None| if not present. - """ + """The value of `keepLines/@val` or |None| if not present.""" keepLines = self.keepLines if keepLines is None: return None @@ -193,9 +179,7 @@ def keepLines_val(self, value): @property def keepNext_val(self): - """ - The value of `keepNext/@val` or |None| if not present. - """ + """The value of `keepNext/@val` or |None| if not present.""" keepNext = self.keepNext if keepNext is None: return None @@ -210,9 +194,7 @@ def keepNext_val(self, value): @property def pageBreakBefore_val(self): - """ - The value of `pageBreakBefore/@val` or |None| if not present. - """ + """The value of `pageBreakBefore/@val` or |None| if not present.""" pageBreakBefore = self.pageBreakBefore if pageBreakBefore is None: return None @@ -227,9 +209,7 @@ def pageBreakBefore_val(self, value): @property def spacing_after(self): - """ - The value of `w:spacing/@w:after` or |None| if not present. - """ + """The value of `w:spacing/@w:after` or |None| if not present.""" spacing = self.spacing if spacing is None: return None @@ -243,9 +223,7 @@ def spacing_after(self, value): @property def spacing_before(self): - """ - The value of `w:spacing/@w:before` or |None| if not present. - """ + """The value of `w:spacing/@w:before` or |None| if not present.""" spacing = self.spacing if spacing is None: return None @@ -259,9 +237,7 @@ def spacing_before(self, value): @property def spacing_line(self): - """ - The value of `w:spacing/@w:line` or |None| if not present. - """ + """The value of `w:spacing/@w:line` or |None| if not present.""" spacing = self.spacing if spacing is None: return None @@ -275,12 +251,13 @@ def spacing_line(self, 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. + """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: @@ -298,10 +275,8 @@ def spacing_lineRule(self, value): @property def style(self): - """ - String contained in child, or None if that element is not - present. - """ + """String contained in child, or None if that element is not + present.""" pStyle = self.pStyle if pStyle is None: return None @@ -309,10 +284,10 @@ def style(self): @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. + """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() @@ -322,9 +297,7 @@ def style(self, style): @property def widowControl_val(self): - """ - The value of `widowControl/@val` or |None| if not present. - """ + """The value of `widowControl/@val` or |None| if not present.""" widowControl = self.widowControl if widowControl is None: return None @@ -339,10 +312,8 @@ def widowControl_val(self, value): class CT_Spacing(BaseOxmlElement): - """ - ```` element, specifying paragraph spacing attributes such as - space before and line spacing. - """ + """```` element, specifying paragraph spacing attributes such as space + before and line spacing.""" after = OptionalAttribute("w:after", ST_TwipsMeasure) before = OptionalAttribute("w:before", ST_TwipsMeasure) @@ -359,16 +330,12 @@ class CT_TabStop(BaseOxmlElement): class CT_TabStops(BaseOxmlElement): - """ - ```` element, container for a sorted sequence of tab stops. - """ + """```` 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. - """ + """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: diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index f4504a1cd..57a095730 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -11,19 +11,17 @@ 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) return XmlString(xml) class XmlString(str): - """ - Provides string comparison override suitable for serialized XML that is - useful for tests. - """ + """Provides string comparison override suitable for serialized XML that is useful + for tests.""" # ' text' # | | || | @@ -47,19 +45,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: @@ -74,10 +70,8 @@ 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 @@ -102,10 +96,8 @@ def __init__(cls, clsname, bases, clsdict): class BaseAttribute(object): - """ - Base class for OptionalAttribute and RequiredAttribute, providing common - methods. - """ + """Base class for OptionalAttribute and RequiredAttribute, providing common + methods.""" def __init__(self, attr_name, simple_type): super(BaseAttribute, self).__init__() @@ -113,20 +105,16 @@ def __init__(self, attr_name, simple_type): self._simple_type = simple_type def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to `element_cls`. - """ + """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_) @@ -139,9 +127,9 @@ def _clark_name(self): 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 + """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. """ @@ -151,10 +139,8 @@ def __init__(self, attr_name, simple_type, default=None): @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) @@ -167,10 +153,8 @@ def get_attr_value(obj): @property def _docstring(self): - """ - Return the string to use as the ``__doc__`` attribute of the property - for this attribute. - """ + """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" @@ -180,10 +164,8 @@ def _docstring(self): @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): if value is None or value == self._default: @@ -197,21 +179,19 @@ def set_attr_value(obj, 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 _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) @@ -227,10 +207,8 @@ def get_attr_value(obj): @property def _docstring(self): - """ - Return the string to use as the ``__doc__`` attribute of the property - for this attribute. - """ + """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, @@ -238,10 +216,8 @@ def _docstring(self): @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) @@ -251,10 +227,8 @@ def set_attr_value(obj, value): class _BaseChildElement(object): - """ - Base class for the child element classes corresponding to varying - cardinalities, such as ZeroOrOne and ZeroOrMore. - """ + """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__() @@ -262,18 +236,12 @@ def __init__(self, nsptagname, successors=()): 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) @@ -291,10 +259,8 @@ def _add_child(obj, **attrs): 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' @@ -303,19 +269,14 @@ def _add_creator(self): 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) @@ -328,10 +289,8 @@ def _insert_child(obj, child): 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. - """ + """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_) @@ -341,9 +300,7 @@ def _add_method_name(self): 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) @@ -357,20 +314,16 @@ def add_child(obj): 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. - """ + """Return a function object that creates a new, empty element of the right type, + having no attributes.""" def new_child_element(obj): return OxmlElement(self._nsptagname) @@ -379,10 +332,11 @@ def new_child_element(obj): @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): @@ -399,10 +353,8 @@ def _insert_method_name(self): @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)) @@ -415,11 +367,11 @@ def get_child_element_list(obj): @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 @@ -433,19 +385,15 @@ def _new_method_name(self): 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`. - """ + """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._group_prop_name = group_prop_name self._successors = successors @@ -457,10 +405,8 @@ def populate_class_members(self, element_cls, group_prop_name, successors): 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) @@ -479,7 +425,7 @@ def get_or_change_to_child(obj): @property def _prop_name(self): - """property name computed from tag name, e.g. a:schemeClr -> schemeClr.""" + """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:] @@ -493,26 +439,20 @@ def _remove_group_method_name(self): 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`. - """ + """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)) @@ -529,15 +469,11 @@ def get_child_element(obj): 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`. - """ + """Add the appropriate methods to `element_cls`.""" super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() @@ -548,14 +484,10 @@ 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`. - """ + """Add the appropriate methods to `element_cls`.""" super(ZeroOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() @@ -566,14 +498,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() @@ -583,10 +511,8 @@ 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) @@ -601,10 +527,7 @@ def get_or_add_child(obj): 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) @@ -620,19 +543,15 @@ def _get_or_add_method_name(self): 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`. - """ + """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: @@ -642,20 +561,15 @@ 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: @@ -668,10 +582,8 @@ def _remove_choice_group(obj): @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) @@ -684,10 +596,8 @@ def get_group_member_element(obj): @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 diff --git a/src/docx/package.py b/src/docx/package.py index e4e9433e9..27a1da937 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -9,7 +9,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. @@ -44,7 +44,7 @@ def _gather_image_parts(self): class ImageParts(object): - """Collection of |ImagePart| objects corresponding to images in the package""" + """Collection of |ImagePart| objects corresponding to images in the package.""" def __init__(self): self._image_parts = [] @@ -74,31 +74,27 @@ 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): diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 1d02033f1..fb72a8b4c 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -34,17 +34,13 @@ 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): @@ -56,20 +52,20 @@ def footer_part(self, 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 + """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) @@ -79,18 +75,15 @@ def header_part(self, 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) @@ -100,34 +93,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) @@ -138,9 +125,9 @@ def _settings_part(self): @property def _styles_part(self): - """ - Instance of |StylesPart| for this document. Creates an empty styles - part if one is not present. + """Instance of |StylesPart| for this document. + + Creates an empty styles part if one is not present. """ try: return self.part_related_by(RT.STYLES) diff --git a/src/docx/parts/image.py b/src/docx/parts/image.py index deb099289..d93c47922 100644 --- a/src/docx/parts/image.py +++ b/src/docx/parts/image.py @@ -8,9 +8,9 @@ class ImagePart(Part): - """ - An image part. Corresponds to the target part of a relationship with type - RELATIONSHIP_TYPE.IMAGE. + """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): @@ -19,10 +19,8 @@ def __init__(self, partname, content_type, blob, image=None): @property def default_cx(self): - """ - Native width of this image, calculated from its width in pixels and - horizontal dots per inch (dpi). - """ + """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 @@ -30,10 +28,8 @@ def default_cx(self): @property def default_cy(self): - """ - Native height of this image, calculated from its height in pixels and - vertical dots per inch (dpi). - """ + """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 @@ -41,12 +37,11 @@ def default_cy(self): @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. + """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 @@ -54,10 +49,8 @@ def filename(self): @classmethod def from_image(cls, image, partname): - """ - Return an |ImagePart| instance newly created from `image` and - assigned `partname`. - """ + """Return an |ImagePart| instance newly created from `image` and assigned + `partname`.""" return ImagePart(partname, image.content_type, image.blob, image) @property @@ -68,15 +61,11 @@ def image(self): @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. - """ + """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. - """ + """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 index 3eae202ab..31ba05e99 100644 --- a/src/docx/parts/numbering.py +++ b/src/docx/parts/numbering.py @@ -5,33 +5,25 @@ class NumberingPart(XmlPart): - """ - Proxy for the numbering.xml part containing numbering definitions for - a document or glossary. - """ + """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. - """ + """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. - """ + """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. - """ + """Collection of |_NumberingDefinition| instances corresponding to the ```` + elements in a numbering part.""" def __init__(self, numbering_elm): super(_NumberingDefinitions, self).__init__() diff --git a/src/docx/parts/settings.py b/src/docx/parts/settings.py index f23bf459a..08d310124 100644 --- a/src/docx/parts/settings.py +++ b/src/docx/parts/settings.py @@ -10,16 +10,12 @@ class SettingsPart(XmlPart): - """ - Document-level settings part of a WordprocessingML (WML) package. - """ + """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. - """ + """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()) @@ -27,17 +23,13 @@ def default(cls, 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. - """ + """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. - """ + """Return a bytestream containing XML for a default settings part.""" path = os.path.join( os.path.split(__file__)[0], "..", "templates", "default-settings.xml" ) diff --git a/src/docx/parts/styles.py b/src/docx/parts/styles.py index c23637e54..5f42f31c7 100644 --- a/src/docx/parts/styles.py +++ b/src/docx/parts/styles.py @@ -10,17 +10,12 @@ class StylesPart(XmlPart): - """ - Proxy for the styles.xml part containing style definitions for a document - or glossary. - """ + """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. - """ + """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()) @@ -28,17 +23,13 @@ def default(cls, package): @property def styles(self): - """ - The |_Styles| instance containing the styles ( element - proxies) for this styles part. - """ + """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. - """ + """Return a bytestream containing XML for a default styles part.""" path = os.path.join( os.path.split(__file__)[0], "..", "templates", "default-styles.xml" ) diff --git a/src/docx/section.py b/src/docx/section.py index 4b81b368d..ccae91975 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -47,10 +47,8 @@ def __init__(self, sectPr, document_part): @property def bottom_margin(self): - """ - |Length| object representing the bottom margin for all pages in this - section in English Metric Units. - """ + """|Length| object representing the bottom margin for all pages in this section + in English Metric Units.""" return self._sectPr.bottom_margin @bottom_margin.setter @@ -117,10 +115,10 @@ def footer(self): @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. + """|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. """ return self._sectPr.footer @@ -130,11 +128,11 @@ def footer_distance(self, 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. + """|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. """ return self._sectPr.gutter @@ -153,10 +151,10 @@ def header(self): @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. + """|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. """ return self._sectPr.header @@ -166,10 +164,8 @@ def header_distance(self, value): @property def left_margin(self): - """ - |Length| object representing the left margin for all pages in this - section in English Metric Units. - """ + """|Length| object representing the left margin for all pages in this section in + English Metric Units.""" return self._sectPr.left_margin @left_margin.setter @@ -178,11 +174,9 @@ def left_margin(self, value): @property def orientation(self): - """ - Member of the :ref:`WdOrientation` enumeration specifying the page + """Member of the :ref:`WdOrientation` enumeration specifying the page orientation for this section, one of ``WD_ORIENT.PORTRAIT`` or - ``WD_ORIENT.LANDSCAPE``. - """ + ``WD_ORIENT.LANDSCAPE``.""" return self._sectPr.orientation @orientation.setter @@ -191,11 +185,11 @@ def orientation(self, 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. + """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. """ return self._sectPr.page_height @@ -205,11 +199,11 @@ def page_height(self, 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. + """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. """ return self._sectPr.page_width @@ -219,10 +213,8 @@ def page_width(self, value): @property def right_margin(self): - """ - |Length| object representing the right margin for all pages in this - section in English Metric Units. - """ + """|Length| object representing the right margin for all pages in this section + in English Metric Units.""" return self._sectPr.right_margin @right_margin.setter @@ -231,12 +223,9 @@ def right_margin(self, 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. - """ + """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.""" return self._sectPr.start_type @start_type.setter @@ -245,10 +234,8 @@ def start_type(self, value): @property def top_margin(self): - """ - |Length| object representing the top margin for all pages in this - section in English Metric Units. - """ + """|Length| object representing the top margin for all pages in this section in + English Metric Units.""" return self._sectPr.top_margin @top_margin.setter @@ -257,7 +244,7 @@ def top_margin(self, value): class _BaseHeaderFooter(BlockItemContainer): - """Base class for header and footer classes""" + """Base class for header and footer classes.""" def __init__(self, sectPr, document_part, header_footer_index): self._sectPr = sectPr diff --git a/src/docx/shape.py b/src/docx/shape.py index 1f7a938a3..bc7818693 100644 --- a/src/docx/shape.py +++ b/src/docx/shape.py @@ -9,19 +9,15 @@ 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,10 +39,8 @@ def _inline_lst(self): class InlineShape(object): - """ - Proxy for an ```` element, representing the container for an - inline graphical object. - """ + """Proxy for an ```` element, representing the container for an inline + graphical object.""" def __init__(self, inline): super(InlineShape, self).__init__() @@ -54,9 +48,9 @@ def __init__(self, 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 @@ -67,9 +61,9 @@ 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 @@ -87,9 +81,9 @@ def type(self): @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 index 63f3969fa..062669724 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -2,11 +2,11 @@ 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. + """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 @@ -20,52 +20,37 @@ def __new__(cls, emu): @property def cm(self): - """ - The equivalent length expressed in centimeters (float). - """ + """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). - """ + """The equivalent length expressed in English Metric Units (int).""" return self @property def inches(self): - """ - The equivalent length expressed in inches (float). - """ + """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). - """ + """The equivalent length expressed in millimeters (float).""" return self / float(self._EMUS_PER_MM) @property def pt(self): - """ - Floating point length in points - """ + """Floating point length in points.""" return self / float(self._EMUS_PER_PT) @property def twips(self): - """ - The equivalent length expressed in twips (int). - """ + """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)``. - """ + """Convenience constructor for length in inches, e.g. ``width = Inches(0.5)``.""" def __new__(cls, inches): emu = int(inches * Length._EMUS_PER_INCH) @@ -73,10 +58,7 @@ def __new__(cls, inches): class Cm(Length): - """ - Convenience constructor for length in centimeters, e.g. - ``height = Cm(12)``. - """ + """Convenience constructor for length in centimeters, e.g. ``height = Cm(12)``.""" def __new__(cls, cm): emu = int(cm * Length._EMUS_PER_CM) @@ -84,20 +66,15 @@ def __new__(cls, cm): class Emu(Length): - """ - Convenience constructor for length in English Metric Units, e.g. - ``width = Emu(457200)``. - """ + """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)``. - """ + """Convenience constructor for length in millimeters, e.g. ``width = Mm(240.5)``.""" def __new__(cls, mm): emu = int(mm * Length._EMUS_PER_MM) @@ -105,9 +82,7 @@ def __new__(cls, mm): class Pt(Length): - """ - Convenience value class for specifying a length in points - """ + """Convenience value class for specifying a length in points.""" def __new__(cls, points): emu = int(points * Length._EMUS_PER_PT) @@ -115,8 +90,8 @@ def __new__(cls, points): class Twips(Length): - """ - Convenience constructor for length in twips, e.g. ``width = Twips(42)``. + """Convenience constructor for length in twips, e.g. ``width = Twips(42)``. + A twip is a twentieth of a point, 635 EMU. """ @@ -126,9 +101,7 @@ def __new__(cls, twips): class RGBColor(tuple): - """ - Immutable value object defining a particular RGB color. - """ + """Immutable value object defining a particular RGB color.""" def __new__(cls, r, g, b): msg = "RGBColor() takes three integer values 0-255" @@ -141,16 +114,12 @@ def __repr__(self): return "RGBColor(0x%02x, 0x%02x, 0x%02x)" % self def __str__(self): - """ - Return a hex string rgb value, like '3C2F80' - """ + """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'``. - """ + """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) @@ -158,10 +127,10 @@ def from_string(cls, rgb_hex_str): 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. + """@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__ @@ -178,9 +147,10 @@ def get_prop_value(obj): def write_only_property(f): - """ - @write_only_property decorator. Creates a property (descriptor attribute) - that accepts assignment, but not getattr (use in an expression). + """@write_only_property decorator. + + Creates a property (descriptor attribute) that accepts assignment, but not getattr + (use in an expression). """ docstring = f.__doc__ @@ -188,11 +158,11 @@ def write_only_property(f): 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. + """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") @@ -202,12 +172,12 @@ def __init__(self, element, parent=None): 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. + """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 @@ -220,25 +190,21 @@ def __ne__(self, other): @property def element(self): - """ - The lxml element proxied by this object. - """ + """The lxml element proxied by this object.""" return self._element @property def part(self): - """ - The package part containing this object - """ + """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. + """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): @@ -247,7 +213,5 @@ def __init__(self, parent): @property def part(self): - """ - The package part containing this object - """ + """The package part containing this object.""" return self._parent.part diff --git a/src/docx/styles/__init__.py b/src/docx/styles/__init__.py index bcd84ac9b..b6b09e0ca 100644 --- a/src/docx/styles/__init__.py +++ b/src/docx/styles/__init__.py @@ -2,10 +2,8 @@ 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. - """ + """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"), @@ -27,16 +25,12 @@ class BabelFish(object): @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 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 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 index 0b9121332..fdcbbb6e7 100644 --- a/src/docx/styles/latent.py +++ b/src/docx/styles/latent.py @@ -5,18 +5,14 @@ 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. - """ + """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. - """ + """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: @@ -30,21 +26,18 @@ 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`. - """ + """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. + """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 @@ -54,10 +47,10 @@ def default_priority(self, 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. + """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") @@ -67,12 +60,12 @@ def default_to_hidden(self, 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). + """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") @@ -82,10 +75,8 @@ def default_to_locked(self, 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. - """ + """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 @@ -94,10 +85,8 @@ def default_to_quick_style(self, 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. - """ + """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 @@ -106,11 +95,11 @@ def default_to_unhide_when_used(self, 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 + """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 @@ -121,31 +110,34 @@ def load_count(self, 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. + """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|. + """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. + """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") @@ -155,12 +147,11 @@ def hidden(self, 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). + """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") @@ -170,16 +161,12 @@ def locked(self, value): @property def name(self): - """ - The name of the built-in style this exception applies to. - """ + """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. - """ + """The integer sort key for this latent style in the Word UI.""" return self._element.uiPriority @priority.setter @@ -188,11 +175,11 @@ def priority(self, 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. + """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") @@ -202,12 +189,11 @@ def quick_style(self, 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. + """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") diff --git a/src/docx/styles/style.py b/src/docx/styles/style.py index 8d5917b23..0f46b67f5 100644 --- a/src/docx/styles/style.py +++ b/src/docx/styles/style.py @@ -8,10 +8,8 @@ def StyleFactory(style_elm): - """ - Return a style object of the appropriate |BaseStyle| subclass, according - to the type of `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, @@ -23,42 +21,42 @@ def StyleFactory(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. + """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. + """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. + """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|. + """|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 @@ -68,12 +66,12 @@ def hidden(self, 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). + """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 @@ -83,9 +81,7 @@ def locked(self, value): @property def name(self): - """ - The UI name of this style. - """ + """The UI name of this style.""" name = self._element.name_val if name is None: return None @@ -97,11 +93,11 @@ def name(self, 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. + """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 @@ -111,9 +107,10 @@ def priority(self, 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. + """|True| if this style should be displayed in the style gallery when + :attr:`.hidden` is |False|. + + Read/write Boolean. """ return self._element.qFormat_val @@ -123,10 +120,10 @@ def quick_style(self, 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. + """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 @@ -136,10 +133,8 @@ def style_id(self, value): @property def type(self): - """ - Member of :ref:`WdStyleType` corresponding to the type of this style, - e.g. ``WD_STYLE_TYPE.PARAGRAPH``. - """ + """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 @@ -147,11 +142,11 @@ def type(self): @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. + """|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 @@ -161,20 +156,18 @@ def unhide_when_used(self, 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. + """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. - """ + """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 @@ -187,17 +180,16 @@ def base_style(self, style): @property def font(self): - """ - The |Font| object providing access to the character formatting - properties for this style, such as font name and size. - """ + """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. + """A paragraph style. + + A paragraph style provides both character formatting and paragraph formatting such + as indentation and line-spacing. """ __slots__ = () @@ -207,12 +199,11 @@ def __repr__(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. + """|_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: @@ -230,17 +221,16 @@ def next_paragraph_style(self, style): @property def paragraph_format(self): - """ - The |ParagraphFormat| object providing access to the paragraph - formatting properties for this style such as indentation. - """ + """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. + """A table style. + + A table style provides character and paragraph formatting for its contents as well + as special table formatting properties. """ __slots__ = () @@ -250,8 +240,9 @@ def __repr__(self): class _NumberingStyle(BaseStyle): - """ - A numbering style. Not yet implemented. + """A numbering style. + + Not yet implemented. """ __slots__ = () diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py index 8e9cffffb..8583e9ede 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -18,17 +18,15 @@ class Styles(ElementProxy): __slots__ = () def __contains__(self, name): - """ - Enables `in` operator on style 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): - """ - 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. + """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: @@ -52,10 +50,10 @@ 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. + """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: @@ -64,10 +62,8 @@ def add_style(self, name, style_type, builtin=False): 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). - """ + """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 @@ -84,13 +80,12 @@ def get_by_id(self, style_id, 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`. + """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 @@ -101,18 +96,15 @@ def get_style_id(self, 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. - """ + """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 + """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) @@ -121,18 +113,19 @@ def _get_by_id(self, style_id, 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 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`. + """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( diff --git a/src/docx/table.py b/src/docx/table.py index 43482ae96..b05bffefc 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -7,19 +7,14 @@ 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 @@ -29,9 +24,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: @@ -41,11 +34,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 @@ -55,11 +48,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 @@ -68,33 +61,24 @@ 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 @@ -102,23 +86,20 @@ def row_cells(self, row_idx): @lazyproperty def rows(self): - """ - |_Rows| instance containing the sequence of rows in this table. - """ + """|_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) @@ -130,20 +111,20 @@ def style(self, style_or_name): @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 @@ -153,10 +134,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 = [] @@ -172,9 +153,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 @@ -183,32 +162,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. + """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) @@ -216,10 +194,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) @@ -227,34 +205,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 + """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) @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() @@ -266,9 +246,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: @@ -282,9 +262,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 @@ -293,9 +271,7 @@ def width(self, value): class _Column(Parented): - """ - Table column - """ + """Table column.""" def __init__(self, gridCol, parent): super(_Column, self).__init__(parent) @@ -303,24 +279,17 @@ def __init__(self, gridCol, parent): @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 @@ -329,15 +298,13 @@ 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. """ @@ -346,9 +313,7 @@ def __init__(self, tbl, 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: @@ -365,25 +330,19 @@ 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) @@ -391,17 +350,13 @@ def __init__(self, tr, parent): @property def cells(self): - """ - Sequence of |_Cell| instances corresponding to cells in this row. - """ + """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 @@ -410,11 +365,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 @@ -423,22 +375,18 @@ 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. """ @@ -447,9 +395,7 @@ def __init__(self, tbl, parent): self._tbl = tbl def __getitem__(self, idx): - """ - Provide indexed access, (e.g. 'rows[0]') - """ + """Provide indexed access, (e.g. 'rows[0]')""" return list(self)[idx] def __iter__(self): @@ -460,7 +406,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/src/docx/text/font.py b/src/docx/text/font.py index 80d0b7dad..8329966ed 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -5,18 +5,16 @@ 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. - """ + """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. + """Read/write. + + Causes text in this font to appear in capital letters. """ return self._get_bool_prop("caps") @@ -26,8 +24,9 @@ def all_caps(self, value): @property def bold(self): - """ - Read/write. Causes text in this font to appear in bold. + """Read/write. + + Causes text in this font to appear in bold. """ return self._get_bool_prop("b") @@ -37,18 +36,16 @@ def bold(self, value): @property def color(self): - """ - A |ColorFormat| object providing a way to get and set the text color - for this font. - """ + """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. + """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") @@ -58,9 +55,10 @@ def complex_script(self, 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. + """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") @@ -70,9 +68,10 @@ def cs_bold(self, 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. + """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") @@ -82,9 +81,9 @@ def cs_italic(self, value): @property def double_strike(self): - """ - Read/write tri-state value. When |True|, causes the text in the run - to appear with double strikethrough. + """Read/write tri-state value. + + When |True|, causes the text in the run to appear with double strikethrough. """ return self._get_bool_prop("dstrike") @@ -94,9 +93,10 @@ def double_strike(self, 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. + """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") @@ -106,10 +106,10 @@ def emboss(self, 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. + """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") @@ -119,10 +119,8 @@ def hidden(self, value): @property def highlight_color(self): - """ - A member of :ref:`WdColorIndex` indicating the color of highlighting - applied, or `None` if no highlighting is applied. - """ + """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 @@ -135,10 +133,10 @@ def highlight_color(self, 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. + """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") @@ -148,9 +146,9 @@ def italic(self, 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. + """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") @@ -160,9 +158,10 @@ def imprint(self, 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. + """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") @@ -172,11 +171,10 @@ def math(self, 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. + """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: @@ -191,10 +189,10 @@ def name(self, 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. + """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") @@ -204,10 +202,11 @@ def no_proof(self, 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. + """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") @@ -217,9 +216,9 @@ def outline(self, value): @property def rtl(self): - """ - Read/write tri-state value. When |True| causes the text in the run - to have right-to-left characteristics. + """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") @@ -229,9 +228,10 @@ def rtl(self, 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. + """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") @@ -241,19 +241,13 @@ def shadow(self, 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 + """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: @@ -267,10 +261,10 @@ def size(self, 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. + """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") @@ -280,10 +274,10 @@ def small_caps(self, 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. + """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") @@ -293,12 +287,12 @@ def snap_to_grid(self, 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. + """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") @@ -308,10 +302,10 @@ def spec_vanish(self, 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. + """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") @@ -321,10 +315,10 @@ def strike(self, 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. + """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: @@ -338,10 +332,11 @@ def subscript(self, 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. + """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: @@ -355,13 +350,13 @@ def superscript(self, 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. + """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: @@ -375,10 +370,10 @@ def underline(self, 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. + """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") @@ -387,17 +382,13 @@ 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`. - """ + """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`. - """ + """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/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index c397bb328..9ca235bdb 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -14,12 +14,12 @@ def __init__(self, p, 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 + """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() @@ -32,11 +32,11 @@ def add_run(self, text=None, style=None): @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 + """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 @@ -46,19 +46,18 @@ def alignment(self, value): self._p.alignment = value def clear(self): - """ - Return this same paragraph after removing all its content. + """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. + """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: @@ -69,29 +68,25 @@ def insert_paragraph_before(self, text=None, style=None): @property def paragraph_format(self): - """ - The |ParagraphFormat| object providing access to the formatting - properties for this paragraph, such as line spacing and indentation. - """ + """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. - """ + """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. + """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) @@ -125,9 +120,6 @@ def text(self, text): self.add_run(text) def _insert_paragraph_before(self): - """ - Return a newly created paragraph, inserted directly before this - paragraph. - """ + """Return a newly created paragraph, inserted directly before this paragraph.""" p = self._p.add_p_before() return Paragraph(p, self._parent) diff --git a/src/docx/text/parfmt.py b/src/docx/text/parfmt.py index 328eb7def..a4d408ec5 100644 --- a/src/docx/text/parfmt.py +++ b/src/docx/text/parfmt.py @@ -6,20 +6,18 @@ class ParagraphFormat(ElementProxy): - """ - Provides access to paragraph formatting such as justification, - indentation, line spacing, space before and after, and widow/orphan - control. - """ + """Provides access to paragraph formatting such as justification, indentation, line + spacing, space before and after, and widow/orphan control.""" __slots__ = ("_tab_stops",) @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: @@ -33,12 +31,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: @@ -52,10 +50,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: @@ -68,12 +66,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: @@ -86,11 +84,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: @@ -104,15 +103,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: @@ -135,12 +134,12 @@ 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: @@ -164,10 +163,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: @@ -180,11 +179,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: @@ -198,13 +198,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: @@ -217,13 +216,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: @@ -236,20 +234,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: @@ -262,12 +257,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 @@ -277,11 +272,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 index 72f49a069..05adcc24c 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -8,11 +8,11 @@ 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 + """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. """ @@ -21,9 +21,9 @@ def __init__(self, r, 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 + """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`. """ @@ -42,16 +42,15 @@ def add_break(self, break_type=WD_BREAK.LINE): br.clear = clear def add_picture(self, image_path_or_stream, width=None, height=None): - """ - Return an |InlineShape| instance containing the image identified by + """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 + + `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) @@ -59,17 +58,15 @@ def add_picture(self, image_path_or_stream, width=None, height=None): return InlineShape(inline) def add_tab(self): - """ - Add a ```` element at the end of the run, which Word - interprets as a tab character. - """ + """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 + """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) @@ -77,8 +74,9 @@ def add_text(self, text): @property def bold(self): - """ - Read/write. Causes the text of the run to appear in bold. + """Read/write. + + Causes the text of the run to appear in bold. """ return self.font.bold @@ -87,26 +85,24 @@ 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. + """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. - """ + """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. + """Read/write tri-state value. + + When |True|, causes the text of the run to appear in italics. """ return self.font.italic @@ -116,12 +112,12 @@ def italic(self, 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. + """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) @@ -133,21 +129,18 @@ def style(self, style_or_name): @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. + """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 @@ -157,16 +150,16 @@ def text(self, 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. + """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 @@ -176,9 +169,7 @@ def underline(self, value): class _Text(object): - """ - Proxy object wrapping ```` element. - """ + """Proxy object wrapping ```` element.""" def __init__(self, t_elm): super(_Text, self).__init__() diff --git a/src/docx/text/tabstops.py b/src/docx/text/tabstops.py index 96cf44751..5906989a1 100644 --- a/src/docx/text/tabstops.py +++ b/src/docx/text/tabstops.py @@ -5,12 +5,12 @@ 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. + """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" @@ -20,9 +20,7 @@ def __init__(self, element): self._pPr = element def __delitem__(self, idx): - """ - Remove the tab at offset `idx` in this sequence. - """ + """Remove the tab at offset `idx` in this sequence.""" tabs = self._pPr.tabs try: tabs.remove(tabs[idx]) @@ -33,9 +31,7 @@ def __delitem__(self, idx): self._pPr.remove(tabs) def __getitem__(self, idx): - """ - Enables list-style access by index. - """ + """Enables list-style access by index.""" tabs = self._pPr.tabs if tabs is None: raise IndexError("TabStops object is empty") @@ -43,10 +39,8 @@ def __getitem__(self, idx): return TabStop(tab) def __iter__(self): - """ - Generate a TabStop object for each of the w:tab elements, in XML - document order. - """ + """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: @@ -61,30 +55,28 @@ def __len__(self): 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`. + """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. - """ + """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. + """An individual tab stop applying to a paragraph or style. + + Accessed using list semantics on its containing |TabStops| object. """ __slots__ = "_tab" @@ -95,9 +87,10 @@ def __init__(self, element): @property def alignment(self): - """ - A member of :ref:`WdTabAlignment` specifying the alignment setting - for this tab stop. Read/write. + """A member of :ref:`WdTabAlignment` specifying the alignment setting for this + tab stop. + + Read/write. """ return self._tab.val @@ -107,10 +100,10 @@ def alignment(self, 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`. + """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 @@ -121,10 +114,10 @@ def leader(self, 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. + """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 From 5cd150e06fd187c24bd18b86cf9202d021418927 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 27 Sep 2023 15:52:24 -0700 Subject: [PATCH 017/131] rfctr: bulk test-layout updates Back in the day I thought it better to extract a test-specific fixture for each test. That had the benefit of making parameterized tests fold cleanly without the parameters in the decorator. The downside was you had to look in two places to understand the test so I dropped that practice. Fix some of these I need to change or add to later. --- src/docx/oxml/text/run.py | 4 ++- src/docx/text/run.py | 25 +++++++------- tests/oxml/text/test_run.py | 32 +++++++++-------- tests/text/test_paragraph.py | 38 ++++++++++----------- tests/text/test_run.py | 66 ++++++++++++++++++------------------ tests/unitutil/cxml.py | 4 +-- 6 files changed, 86 insertions(+), 83 deletions(-) diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 9d930aad0..0f7261979 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -1,5 +1,7 @@ """Custom element classes related to text runs (CT_R).""" +from __future__ import annotations + from docx.oxml.ns import qn from docx.oxml.simpletypes import ST_BrClear, ST_BrType from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne @@ -18,7 +20,7 @@ class CT_R(BaseOxmlElement): tab = ZeroOrMore("w:tab") drawing = ZeroOrMore("w:drawing") - def add_t(self, text): + 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): diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 05adcc24c..062f58332 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -128,18 +128,19 @@ def style(self, style_or_name): 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 + 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 diff --git a/tests/oxml/text/test_run.py b/tests/oxml/text/test_run.py index db5ae727a..69aefe8ed 100644 --- a/tests/oxml/text/test_run.py +++ b/tests/oxml/text/test_run.py @@ -1,20 +1,20 @@ """Test suite for the docx.oxml.text.run module.""" +from typing import cast + import pytest -from ...unitutil.cxml import element, xml +from docx.oxml.text.run import CT_R +from ...unitutil.cxml import element, xml -class DescribeCT_R(object): - 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 ------------------------------------------------------- +class DescribeCT_R: + """Unit-test suite for the CT_R (run, ) element.""" - @pytest.fixture( - params=[ + @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 "'), ( @@ -22,10 +22,14 @@ def it_can_add_a_t_preserving_edge_whitespace(self, add_t_fixture): "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) + 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) - return r, text, expected_xml + + r.add_t(text) + + assert r.xml == expected_xml diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index 29778c284..598d74e77 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -34,9 +34,23 @@ 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", "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"), + ], + ) + def it_knows_the_text_it_contains(self, p_cxml: str, expected_value: str): + 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 @@ -223,24 +237,6 @@ 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) diff --git a/tests/text/test_run.py b/tests/text/test_run.py index dd546f005..4762c6248 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -1,9 +1,14 @@ """Test suite for the docx.text.run module.""" +from __future__ import annotations + +from typing import cast + import pytest 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 @@ -64,9 +69,23 @@ 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: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 it_can_add_a_break(self, break_type: WD_BREAK, expected_cxml: str): + run = Run(element("w:r"), None) + 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): @@ -91,8 +110,18 @@ def it_can_remove_its_content_but_keep_formatting(self, clear_fixture): assert run._r.xml == expected_xml assert _run is run - def it_knows_the_text_it_contains(self, text_get_fixture): - run, expected_text = text_get_fixture + @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)', "\nabcdef\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): @@ -102,22 +131,6 @@ 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) @@ -247,19 +260,6 @@ 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"'), diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index 9c687df67..bccbd2b83 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -28,13 +28,13 @@ # ==================================================================== -def element(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): +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 From ae6592bd5bddf0a807d9bfd54e3f6f60fa29df38 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 29 Sep 2023 19:25:12 -0700 Subject: [PATCH 018/131] rfctr: bulk remove __slots__ --- src/docx/dml/color.py | 2 -- src/docx/document.py | 2 -- src/docx/settings.py | 2 -- src/docx/shared.py | 2 -- src/docx/styles/latent.py | 4 ---- src/docx/styles/style.py | 10 ---------- src/docx/styles/styles.py | 2 -- src/docx/text/font.py | 2 -- src/docx/text/parfmt.py | 2 -- src/docx/text/tabstops.py | 4 ---- 10 files changed, 32 deletions(-) diff --git a/src/docx/dml/color.py b/src/docx/dml/color.py index ec0a0f3e0..d7ee0a21c 100644 --- a/src/docx/dml/color.py +++ b/src/docx/dml/color.py @@ -9,8 +9,6 @@ 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) diff --git a/src/docx/document.py b/src/docx/document.py index 1baca5643..07751f155 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -15,8 +15,6 @@ class Document(ElementProxy): a document. """ - __slots__ = ("_part", "__body") - def __init__(self, element, part): super(Document, self).__init__(element) self._part = part diff --git a/src/docx/settings.py b/src/docx/settings.py index 3485e76d3..78f816e87 100644 --- a/src/docx/settings.py +++ b/src/docx/settings.py @@ -9,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/src/docx/shared.py b/src/docx/shared.py index 062669724..b4878d6f8 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -165,8 +165,6 @@ class ElementProxy(object): 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 diff --git a/src/docx/styles/latent.py b/src/docx/styles/latent.py index fdcbbb6e7..c9db62f82 100644 --- a/src/docx/styles/latent.py +++ b/src/docx/styles/latent.py @@ -9,8 +9,6 @@ class LatentStyles(ElementProxy): 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) @@ -118,8 +116,6 @@ class _LatentStyle(ElementProxy): `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 diff --git a/src/docx/styles/style.py b/src/docx/styles/style.py index 0f46b67f5..233ee0d37 100644 --- a/src/docx/styles/style.py +++ b/src/docx/styles/style.py @@ -27,8 +27,6 @@ class BaseStyle(ElementProxy): These properties and methods are inherited by all style objects. """ - __slots__ = () - @property def builtin(self): """Read-only. @@ -162,8 +160,6 @@ class _CharacterStyle(BaseStyle): 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 @@ -192,8 +188,6 @@ class _ParagraphStyle(_CharacterStyle): as indentation and line-spacing. """ - __slots__ = () - def __repr__(self): return "_ParagraphStyle('%s') id: %s" % (self.name, id(self)) @@ -233,8 +227,6 @@ class _TableStyle(_ParagraphStyle): as special table formatting properties. """ - __slots__ = () - def __repr__(self): return "_TableStyle('%s') id: %s" % (self.name, id(self)) @@ -244,5 +236,3 @@ class _NumberingStyle(BaseStyle): Not yet implemented. """ - - __slots__ = () diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py index 8583e9ede..c9b8d47fb 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -15,8 +15,6 @@ class Styles(ElementProxy): and dictionary-style access by style name. """ - __slots__ = () - def __contains__(self, name): """Enables `in` operator on style name.""" internal_name = BabelFish.ui2internal(name) diff --git a/src/docx/text/font.py b/src/docx/text/font.py index 8329966ed..7a15eb7c1 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -8,8 +8,6 @@ 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. diff --git a/src/docx/text/parfmt.py b/src/docx/text/parfmt.py index a4d408ec5..ea374373b 100644 --- a/src/docx/text/parfmt.py +++ b/src/docx/text/parfmt.py @@ -9,8 +9,6 @@ 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",) - @property def alignment(self): """A member of the :ref:`WdParagraphAlignment` enumeration specifying the diff --git a/src/docx/text/tabstops.py b/src/docx/text/tabstops.py index 5906989a1..824085d2b 100644 --- a/src/docx/text/tabstops.py +++ b/src/docx/text/tabstops.py @@ -13,8 +13,6 @@ class TabStops(ElementProxy): to be constructed directly. """ - __slots__ = "_pPr" - def __init__(self, element): super(TabStops, self).__init__(element, None) self._pPr = element @@ -79,8 +77,6 @@ class TabStop(ElementProxy): Accessed using list semantics on its containing |TabStops| object. """ - __slots__ = "_tab" - def __init__(self, element): super(TabStop, self).__init__(element, None) self._tab = element From 99e9a0e0a741d53bac466357904e054a08e159b8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 29 Sep 2023 19:29:46 -0700 Subject: [PATCH 019/131] rfctr: modernize lazyproperty The old @lazyproperty implementation worked, but was much messier than the modern version, adding a member to the object __dict__. This later version that stores the value in the descriptor is much more tidy and more performant too I expect. --- src/docx/shared.py | 129 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 15 deletions(-) diff --git a/src/docx/shared.py b/src/docx/shared.py index b4878d6f8..19375f53e 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -1,5 +1,10 @@ """Objects shared by docx modules.""" +from __future__ import annotations + +import functools +from typing import Any, Callable, Generic, TypeVar, cast + class Length(int): """Base class for length constructor classes Inches, Cm, Mm, Px, and Emu. @@ -126,24 +131,118 @@ def from_string(cls, rgb_hex_str): return cls(r, g, b) -def lazyproperty(f): - """@lazyprop decorator. +T = TypeVar("T") - 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 +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) - return property(get_prop_value, doc=docstring) + @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): From 08ab7e6313df3db664bf83076ab5f1708e3acbf5 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 26 Sep 2023 15:22:31 -0700 Subject: [PATCH 020/131] rfctr: improve typing --- pyrightconfig.json | 19 +++++++++ requirements-test.txt | 1 + src/docx/api.py | 5 ++- src/docx/opc/oxml.py | 4 +- src/docx/opc/rel.py | 20 ++++++---- src/docx/oxml/__init__.py | 2 + src/docx/oxml/coreprops.py | 17 +++++---- src/docx/oxml/ns.py | 68 +++++++++++++++++++-------------- src/docx/oxml/shape.py | 2 + src/docx/oxml/text/paragraph.py | 35 ++++++++++------- src/docx/oxml/text/parfmt.py | 19 +++++---- src/docx/oxml/text/run.py | 3 +- src/docx/oxml/xmlchemy.py | 24 +++++++++--- src/docx/shared.py | 7 +++- src/docx/text/run.py | 4 +- 15 files changed, 149 insertions(+), 81 deletions(-) create mode 100644 pyrightconfig.json diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..4690b8cb8 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,19 @@ +{ + "exclude": [ + "**/__pycache__", + "**/.*" + ], + "ignore": [ + ], + "include": [ + "src/docx/", + "tests" + ], + "pythonPlatform": "All", + "pythonVersion": "3.7", + "reportImportCycles": true, + "reportUnnecessaryCast": true, + "typeCheckingMode": "strict", + "useLibraryCodeForTypes": false, + "verboseOutput": true +} diff --git a/requirements-test.txt b/requirements-test.txt index 85d9f6ba3..868eea2c9 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,3 +3,4 @@ behave>=1.2.3 pyparsing>=2.0.1 pytest>=2.5 ruff +typing-extensions diff --git a/src/docx/api.py b/src/docx/api.py index ab37ed608..a17c1dad4 100644 --- a/src/docx/api.py +++ b/src/docx/api.py @@ -3,13 +3,16 @@ Provides a syntactically more convenient API for interacting with the OpcPackage graph. """ +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): +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. diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index dfbad8d06..570dcf413 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -27,8 +27,8 @@ # =========================================================================== -def parse_xml(text): - """``etree.fromstring()`` replacement that uses oxml parser.""" +def parse_xml(text: str) -> etree._Element: # pyright: ignore[reportPrivateUsage] + """`etree.fromstring()` replacement that uses oxml parser.""" return etree.fromstring(text, oxml_parser) diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index 58acfc6e3..3155e2c6b 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -1,17 +1,23 @@ """Relationship-related objects.""" -from .oxml import CT_Relationships +from __future__ import annotations +from typing import Any, Dict -class Relationships(dict): +from docx.opc.oxml import CT_Relationships + + +class Relationships(Dict[str, "_Relationship"]): """Collection object for |_Relationship| instances, having list semantics.""" - def __init__(self, baseURI): + 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): + 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 @@ -105,7 +111,7 @@ def _next_rId(self): class _Relationship(object): """Value object for relationship to part.""" - def __init__(self, rId, reltype, target, baseURI, external=False): + def __init__(self, rId: str, reltype, target, baseURI, external=False): super(_Relationship, self).__init__() self._rId = rId self._reltype = reltype @@ -135,7 +141,7 @@ def target_part(self): 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/oxml/__init__.py b/src/docx/oxml/__init__.py index b8278b3e4..e1ac4d6bd 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -3,6 +3,8 @@ This including registering custom element classes corresponding to Open XML elements. """ +from __future__ import annotations + from lxml import etree from docx.oxml.ns import NamespacePrefixedTag, nsmap diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index ddb9f703c..940d4d3f2 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -2,6 +2,7 @@ import re from datetime import datetime, timedelta +from typing import Any from docx.oxml import parse_xml from docx.oxml.ns import nsdecls, qn @@ -47,23 +48,23 @@ def author_text(self): return self._text_of_element("creator") @author_text.setter - def author_text(self, value): + def author_text(self, value: str): self._set_element_text("creator", value) @property - def category_text(self): + def category_text(self) -> str: return self._text_of_element("category") @category_text.setter - def category_text(self, value): + def category_text(self, value: str): self._set_element_text("category", value) @property - def comments_text(self): + def comments_text(self) -> str: return self._text_of_element("description") @comments_text.setter - def comments_text(self, value): + def comments_text(self, value: str): self._set_element_text("description", value) @property @@ -71,7 +72,7 @@ def contentStatus_text(self): return self._text_of_element("contentStatus") @contentStatus_text.setter - def contentStatus_text(self, value): + def contentStatus_text(self, value: str): self._set_element_text("contentStatus", value) @property @@ -264,7 +265,7 @@ def _set_element_datetime(self, prop_name, value): element.set(qn("xsi:type"), "dcterms:W3CDTF") del self.attrib[qn("xsi:foo")] - def _set_element_text(self, prop_name, value): + 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) @@ -275,7 +276,7 @@ def _set_element_text(self, prop_name, value): element = self._get_or_add(prop_name) element.text = value - def _text_of_element(self, property_name): + 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. diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py index dd8745634..3238864e9 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -1,5 +1,9 @@ """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", @@ -25,74 +29,80 @@ class NamespacePrefixedTag(str): """Value object that knows the semantics of an XML tag having a namespace prefix.""" - def __new__(cls, nstag, *args): + def __new__(cls, nstag: str, *args: Any): return super(NamespacePrefixedTag, cls).__new__(cls, nstag) - def __init__(self, nstag): + def __init__(self, nstag: str): self._pfx, self._local_part = nstag.split(":") self._ns_uri = nsmap[self._pfx] @property - def clark_name(self): + def clark_name(self) -> str: return "{%s}%s" % (self._ns_uri, self._local_part) @classmethod - def from_clark_name(cls, clark_name): + 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): - """Return the local part of the tag as a string. + def local_part(self) -> str: + """The local part of this tag. - E.g. 'foobar' is returned for tag 'f:foobar'. + 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'}). + def nsmap(self) -> Dict[str, str]: + """Single-member dict mapping prefix of this tag to it's namespace name. - This is handy for passing to xpath calls and other uses. + 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): - """Return the string namespace prefix for the tag, e.g. 'f' is returned for tag - 'f:foobar'.""" + 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): - """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.""" + 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): - """Return a string containing a namespace declaration for each of the namespace - prefix strings, e.g. 'p', 'ct', passed as `prefixes`.""" +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): - """Return a dict containing the subset namespace prefix mappings specified by - `nspfxs`. +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'). + 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): - """Stands for "qualified name", a utility function to turn a namespace prefixed tag - name into a Clark-notation qualified tag name for lxml. +def qn(tag: str) -> str: + """Stands for "qualified name". - For - example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``. + 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] diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index f26179fbb..fc79ce52e 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -1,5 +1,7 @@ """Custom element classes for shape-related elements like ``.""" +from __future__ import annotations + from docx.oxml import parse_xml from docx.oxml.ns import nsdecls from docx.oxml.simpletypes import ( diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index e50914c2c..244107311 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -1,27 +1,34 @@ """Custom element classes related to paragraphs (CT_P).""" -from docx.oxml.ns import qn +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, List + from docx.oxml.xmlchemy import BaseOxmlElement, OxmlElement, ZeroOrMore, ZeroOrOne +if TYPE_CHECKING: + from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + 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.""" - pPr = ZeroOrOne("w:pPr") - r = ZeroOrMore("w:r") + get_or_add_pPr: Callable[[], CT_PPr] + r_lst: List[CT_R] - def _insert_pPr(self, pPr): - self.insert(0, pPr) - return pPr + pPr: CT_PPr | None = ZeroOrOne("w:pPr") # pyright: ignore[reportGeneralTypeIssues] + r = ZeroOrMore("w:r") - def add_p_before(self): + def add_p_before(self) -> CT_P: """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): + 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: @@ -29,15 +36,13 @@ def alignment(self): return pPr.jc_val @alignment.setter - def alignment(self, value): + 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[:]: - if child.tag == qn("w:pPr"): - continue + for child in self.xpath("./*[not(self::w:pPr)]"): self.remove(child) def set_sectPr(self, sectPr): @@ -47,7 +52,7 @@ def set_sectPr(self, sectPr): pPr._insert_sectPr(sectPr) @property - def style(self): + def style(self) -> str | None: """String contained in `w:val` attribute of `./w:pPr/w:pStyle` grandchild. |None| if not present. @@ -61,3 +66,7 @@ def style(self): def style(self, style): pPr = self.get_or_add_pPr() pPr.style = style + + 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 index 255a71f36..086012599 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -1,20 +1,22 @@ """Custom element classes related to paragraph properties (CT_PPr).""" -from ...enum.text import ( +from __future__ import annotations + +from docx.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 ( +from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure +from docx.oxml.xmlchemy import ( BaseOxmlElement, OneOrMore, OptionalAttribute, RequiredAttribute, ZeroOrOne, ) +from docx.shared import Length class CT_Ind(BaseOxmlElement): @@ -148,12 +150,9 @@ def ind_right(self, value): 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 + 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): diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 0f7261979..aedede812 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -4,6 +4,7 @@ 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 # ------------------------------------------------------------------------------------ @@ -85,7 +86,7 @@ def text(self, text): self.clear_content() _RunContentAppender.append_to_run_from_text(self, text) - def _insert_rPr(self, rPr): + def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr: self.insert(0, rPr) return rPr diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 57a095730..e4f4cf177 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -1,8 +1,12 @@ """Enabling declarative definition of lxml custom element classes.""" +from __future__ import annotations + import re +from typing import Any, Callable, List from lxml import etree +from lxml.etree import ElementBase from docx.oxml import OxmlElement from docx.oxml.exceptions import InvalidXmlError @@ -615,6 +619,12 @@ class _OxmlElementBase(etree.ElementBase): __metaclass__ = MetaOxmlElement + append: Callable[[ElementBase], None] + find: Callable[[str], ElementBase | None] + findall: Callable[[str], List[ElementBase]] + remove: Callable[[ElementBase], None] + tag: str + def __repr__(self): return "<%s '<%s>' at 0x%0x>" % ( self.__class__.__name__, @@ -622,7 +632,7 @@ def __repr__(self): id(self), ) - def first_child_found_in(self, *tagnames): + 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)) @@ -630,7 +640,7 @@ def first_child_found_in(self, *tagnames): 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) @@ -638,7 +648,7 @@ def insert_element_before(self, elm, *tagnames): self.append(elm) return elm - def remove_all(self, *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)) @@ -646,14 +656,16 @@ def remove_all(self, *tagnames): self.remove(child) @property - def xml(self): + 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): + def xpath( # pyright: ignore[reportIncompatibleMethodOverride] + self, xpath_str: str + ) -> Any: """Override of `lxml` _Element.xpath() method. Provides standard Open XML namespace mapping (`nsmap`) in centralized location. @@ -661,7 +673,7 @@ def xpath(self, xpath_str): return super(BaseOxmlElement, self).xpath(xpath_str, namespaces=nsmap) @property - def _nsptag(self): + def _nsptag(self) -> str: return NamespacePrefixedTag.from_clark_name(self.tag) diff --git a/src/docx/shared.py b/src/docx/shared.py index 19375f53e..c24e1ac8a 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -3,7 +3,10 @@ from __future__ import annotations import functools -from typing import Any, Callable, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast + +if TYPE_CHECKING: + from docx.oxml.xmlchemy import BaseOxmlElement class Length(int): @@ -264,7 +267,7 @@ class ElementProxy(object): common type of class in python-docx other than custom element (oxml) classes. """ - def __init__(self, element, parent=None): + def __init__(self, element: "BaseOxmlElement", parent=None): self._element = element self._parent = parent diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 062f58332..06ccd2e54 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -20,7 +20,7 @@ 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): + 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 @@ -170,7 +170,7 @@ def underline(self, value): class _Text(object): - """Proxy object wrapping ```` element.""" + """Proxy object wrapping `` element.""" def __init__(self, t_elm): super(_Text, self).__init__() From 8280771a138744bc9a9c86bd6ead688143c4f8d1 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 12:35:30 -0700 Subject: [PATCH 021/131] rfctr: add type-stubs for behave --- features/steps/helpers.py | 20 ++++++++------------ pyrightconfig.json | 2 ++ typings/behave/__init__.pyi | 18 ++++++++++++++++++ typings/behave/runner.pyi | 3 +++ 4 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 typings/behave/__init__.pyi create mode 100644 typings/behave/runner.pyi diff --git a/features/steps/helpers.py b/features/steps/helpers.py index 59495681f..fc40697b2 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -3,15 +3,15 @@ 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} @@ -24,15 +24,11 @@ def absjoin(*paths): } -def test_docx(name): - """ - Return the absolute path to test .docx file with root name `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 - """ +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/pyrightconfig.json b/pyrightconfig.json index 4690b8cb8..7808af007 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -13,6 +13,8 @@ "pythonVersion": "3.7", "reportImportCycles": true, "reportUnnecessaryCast": true, + "reportUnnecessaryTypeIgnoreComment": true, + "stubPath": "./typings", "typeCheckingMode": "strict", "useLibraryCodeForTypes": false, "verboseOutput": true diff --git a/typings/behave/__init__.pyi b/typings/behave/__init__.pyi new file mode 100644 index 000000000..efc9b36ad --- /dev/null +++ b/typings/behave/__init__.pyi @@ -0,0 +1,18 @@ +from __future__ import annotations + +from typing import Callable + +from typing_extensions import Concatenate, ParamSpec, TypeAlias + +from .runner import Context + +_P = ParamSpec("_P") + +_ArgsStep: TypeAlias = Callable[Concatenate[Context, _P], None] +_NoArgsStep: TypeAlias = Callable[[Context], None] + +_Step: TypeAlias = _NoArgsStep | _ArgsStep[str] + +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): ... From 160e7090c7f5032a315e71f8486c94ce4eb898d1 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 29 Sep 2023 17:57:34 -0700 Subject: [PATCH 022/131] rfctr: rename private classes now used for typing Also for example `BaseStoryPart` -> `StoryPart` for the same reason. --- src/docx/parts/document.py | 4 ++-- src/docx/parts/hdrftr.py | 6 +++--- src/docx/parts/story.py | 4 ++-- src/docx/styles/style.py | 18 +++++++++++----- tests/parts/test_story.py | 24 ++++++++++----------- tests/styles/test_style.py | 44 +++++++++++++++++++------------------- 6 files changed, 54 insertions(+), 46 deletions(-) diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index fb72a8b4c..c877e5a57 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -5,13 +5,13 @@ 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 -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 diff --git a/src/docx/parts/hdrftr.py b/src/docx/parts/hdrftr.py index 1a4522dcf..f7effa61a 100644 --- a/src/docx/parts/hdrftr.py +++ b/src/docx/parts/hdrftr.py @@ -4,10 +4,10 @@ from docx.opc.constants import CONTENT_TYPE as CT from docx.oxml import parse_xml -from docx.parts.story import BaseStoryPart +from docx.parts.story import StoryPart -class FooterPart(BaseStoryPart): +class FooterPart(StoryPart): """Definition of a section footer.""" @classmethod @@ -29,7 +29,7 @@ def _default_footer_xml(cls): return xml_bytes -class HeaderPart(BaseStoryPart): +class HeaderPart(StoryPart): """Definition of a section header.""" @classmethod diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py index fa3cacf0a..41d365c49 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -1,4 +1,4 @@ -"""|BaseStoryPart| and related objects.""" +"""|StoryPart| and related objects.""" from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.part import XmlPart @@ -6,7 +6,7 @@ from docx.shared import lazyproperty -class BaseStoryPart(XmlPart): +class StoryPart(XmlPart): """Base class for story parts. A story part is one that can contain textual content, such as the document-part and diff --git a/src/docx/styles/style.py b/src/docx/styles/style.py index 233ee0d37..6ca4ba8df 100644 --- a/src/docx/styles/style.py +++ b/src/docx/styles/style.py @@ -11,8 +11,8 @@ 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.PARAGRAPH: ParagraphStyle, + WD_STYLE_TYPE.CHARACTER: CharacterStyle, WD_STYLE_TYPE.TABLE: _TableStyle, WD_STYLE_TYPE.LIST: _NumberingStyle, }[style_elm.type] @@ -153,7 +153,7 @@ def unhide_when_used(self, value): self._element.unhideWhenUsed_val = value -class _CharacterStyle(BaseStyle): +class CharacterStyle(BaseStyle): """A character style. A character style is applied to a |Run| object and primarily provides character- @@ -181,7 +181,11 @@ def font(self): return Font(self._element) -class _ParagraphStyle(_CharacterStyle): +# -- 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 @@ -220,7 +224,11 @@ def paragraph_format(self): return ParagraphFormat(self._element) -class _TableStyle(_ParagraphStyle): +# -- 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 diff --git a/tests/parts/test_story.py b/tests/parts/test_story.py index 5fb461fe1..50bbf0953 100644 --- a/tests/parts/test_story.py +++ b/tests/parts/test_story.py @@ -8,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 @@ -16,12 +16,12 @@ from ..unitutil.mock import instance_mock, method_mock, property_mock -class DescribeBaseStoryPart(object): +class DescribeStoryPart(object): 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") @@ -37,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) @@ -50,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) @@ -63,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) @@ -73,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 @@ -81,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 @@ -115,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): @@ -131,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): @@ -139,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/styles/test_style.py b/tests/styles/test_style.py index 113da3339..ffd9baf22 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -5,10 +5,10 @@ from docx.enum.style import WD_STYLE_TYPE from docx.styles.style import ( BaseStyle, + CharacterStyle, + ParagraphStyle, StyleFactory, - _CharacterStyle, _NumberingStyle, - _ParagraphStyle, _TableStyle, ) from docx.text.font import Font @@ -32,9 +32,9 @@ def factory_fixture( self, request, paragraph_style_, - _ParagraphStyle_, + ParagraphStyle_, character_style_, - _CharacterStyle_, + CharacterStyle_, table_style_, _TableStyle_, numbering_style_, @@ -42,8 +42,8 @@ def factory_fixture( ): type_attr_val = request.param StyleCls_, style_mock = { - "paragraph": (_ParagraphStyle_, paragraph_style_), - "character": (_CharacterStyle_, character_style_), + "paragraph": (ParagraphStyle_, paragraph_style_), + "character": (CharacterStyle_, character_style_), "table": (_TableStyle_, table_style_), "numbering": (_NumberingStyle_, numbering_style_), }[request.param] @@ -54,24 +54,24 @@ def factory_fixture( # 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_): @@ -396,7 +396,7 @@ def unhide_set_fixture(self, request): return style, value, expected_xml -class Describe_CharacterStyle(object): +class DescribeCharacterStyle(object): def it_knows_which_style_it_is_based_on(self, base_get_fixture): style, StyleFactory_, StyleFactory_calls, base_style_ = base_get_fixture base_style = style.base_style @@ -427,7 +427,7 @@ def it_provides_access_to_its_font(self, font_fixture): 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)] @@ -446,7 +446,7 @@ def base_get_fixture(self, request, StyleFactory_): ) 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) @@ -454,7 +454,7 @@ 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 --------------------------------------------- @@ -476,7 +476,7 @@ def StyleFactory_(self, request): return function_mock(request, "docx.styles.style.StyleFactory") -class Describe_ParagraphStyle(object): +class DescribeParagraphStyle(object): def it_knows_its_next_paragraph_style(self, next_get_fixture): style, expected_value = next_get_fixture assert style.next_paragraph_style == expected_value @@ -515,8 +515,8 @@ def next_get_fixture(self, request): 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) - next_style = _ParagraphStyle(next_style_elm) if style_name == "H1" else style + style = ParagraphStyle(style_elm) + next_style = ParagraphStyle(next_style_elm) if style_name == "H1" else style return style, next_style @pytest.fixture( @@ -534,18 +534,18 @@ def next_set_fixture(self, request): "w:style{w:type=paragraph,w:styleId=B})" ) style_elms = {"H": styles[0], "B": styles[1]} - style = _ParagraphStyle(style_elms[style_name]) + style = ParagraphStyle(style_elms[style_name]) next_style = ( None if next_style_name is None - else _ParagraphStyle(style_elms[next_style_name]) + 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 --------------------------------------------- From 5c1b2f01f4645fc780fbad094969c250fb80b7cf Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 27 Sep 2023 12:40:00 -0700 Subject: [PATCH 023/131] rfctr: extract oxml.parser This removes some import cycles. --- features/steps/text.py | 2 +- src/docx/opc/part.py | 2 +- src/docx/oxml/__init__.py | 50 +-------------------------- src/docx/oxml/coreprops.py | 2 +- src/docx/oxml/numbering.py | 2 +- src/docx/oxml/parser.py | 60 +++++++++++++++++++++++++++++++++ src/docx/oxml/shape.py | 2 +- src/docx/oxml/shared.py | 2 +- src/docx/oxml/table.py | 2 +- src/docx/oxml/text/font.py | 2 +- src/docx/oxml/text/paragraph.py | 3 +- src/docx/oxml/xmlchemy.py | 2 +- src/docx/parts/hdrftr.py | 2 +- src/docx/parts/settings.py | 2 +- src/docx/parts/styles.py | 2 +- tests/opc/test_coreprops.py | 2 +- tests/oxml/test__init__.py | 2 +- tests/oxml/test_table.py | 2 +- tests/oxml/test_xmlchemy.py | 2 +- tests/test_table.py | 2 +- tests/unitdata.py | 2 +- tests/unitutil/cxml.py | 2 +- 22 files changed, 82 insertions(+), 69 deletions(-) create mode 100644 src/docx/oxml/parser.py diff --git a/features/steps/text.py b/features/steps/text.py index 3eb5b0fee..99b3ba0f0 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -6,8 +6,8 @@ from docx import Document from docx.enum.text import WD_BREAK, WD_UNDERLINE -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls, qn +from docx.oxml.parser import parse_xml from docx.text.font import Font from docx.text.run import Run diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index 1dd2c4b29..e8cf4a3d3 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -4,7 +4,7 @@ from docx.opc.packuri import PackURI from docx.opc.rel import Relationships from docx.opc.shared import cls_method_fn, lazyproperty -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml class Part(object): diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index e1ac4d6bd..8f53b5d99 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -5,59 +5,11 @@ from __future__ import annotations -from lxml import etree - -from docx.oxml.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) - +from docx.oxml.parser import register_element_cls # =========================================================================== # custom element class mappings # =========================================================================== - from .shared import CT_DecimalNumber, CT_OnOff, CT_String # noqa register_element_cls("w:evenAndOddHeaders", CT_OnOff) diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 940d4d3f2..2cafcd960 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -4,8 +4,8 @@ from datetime import datetime, timedelta from typing import Any -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls, qn +from docx.oxml.parser import parse_xml from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne diff --git a/src/docx/oxml/numbering.py b/src/docx/oxml/numbering.py index 99ca7a74d..3512de655 100644 --- a/src/docx/oxml/numbering.py +++ b/src/docx/oxml/numbering.py @@ -1,6 +1,6 @@ """Custom element classes related to the numbering part.""" -from docx.oxml import OxmlElement +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 ( 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/shape.py b/src/docx/oxml/shape.py index fc79ce52e..f9ae2d9af 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -2,8 +2,8 @@ from __future__ import annotations -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls +from docx.oxml.parser import parse_xml from docx.oxml.simpletypes import ( ST_Coordinate, ST_DrawingElementId, diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index b8a79550c..d2b2aa24a 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -1,7 +1,7 @@ """Objects shared by modules in the docx.oxml subpackage.""" -from docx.oxml import OxmlElement 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 diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index e86214a62..cefc545bf 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -2,8 +2,8 @@ from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE from docx.exceptions import InvalidSpanError -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls, qn +from docx.oxml.parser import parse_xml from docx.oxml.simpletypes import ( ST_Merge, ST_TblLayoutType, diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 45b2335e0..78b395d03 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -2,8 +2,8 @@ from docx.enum.dml import MSO_THEME_COLOR from docx.enum.text import WD_COLOR, WD_UNDERLINE -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls, qn +from docx.oxml.parser import parse_xml from docx.oxml.simpletypes import ( ST_HexColor, ST_HpsMeasure, diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index 244107311..9405edd39 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Callable, List -from docx.oxml.xmlchemy import BaseOxmlElement, OxmlElement, ZeroOrMore, ZeroOrOne +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 diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index e4f4cf177..d608bd5d7 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -8,9 +8,9 @@ from lxml import etree from lxml.etree import ElementBase -from docx.oxml import OxmlElement from docx.oxml.exceptions import InvalidXmlError from docx.oxml.ns import NamespacePrefixedTag, nsmap, qn +from docx.oxml.parser import OxmlElement from docx.shared import lazyproperty diff --git a/src/docx/parts/hdrftr.py b/src/docx/parts/hdrftr.py index f7effa61a..46821d780 100644 --- a/src/docx/parts/hdrftr.py +++ b/src/docx/parts/hdrftr.py @@ -3,7 +3,7 @@ import os from docx.opc.constants import CONTENT_TYPE as CT -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml from docx.parts.story import StoryPart diff --git a/src/docx/parts/settings.py b/src/docx/parts/settings.py index 08d310124..d83c9d5ca 100644 --- a/src/docx/parts/settings.py +++ b/src/docx/parts/settings.py @@ -5,7 +5,7 @@ from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.packuri import PackURI from docx.opc.part import XmlPart -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml from docx.settings import Settings diff --git a/src/docx/parts/styles.py b/src/docx/parts/styles.py index 5f42f31c7..9016163ab 100644 --- a/src/docx/parts/styles.py +++ b/src/docx/parts/styles.py @@ -5,7 +5,7 @@ from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.packuri import PackURI from docx.opc.part import XmlPart -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml from docx.styles.styles import Styles diff --git a/tests/opc/test_coreprops.py b/tests/opc/test_coreprops.py index a5e08bed4..73b9a1280 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -5,7 +5,7 @@ import pytest from docx.opc.coreprops import CoreProperties -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml class DescribeCoreProperties(object): diff --git a/tests/oxml/test__init__.py b/tests/oxml/test__init__.py index 6a5a2f901..9331780b0 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -3,8 +3,8 @@ 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 diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 519151be4..2cddf925b 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -3,7 +3,7 @@ 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 diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index 0cb769551..3242223d7 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -2,9 +2,9 @@ import pytest -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, diff --git a/tests/test_table.py b/tests/test_table.py index 11823117f..e5f8a3c31 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -9,7 +9,7 @@ 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 diff --git a/tests/unitdata.py b/tests/unitdata.py index 057b54490..c894c4796 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -1,7 +1,7 @@ """Shared code for unit test data builders.""" -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls +from docx.oxml.parser import parse_xml class BaseBuilder(object): diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index bccbd2b83..b88e67bd4 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -20,8 +20,8 @@ stringEnd, ) -from docx.oxml import parse_xml from docx.oxml.ns import nsmap +from docx.oxml.parser import parse_xml # ==================================================================== # api functions From f07823c0867cf8f71890277ee1a0784b7a20921e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 27 Sep 2023 14:08:13 -0700 Subject: [PATCH 024/131] spike: rejigger xmlchemy Metaclasses are different and somewhat better in Python 3. Modernize the `xmlchemy` custom element metaclass mechanism and remove Python 2 work-arounds that are no longer required. --- src/docx/oxml/xmlchemy.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index d608bd5d7..5d5050a82 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -3,14 +3,13 @@ from __future__ import annotations import re -from typing import Any, Callable, List +from typing import Any, Callable, Dict, List, Tuple from lxml import etree from lxml.etree import ElementBase from docx.oxml.exceptions import InvalidXmlError from docx.oxml.ns import NamespacePrefixedTag, nsmap, qn -from docx.oxml.parser import OxmlElement from docx.shared import lazyproperty @@ -84,7 +83,11 @@ def _parse_line(cls, line): class MetaOxmlElement(type): """Metaclass for BaseOxmlElement.""" - def __init__(cls, clsname, bases, clsdict): + def __new__(cls, clsname: str, bases: Tuple[type, ...], clsdict: Dict[str, Any]): + bases = (*bases, etree.ElementBase) + return super().__new__(cls, clsname, bases, clsdict) + + def __init__(cls, clsname: str, bases: Tuple[type, ...], clsdict: Dict[str, Any]): dispatchable = ( OneAndOnlyOne, OneOrMore, @@ -326,8 +329,8 @@ def _add_to_class(self, 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) @@ -609,21 +612,20 @@ def _remove_choice_group_method_name(self): return "_remove_%s" % self._prop_name -class _OxmlElementBase(etree.ElementBase): +class BaseOxmlElement(metaclass=MetaOxmlElement): """Effective base class for all custom element classes. - Adds standardized behavior to all classes in one place. Actual inheritance is from - BaseOxmlElement below, needed to manage Python 2-3 metaclass declaration - compatibility. + Adds standardized behavior to all classes in one place. """ - __metaclass__ = MetaOxmlElement - append: Callable[[ElementBase], None] find: Callable[[str], ElementBase | None] findall: Callable[[str], List[ElementBase]] - remove: Callable[[ElementBase], None] + getparent: Callable[[], BaseOxmlElement] + insert: Callable[[int, BaseOxmlElement], None] + remove: Callable[[BaseOxmlElement], None] tag: str + text: str | None def __repr__(self): return "<%s '<%s>' at 0x%0x>" % ( @@ -670,13 +672,8 @@ def xpath( # pyright: ignore[reportIncompatibleMethodOverride] 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) -> str: return NamespacePrefixedTag.from_clark_name(self.tag) - - -BaseOxmlElement = MetaOxmlElement( - "BaseOxmlElement", (etree.ElementBase,), dict(_OxmlElementBase.__dict__) -) From 4935c302c440eaf97a412a51e5d1311e8ce4687f Mon Sep 17 00:00:00 2001 From: yunshi Date: Mon, 14 Mar 2016 21:10:40 +0100 Subject: [PATCH 025/131] docs: document hyperlink analysis --- docs/conf.py | 2 + docs/dev/analysis/features/text/hyperlink.rst | 352 ++++++++++++++++++ docs/dev/analysis/features/text/index.rst | 1 + 3 files changed, 355 insertions(+) create mode 100644 docs/dev/analysis/features/text/hyperlink.rst diff --git a/docs/conf.py b/docs/conf.py index ceea98093..0190aba5d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -115,6 +115,8 @@ .. |HeaderPart| replace:: :class:`.HeaderPart` +.. |Hyperlink| replace:: :class:`.Hyperlink` + .. |ImageParts| replace:: :class:`.ImageParts` .. |Inches| replace:: :class:`.Inches` 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 From fd54be19a09bf394a6ee4471631fe81e4218e05f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 13:11:51 -0700 Subject: [PATCH 026/131] acpt: add Run inner-content scenarios --- features/run-access-content.feature | 9 ---- features/run-access-inner-content.feature | 28 ++++++++++ features/run-add-content.feature | 2 +- .../test_files/par-rendered-page-breaks.docx | Bin 0 -> 12244 bytes features/steps/text.py | 48 +++++++++++++++--- 5 files changed, 69 insertions(+), 18 deletions(-) delete mode 100644 features/run-access-content.feature create mode 100644 features/run-access-inner-content.feature create mode 100644 features/steps/test_files/par-rendered-page-breaks.docx 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..d09d382bb --- /dev/null +++ b/features/run-access-inner-content.feature @@ -0,0 +1,28 @@ +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 + + + @wip + 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 | + + + @wip + 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 + + + @wip + 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/steps/test_files/par-rendered-page-breaks.docx b/features/steps/test_files/par-rendered-page-breaks.docx new file mode 100644 index 0000000000000000000000000000000000000000..c3c678ec446f62ced553172688d71b8d841ae8ea GIT binary patch literal 12244 zcmeHt1y^0k()PjKgS)#02?Tcy?(XgoToM8VC%C&qa3{FCySux4zLU(%olM^Oe!;za zt*&*tc0awFrfOH$vrA424Ez-U5&#VV0EhvrITKbIAOHXn1OR{rfCkkRvbAwCwsF!` za1mSzWGBCY9+8dijrP03FAgsyf62&ajSP$6>@nmaT!pMbybag^RA7W%IHTC&4nBcbrr+jg|J(t9})9;JH7Q9^)A=_r+oH zlOz6FI1UzHf}CnN)R7aR6RSLJUk^#1i+*89nWvJLTSIvcoy;QM+LdxHdG*5ry)Yw4 z8IyU{3*OiZ+lyJD1~Z7vlUnbEPfKT- z&x={I9k3h;Zu#`RyrKvP0jXeB)6OYR(6i$PJ#b@Fu#3j_0~w*e1et@qjuCUd_?@A> zc;2f@b@YNdCJd~d!iVjY>sPkxbG)Ie`s~|wT=_!&;jlF|q!FuTk{c#6L2sj)td~xx z&OfmQQUse>b?e`T8h`a#LUYZBp(xnu+y!{2rV03L-F>UmVXB0;gDfi{(n2mqh} zE$gRU{c2IlN-zMhr$$WUAML#>8~_CT8faO-(-Z(u6E9`E$&UQt6ykvp8viCvE{f28 zdJfL&)AG(($DEZ9;N5r>P7D8p%93uYP=6GmB)qx=QUE8#?xvki&M99jba?I1oM68j z@^S!(K~z!omwE2H-QL+`^$%Z3Uz2M|up>DSL8fE2zBuSo6lUSJ=LmiH!cNVW3f18F zwta3Q1;)TuL?Eov;@GATRjp(e!nj_EqT3$Je_9+@B(bJ^l~C9w2;qj~VIm?9N5Que zDx7&Z$o^=%EwD5|`pnjK2BI5`->!q>AWKv=J8w>N8YB$rV_OlT`RmZDf%Uvjar=0u zH=(QtQ6`<@sZOZN;HpUy30kzIfneiCYRH#O<8J^nUFgNWm^M&Db_;sq#OY9K;2QJ- zp%cY0E@}la=kg13Q`~b{iAUGA8eX>U3WukyI0%!wPc;LbEu2b}y#e-T{!#3VPMYfm zR0tH8kd@6&fhB7Ki>X;@+ZQSx?giRzfE#9?Q=fLrK}NLfaxr1ZjA@uWV1i;)2@hl za8G$tmvQNF@Z8qap0eyvQ{rg&X7n3U#1hjYVhA7-i-ReN0H)7{TlPR*>4?Bb{bMCv zm~Irv4@G#sf=|N6?qT@Q=1BC64KQ6;omPA=(T+fZ_5Qlrb+CNQK_C|K;{?!YyIvje6Xjo@M?0pAZl&iPQ3!?j>&``DmtQ(-k4>|ipNKlFUU zm%m#&<6yAtLZPS@>t4bbi^>b-y}|!sRKvbhGA?Tx5ylwH_i!32{%Y<;Su~E3gnECcg77j-a>5f6x=N7+YO88dcX!U40%#I65y%#HI-k@&F z@x7kxFSa7trraYW8*B9i??os(?l_K{rWl%)7+Y%5iedZ}rUKMWYU$L7TDfr7+-dJ zMQB{(^6ut}knZClPw=L_+B9@1B@5Q(q`waek|V4R>3F@4hB8TDRXM8a04F)(WJ*bd ziKFOR+vOTVX;+QHiTDtEDe7o#Zm?#;@@}%O$7=*1_*U%I7c_CrS~@(4L$S$Dn$y?? zlY_g^q_9rb7D}139Ts#|YUO>`Zrqeff{F%y6|~ew&!|ANT%eSLG^nPtMJ=Ic z+b0r=E-j)41I`QC!dyI6JUuNLYQB*P-rIJwV<+zA`;e4d$Em+!d=^Vpi0Mgz$ZDyO z#Wc58p69cr=A6JUmQcg}1?_B;ms6g=K#gY3%uV_7oF+M*mJ>x0&J50p=dyqAweMQF z&7LqvAB<&l&qMBUN-GExTg0LBw^SSYT~ol^ZM#CT6w2aW$@BtR8pId;k;@aM79}%V_X~EE@l#%Bvua7)$1sK@lVXMZMZMc%i*yz#Nno z6w6y=CAcebpN+HrjIkG)hRo zv8D-%6nw@T79{+pk9lF28EXfbA97=w>H#HBB)EhuhNC^H=i^CHfnN)A4?LHhwZhGs zH3)qhMAO?wO@%umRMOb)Z{SNo8>n{ijx#$Q=9A%Z%2yp!Fv}g?1%ZpbcYWmFSkP8a zsQk6^7~)a`W2Tl2ksa`&{HStn?}&*JE4gCgo*JdR+Q|uNoIYo<=gGuh?PtWiQ_Qkt z4X6**h81j*YME~iTS#e`vyIMvjamfCf+^J@Y10%e2HPtWW=9K#tVY!N{pa z6X+z4CAR!Qd_Sv!x`+I|OUC>9&X&O*du-}gJWj-Ah9e<;*4}eLb}4V)=Se7ad6OX# zi!d*24ithjss^SB;n`I(E!9y|`#&Bnw1{*@Nlt=~bCIsoz;p~=bC?K8!(R?amxY^O zRQBKtt&YI+we}#CT+G)YEvH_mTFq;&fvXM=u^))k&)+PqlVqHFyR7tN8%6L7aAce& zUq0?UrQ42#9Na%?UR1*+)81`mSJ=>fC@1&2)vN;#%X6l zQ|<5vO=d>b@)A^bLSBE>H5KY7XYy4jNP%H}yn~L*?GS;3FGkx^hJXenY?=Y-=tA;b zpT*SBB-1S;68enl+x|m76 z&im|{Qrkql6sL!k-zIw?^GQ9Snp;;LY_zl+J7jiQUAb6hW*43zNZem(QB5>%z{*&d zF7ZIumc5#Aq!%E@qAn8>TeHww;D1jY5C{LcOBGHfZ_+9v;uQ*%yj>`y71<5*-g5ao zx*SaE%2rYubf0w!XFNOczB>aed9ru z0JiPiM&Zw)3U)GK8J0wF1*%ST*UAM+{shHgs$)*dqT4($2?vmA@54jT{i zU7Y@r@bp}i?6UR@kXAJF8`d$G7>ORidM{uz`Ujh&p#ZA=}11f*(}MH}F; z(u;A-hv`a71XHR~Rn1hN%l?hP62bx9gS15TZ~*4VoFmte7mv>|W}LW&kxr5Een~8j zCLAdteBSN`PUW2nDD9LwPAKkRLsIf6Es3tWgcvV#xKv@T{sn8U8~EiHyxK1hgS}p* z?1_By1TpQ;?s$aKhc=w3hV8QsD+P)DnONQFjs5FGOi@vfud3 z1D4)KEk`g^7*6dUXP=?ZF2kwg)9aw6F&E@gRVbYChc*IWPVp%j1H)Sw+gw~kq#$iGDCNF)M=n|g0a(&CqQPXUg5bcr*FN@Kc#&^ZPNFSpKSQQ*zrEg6ENZTC>2ysN#}XT+>=%Dy6NUA?tH zfxb}<2WpxjpMR_9QAEgmn)w(4H_dbZ@3YjeW3gxcZ@B&rU_1Wp+t4z3Q-g zG?XVQz);vO<5@k@XBk(#Ss=r*O~gWj_^=(*QpXyMM^iXxEJkW5H`2)UWZKBI}vd9VBnp$IaINom7 zVwKMrgsTz5;Lr{0z=Vnt6Po&QHMKFtpHWv@z(PHE!%+oLX}3jlwBnyTp zlQitRaUr;=z$zZx^choN#kER}PtiBV7udip6$(pCsH2viLOVK4zoIqm){jc#M<{DK z${AKnPxC6x)H}+k7pgt`%CPe?8%vEI8k0ujOR=)r*V=q)5jzlD$kHVIemTyvB8Y;3 zJh>!1G%=tI);n@8!T`-h&8nil`v{6=*jV5j*Hz_QzsyyD4EvFBP&9cn-TTVwcw?D3 z?0F2xm>fSo!QJ}o$TEmrgNWi$o)^KXV{{guyf6$KuBGUS5w4IT`l55?Z|)fcBD8zX zkvK;VPvBCps=}S4ZAnV-Jnh*56~{R;$qU05(zAM^4c;Si#9@sRsN5{IJm=bUCggG8 z2QKgRL|XS57A)nn^kApUU)6j;d>l5A2aRX2&A47O+V1Bd=rNzEZT+Z8`3BzlO!=L= zNL^U=m3U-rA7A#K`&&57NWLT==;vZ2iGzsf!?Dqw?0r{uS+Px#8{66Cllb#(AK*LU z@5J>l4xbi0&%v z`_p;Pua0DlR_G@KXtA@`ut*~$ih8tU46gJ!I4np<)ku2D6r;M$M&REbHhaH+dq^Rv zu_@5Zsx4p`qmk_5wK^69V-T)9LPmy`3sEsPji!tIY@b9ZPwrrFA85KX@{GJmQDk&Q zOhY5VVH_VU0}(wl{g`B&QII3yW)o*oB{XFs8R1w$#R7RlSE(S2%%J*OpqiMBV|t*V zi=fk_2%VFf3aQ*O=qQ3eJB`1dcr3?LOEz2G{7$YR&rppgu}7h22;!SiTE=o6S|Ka8 zaR#^<_gw0(U&yLKrOq~j?M$q2TLCW$>2Q$LmM~2xXy0-aohU~aip?6DE6I|d&mL(j zmbhXT0i84B4##Hylh%{RSv9-*Qi}o zG$i9Rs2Gm1s_2~eT!N|LhmiQHBB>)5@I(9Y;OydBt+%BOIfV{%;wH94OFB2Y2IGumb_)nXb$OZb&_@Ut`W^aid5l(WnY?{KTV6WqQ7mD zXWzbpxTQPsVL0)jCm_JMU3GEW>u@Vjb~BsBtXmaQn9oeBT<13Z9+dw;wrTh3_-vxh zt)M=8fig1D-q6Bre<@P!R>2BonRyE0Q8fW@59u{ys^ma$(y##cEb z)mfXUHs76nNmi$G!J2|1yLm;+gN>;n;52ntUwXB8_`Ih=JHpP+F6SqC`tiE5rA6b) z7^xVuPhF-GmE0kybRs$B+XR*~F8`q60nvbo6pVG8u?BASzSm+Hp1x_7?)}>3igbuU zX@vA!vCP_Q#FIX26a#{TcfLd{vKl_&Gl~|C!iWjvNCAvUB@wGbTRVh)E93?i zmuek&x~y^vywm~M0fc(a*i6q*L#eh5EIVSI;@ga$*I%1B8@NW4Pr_>7*N4X((FBs| zW|O7jtaZB9e~m+|AeQ_7bftITe?Q51P5G}r?|evv=?~n?W#C>S14&RIPbKGIYv;(Q zZ)f+zRiTWXsL*$eZAFNU}5T`>`jBdLS&y#F(@#x*C|Bzpp56xGe@ z>6DIT_nSvnN12_iA@j9^?xYQD!-7e6w0FO<{^v zeOg3W5+!6Z2`eOmiFe*kaI80?>qjXOM;{MQSP`RrU1P7;a$3`c%a~5rI3#oM5V|fU znlFd{S-D*66=V9Psz_{nRjbqLyN_2KPG+A_u%Mg}= zLC|bJ&_i(F6`=AJexye~5<~GHdPQWL1gDowP%6Zv5iC7^!KWdb?wKCqQ|2AwMlg2+ zJ%&g>kY8}wwI5&&*IJ_)N(X@*Vh~6Y3gu!6Rq_h*wwmYEUjq6F|3h<~Kz-2Z`=}PP z{Fw2)({4se+DdPt-ERbw+IvsKDZ>KWr1kTU0=pFmf+IIFqjfM=SY|LHBD5u?Dgf^Z z*+myL{m*uR`v$y1qvU0QA6!4IzI#HFS{3Wf6OCg=W#*PJ02STTv_>kh(w-y;efIbk zhG6P?R^oEI9Di8Te-vZ7f)X0*pA>aYMMOZQ16^3Fcu}i!7tvMK7o(@1sV7NOZWOn& zLZ;Q*#B^o>ivUTkHkud{){qv3pA}AB;|axCMJh(6y7Y>|bWgAUK!q3T9h70DW3*|NuaeG#v}+NjSY^c9<3kZa@6kg+L_vX(AS?p??7qd zR4lScV9>SoL{e7A($~Hva!3FShE46QRJ=z*4IfG9&h?lJ13y=)325bve{C0YZ7QA} zu-CcOH1AA+oVF;wbW&E_BzC!b?VF>IEb7g4-r4^ae&ZVzqVK4Cj=*=3gmkF8)^l&O zBk<$YfY#T~{~D{%d>nH#eq?2i000v3fAKR`IXbyn8UM`7no>0#7aLJ}SGgun+r||i zzo=Cja`ofwhL+Xj+iy5-v5q3cs22zbZj;wO?!$eE0e6E5hO>E&QX{$jJa|Ib=8O4! z^XN#Et;@~mHy|S#eY+{&Ri#Ew;=zIaT;J+`YyS$Jd=QmFxkpx%u?g$>WxLGh{&?R} zzC~O|80QOm`sAHaHPztuZgVq<&RGol>2#Fo!q z45P>6=J|L*`<54#1urIZtDJr8#nsc_Hy~?N7%<`XQ%pD2xt-J;h>rCg zuNbL)2t>@E?ste4Ig=&cO=zR-&xl}Hy|DRvjkiY@-1&~GlMi-y4xL`?7+&_jxa3w= z2Bz^=kzj1-sEVQ~X0FfXs}qYpK%0uVX+ZA~G+DK<^kVLGsSMUrrIw8flZblcjcv3( ztT9r^iarvmtfnMOT6s~b-;_Uuo1S`0FAs3&4zu1GbzQhjDjL?$R+qf2nx8v77xEq| z-|o##M{<`y~>iZ zqrNz{R}n(Uvaf!(HwJ6HCs9$(`_9Y3Tg?zH;&6|5nHw3&o5Vcnu^Rf3)l}6$v7YVw zC@j1A!`ET&_C#!{tbR+s5?e!M z0m9WHqL6zZ46;f;zF9%PZomOUxLBB3xR@2NNdX%Tu)&yxQ;v<}$>L>mrZI`wNuoby zjltZ2$yU2Kx{RXeFD)wyV!7PHBlj%xtbMk3i8uwSI^BS(J$#@_ZL{H8fwpn{_257| zO){;rNzs;kfBNT1wLEq9_N?_piHKdYFtWBOQcV^Vq`dAS?-hJvm7MiUH* zbjKyxZJZ`44shAS2$R&ZB#3O9;U@W5;?_y-TpZtT^3nNS^r&>P`aF$>)B*Vdb+mGG z27W2K3SvZg7@$;G=SzPEz7%r%>X5@seYC;W;*^7HH~ZK1N_QTbNR=`H)G39cyGRqP z7H{y$u&rVk~-|EKgS@4)g5|x^Hs%(Bsc&sq1F}WVVh*oRY z84o>^?C#EVE;TkszjW4PvtEGX!JsY=eM&y1N1*Je%6)KyDEoqw7Dbb;8dVCW%3n#K z1=8Or*H1GHJAk1{p#Owf;}<8icPbZ7 zg${A8$i$4~K6>D|Ic&!as*aa>p9T(+>tVi_L};Kg)LP=jM*Cb~t53s+RO*0pWl2ls z2HwoDqlMQvu_Ge!9kW)AYQdLwq#7~nQYD@(O;WU$jKt?lVZ2$qT654XvmPzm(3cmT z>~^nP?Zyjp(I`c|p|4tosJ*@)_x?OjFDo2dN&`!n$MJU0){4}9(Diyq>hrk?d4;jt z+$3t8TlJztmE=k#_lY^o;M3gX#DUXbr4$k-osSiQ-)8BZ`mSr*8rMm1R*k)7h4#nl zS}VBr%~Gm`C`l)r-W>~&D;d7wyW_{8UfT%y$aLSx==2FqRd3<=wwJ9|zr=awPkHpV zUbkI`8;D0FjkA6CVz~!ik0o$2&x8k!<8b+3*W_t;Grs#5m&o_5Ml^rM<;%LQ-xRwd z`0x~{LmD}Y$3}Kf-P}t7R1heDMJdP$Ti2&++m{1Jmj7+&JL*)b^b_4|d zUscQfsjBgwV$v!YBcN0=BLqTVP-t@gSlXWt`njI}yF?2{3`As_>RuT1h0oCF3W9#H z)c;xAACMni$$~a+@xNEEeLk<1b+v55*M92memZyRvtLv=m41@F#Bh+oXQ52XSNy#S z+EkK9v;>KOV9qm}PQXx0;V>NbCU}O+se`o$%}N2ODYrJ@s=#zD&y{UvC`* z9U@Uar+4C|j?aFj_kGs|>0@m84K`P3axYf~XW`e`V+1$Z9a;!C3ge{#;9rZ0K@Kg% zp&i)T{J!C2`D!D`>z6`)vo})}0-c%u^5H!j8F&MZUI$R$UK&uqq}o{s4;Q zO29$v!MqMs>!}JUB^0-Z0~Y>(=xHOs2$Vu5*a7wB#l$p2(?t2g-+n-TK9RU@8i00U z5eVzdCj7o(;e^ww0cTsk7q1T55#AoH(l8J=1;6uyReuHIP#m71UM;v+%>^ferbFru z<<)gs`q*rkH|RjR4*W`m_p60YZ{VZt=M|ERJN*wR^|ZqVIN+oP-_{h2Rsr8bP<~AJyuLnwi{qTYU{D zn+N~6Y5wq}@MfxMYtfB_vQXDy0dO4jrvD*6A+(}Ue(LG1@?}kltD{II!#GYy)`mX= zt&Ps;7Il%^@WBHo@nh8VEQH5Ow*_rwP}21LyIGruVqV+gy0?egNmR!LRbd+=yhUKk zs_n924ldJjv!)b>tBjLAOGZJE+-$9Z%CoF)CTx;~e7g!Ol@C@mSLrnOOHI43!h~9C|-Pt`cU}>=#!HOs={hB=# zJ>`7FgH2yct5Gt-UZ!6vG<)yQskCbqP&U1H-59L_$x*bjP(rP>iywx>h%}0OV_=OX zHDXnK+)qiW5_>cJ-I4fue6YWJx?9A9u=Itd@H4IW|J1Jin27-F2T}HK^+{Mi zrvm0KfSiwTFn`YU&uS%zKdO~}t5o`Zfxj0<{Jq3|_&=xpy-ecwDSrQq_4gFsXn& abc - + def ghi - jkl + + mno + + pqr + + stu """ % nsdecls( "w" ) @@ -79,6 +85,14 @@ def given_a_run_having_style(context, style): context.run = document.paragraphs[0].runs[run_idx] +@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() @@ -143,7 +157,7 @@ def when_I_add_text_to_the_run(context): @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") @@ -203,6 +217,13 @@ def then_type_is_page_break(context): 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") def then_run_font_is_the_Font_object_for_the_run(context): run, font = context.run, context.run.font @@ -210,6 +231,15 @@ def then_run_font_is_the_Font_object_for_the_run(context): assert font.element is run.element +@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] @@ -217,6 +247,13 @@ def then_run_style_is_style(context, style_name): 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") def then_last_item_in_run_is_a_break(context): run = context.run @@ -291,8 +328,3 @@ def then_the_tab_appears_at_the_end_of_the_run(context): r = context.run._r 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 From 45bf74bf48bdf806f47d0fab5f9ea84326f4dc43 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 14:24:54 -0700 Subject: [PATCH 027/131] rfctr: add types to Run and its tests --- src/docx/oxml/text/run.py | 26 ++++++----- src/docx/text/run.py | 44 +++++++++++------- src/docx/types.py | 19 ++++++++ tests/text/test_run.py | 9 ++-- tests/unitutil/file.py | 14 +++--- tests/unitutil/mock.py | 94 ++++++++++++++++++++++++++++----------- 6 files changed, 145 insertions(+), 61 deletions(-) create mode 100644 src/docx/types.py diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index aedede812..74a4e3065 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Callable + from docx.oxml.ns import qn from docx.oxml.simpletypes import ST_BrClear, ST_BrType from docx.oxml.text.font import CT_RPr @@ -14,12 +16,16 @@ class CT_R(BaseOxmlElement): """`` element, containing the properties and text for a run.""" + add_br: Callable[[], CT_Br] + get_or_add_rPr: Callable[[], CT_RPr] + _add_t: Callable[..., CT_Text] + rPr = ZeroOrOne("w:rPr") - t = ZeroOrMore("w:t") br = ZeroOrMore("w:br") cr = ZeroOrMore("w:cr") - tab = ZeroOrMore("w:tab") 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`.""" @@ -44,7 +50,7 @@ def clear_content(self): self.remove(child) @property - def style(self): + def style(self) -> str | None: """String contained in `w:val` attribute of `w:rStyle` grandchild. |None| if that element is not present. @@ -64,7 +70,7 @@ def style(self, style): rPr.style = style @property - def text(self): + def text(self) -> str: """The textual content of this run. Inner-content child elements like `w:tab` are translated to their text @@ -82,7 +88,7 @@ def text(self): return text @text.setter - def text(self, text): + def text(self, text: str): self.clear_content() _RunContentAppender.append_to_run_from_text(self, text) @@ -98,7 +104,7 @@ def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr: class CT_Br(BaseOxmlElement): """`` element, indicating a line, page, or column break in a run.""" - type = OptionalAttribute("w:type", ST_BrType) + type = OptionalAttribute("w:type", ST_BrType, default="textWrapping") clear = OptionalAttribute("w:clear", ST_BrClear) @@ -119,23 +125,23 @@ class _RunContentAppender(object): appended. """ - def __init__(self, r): + def __init__(self, r: CT_R): self._r = r self._bfr = [] @classmethod - def append_to_run_from_text(cls, r, text): + 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): + 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): + 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 diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 06ccd2e54..10acdbf92 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -1,14 +1,21 @@ """Run-related proxy objects for python-docx, Run in particular.""" +from __future__ import annotations + +from typing import IO + +from docx import types as t from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_BREAK +from docx.oxml.text.run import CT_R, CT_Text from docx.shape import InlineShape -from docx.shared import Parented +from docx.shared import Length, Parented +from docx.styles.style import CharacterStyle from docx.text.font import Font class Run(Parented): - """Proxy object wrapping ```` element. + """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 @@ -16,11 +23,11 @@ class Run(Parented): the style hierarchy. """ - def __init__(self, r, parent): + def __init__(self, r: CT_R, parent: t.StoryChild): super(Run, self).__init__(parent) self._r = self._element = self.element = r - def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): + def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): # pyright: ignore """Add a break element of `break_type` to this run. `break_type` can take the values `WD_BREAK.LINE`, `WD_BREAK.PAGE`, and @@ -41,7 +48,12 @@ def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): if clear is not None: br.clear = clear - def add_picture(self, image_path_or_stream, width=None, height=None): + def add_picture( + self, + image_path_or_stream: str | IO[bytes], + width: Length | None = None, + height: Length | None = None, + ) -> InlineShape: """Return an |InlineShape| instance containing the image identified by `image_path_or_stream`, added to the end of this run. @@ -62,7 +74,7 @@ def add_tab(self): tab character.""" self._r._add_tab() - def add_text(self, text): + def add_text(self, text: str): """Returns a newly appended |_Text| object (corresponding to a new ```` child element) to the run, containing `text`. @@ -73,7 +85,7 @@ def add_text(self, text): return _Text(t) @property - def bold(self): + def bold(self) -> bool: """Read/write. Causes the text of the run to appear in bold. @@ -81,7 +93,7 @@ def bold(self): return self.font.bold @bold.setter - def bold(self, value): + def bold(self, value: bool): self.font.bold = value def clear(self): @@ -99,7 +111,7 @@ def font(self): return Font(self._element) @property - def italic(self): + def italic(self) -> bool: """Read/write tri-state value. When |True|, causes the text of the run to appear in italics. @@ -107,11 +119,11 @@ def italic(self): return self.font.italic @italic.setter - def italic(self, value): + def italic(self, value: bool): self.font.italic = value @property - def style(self): + def style(self) -> CharacterStyle | None: """Read/write. A |_CharacterStyle| object representing the character style applied to this run. @@ -123,7 +135,7 @@ def style(self): return self.part.get_style(style_id, WD_STYLE_TYPE.CHARACTER) @style.setter - def style(self, style_or_name): + 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 @@ -146,11 +158,11 @@ def text(self) -> str: return self._r.text @text.setter - def text(self, text): + def text(self, text: str): self._r.text = text @property - def underline(self): + def underline(self) -> bool: """The underline style for this |Run|, one of |None|, |True|, |False|, or a value from :ref:`WdUnderline`. @@ -165,13 +177,13 @@ def underline(self): return self.font.underline @underline.setter - def underline(self, value): + def underline(self, value: bool): self.font.underline = value class _Text(object): """Proxy object wrapping `` element.""" - def __init__(self, t_elm): + def __init__(self, t_elm: CT_Text): super(_Text, self).__init__() self._t = t_elm 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/text/test_run.py b/tests/text/test_run.py index 4762c6248..cf0d0f711 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -75,13 +75,14 @@ def it_can_add_text(self, add_text_fixture, Text_): (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}"), + (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): - run = Run(element("w:r"), None) + r = cast(CT_R, element("w:r")) + run = Run(r, None) # pyright:ignore[reportGeneralTypeIssues] expected_xml = xml(expected_cxml) run.add_break(break_type) diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 432c8635e..795052c8e 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,28 +1,30 @@ """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")) -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 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, @@ -36,7 +38,7 @@ def snippet_seq(name, offset=0, count=1024): 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`. @@ -49,7 +51,7 @@ def snippet_text(snippet_file_name): return snippet_bytes.decode("utf-8") -def test_file(name): +def test_file(name: str): """ Return the absolute path to test file having `name`. """ diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index c2c15bd62..d0e41ce93 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,18 +1,40 @@ """Utility functions wrapping the excellent `mock` library.""" -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): +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 @@ -24,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) @@ -35,7 +63,9 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): return _patch.start() -def function_mock(request, q_function_name, autospec=True, **kwargs): +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. @@ -45,7 +75,9 @@ def function_mock(request, q_function_name, autospec=True, **kwargs): return _patch.start() -def initializer_mock(request, cls, autospec=True, **kwargs): +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. @@ -57,7 +89,13 @@ 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 @@ -69,7 +107,7 @@ def instance_mock(request, cls, name=None, spec_set=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 @@ -80,7 +118,13 @@ def loose_mock(request, name=None, **kwargs): return Mock(name=name, **kwargs) -def method_mock(request, cls, method_name, autospec=True, **kwargs): +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. @@ -90,7 +134,7 @@ 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`. """ @@ -100,7 +144,7 @@ def open_mock(request, module_name, **kwargs): 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 reversed after pytest uses it. @@ -110,7 +154,7 @@ 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`. Patch is reversed after calling test returns. From ceb8cbec430bf93541ed23c8dcad644f9683e904 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 19:51:48 -0700 Subject: [PATCH 028/131] rfctr: add types to Paragraph and its tests --- src/docx/text/paragraph.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 9ca235bdb..d2db02dfd 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -1,7 +1,17 @@ """Paragraph-related proxy types.""" +from __future__ import annotations + +from typing import List + +from typing_extensions import Self + +from docx import types as t 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.shared import Parented +from docx.styles.style import CharacterStyle, ParagraphStyle from docx.text.parfmt import ParagraphFormat from docx.text.run import Run @@ -9,18 +19,19 @@ class Paragraph(Parented): """Proxy object wrapping a `` element.""" - def __init__(self, p, parent): + def __init__(self, p: CT_P, parent: t.StoryChild): 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`. + 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. + break. When `text` is `None`, the new run is empty. """ r = self._p.add_r() run = Run(r, self) @@ -31,7 +42,7 @@ def add_run(self, text=None, style=None): return run @property - def alignment(self): + def alignment(self) -> WD_PARAGRAPH_ALIGNMENT | None: """A member of the :ref:`WdParagraphAlignment` enumeration specifying the justification setting for this paragraph. @@ -42,7 +53,7 @@ def alignment(self): return self._p.alignment @alignment.setter - def alignment(self, value): + def alignment(self, value: WD_PARAGRAPH_ALIGNMENT): self._p.alignment = value def clear(self): @@ -53,7 +64,9 @@ def clear(self): self._p.clear_content() return self - def insert_paragraph_before(self, text=None, style=None): + 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 @@ -73,13 +86,13 @@ def paragraph_format(self): return ParagraphFormat(self._element) @property - def runs(self): + 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): + def style(self) -> ParagraphStyle | None: """Read/Write. |_ParagraphStyle| object representing the style assigned to this paragraph. If @@ -92,7 +105,7 @@ def style(self): return self.part.get_style(style_id, WD_STYLE_TYPE.PARAGRAPH) @style.setter - def style(self, style_or_name): + 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 @@ -115,7 +128,7 @@ def text(self) -> str: return text @text.setter - def text(self, text): + def text(self, text: str | None): self.clear() self.add_run(text) From 2e13d5cc1341341421fd71f62bb20732a828d149 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 15:56:19 -0700 Subject: [PATCH 029/131] run: Run.text includes no-break hyphen, etc. Add additional run inner-content elements having a text equivalent, in particular `w:noBreakHyphen` and `w:ptab`. Give each of them their own custom element class having a `__str__()` method so they can each report their text content (constant in some cases like "-" for no-break hyphen). --- features/run-access-inner-content.feature | 1 - features/steps/paragraph.py | 12 ++- src/docx/enum/base.py | 15 +++- src/docx/enum/text.py | 10 ++- src/docx/oxml/__init__.py | 30 +++++--- src/docx/oxml/text/parfmt.py | 14 +++- src/docx/oxml/text/run.py | 92 ++++++++++++++++++++--- tests/oxml/text/test_run.py | 6 ++ tests/text/test_run.py | 2 +- 9 files changed, 150 insertions(+), 32 deletions(-) diff --git a/features/run-access-inner-content.feature b/features/run-access-inner-content.feature index d09d382bb..5bce127d6 100644 --- a/features/run-access-inner-content.feature +++ b/features/run-access-inner-content.feature @@ -22,7 +22,6 @@ Feature: Access run inner-content including rendered page-breaks Then run.iter_inner_content() generates the run text and rendered page-breaks - @wip 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/steps/paragraph.py b/features/steps/paragraph.py index ae827a1ab..b79f30158 100644 --- a/features/steps/paragraph.py +++ b/features/steps/paragraph.py @@ -118,9 +118,11 @@ def then_the_document_contains_four_paragraphs(context): def then_document_contains_text_I_added(context): document = Document(saved_docx_path) paragraphs = document.paragraphs - p = paragraphs[-1] - r = p.runs[0] - assert r.text == test_text + paragraph = paragraphs[-1] + run = paragraph.runs[0] + actual = run.text + expected = test_text + assert actual == expected, f"expected: {expected}, got: {actual}" @then("the paragraph alignment property value is {align_value}") @@ -153,7 +155,9 @@ def then_the_paragraph_has_the_style_I_set(context): @then("the paragraph has the text I set") def then_the_paragraph_has_the_text_I_set(context): - assert context.paragraph.text == "bar\tfoo\n" + actual = context.paragraph.text + expected = "bar\tfoo\n" + assert actual == expected, f"expected: {expected}, got: {actual}" @then("the style of the second paragraph matches the style I set") diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index 054e1e3b4..679174ac2 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -1,14 +1,21 @@ """Base classes and other objects used by enumerations.""" +from __future__ import annotations + +import enum import sys import textwrap +from typing import Callable, Type + +from docx.exceptions import InvalidXmlError -from ..exceptions import InvalidXmlError +def alias(*aliases: str) -> Callable[..., Type[enum.Enum]]: + """Adds alternate name for an enumeration. -def alias(*aliases): - """Decorating a class with @alias('FOO', 'BAR', ..) allows the class to be - referenced by each of the names provided as arguments.""" + 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 diff --git a/src/docx/enum/text.py b/src/docx/enum/text.py index 2ccbdc522..2c9d25e37 100644 --- a/src/docx/enum/text.py +++ b/src/docx/enum/text.py @@ -1,6 +1,9 @@ """Enumerations related to text in WordprocessingML files.""" -from .base import EnumMember, XmlEnumeration, XmlMappedEnumMember, alias +import enum +from typing import ClassVar + +from docx.enum.base import EnumMember, XmlEnumeration, XmlMappedEnumMember, alias @alias("WD_ALIGN_PARAGRAPH") @@ -59,6 +62,9 @@ class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): ) +WD_ALIGN_PARAGRAPH = WD_PARAGRAPH_ALIGNMENT + + class WD_BREAK_TYPE(object): """Corresponds to WdBreakType enumeration http://msdn.microsoft.com/en- us/library/office/ff195905.aspx.""" @@ -184,6 +190,8 @@ class WD_TAB_ALIGNMENT(XmlEnumeration): class WD_TAB_LEADER(XmlEnumeration): """Specifies the character to use as the leader with formatted tabs.""" + SPACES: ClassVar[enum.Enum] + __ms_name__ = "WdTabLeader" __url__ = "https://msdn.microsoft.com/en-us/library/office/ff845050.aspx" diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 8f53b5d99..4c2821c8b 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -6,10 +6,28 @@ from __future__ import annotations from docx.oxml.parser import register_element_cls +from docx.oxml.text.run import ( + CT_R, + CT_Br, + CT_Cr, + CT_NoBreakHyphen, + CT_PTab, + CT_Text, +) + +# --------------------------------------------------------------------------- +# text-related elements + +register_element_cls("w:br", CT_Br) +register_element_cls("w:cr", CT_Cr) +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) + +# --------------------------------------------------------------------------- +# other custom element class mappings -# =========================================================================== -# custom element class mappings -# =========================================================================== from .shared import CT_DecimalNumber, CT_OnOff, CT_String # noqa register_element_cls("w:evenAndOddHeaders", CT_OnOff) @@ -199,9 +217,3 @@ 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/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index 086012599..74c2504ab 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -321,12 +321,24 @@ class CT_Spacing(BaseOxmlElement): class CT_TabStop(BaseOxmlElement): - """`` element, representing an individual tab stop.""" + """`` 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.""" diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 74a4e3065..60294e7dc 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -76,16 +76,10 @@ def text(self) -> str: Inner-content child elements like `w:tab` are translated to their text 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 + return "".join( + str(e) + for e in self.xpath("w:br | w:cr | w:noBreakHyphen | w:ptab | w:t | w:tab") + ) @text.setter def text(self, text: str): @@ -104,13 +98,89 @@ def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr: class CT_Br(BaseOxmlElement): """`` element, indicating a line, page, or column break in a run.""" - type = OptionalAttribute("w:type", ST_BrType, default="textWrapping") + type: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:type", ST_BrType, default="textWrapping" + ) clear = OptionalAttribute("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 diff --git a/tests/oxml/text/test_run.py b/tests/oxml/text/test_run.py index 69aefe8ed..6aad7cd02 100644 --- a/tests/oxml/text/test_run.py +++ b/tests/oxml/text/test_run.py @@ -33,3 +33,9 @@ def it_can_add_a_t_preserving_edge_whitespace( r.add_t(text) assert r.xml == 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/text/test_run.py b/tests/text/test_run.py index cf0d0f711..80f7a09c7 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -117,7 +117,7 @@ def it_can_remove_its_content_but_keep_formatting(self, clear_fixture): ("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"), + ('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): From 2364e909429fdda7c28d1fc3053d601d2f3941e8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 16:14:30 -0700 Subject: [PATCH 030/131] run: add Run.contains_page_break --- features/run-access-inner-content.feature | 1 - src/docx/oxml/__init__.py | 2 ++ src/docx/oxml/text/pagebreak.py | 18 ++++++++++++++++++ src/docx/oxml/text/run.py | 10 +++++++++- src/docx/text/run.py | 13 +++++++++++++ tests/text/test_run.py | 17 +++++++++++++++++ 6 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/docx/oxml/text/pagebreak.py diff --git a/features/run-access-inner-content.feature b/features/run-access-inner-content.feature index 5bce127d6..6c855856b 100644 --- a/features/run-access-inner-content.feature +++ b/features/run-access-inner-content.feature @@ -4,7 +4,6 @@ Feature: Access run inner-content including rendered page-breaks I need to access differentiated run content in document order - @wip Scenario Outline: Run.contains_page_break reports presence of page-break Given a run having rendered page breaks Then run.contains_page_break is diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 4c2821c8b..a3f81aad1 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -6,6 +6,7 @@ from __future__ import annotations from docx.oxml.parser import register_element_cls +from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak from docx.oxml.text.run import ( CT_R, CT_Br, @@ -20,6 +21,7 @@ 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) diff --git a/src/docx/oxml/text/pagebreak.py b/src/docx/oxml/text/pagebreak.py new file mode 100644 index 000000000..201de9267 --- /dev/null +++ b/src/docx/oxml/text/pagebreak.py @@ -0,0 +1,18 @@ +"""Custom element class for rendered page-break (CT_LastRenderedPageBreak).""" + +from __future__ import annotations + +from docx.oxml.xmlchemy import BaseOxmlElement + + +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. + """ diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 60294e7dc..8a2e2da2e 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -2,13 +2,16 @@ from __future__ import annotations -from typing import Callable +from typing import TYPE_CHECKING, Callable, List 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 +if TYPE_CHECKING: + from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak + # ------------------------------------------------------------------------------------ # Run-level elements @@ -49,6 +52,11 @@ def clear_content(self): for child in content_child_elms: self.remove(child) + @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. diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 10acdbf92..49fdce8a9 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -104,6 +104,19 @@ def clear(self): 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): """The |Font| object providing access to the character formatting properties for diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 80f7a09c7..324faf2e3 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -28,6 +28,23 @@ 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 + def it_knows_its_character_style(self, style_get_fixture): run, style_id_, style_ = style_get_fixture style = run.style From 08ee10a1d787327d04791cb0adfce651c4721f29 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 17:02:32 -0700 Subject: [PATCH 031/131] run: add Run.iter_inner_content() --- features/run-access-inner-content.feature | 1 - src/docx/drawing/__init__.py | 16 ++++++ src/docx/oxml/__init__.py | 64 ++++++++++++----------- src/docx/oxml/drawing.py | 11 ++++ src/docx/oxml/text/run.py | 33 +++++++++++- src/docx/shared.py | 31 ++++++++++- src/docx/text/pagebreak.py | 29 ++++++++++ src/docx/text/run.py | 31 ++++++++++- tests/text/test_run.py | 45 +++++++++++++++- 9 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 src/docx/drawing/__init__.py create mode 100644 src/docx/oxml/drawing.py create mode 100644 src/docx/text/pagebreak.py diff --git a/features/run-access-inner-content.feature b/features/run-access-inner-content.feature index 6c855856b..a9bbb170c 100644 --- a/features/run-access-inner-content.feature +++ b/features/run-access-inner-content.feature @@ -15,7 +15,6 @@ Feature: Access run inner-content including rendered page-breaks | two | True | - @wip 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 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/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index a3f81aad1..f31c67193 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -5,7 +5,22 @@ 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_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.text.pagebreak import CT_LastRenderedPageBreak from docx.oxml.text.run import ( CT_R, @@ -16,6 +31,25 @@ 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:docPr", CT_NonVisualDrawingProps) +register_element_cls("wp:extent", CT_PositiveSize2D) +register_element_cls("wp:inline", CT_Inline) + # --------------------------------------------------------------------------- # text-related elements @@ -78,36 +112,6 @@ 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) 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/text/run.py b/src/docx/oxml/text/run.py index 8a2e2da2e..c995bfbbb 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -2,12 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, List +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.text.pagebreak import CT_LastRenderedPageBreak @@ -52,6 +54,35 @@ def clear_content(self): for child in content_child_elms: self.remove(child) + @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.""" diff --git a/src/docx/shared.py b/src/docx/shared.py index c24e1ac8a..83e064098 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools -from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, List, TypeVar, cast if TYPE_CHECKING: from docx.oxml.xmlchemy import BaseOxmlElement @@ -315,3 +315,32 @@ def __init__(self, parent): def part(self): """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/text/pagebreak.py b/src/docx/text/pagebreak.py new file mode 100644 index 000000000..e468a613a --- /dev/null +++ b/src/docx/text/pagebreak.py @@ -0,0 +1,29 @@ +"""Proxy objects related to rendered page-breaks.""" + +from __future__ import annotations + +from docx import types as t +from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak +from docx.shared import Parented + + +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". + """ + + def __init__( + self, lastRenderedPageBreak: CT_LastRenderedPageBreak, parent: t.StoryChild + ): + super().__init__(parent) + self._element = lastRenderedPageBreak + self._lastRenderedPageBreak = lastRenderedPageBreak diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 49fdce8a9..4a01aebc5 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -2,16 +2,20 @@ from __future__ import annotations -from typing import IO +from typing import IO, Iterator from docx import types as t +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.oxml.text.run import CT_R, CT_Text from docx.shape import InlineShape from docx.shared import Length, Parented from docx.styles.style import CharacterStyle from docx.text.font import Font +from docx.text.pagebreak import RenderedPageBreak class Run(Parented): @@ -135,6 +139,31 @@ def italic(self) -> bool: def italic(self, value: bool): 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 | None: """Read/write. diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 324faf2e3..558885176 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -2,14 +2,16 @@ from __future__ import annotations -from typing import cast +from typing import 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.parts.story import StoryPart from docx.shape import InlineShape from docx.text.font import Font from docx.text.run import Run @@ -19,6 +21,8 @@ class DescribeRun(object): + """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 assert getattr(run, prop_name) == expected_state @@ -45,6 +49,36 @@ def it_knows_whether_it_contains_a_page_break( 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 @@ -244,6 +278,15 @@ def clear_fixture(self, request): expected_xml = xml(expected_cxml) return run, expected_xml + @pytest.fixture + def fake_parent(self) -> t.StoryChild: + class StoryChild: + @property + def part(self) -> StoryPart: + raise NotImplementedError + + return StoryChild() + @pytest.fixture def font_fixture(self, Font_, font_): run = Run(element("w:r"), None) From 1afedd0e2dc2702e449fba5f539072935123d5a4 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 17:55:52 -0700 Subject: [PATCH 032/131] acpt: add Paragraph inner-content scenarios --- features/par-access-inner-content.feature | 48 +++++++ features/steps/paragraph.py | 133 ++++++++++++++---- features/steps/test_files/par-hyperlinks.docx | Bin 0 -> 12385 bytes 3 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 features/par-access-inner-content.feature create mode 100644 features/steps/test_files/par-hyperlinks.docx diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature new file mode 100644 index 000000000..b454f6c95 --- /dev/null +++ b/features/par-access-inner-content.feature @@ -0,0 +1,48 @@ +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 + + + @wip + 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 | + + + @wip + 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 | + + + @wip + 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 + + + @wip + 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 | diff --git a/features/steps/paragraph.py b/features/steps/paragraph.py index b79f30158..326786e29 100644 --- a/features/steps/paragraph.py +++ b/features/steps/paragraph.py @@ -1,9 +1,14 @@ """Step implementations for paragraph-related features.""" +from __future__ import annotations + +from typing import Any + from behave import given, then, when +from behave.runner import Context from docx import Document -from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.text.parfmt import ParagraphFormat from helpers import saved_docx_path, test_docx, test_text @@ -12,7 +17,7 @@ @given("a document containing three paragraphs") -def given_a_document_containing_three_paragraphs(context): +def given_a_document_containing_three_paragraphs(context: Context): document = Document() document.add_paragraph("foo") document.add_paragraph("bar") @@ -21,7 +26,7 @@ def given_a_document_containing_three_paragraphs(context): @given("a paragraph having {align_type} alignment") -def given_a_paragraph_align_type_alignment(context, align_type): +def given_a_paragraph_align_type_alignment(context: Context, align_type: str): paragraph_idx = { "inherited": 0, "left": 1, @@ -34,7 +39,7 @@ def given_a_paragraph_align_type_alignment(context, align_type): @given("a paragraph having {style_state} style") -def given_a_paragraph_having_style(context, style_state): +def given_a_paragraph_having_style(context: Context, style_state: str): paragraph_idx = { "no specified": 0, "a missing": 1, @@ -45,8 +50,30 @@ def given_a_paragraph_having_style(context, style_state): context.paragraph = document.paragraphs[paragraph_idx] +@given("a paragraph having {zero_or_more} hyperlinks") +def given_a_paragraph_having_hyperlinks(context: Context, zero_or_more: str): + paragraph_idx = { + "no": 0, + "one": 1, + "three": 2, + }[zero_or_more] + document = context.document = Document(test_docx("par-hyperlinks")) + context.paragraph = document.paragraphs[paragraph_idx] + + +@given("a paragraph having {zero_or_more} rendered page breaks") +def given_a_paragraph_having_rendered_page_breaks(context: Context, zero_or_more: str): + paragraph_idx = { + "no": 0, + "one": 1, + "two": 2, + }[zero_or_more] + document = Document(test_docx("par-rendered-page-breaks")) + context.paragraph = document.paragraphs[paragraph_idx] + + @given("a paragraph with content and formatting") -def given_a_paragraph_with_content_and_formatting(context): +def given_a_paragraph_with_content_and_formatting(context: Context): document = Document(test_docx("par-known-paragraphs")) context.paragraph = document.paragraphs[0] @@ -55,12 +82,12 @@ def given_a_paragraph_with_content_and_formatting(context): @when("I add a run to the paragraph") -def when_add_new_run_to_paragraph(context): +def when_add_new_run_to_paragraph(context: Context): context.run = context.p.add_run() @when("I assign a {style_type} to paragraph.style") -def when_I_assign_a_style_type_to_paragraph_style(context, style_type): +def when_I_assign_a_style_type_to_paragraph_style(context: Context, style_type: str): paragraph = context.paragraph style = context.style = context.document.styles["Heading 1"] style_spec = { @@ -71,34 +98,88 @@ def when_I_assign_a_style_type_to_paragraph_style(context, style_type): @when("I clear the paragraph content") -def when_I_clear_the_paragraph_content(context): +def when_I_clear_the_paragraph_content(context: Context): context.paragraph.clear() @when("I insert a paragraph above the second paragraph") -def when_I_insert_a_paragraph_above_the_second_paragraph(context): +def when_I_insert_a_paragraph_above_the_second_paragraph(context: Context): paragraph = context.document.paragraphs[1] paragraph.insert_paragraph_before("foobar", "Heading1") @when("I set the paragraph text") -def when_I_set_the_paragraph_text(context): +def when_I_set_the_paragraph_text(context: Context): context.paragraph.text = "bar\tfoo\r" # then ===================================================== +@then("paragraph.contains_page_break is {value}") +def then_paragraph_contains_page_break_is_value(context: Context, value: str): + actual_value = context.paragraph.contains_page_break + expected_value = {"True": True, "False": False}[value] + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" + + +@then("paragraph.hyperlinks contains only Hyperlink instances") +def then_paragraph_hyperlinks_contains_only_Hyperlink_instances(context: Context): + assert all( + type(item).__name__ == "Hyperlink" for item in context.paragraph.hyperlinks + ) + + +@then("paragraph.hyperlinks has length {value}") +def then_paragraph_hyperlinks_has_length(context: Context, value: str): + expected_value = int(value) + assert len(context.paragraph.hyperlinks) == expected_value + + +@then("paragraph.iter_inner_content() generates the paragraph runs and hyperlinks") +def then_paragraph_iter_inner_content_generates_runs_and_hyperlinks(context: Context): + assert [type(item).__name__ for item in context.paragraph.iter_inner_content()] == [ + "Run", + "Hyperlink", + "Run", + "Hyperlink", + "Run", + "Hyperlink", + "Run", + ] + + @then("paragraph.paragraph_format is its ParagraphFormat object") -def then_paragraph_paragraph_format_is_its_parfmt_object(context): +def then_paragraph_paragraph_format_is_its_parfmt_object(context: Context): paragraph = context.paragraph paragraph_format = paragraph.paragraph_format assert isinstance(paragraph_format, ParagraphFormat) assert paragraph_format.element is paragraph._element +@then("paragraph.rendered_page_breaks has length {value}") +def then_paragraph_rendered_page_breaks_has_length(context: Context, value: str): + actual_value = len(context.paragraph.rendered_page_breaks) + expected_value = int(value) + assert ( + actual_value == expected_value + ), f"got: {actual_value}, expected: {expected_value}" + + +@then("paragraph.rendered_page_breaks contains only RenderedPageBreak instances") +def then_paragraph_rendered_page_breaks_contains_only_RenderedPageBreak_instances( + context: Context, +): + assert all( + type(item).__name__ == "RenderedPageBreak" + for item in context.paragraph.rendered_page_breaks + ) + + @then("paragraph.style is {value_key}") -def then_paragraph_style_is_value(context, value_key): +def then_paragraph_style_is_value(context: Context, value_key: str): styles = context.document.styles expected_value = { "Normal": styles["Normal"], @@ -110,12 +191,12 @@ def then_paragraph_style_is_value(context, value_key): @then("the document contains four paragraphs") -def then_the_document_contains_four_paragraphs(context): +def then_the_document_contains_four_paragraphs(context: Context): assert len(context.document.paragraphs) == 4 @then("the document contains the text I added") -def then_document_contains_text_I_added(context): +def then_document_contains_text_I_added(context: Context): document = Document(saved_docx_path) paragraphs = document.paragraphs paragraph = paragraphs[-1] @@ -126,47 +207,49 @@ def then_document_contains_text_I_added(context): @then("the paragraph alignment property value is {align_value}") -def then_the_paragraph_alignment_prop_value_is_value(context, align_value): - expected_value = { +def then_the_paragraph_alignment_prop_value_is_value( + context: Context, align_value: str +): + expected_value: Any = { "None": None, - "WD_ALIGN_PARAGRAPH.LEFT": WD_ALIGN_PARAGRAPH.LEFT, - "WD_ALIGN_PARAGRAPH.CENTER": WD_ALIGN_PARAGRAPH.CENTER, - "WD_ALIGN_PARAGRAPH.RIGHT": WD_ALIGN_PARAGRAPH.RIGHT, + "WD_ALIGN_PARAGRAPH.LEFT": WD_PARAGRAPH_ALIGNMENT.LEFT, # pyright: ignore + "WD_ALIGN_PARAGRAPH.CENTER": WD_PARAGRAPH_ALIGNMENT.CENTER, # pyright: ignore + "WD_ALIGN_PARAGRAPH.RIGHT": WD_PARAGRAPH_ALIGNMENT.RIGHT, # pyright: ignore }[align_value] assert context.paragraph.alignment == expected_value @then("the paragraph formatting is preserved") -def then_the_paragraph_formatting_is_preserved(context): +def then_the_paragraph_formatting_is_preserved(context: Context): paragraph = context.paragraph assert paragraph.style.name == "Heading 1" @then("the paragraph has no content") -def then_the_paragraph_has_no_content(context): +def then_the_paragraph_has_no_content(context: Context): assert context.paragraph.text == "" @then("the paragraph has the style I set") -def then_the_paragraph_has_the_style_I_set(context): +def then_the_paragraph_has_the_style_I_set(context: Context): paragraph, expected_style = context.paragraph, context.style assert paragraph.style == expected_style @then("the paragraph has the text I set") -def then_the_paragraph_has_the_text_I_set(context): +def then_the_paragraph_has_the_text_I_set(context: Context): actual = context.paragraph.text expected = "bar\tfoo\n" assert actual == expected, f"expected: {expected}, got: {actual}" @then("the style of the second paragraph matches the style I set") -def then_the_style_of_the_second_paragraph_matches_the_style_I_set(context): +def then_the_style_of_the_second_paragraph_matches_the_style_I_set(context: Context): second_paragraph = context.document.paragraphs[1] assert second_paragraph.style.name == "Heading 1" @then("the text of the second paragraph matches the text I set") -def then_the_text_of_the_second_paragraph_matches_the_text_I_set(context): +def then_the_text_of_the_second_paragraph_matches_the_text_I_set(context: Context): second_paragraph = context.document.paragraphs[1] assert second_paragraph.text == "foobar" diff --git a/features/steps/test_files/par-hyperlinks.docx b/features/steps/test_files/par-hyperlinks.docx new file mode 100644 index 0000000000000000000000000000000000000000..b3a4ea27ae89e9462c09941b9004c65874d49e5b GIT binary patch literal 12385 zcmeHt1y@|l()QpQG`IyBWboj@-4fj0-66On1a~L6yM*8noZ#;61a}SoO>*wJC&|6v zFL>`>t82}!-B0hHUR7OP&n`J>DCp+^SO7c#03Zdd|=jt#oE-2_nIAoiM*yWW=IiHHZ>>p1D4u%Km;g0QiZ}joQ2r$ z9-C$Lx_KEYaT#bwBU=MUfTEL$ffQRg!{%ipL56EI=QyEe8voi)uljlLp6BjlMx1A& zT$IDY2S=il2z*@rk8-LJa0gB#POS2@z1?K_F8W2GpFEYc+!`u!>0}lNS1*+FfYrD2 z^dgK9(Zh?$*=1^3OX*M4aQWd7O+2C-3q+_i(X03X?rj-Atk?XX#7+mAFmtmPgFoWe z^N3k4Aw-)&Ww~9AY|YN6hU5<0UqG1WZ18t4f3hkYHeUrW?@st=r$|hjj=)`7X9^JY zZY|tde}@79o}Qopa=*DGaWsD2G1$&zz#fMLc1c|aV=G5Sh9B~OuK0i0|9<)Fr7`Um zolM9A#~&U(j5W*8x8vr>FdB|7;LgFpYf8SxT$r_(d3@lRorToi)fXQapNJiGcT5#~ zyBeo`f}f!LETkD>`bMWy^RCGi5bxiV$Lu<7w~m~$b!GCGsMq*B`xEGLx96rY$5iJ>ZIYMOy-h8i(<`5l#3% zC$i8-MRqJ#YJXMQi>-edZb5TNhi}Ei2uzJOC`H6SX&T#e&uJ~_3ihW%$U+Eayz?~C z!q9l!4%x!vXFn!Yuhw0&B}mm4JO>A+|MU+G#A+E1@RLvAkVOc9hj6iVFk<}6NsMd_ z-&%vC*N?FEx5GeyV;8vh|LtoqZb$}fFz=2fnuO!I-*D^Qn;kN|tA@g%Mvcm|QKSzq z`Pz1SCGw$CCM-m9>g!m<*-h-B5xefpD_5v}5Gz7xXAq1Y#a28G=2Z-Ds&-XdG*iQ< z*$aJoPIg&YSrgezm>X#ss{$z(rjZ?VdOzi|f~ z?ul~^{KkTIgYua|l0+?nI8Pq!w%wEDK6Tfa z)*T~8i)FYgHzpIPU~Bou@BrYml4CxvDnX_6Y9M&ajzR|j(7~qn)9`*ZIb|gT0F-13 zj`5H7))ff=0euNJJ@9c002ul>WV67D_U;(zfE$g6Ba338QZ-01^V#@T#TT&&FF-&x zJt-MYR5j7{F85?>8AW_NOtPZHv+a$0d#2&39;)apq8UnMhu{ZkRC0!-5XA=d^{18< z9Xh6$3H={Uq|ShRb|3h4o^E=k_(HH25E_BuR(sY>J7UW z#rBe8f`lf9^(JBCBEZ5hRYqkcf`Ke?&z<8OXBqb2kPysZB@s`)kPNlY)!_>EeqWL* zjaD9yJ=vjR8HYx&$a zP#FW#hr9X6#z=$)-dH}-@d>Rm7tIiO_CaEXK?prA(+Ti}9arn}S-a3PB(nk*0;5oV zbVO?7Y~-c5(5&2qogp@$Kj)&Z}Y z`gl^I6}5aC*DM={N@G3GOhc0u%=b(btwJFvUUC`hiI+F5x%Zr2@*`@aN7W|hmh5qA zBVpH7rR+jxWPXYj9)1hkJkx~ixbNv_S)hvIe5l*!^G(x`V?5eJ{g#2jN7jN1R<`Me)&O9gLMLWx(cSW@3g(O z=z}`UMjbzW6f{A0}ls!#Uu!G zWtyXdXik-bQ0H?Q=&X&B$(I%07_|jjO8o+H6KLk$q;;iF(U1!nzaOF~td6Rrg$Q4~g!Bh+^R3x+0!g!;1uGt#d9D z9it>IsSkz4ephM#tdLh+y_Y4?d?{BW3R#46Yd)?}qqI(Y!OyaPSl>osS+RY3 z*Gj_jrN#3bg~?73yrkRsnFUS>Fsin} zZC6`%5?;Qy$$52L`pd>A@zg~)o|Gu8mI~QSv#S;PKAURJ9|goe*6>7Oo^0@O$rBr> zfo9Fzl+RB=DT%aP=!!^YNKU-xeLF9GS1W9GL^yj9EStM;^A1y6A(+@A_np6_+0bvB z0%ot<6-uPh7k0{~<}uS@qKJmikCalT#PD=iTVDq1+SV-$U1BW1JE&j|)GaAzuiXPS zLConp>O5Vk6jW(+5JVA`53hM7POB0aH$jMoQ3g7i5smP?+K$!F77-^4+(K$A<|Ie(u8ZH3~Wf-lxaVRub=!NB8@sb2#0G)5@Ty zhk_CByGv;fIjr|1E#4PBvEm~wH}@kX#GI>|kf40gBoF}qpL7zq33%q@Cys($`Q4{Rz5>QT2S2Nq9K|CVT>Pj?)7(4Qj|*W zxP<#gX|Hx53CJlhn>}A9@nSbK?u}x$C2K%^m^PwNlXS~mbNGB}!>ny=&P$A9NERIF z4k??a5OKsFnQ;5TkluYYQ3fDtcLyVv7AVL`0#|(Loy2Z-15G#ZtxM+H`p%YtZhJhM z=e$m&pA3gXd#$}^gY8mZz0H?Y`sz)AN-D}cztLX=&8QlbE=*`w$+TEUL*svUFyA8D z86!0TJ;qJG214iEyb*j%|YG{(>mYD{xF@~IgRqL=j zGv8FbXjH&@9_xm=2dn=8%Bt0<%O;g)B7cAN#h3|UKoeQsq=t>61HzsT=b!}7CH)IW zXs|;q#`eZ0!iS>Rw{t^En9}_$PK#&LDVOq>Bl9xCL#pdomx?KUVYTszZ&G zcj1N3EU7D($V_h|GXzWcD=nyrCG=Yvi_j(Q>DsbalMMFap3LgQ=ZTMPYfXaW+D z1HY;wspL;sMMgeHhm*GpgSDc#V%}M*n8T7oNL$`aPKWQcu7av^STm_%zU!wf!5bWh zH=Qfj9ePbyZO1bFmZ{3u{l-e{dWKb0_1-N>;q}H-63r{eHSb1@oh;OX`ZkGevw;mE zQNl(!eEC+Y)kTg<0Y+%!EM=kBt?#BAV{*(_DtTTDw)+boPM{$mrpbuICj67^uI0F(XyIv+G}UQ zGTKeVQZc*r&(V7rQc~LjFa%W*S;}ZNH=JxH@meUl_6yu1;d5Z|rRd1sd6HyDo;eZ= zlIe_UzcJNs;RK`|X2<8Nt0UMxVaE-m**kyc93Gpy0a|p=G2&d`4iAd~b#cyn*Y^57 zgH;emNKM$U#h;Tb!ptq4xUv{Hi{>*`ec;tcCO*+ieU?;B-~P&Ad|bU!HXsO=Dz;+S zmG!QGihuCB3}EdcQ}4j8A27qYkmj&{JNK2#KN^{yn~Gi5o&nYhG`DUYcaELp5dySb zuLQ=qd>Kq?9%!@*7HAF_nCu(SPH-z;m*R2AhCX`z*ywv-6ZX2_=JCJg|CBaS6SU9( zKpE1HW9GktlB2PclevwlQ>;dW5o`R?tEE+6sB;CM(jE&w7Wi$+*ln!?*Hx5`=lkWH*l%usfn zFrtWjUDG)dJnS`^b{gcO4hraRFN9WP(;@;Q)5I!DbL?nnpc6fFswKNQ2ozS2GHTF1 zoh~E~x{b*V#+~jUXbB>x_7N538RtADK5I(@&OPm5cxb3M3$M1WH^MRW$97qEOu*Kx z!X#i22dS_NbbyaaNZGN+v?oj2V{-4^R#PV2Hv1OfJ8+=Q1^eslbBO}s5=?n8TEi(n zvz|$u#sXueEEN`nTqtlPs{I$f#beJ6CD^||L+p5ekMcqk3N!7oguEpn|E8OZh_Dhr z6cS{5AkuQ|icftC%NFl0aHDaGuXF_ui620ubr2=s-(jGg14;b0I|-wy(jEBpp#+aL z{vF3L-Rsa@Me^r4guU#C)*a_&`AOsOVWu^wL!o_Zn;Knn3Tj^s|n$^ujZpPvjNaB-eP)POW zo96Dl9BIxi@;Pow)mM!lCw|=2N>h5+rAVfuw9dspMCT8gc+uW721U(PFw{p*yHiWBFc#A5 zCeU#5n9glvwcLpfPQR?aS;~fczgMeWDuvdQ2Grk+kfTj}(^9K~ymf{*S#dU?^O5Ir zm&*vTeN|xr{{%jp=P4&eA3|eV-bqnL7ghRc> z;E)RO9bu2jvuB?`-fiEuiG4`-YU9#PV#VygSRP60=NAZmLz!}2W_VdBzKqy7L-Y4Mv!7d>3=Ytw;&^U-+Cuw( zkS(FHnW10t@%4l8DvBwIr^_R$%%XhL$&wR$YFK`i5vNA}lk<|bkE!kx+^-4j-(){6 zI!Uf8m{mLnKgIZaXE6a2{!01=R>nWJmdQANn+2xl-Pe%KAKG})^Ll;Ixg=8SccPHX zJOIi0blKn()rs&^-*SVnhhMfZl2e~Gve6LMtw>)v(!ysLry}UNO%G3!z6`$8ivt;> zW%`K#TD+W9T=GcC;%+S&gA095P7Cr8HL@NG#h5O$VdPi)&E9Wc?Ndr=YzQ{9Y6}|1 zX{5M#t&GMY7(^%!Q&3>$!F(Q_!qi23uumqD2Razs1eq=lKcH<;78_lVfyk=f0t{>H&o+I>Q?9;g!v+zp1D+qS;UHGoC$5lGn=;U z7rJ6lsq^ib?R30ITOl7h`B1R*rUi9l7tCzrggLaxs+_o)FEPGSgUH4}7}$>S zs#sh%+(Ky)`>;f+qG`hx$b-9y(CiXgt=HuZxkV0i5+=4UnzSy?H|n-~NR}tIWo30+ zR8rT z!*Jw7PfUz`z2f4w)8SUA>}EEDQ@0|lFqf5HxyEDq6kKpiv0?Z8@MOHrt*}04o+>)Y z-q6BrcQIP+TEQN}zrIGszb7iL7gNi$%V|9L&B4fXHNThN?uCqE`ZN5G8 zn4(VSf;$OEarK;*7Y|26&}s5}efh=0{=<$6?Jzq#yPTiY@%zilmKKc*W7HCyUUivD z44^}B`FKj|mvLNY0)YX;y%+r^(g@ZG#u^0GyIu?7g!-mcx;Lxm%dbNX$|GOD63?o= zL^Oi(WiO0G*K3bk@dT`Ek( zB@V<|yn(f2a6x7-uqf8XlMm55L(X%rM~hfNceMj*C?zZfrU6UQ5B1 z!l1-CYE$b3B8ZA4g(C47nj~uWKv@aD=)Hryf@?=t4_fIMVo}Xm$=Ddxih=#g9sQYG zdjZgQS?;}e1Z-sGb5E%*oQ)mtmmGkX*i#I}75ohZNrlyw6p2XeZYg&f}94z*Y?JD7AVXu|tlDn(byg!zIr631|2nn+g_pri#CE^9mm&%$h zp(HyFtpqn2MRp$3ZqHQ3w0M^cGtY;gw#KynSNCX`FJ!R+yT>|st3m(OJq&Ffen!Oq z`3HF2z*ktz8`+BSJ zl<<+ID$-eKQXF_mHAT`CEN-x+OrXT6Rkc*}jdp~pvXW1U769R$C+P|AUNae0W8Jzl z#l$L{2o_!E??C%1QfET@Cx{7(Fw*(b98Lw4pM}x^+40M>x7P(o$_U#U%Rju@mE-Uc z;m2kiI4W*YIxOdm;=$w*4TjmoYbIRkJ8ErtZ9Pfjt3s(uM%b3ZoR=KIwY?C2 zoLNLmUb;^#ih`-v5RJdYeRU+?F$PmrZ-@FN;YE7#@(IVb0xU}91G3vVy70#|_Jt9s z4o3pXyN5K|iS zjDGzxVzJj7@?WdefMgz=cn|fh?$`Fuv48^@l*-+*VvJ3=50BqI`P>}tI?A_5=!oD) zVP#BwH>#!{__p2LOr~=Zhjly^V|u%)^^J!1vgcl((fG!&#uKN+sZWX-Qz4Gg<8I@0 ztgwC4i`s$@hq+bGKK`t<`pSbVF}bkPfHa{i}w)-_7 zKn}F7g=L}BG81lYVI)8A`Lfn0&jE__E5sGVDm6Ajg#9GbRdrq`4JV3YeaCY~8Xsa& z^ZT1EQbn#5$v5NLn7h-W*jA5h{$6A4(S_fAN7R7>9iD^7XIqBneUC1Am6bv1d{t!F z>pH4pn2K3zGX?6TVz=<7qHY@STf|LPEi64aTVGWM>Z#K{jfjwmdE}3-x8ANYQp$?m zk*KVsrbthMs+x37G?GdmT{ zQwEb4d3K|U#eZg`u?Iz!^O;7<=VoJz0S7db&4uT6JL$>wt*9PbvV&#&s}(86qhos& z35+cJ%9Fh@RO=0yigNxFA17ZmL#(L74dHoSbTnTw^MuDr*n3t}RRhI(wxLhy^u=gL}#ghw6pCD9go}sJ6*6JR@tO@ zQ@$@Fa6&C#oxMGKElDzRnzKCTr2pGJD4(X_)(oi5SWbkU+G5wrwEAIUpxOlP+d6hgC-&w6nxR=-?80$xx z=#sGhZw0D~wQ!^1eKTd$Q%lyA6~nANzk@>7yo_IrsRMyj23|P`%meu81ZsmPlWhh8 zrm)y7Jj`~UVpF2e!V3%V@#O>AShKxLHe!ocz&EWwZY<3@?+$h1#!*g>iVeqoj*qlJ zRY1OaV#U)a=4Jpc-x8}w{ut4~obYf$Xs;D%o#7I@;Q*InpR|)Kb3vJ;(!M(Z*>>X; zSxJD)4tBVdmL+j?(=-p+`!csqpmRxLpUHdYH?bqHOVsB;8q#~fd74<|=1ih;b`_M! zif}-=h)z^rCXqB+`^uohbbYMB=E9_dYZrUzTBSQLC|c!{AjYIZ@%LyG+!k;3Gpz#S z$PbdLQc{;+F<-4$4=NK`mTz8{NxMrvzSL$=Beg28_TS;3`oxegUT4KsFX$2z9M)}{ zc&tEaBvR*F-_rVkzf_VZ$?~RW6fmNsUK|%TcL4o(NR{!BmhW5e3Sn3$NW(*_n%78w z+gC59ND@R!wriZ<3*AR(;r6u2$kKJ@7vP+De)||7gNa^1r#&5t0%^T#e<~Tt4_!h! zaECMCKZlczF~y58ILA&`JlE>R<5_r@PZpDwb*yZDO>(F(qcO1-z=&CE*O>@Eo#O7! zbSgbMOTT#1ZL?O0@4=uh4}T1t)FW1QROQ*bLXmyMPmcj*sK%5-sR~pQYeDoi%JqSU z5c{z;iS<9wM>aP5ks7)|YdR!cM`}?R4OcLMWYuuv3FyN5;jqQ_-mSUpa8|69596*(ldqLDHJ zee|FSa@h_UR2|RtKJ@RU)FVWhL~39#)LIh8$NHS%sZSw?R_Z`=XTO%p3%Z(S$Bd|P zVn;#cKV+>M(LyfoNHb#ArA|6ooS>7K^&)V;+YpsylH_EB! zW2Bt$d$uefE@b$Jz8~HN_t-|tM`!p($7YOcs(OngwmojP`X$XVf5@k|^}7DLzm9T1 z);QC9Bc8YCbytQY^FXrKIEGYEx++h*o%!TnQYPQM64@L`z@L3xzaf4>{O&$Thdg?Q zkd5Morn!d_tbXxLJX4&OlYCW-)+~~sEYqvk42Fg@XX>5uJCZREu2u0&e?)Y%EEF}G z@95N7kmoP5gb+0f(cWW&M8HpQ^f`(a-t(=M*g%u~E=K)jbGU^MUYK3POH}H2GZ@cGOP|`Liq+eZ6&_>AVo*cX}gH z?)czWe$#tdm@&#m)L?UgDff6`a1wEuGfI4w)1me3N@1)#0J^k<6k^{(0^WhG&F>3- zwy*XxdHr(OFZO1t!jRKbQSaWeQ9w7~>ve$j?d1W5Osbtl$l(5MjrbT9<;0w%9?WZC zwVtZ5(#Mka2*CVb5IyZ@2!iFXAML>UiV{-L;M9u(kuN_WKi^2&I|aZzvIs(SW)pea zFn`2l)qwv^zlX36-Vxazv(hk#AeE@|omJmwl)(f-A-!5?ubMM122F>wEvk#l^o-G& zaBs-|3?1a<&)(1HJH5fLwx3f-De3gzqteq3A4q5_?>3fB`?v5`-KflC|4Rg-4tonl z{pertw}@Gps<$c{`b9ud88K2W{!mPSMc=W7R_)~0Z({Q;8-wPa)8FFk3w|PfO~Y@( z?Z-@B{ae`71ix_AA7?do!y9YXK1{|6jd|u1zY+R4<1g;n9}!+gb1ORDX%;SMfS zaxdY7%aylQH5VD6o2Jr8neMBid(|G(OQfx) zs|F&?=6B79{gWgqB%t#PmxunFEsO>ZiXp)G_|lbkcoy#xOTbLcTo~Rb+G;KZ7s?43 zPmXbCC^znTm&s-z-I>rwwTgo99@E&)$k;9Cyshmc1C|z>Vcf`1<)t};v6IdRym<7r zv>Ii@?4R_@g=cR3xs ztHfUoJvowIjt%s6O?8QSkd!}yTJFJT^}i1D5Rmj>BI|ET=J%Rw@Ol2X>%UZB%Srz} z!0#2JzoKKoEYC0XqJIK^FP-}pSOu%-C_sS@kzbd2tsEYb?f`1ld{4>Qz Date: Sat, 30 Sep 2023 19:44:46 -0700 Subject: [PATCH 033/131] para: add Paragraph.contains_page_break --- features/par-access-inner-content.feature | 1 - src/docx/oxml/text/paragraph.py | 12 ++++++++++++ src/docx/text/paragraph.py | 5 +++++ tests/conftest.py | 16 ++++++++++++++++ tests/text/test_paragraph.py | 22 ++++++++++++++++++++++ tests/text/test_run.py | 10 ---------- 6 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 tests/conftest.py diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature index b454f6c95..f159ccb79 100644 --- a/features/par-access-inner-content.feature +++ b/features/par-access-inner-content.feature @@ -4,7 +4,6 @@ Feature: Access paragraph inner-content including hyperlinks I need to access differentiated paragraph content in document order - @wip Scenario Outline: Paragraph.contains_page_break reports presence of page-break Given a paragraph having rendered page breaks Then paragraph.contains_page_break is diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index 9405edd39..b855d89fe 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak from docx.oxml.text.parfmt import CT_PPr from docx.oxml.text.run import CT_R @@ -46,6 +47,17 @@ def clear_content(self): for child in self.xpath("./*[not(self::w:pPr)]"): self.remove(child) + @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): """Unconditionally replace or add `sectPr` as grandchild in correct sequence.""" pPr = self.get_or_add_pPr() diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index d2db02dfd..2518e11af 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -64,6 +64,11 @@ def clear(self): 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) + def insert_paragraph_before( self, text: str | None = None, style: str | ParagraphStyle | None = None ) -> Self: 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/text/test_paragraph.py b/tests/text/test_paragraph.py index 598d74e77..9b4781459 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -1,7 +1,10 @@ """Unit test suite for the docx.text.paragraph module.""" +from typing import cast + 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 @@ -16,6 +19,25 @@ class DescribeParagraph(object): + """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 + def it_knows_its_paragraph_style(self, style_get_fixture): paragraph, style_id_, style_ = style_get_fixture style = paragraph.style diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 558885176..6705595a0 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -11,7 +11,6 @@ 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.parts.story import StoryPart from docx.shape import InlineShape from docx.text.font import Font from docx.text.run import Run @@ -278,15 +277,6 @@ def clear_fixture(self, request): expected_xml = xml(expected_cxml) return run, expected_xml - @pytest.fixture - def fake_parent(self) -> t.StoryChild: - class StoryChild: - @property - def part(self) -> StoryPart: - raise NotImplementedError - - return StoryChild() - @pytest.fixture def font_fixture(self, Font_, font_): run = Run(element("w:r"), None) From e654522916fc11b3ac39f7cd74e5755f383ebae8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 20:11:38 -0700 Subject: [PATCH 034/131] para: add Paragraph.hyperlinks --- features/par-access-inner-content.feature | 1 - src/docx/oxml/__init__.py | 6 ++++++ src/docx/oxml/text/hyperlink.py | 9 ++++++++ src/docx/oxml/text/paragraph.py | 3 +++ src/docx/text/hyperlink.py | 26 +++++++++++++++++++++++ src/docx/text/paragraph.py | 6 ++++++ tests/text/test_paragraph.py | 23 ++++++++++++++++++++ 7 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/docx/oxml/text/hyperlink.py create mode 100644 src/docx/text/hyperlink.py diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature index f159ccb79..943c43c53 100644 --- a/features/par-access-inner-content.feature +++ b/features/par-access-inner-content.feature @@ -15,7 +15,6 @@ Feature: Access paragraph inner-content including hyperlinks | two | True | - @wip Scenario Outline: Paragraph.hyperlinks contains Hyperlink for each link in paragraph Given a paragraph having hyperlinks Then paragraph.hyperlinks has length diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index f31c67193..53cbd5601 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -21,6 +21,7 @@ CT_ShapeProperties, CT_Transform2D, ) +from docx.oxml.text.hyperlink import CT_Hyperlink from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak from docx.oxml.text.run import ( CT_R, @@ -50,6 +51,11 @@ 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 diff --git a/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py new file mode 100644 index 000000000..0b895ed27 --- /dev/null +++ b/src/docx/oxml/text/hyperlink.py @@ -0,0 +1,9 @@ +"""Custom element classes related to hyperlinks (CT_Hyperlink).""" + +from __future__ import annotations + +from docx.oxml.xmlchemy import BaseOxmlElement + + +class CT_Hyperlink(BaseOxmlElement): + """`` element, containing the text and address for a hyperlink.""" diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index b855d89fe..71e2d84b2 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + 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 @@ -18,9 +19,11 @@ class CT_P(BaseOxmlElement): """`` element, containing the properties and text for a paragraph.""" 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: diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py new file mode 100644 index 000000000..efa867e22 --- /dev/null +++ b/src/docx/text/hyperlink.py @@ -0,0 +1,26 @@ +"""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 docx import types as t +from docx.oxml.text.hyperlink import CT_Hyperlink +from docx.shared import Parented + + +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 diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 2518e11af..b962d8bd1 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -12,6 +12,7 @@ from docx.oxml.text.paragraph import CT_P from docx.shared import Parented from docx.styles.style import CharacterStyle, ParagraphStyle +from docx.text.hyperlink import Hyperlink from docx.text.parfmt import ParagraphFormat from docx.text.run import Run @@ -69,6 +70,11 @@ 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: diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index 9b4781459..a14de618d 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -38,6 +38,29 @@ def it_knows_whether_it_contains_a_page_break( 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}" + def it_knows_its_paragraph_style(self, style_get_fixture): paragraph, style_id_, style_ = style_get_fixture style = paragraph.style From 57d93e1b6fc3914134d479d928b6d1e6b4ddb8f0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 20:32:25 -0700 Subject: [PATCH 035/131] para: add Paragraph.iter_inner_content() --- features/par-access-inner-content.feature | 1 - src/docx/oxml/text/paragraph.py | 5 +++++ src/docx/text/paragraph.py | 18 +++++++++++++++++- tests/text/test_paragraph.py | 23 ++++++++++++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature index 943c43c53..1b0844ae1 100644 --- a/features/par-access-inner-content.feature +++ b/features/par-access-inner-content.feature @@ -27,7 +27,6 @@ Feature: Access paragraph inner-content including hyperlinks | three | 3 | - @wip 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 diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index 71e2d84b2..127e2b30b 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -50,6 +50,11 @@ def clear_content(self): 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. diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index b962d8bd1..d202d37ba 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List +from typing import Iterator, List from typing_extensions import Self @@ -10,6 +10,7 @@ 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 Parented from docx.styles.style import CharacterStyle, ParagraphStyle from docx.text.hyperlink import Hyperlink @@ -90,6 +91,21 @@ def insert_paragraph_before( 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 diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index a14de618d..e9aba4f67 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -1,6 +1,6 @@ """Unit test suite for the docx.text.paragraph module.""" -from typing import cast +from typing import List, cast import pytest @@ -61,6 +61,27 @@ def it_provides_access_to_the_hyperlinks_it_contains( 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 style = paragraph.style From 9dd28517b6343a5549a358702dc309fb87a4cacd Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 20:45:25 -0700 Subject: [PATCH 036/131] para: add Paragraph.rendered_page_breaks --- features/par-access-inner-content.feature | 1 - src/docx/text/paragraph.py | 12 +++++++++ tests/text/test_paragraph.py | 31 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature index 1b0844ae1..039c3ed4b 100644 --- a/features/par-access-inner-content.feature +++ b/features/par-access-inner-content.feature @@ -32,7 +32,6 @@ Feature: Access paragraph inner-content including hyperlinks Then paragraph.iter_inner_content() generates the paragraph runs and hyperlinks - @wip Scenario Outline: Paragraph.rendered_page_breaks contains paragraph RenderedPageBreaks Given a paragraph having rendered page breaks Then paragraph.rendered_page_breaks has length diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index d202d37ba..a0aaf8ff4 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -14,6 +14,7 @@ from docx.shared import Parented 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 @@ -112,6 +113,17 @@ def paragraph_format(self): 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 diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index e9aba4f67..ecd555ef5 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -100,6 +100,37 @@ def it_can_change_its_paragraph_style(self, style_set_fixture): ) assert paragraph._p.xml == expected_xml + @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"), [ From 7868f3edaa7a39c45c85f7f7822602eaa6b706b0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 21:09:35 -0700 Subject: [PATCH 037/131] acpt: add Hyperlink properties scenarios --- features/hlk-props.feature | 39 ++++++++++++++++ features/steps/hyperlink.py | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 features/hlk-props.feature create mode 100644 features/steps/hyperlink.py diff --git a/features/hlk-props.feature b/features/hlk-props.feature new file mode 100644 index 000000000..84e81763c --- /dev/null +++ b/features/hlk-props.feature @@ -0,0 +1,39 @@ +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 + + + @wip + Scenario: Hyperlink.address has the URL of the hyperlink + Given a hyperlink + Then hyperlink.address is the URL of the hyperlink + + + @wip + 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 | + + + @wip + 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 | + + + @wip + 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/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}" From 16e3f10aa864ff4bab927adb595f1e4c4afc45a0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 21:40:54 -0700 Subject: [PATCH 038/131] hlink: add Hyperlink.address --- features/hlk-props.feature | 1 - src/docx/oxml/text/hyperlink.py | 18 +++++++++++- src/docx/text/hyperlink.py | 10 +++++++ tests/oxml/text/test_hyperlink.py | 47 +++++++++++++++++++++++++++++++ tests/text/test_hyperlink.py | 44 +++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 tests/oxml/text/test_hyperlink.py create mode 100644 tests/text/test_hyperlink.py diff --git a/features/hlk-props.feature b/features/hlk-props.feature index 84e81763c..3d99cc19f 100644 --- a/features/hlk-props.feature +++ b/features/hlk-props.feature @@ -4,7 +4,6 @@ Feature: Access hyperlink properties I need properties on Hyperlink - @wip Scenario: Hyperlink.address has the URL of the hyperlink Given a hyperlink Then hyperlink.address is the URL of the hyperlink diff --git a/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py index 0b895ed27..45a69d743 100644 --- a/src/docx/oxml/text/hyperlink.py +++ b/src/docx/oxml/text/hyperlink.py @@ -2,8 +2,24 @@ from __future__ import annotations -from docx.oxml.xmlchemy import BaseOxmlElement +from typing import 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, +) 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") diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py index efa867e22..5a1481770 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -24,3 +24,13 @@ 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 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/text/test_hyperlink.py b/tests/text/test_hyperlink.py new file mode 100644 index 000000000..8e0710bc1 --- /dev/null +++ b/tests/text/test_hyperlink.py @@ -0,0 +1,44 @@ +"""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/" + + # -- 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}) From d0499b9ccc98cec37fcc14137fc5eef2231d125d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 22:08:45 -0700 Subject: [PATCH 039/131] hlink: add Hyperlink.contains_page_break --- features/hlk-props.feature | 1 - src/docx/oxml/text/hyperlink.py | 10 +++++++++- src/docx/text/hyperlink.py | 12 ++++++++++++ tests/text/test_hyperlink.py | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/features/hlk-props.feature b/features/hlk-props.feature index 3d99cc19f..5569e4341 100644 --- a/features/hlk-props.feature +++ b/features/hlk-props.feature @@ -9,7 +9,6 @@ Feature: Access hyperlink properties Then hyperlink.address is the URL of the hyperlink - @wip Scenario Outline: Hyperlink.contains_page_break reports presence of page-break Given a hyperlink having rendered page breaks Then hyperlink.contains_page_break is diff --git a/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py index 45a69d743..dc38afd23 100644 --- a/src/docx/oxml/text/hyperlink.py +++ b/src/docx/oxml/text/hyperlink.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING, List from docx.oxml.simpletypes import ST_OnOff, XsdString from docx.oxml.text.run import CT_R @@ -13,6 +13,9 @@ 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.""" @@ -23,3 +26,8 @@ class CT_Hyperlink(BaseOxmlElement): 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") diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py index 5a1481770..23a236042 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -34,3 +34,15 @@ def address(self) -> str: 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) diff --git a/tests/text/test_hyperlink.py b/tests/text/test_hyperlink.py index 8e0710bc1..f656bd6a1 100644 --- a/tests/text/test_hyperlink.py +++ b/tests/text/test_hyperlink.py @@ -24,6 +24,24 @@ def it_knows_the_hyperlink_URL(self, fake_parent: t.StoryChild): 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 + # -- fixtures -------------------------------------------------------------------- @pytest.fixture From 01061a8f5da7da154511a6b9e510addbd6593e90 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 22:36:08 -0700 Subject: [PATCH 040/131] hlink: add Hyperlink.runs --- features/hlk-props.feature | 1 - src/docx/text/hyperlink.py | 14 ++++++++++++++ tests/text/test_hyperlink.py | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/features/hlk-props.feature b/features/hlk-props.feature index 5569e4341..dc7e7a993 100644 --- a/features/hlk-props.feature +++ b/features/hlk-props.feature @@ -19,7 +19,6 @@ Feature: Access hyperlink properties | one | True | - @wip Scenario Outline: Hyperlink.runs contains Run for each run in hyperlink Given a hyperlink having runs Then hyperlink.runs has length diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py index 23a236042..133227faf 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -7,9 +7,12 @@ 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): @@ -46,3 +49,14 @@ def contains_page_break(self) -> bool: 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] diff --git a/tests/text/test_hyperlink.py b/tests/text/test_hyperlink.py index f656bd6a1..28640b4a7 100644 --- a/tests/text/test_hyperlink.py +++ b/tests/text/test_hyperlink.py @@ -42,6 +42,29 @@ def it_knows_whether_it_contains_a_page_break( 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, f"expected: {expected}, got: {actual}" + # -- fixtures -------------------------------------------------------------------- @pytest.fixture From a02c2206a3cc4c36a452b005c20766140968d1b2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 22:50:23 -0700 Subject: [PATCH 041/131] hlink: add Hyperlink.text --- features/hlk-props.feature | 1 - src/docx/oxml/text/hyperlink.py | 8 ++++++++ src/docx/text/hyperlink.py | 10 ++++++++++ tests/text/test_hyperlink.py | 22 +++++++++++++++++++++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/features/hlk-props.feature b/features/hlk-props.feature index dc7e7a993..5472f49a3 100644 --- a/features/hlk-props.feature +++ b/features/hlk-props.feature @@ -30,7 +30,6 @@ Feature: Access hyperlink properties | two | 2 | - @wip 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/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py index dc38afd23..76733457b 100644 --- a/src/docx/oxml/text/hyperlink.py +++ b/src/docx/oxml/text/hyperlink.py @@ -31,3 +31,11 @@ class CT_Hyperlink(BaseOxmlElement): 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/text/hyperlink.py b/src/docx/text/hyperlink.py index 133227faf..6082a53d1 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -60,3 +60,13 @@ def runs(self) -> List[Run]: 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/tests/text/test_hyperlink.py b/tests/text/test_hyperlink.py index 28640b4a7..484196902 100644 --- a/tests/text/test_hyperlink.py +++ b/tests/text/test_hyperlink.py @@ -63,7 +63,27 @@ def it_provides_access_to_the_runs_it_contains( actual = [type(item).__name__ for item in runs] expected = ["Run" for _ in range(count)] - assert actual == expected, f"expected: {expected}, got: {actual}" + 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 -------------------------------------------------------------------- From 9abd14a8e9996f5e9ef5d072fe752c89ceca712f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 1 Oct 2023 11:42:26 -0700 Subject: [PATCH 042/131] para: Paragraph.text includes hyperlink text --- features/par-access-inner-content.feature | 5 +++++ features/steps/paragraph.py | 9 +++++++++ src/docx/oxml/text/paragraph.py | 9 +++++++++ src/docx/text/paragraph.py | 8 +++----- tests/text/test_paragraph.py | 6 ++++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature index 039c3ed4b..047168fcf 100644 --- a/features/par-access-inner-content.feature +++ b/features/par-access-inner-content.feature @@ -42,3 +42,8 @@ Feature: Access paragraph inner-content including hyperlinks | 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/steps/paragraph.py b/features/steps/paragraph.py index 326786e29..4f8e2395a 100644 --- a/features/steps/paragraph.py +++ b/features/steps/paragraph.py @@ -190,6 +190,15 @@ def then_paragraph_style_is_value(context: Context, value_key: str): assert paragraph.style == expected_value +@then("paragraph.text contains the text of both the runs and the hyperlinks") +def then_paragraph_text_contains_the_text_of_both_the_runs_and_the_hyperlinks( + context: Context, +): + actual = context.paragraph.text + expected = "Three hyperlinks: the first one here, the second one, and the third." + assert actual == expected, f"expected:\n'{expected}'\n\ngot:\n'{actual}'" + + @then("the document contains four paragraphs") def then_the_document_contains_four_paragraphs(context: Context): assert len(context.document.paragraphs) == 4 diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index 127e2b30b..21384285f 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -88,6 +88,15 @@ def style(self, style): 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/text/paragraph.py b/src/docx/text/paragraph.py index a0aaf8ff4..72b0174d0 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -150,8 +150,9 @@ def style(self, style_or_name: str | ParagraphStyle | None): @property def text(self) -> str: - """String formed by concatenating the text of each run in the paragraph. + """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. @@ -161,10 +162,7 @@ def text(self) -> str: 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 + return self._p.text @text.setter def text(self, text: str | None): diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index ecd555ef5..ff980aa0d 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -143,9 +143,15 @@ def it_provides_access_to_the_rendered_page_breaks_it_contains( ('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 From 557fdeec8b14bcc89a66285c509bb333f30a4aa5 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 30 Sep 2023 22:57:04 -0700 Subject: [PATCH 043/131] acpt: add RenderedPageBreak split-para scenarios --- features/pbk-split-para.feature | 28 ++++++ features/steps/pagebreak.py | 171 ++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 features/pbk-split-para.feature create mode 100644 features/steps/pagebreak.py diff --git a/features/pbk-split-para.feature b/features/pbk-split-para.feature new file mode 100644 index 000000000..438378f4d --- /dev/null +++ b/features/pbk-split-para.feature @@ -0,0 +1,28 @@ +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 + + + @wip + 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 + + + @wip + 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 + + + @wip + 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 + + + @wip + 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/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>>< Date: Sat, 30 Sep 2023 23:56:39 -0700 Subject: [PATCH 044/131] lrpb: add RenderedPageBreak.preceding_pa..fragment --- features/pbk-split-para.feature | 2 - src/docx/oxml/text/pagebreak.py | 156 ++++++++++++++++++++++++++++++++ src/docx/text/pagebreak.py | 37 ++++++++ tests/text/test_pagebreak.py | 68 ++++++++++++++ 4 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 tests/text/test_pagebreak.py diff --git a/features/pbk-split-para.feature b/features/pbk-split-para.feature index 438378f4d..9721abe16 100644 --- a/features/pbk-split-para.feature +++ b/features/pbk-split-para.feature @@ -4,13 +4,11 @@ Feature: Split paragraph on rendered page-breaks I need to a way to split a paragraph on its first rendered page break - @wip 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 - @wip 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 diff --git a/src/docx/oxml/text/pagebreak.py b/src/docx/oxml/text/pagebreak.py index 201de9267..ef4953b28 100644 --- a/src/docx/oxml/text/pagebreak.py +++ b/src/docx/oxml/text/pagebreak.py @@ -2,7 +2,15 @@ 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): @@ -16,3 +24,151 @@ class CT_LastRenderedPageBreak(BaseOxmlElement): `w:lastRenderedPageBreak` maps to `CT_Empty`. This name was added to give it distinguished behavior. CT_Empty is used for many elements. """ + + @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 _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/text/pagebreak.py b/src/docx/text/pagebreak.py index e468a613a..899d915e7 100644 --- a/src/docx/text/pagebreak.py +++ b/src/docx/text/pagebreak.py @@ -2,10 +2,15 @@ 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. @@ -27,3 +32,35 @@ def __init__( 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. + + Also note that a rendered page-break can occur within a hyperlink; consider a + multi-word hyperlink like "excellent Wikipedia article on LLMs" that happens to + fall at the end of the last line on a page. THIS METHOD WILL "MOVE" the + page-break to occur after such a hyperlink. 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. + """ + if self._lastRenderedPageBreak.precedes_all_content: + return None + + from docx.text.paragraph import Paragraph + + return Paragraph(self._lastRenderedPageBreak.preceding_fragment_p, self._parent) diff --git a/tests/text/test_pagebreak.py b/tests/text/test_pagebreak.py new file mode 100644 index 000000000..73060f6a3 --- /dev/null +++ b/tests/text/test_pagebreak.py @@ -0,0 +1,68 @@ +# pyright: reportPrivateUsage=false + +"""Unit-test suite for the docx.text.pagebreak module.""" + +from typing import cast + +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_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) From 1e42c55f9982e7fb88dd0b8f488e0acf27c7a7cd Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 1 Oct 2023 12:50:19 -0700 Subject: [PATCH 045/131] lrpb: add RenderedPageBreak.following_pa..fragment --- features/pbk-split-para.feature | 2 - src/docx/oxml/text/pagebreak.py | 110 ++++++++++++++++++++++++++++++++ src/docx/text/pagebreak.py | 48 ++++++++++++-- tests/text/test_pagebreak.py | 79 +++++++++++++++++++++++ 4 files changed, 231 insertions(+), 8 deletions(-) diff --git a/features/pbk-split-para.feature b/features/pbk-split-para.feature index 9721abe16..8ce048a40 100644 --- a/features/pbk-split-para.feature +++ b/features/pbk-split-para.feature @@ -14,13 +14,11 @@ Feature: Split paragraph on rendered page-breaks Then rendered_page_break.preceding_paragraph_fragment includes the hyperlink - @wip 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 - @wip 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/src/docx/oxml/text/pagebreak.py b/src/docx/oxml/text/pagebreak.py index ef4953b28..943f9b6c2 100644 --- a/src/docx/oxml/text/pagebreak.py +++ b/src/docx/oxml/text/pagebreak.py @@ -25,6 +25,57 @@ class CT_LastRenderedPageBreak(BaseOxmlElement): 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. @@ -95,6 +146,65 @@ def _first_lrpb_in_p(self, p: CT_P) -> CT_LastRenderedPageBreak: 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.""" diff --git a/src/docx/text/pagebreak.py b/src/docx/text/pagebreak.py index 899d915e7..f3c16bc5c 100644 --- a/src/docx/text/pagebreak.py +++ b/src/docx/text/pagebreak.py @@ -24,6 +24,15 @@ class RenderedPageBreak(Parented): 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__( @@ -51,12 +60,7 @@ def preceding_paragraph_fragment(self) -> Paragraph | None: familiar container (`Paragraph`) to interrogate for the content preceding this page-break in the paragraph in which it occured. - Also note that a rendered page-break can occur within a hyperlink; consider a - multi-word hyperlink like "excellent Wikipedia article on LLMs" that happens to - fall at the end of the last line on a page. THIS METHOD WILL "MOVE" the - page-break to occur after such a hyperlink. 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. + Contains the entire hyperlink when this break occurs within a hyperlink. """ if self._lastRenderedPageBreak.precedes_all_content: return None @@ -64,3 +68,35 @@ def preceding_paragraph_fragment(self) -> Paragraph | 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/tests/text/test_pagebreak.py b/tests/text/test_pagebreak.py index 73060f6a3..6bb770619 100644 --- a/tests/text/test_pagebreak.py +++ b/tests/text/test_pagebreak.py @@ -4,6 +4,8 @@ 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 @@ -14,6 +16,17 @@ 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 ): @@ -66,3 +79,69 @@ def and_it_can_split_off_the_preceding_paragraph_content_when_in_a_hyperlink( 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) From e47dfa2ae49731a98c8118536b4959fc04fc6876 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 1 Oct 2023 15:59:39 -0700 Subject: [PATCH 046/131] docs: update docs with recent additions --- docs/api/text.rst | 14 ++++++++++++++ docs/conf.py | 2 ++ 2 files changed, 16 insertions(+) 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 0190aba5d..db96ecf52 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -157,6 +157,8 @@ .. |Relationships| replace:: :class:`._Relationships` +.. |RenderedPageBreak| replace:: :class:`.RenderedPageBreak` + .. |RGBColor| replace:: :class:`.RGBColor` .. |_Row| replace:: :class:`._Row` From 5b3ee80ed950153afb0b59e6c8d9ac5a2e8c5c62 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 1 Oct 2023 17:08:58 -0700 Subject: [PATCH 047/131] release: prepare v1.0.0rc1 release --- HISTORY.rst | 19 +++++++++++++++++++ README.md | 26 ++++++++++++++++++++++++++ README.rst | 10 ---------- pyproject.toml | 5 +++-- requirements-test.txt | 1 - requirements.txt | 1 + tox.ini | 6 +----- 7 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/HISTORY.rst b/HISTORY.rst index 2b33a4d5d..c465665d1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,25 @@ 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() + + 0.8.11 (2021-05-15) +++++++++++++++++++ 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 afc1dadeb..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 reading, 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/pyproject.toml b/pyproject.toml index 72591d894..1722272e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,13 +21,14 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "lxml>=2.3.2", + "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.rst" +readme = "README.md" requires-python = ">=3.7" [project.urls] diff --git a/requirements-test.txt b/requirements-test.txt index 868eea2c9..85d9f6ba3 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,4 +3,3 @@ behave>=1.2.3 pyparsing>=2.0.1 pytest>=2.5 ruff -typing-extensions diff --git a/requirements.txt b/requirements.txt index 17b9696d6..a156cfe60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ lxml>=3.1.0 +typing-extensions diff --git a/tox.ini b/tox.ini index f8c0ce994..1c4e3aea7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,11 +2,7 @@ envlist = py37, py38, py39, py310, py311 [testenv] -deps = - behave - lxml - pyparsing - pytest +deps = -rrequirements-test.txt commands = py.test -qx From 8122909dc64b4e33555a4ecd47894443f33a64d3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 7 Oct 2023 14:10:36 -0700 Subject: [PATCH 048/131] rfctr: rework enums so they type-check Replace meta-programming with `enum.Enum` features built into Python 3 to support XML attribute mapping. Also some random typing and docstring clean-up along the way. --- src/docx/enum/base.py | 301 ++++--------------- src/docx/enum/dml.py | 152 +++++----- src/docx/enum/section.py | 88 +++--- src/docx/enum/shape.py | 10 +- src/docx/enum/style.py | 590 +++++++++++++++++++++++++++----------- src/docx/enum/table.py | 175 +++++------ src/docx/enum/text.py | 503 +++++++++++++++++++------------- src/docx/oxml/section.py | 27 +- src/docx/oxml/xmlchemy.py | 19 +- src/docx/section.py | 117 ++++---- src/docx/text/font.py | 8 +- tests/test_enum.py | 130 ++++----- tests/text/test_run.py | 2 +- 13 files changed, 1172 insertions(+), 950 deletions(-) diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index 679174ac2..4c20af644 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -3,39 +3,81 @@ from __future__ import annotations import enum -import sys import textwrap -from typing import Callable, Type +from typing import Any, Dict, Type, TypeVar -from docx.exceptions import InvalidXmlError +from typing_extensions import Self +_T = TypeVar("_T", bound="BaseXmlEnum") -def alias(*aliases: str) -> Callable[..., Type[enum.Enum]]: - """Adds alternate name for an enumeration. - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to be referenced - by each of the names provided as arguments. +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 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 + 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:: - return decorator + >>> 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 -class _DocsPageFormatter(object): + @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, clsdict): + def __init__(self, clsname: str, clsdict: Dict[str, Any]): self._clsname = clsname self._clsdict = clsdict @@ -67,10 +109,11 @@ def _intro_text(self): return textwrap.dedent(cls_docstring).strip() - def _member_def(self, member): + def _member_def(self, member: BaseEnum | BaseXmlEnum): """Return an individual member definition formatted as an RST glossary entry, wrapped to fit within 78 columns.""" - member_docstring = textwrap.dedent(member.docstring).strip() + assert member.__doc__ is not None + member_docstring = textwrap.dedent(member.__doc__).strip() member_docstring = textwrap.fill( member_docstring, width=78, @@ -100,223 +143,3 @@ def _page_title(self): 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"] = {} - 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"] = {} - return clsdict["_xml_to_member"] diff --git a/src/docx/enum/dml.py b/src/docx/enum/dml.py index 16237e70b..27c63a283 100644 --- a/src/docx/enum/dml.py +++ b/src/docx/enum/dml.py @@ -1,35 +1,33 @@ """Enumerations used by DrawingML objects.""" -from .base import Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember, alias +from .base import BaseEnum, BaseXmlEnum -class MSO_COLOR_TYPE(Enumeration): +class MSO_COLOR_TYPE(BaseEnum): """Specifies the color specification scheme. Example:: - from docx.enum.dml import MSO_COLOR_TYPE + from docx.enum.dml import MSO_COLOR_TYPE - assert font.color.type == MSO_COLOR_TYPE.SCHEME + 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 """ - __ms_name__ = "MsoColorType" + RGB = (1, "Color is specified by an |RGBColor| value.") + """Color is specified by an |RGBColor| value.""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15" ").aspx" - ) + THEME = (2, "Color is one of the preset theme colors.") + """Color is one of the preset theme colors.""" - __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." - ), - ) + AUTO = (101, "Color is determined automatically by the application.") + """Color is determined automatically by the application.""" -@alias("MSO_THEME_COLOR") -class MSO_THEME_COLOR_INDEX(XmlEnumeration): +class MSO_THEME_COLOR_INDEX(BaseXmlEnum): """Indicates the Office theme color, one of those shown in the color gallery on the formatting ribbon. @@ -37,69 +35,69 @@ class MSO_THEME_COLOR_INDEX(XmlEnumeration): Example:: - from docx.enum.dml import MSO_THEME_COLOR + from docx.enum.dml import MSO_THEME_COLOR + + font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 + + MS API name: `MsoThemeColorIndex` - font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 + http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx """ - __ms_name__ = "MsoThemeColorIndex" + NOT_THEME_COLOR = (0, "UNMAPPED", "Indicates the color is not a theme color.") + """Indicates the color is not a theme color.""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15" ").aspx" - ) + 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.""" - __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."), + 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 index 583ceb999..982e19111 100644 --- a/src/docx/enum/section.py +++ b/src/docx/enum/section.py @@ -1,80 +1,86 @@ """Enumerations related to the main document in WordprocessingML files.""" -from .base import XmlEnumeration, XmlMappedEnumMember, alias +from .base import BaseXmlEnum -@alias("WD_HEADER_FOOTER") -class WD_HEADER_FOOTER_INDEX(XmlEnumeration): +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 """ - __ms_name__ = "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.""" - __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." - ), - ) +WD_HEADER_FOOTER = WD_HEADER_FOOTER_INDEX -@alias("WD_ORIENT") -class WD_ORIENTATION(XmlEnumeration): +class WD_ORIENTATION(BaseXmlEnum): """Alias: **WD_ORIENT** Specifies the page layout orientation. Example:: - from docx.enum.section import WD_ORIENT + from docx.enum.section import WD_ORIENT - section = document.sections[-1] section.orientation = WD_ORIENT.LANDSCAPE + 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 """ - __ms_name__ = "WdOrientation" + PORTRAIT = (0, "portrait", "Portrait orientation.") + """Portrait orientation.""" + + LANDSCAPE = (1, "landscape", "Landscape orientation.") + """Landscape orientation.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff837902.aspx" - __members__ = ( - XmlMappedEnumMember("PORTRAIT", 0, "portrait", "Portrait orientation."), - XmlMappedEnumMember("LANDSCAPE", 1, "landscape", "Landscape orientation."), - ) +WD_ORIENT = WD_ORIENTATION -@alias("WD_SECTION") -class WD_SECTION_START(XmlEnumeration): +class WD_SECTION_START(BaseXmlEnum): """Alias: **WD_SECTION** Specifies the start type of a section break. Example:: - from docx.enum.section import WD_SECTION + from docx.enum.section import WD_SECTION - section = document.sections[0] section.start_type = WD_SECTION.NEW_PAGE + 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 """ - __ms_name__ = "WdSectionStart" + 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.""" - __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." - ), - ) +WD_SECTION = WD_SECTION_START diff --git a/src/docx/enum/shape.py b/src/docx/enum/shape.py index 6b49ee8f0..ed086c38d 100644 --- a/src/docx/enum/shape.py +++ b/src/docx/enum/shape.py @@ -1,9 +1,13 @@ """Enumerations related to DrawingML shapes in WordprocessingML files.""" +import enum -class WD_INLINE_SHAPE_TYPE(object): - """Corresponds to WdInlineShapeType enumeration http://msdn.microsoft.com/en- - us/library/office/ff192587.aspx.""" + +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 diff --git a/src/docx/enum/style.py b/src/docx/enum/style.py index f7a8a16e0..d2474611d 100644 --- a/src/docx/enum/style.py +++ b/src/docx/enum/style.py @@ -1,182 +1,452 @@ """Enumerations related to styles.""" -from .base import EnumMember, XmlEnumeration, XmlMappedEnumMember, alias +from .base import BaseEnum, BaseXmlEnum -@alias("WD_STYLE") -class WD_BUILTIN_STYLE(XmlEnumeration): +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 + from docx import Document + from docx.enum.style import WD_STYLE - document = Document() styles = document.styles style = styles[WD_STYLE.BODY_TEXT] + 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 """ - __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): + 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 + from docx import Document + from docx.enum.style import WD_STYLE_TYPE + + styles = Document().styles + assert styles[0].type == WD_STYLE_TYPE.PARAGRAPH - 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 """ - __ms_name__ = "WdStyleType" + CHARACTER = (2, "character", "Character style.") + """Character style.""" + + LIST = (4, "numbering", "List style.") + """List style.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff196870.aspx" + PARAGRAPH = (1, "paragraph", "Paragraph style.") + """Paragraph style.""" - __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."), - ) + TABLE = (3, "table", "Table style.") + """Table style.""" diff --git a/src/docx/enum/table.py b/src/docx/enum/table.py index be8fe7050..eb1eb9dc0 100644 --- a/src/docx/enum/table.py +++ b/src/docx/enum/table.py @@ -1,135 +1,136 @@ """Enumerations related to tables in WordprocessingML files.""" -from .base import Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember, alias +from docx.enum.base import BaseEnum, BaseXmlEnum -@alias("WD_ALIGN_VERTICAL") -class WD_CELL_VERTICAL_ALIGNMENT(XmlEnumeration): +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 + from docx.enum.table import WD_ALIGN_VERTICAL - table = document.add_table(3, 3) table.cell(0, 0).vertical_alignment = - WD_ALIGN_VERTICAL.BOTTOM + 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 """ - __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.", - ), + 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. + """ -@alias("WD_ROW_HEIGHT") -class WD_ROW_HEIGHT_RULE(XmlEnumeration): + +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 + 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` - table = document.add_table(3, 3) table.rows[0].height_rule = - WD_ROW_HEIGHT_RULE.EXACTLY + https://msdn.microsoft.com/en-us/library/office/ff193620.aspx """ - __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."), + 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.""" -class WD_TABLE_ALIGNMENT(XmlEnumeration): + 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 + from docx.enum.table import WD_TABLE_ALIGNMENT + + table = document.add_table(3, 3) + table.alignment = WD_TABLE_ALIGNMENT.CENTER + + MS API name: `WdRowAlignment` - table = document.add_table(3, 3) table.alignment = WD_TABLE_ALIGNMENT.CENTER + http://office.microsoft.com/en-us/word-help/HV080607259.aspx """ - __ms_name__ = "WdRowAlignment" + LEFT = (0, "left", "Left-aligned") + """Left-aligned""" - __url__ = " http://office.microsoft.com/en-us/word-help/HV080607259.aspx" + CENTER = (1, "center", "Center-aligned.") + """Center-aligned.""" - __members__ = ( - XmlMappedEnumMember("LEFT", 0, "left", "Left-aligned"), - XmlMappedEnumMember("CENTER", 1, "center", "Center-aligned."), - XmlMappedEnumMember("RIGHT", 2, "right", "Right-aligned."), - ) + RIGHT = (2, "right", "Right-aligned.") + """Right-aligned.""" -class WD_TABLE_DIRECTION(Enumeration): +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 + from docx.enum.table import WD_TABLE_DIRECTION + + table = document.add_table(3, 3) + table.direction = WD_TABLE_DIRECTION.RTL + + MS API name: `WdTableDirection` - table = document.add_table(3, 3) table.direction = WD_TABLE_DIRECTION.RTL + http://msdn.microsoft.com/en-us/library/ff835141.aspx """ - __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.", - ), + 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 index 2c9d25e37..99e776fea 100644 --- a/src/docx/enum/text.py +++ b/src/docx/enum/text.py @@ -1,79 +1,83 @@ """Enumerations related to text in WordprocessingML files.""" +from __future__ import annotations + import enum -from typing import ClassVar -from docx.enum.base import EnumMember, XmlEnumeration, XmlMappedEnumMember, alias +from docx.enum.base import BaseXmlEnum -@alias("WD_ALIGN_PARAGRAPH") -class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): +class WD_PARAGRAPH_ALIGNMENT(BaseXmlEnum): """Alias: **WD_ALIGN_PARAGRAPH** Specifies paragraph justification type. Example:: - from docx.enum.text import WD_ALIGN_PARAGRAPH + from docx.enum.text import WD_ALIGN_PARAGRAPH - paragraph = document.add_paragraph() paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + 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 character 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 Thai formatting layout.", - ), + 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(object): - """Corresponds to WdBreakType enumeration http://msdn.microsoft.com/en- - us/library/office/ff195905.aspx.""" +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 + LINE_CLEAR_ALL = 11 # -- added for consistency, not in MS version -- PAGE = 7 SECTION_CONTINUOUS = 3 SECTION_EVEN_PAGE = 4 @@ -85,186 +89,279 @@ class WD_BREAK_TYPE(object): WD_BREAK = WD_BREAK_TYPE -@alias("WD_COLOR") -class WD_COLOR_INDEX(XmlEnumeration): +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 """ - __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."), - ) + 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.""" -class WD_LINE_SPACING(XmlEnumeration): + 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 + from docx.enum.text import WD_LINE_SPACING + + paragraph = document.add_paragraph() + paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY - 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 """ - __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)."), - ) + 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.""" -class WD_TAB_ALIGNMENT(XmlEnumeration): - """Specifies the tab stop alignment to apply.""" + DOUBLE = (2, "UNMAPPED", "Double spaced.") + """Double spaced.""" - __ms_name__ = "WdTabAlignment" + AT_LEAST = ( + 3, + "atLeast", + "Minimum line spacing is specified amount. Amount is specified separately.", + ) + """Minimum line spacing is specified amount. Amount is specified separately.""" - __url__ = "https://msdn.microsoft.com/EN-US/library/office/ff195609.aspx" + EXACTLY = ( + 4, + "exact", + "Line spacing is exactly specified amount. Amount is specified separately.", + ) + """Line spacing is exactly specified amount. Amount is specified separately.""" - __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)"), + 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. -class WD_TAB_LEADER(XmlEnumeration): - """Specifies the character to use as the leader with formatted tabs.""" + MS API name: `WdTabAlignment` + + URL: https://msdn.microsoft.com/EN-US/library/office/ff195609.aspx + """ - SPACES: ClassVar[enum.Enum] + LEFT = (0, "left", "Left-aligned.") + """Left-aligned.""" - __ms_name__ = "WdTabLeader" + CENTER = (1, "center", "Center-aligned.") + """Center-aligned.""" - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff845050.aspx" + RIGHT = (2, "right", "Right-aligned.") + """Right-aligned.""" - __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."), + 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. + """ -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." - ), + 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/oxml/section.py b/src/docx/oxml/section.py index 75d80eeb1..2937880ba 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -1,6 +1,9 @@ """Section-related custom element classes.""" +from __future__ import annotations + from copy import deepcopy +from typing import TYPE_CHECKING from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString @@ -12,6 +15,9 @@ ZeroOrOne, ) +if TYPE_CHECKING: + from docx.shared import Length + class CT_HdrFtr(BaseOxmlElement): """`w:hdr` and `w:ftr`, the root element for header and footer part respectively.""" @@ -78,7 +84,9 @@ class CT_SectPr(BaseOxmlElement): 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:]) + pgMar: CT_PageMar | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:pgMar", successors=_tag_seq[5:] + ) titlePg = ZeroOrOne("w:titlePg", successors=_tag_seq[14:]) del _tag_seq @@ -103,10 +111,11 @@ def add_headerReference(self, type_, 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.""" + 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 @@ -160,7 +169,7 @@ def get_headerReference(self, type_): return matching_headerReferences[0] @property - def gutter(self): + 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.""" @@ -175,7 +184,7 @@ def gutter(self, value): pgMar.gutter = value @property - def header(self): + def header(self) -> Length | None: """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.""" @@ -190,7 +199,7 @@ def header(self, value): pgMar.header = value @property - def left_margin(self): + 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.""" @@ -302,7 +311,7 @@ def start_type(self, value): type.val = value @property - def titlePg_val(self): + def titlePg_val(self) -> bool: """Value of `w:titlePg/@val` or |None| if not present.""" titlePg = self.titlePg if titlePg is None: diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 5d5050a82..0e97ce965 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Tuple, Type, TypeVar from lxml import etree from lxml.etree import ElementBase @@ -80,14 +80,19 @@ def _parse_line(cls, line): return front, attrs, close, text +_T = TypeVar("_T") + + class MetaOxmlElement(type): """Metaclass for BaseOxmlElement.""" - def __new__(cls, clsname: str, bases: Tuple[type, ...], clsdict: Dict[str, Any]): + 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, clsdict) + return super().__new__(cls, clsname, bases, namespace) - def __init__(cls, clsname: str, bases: Tuple[type, ...], clsdict: Dict[str, Any]): + def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any]): dispatchable = ( OneAndOnlyOne, OneOrMore, @@ -97,7 +102,7 @@ def __init__(cls, clsname: str, bases: Tuple[type, ...], clsdict: Dict[str, Any] ZeroOrOne, ZeroOrOneChoice, ) - for key, value in clsdict.items(): + for key, value in namespace.items(): if isinstance(value, dispatchable): value.populate_class_members(cls, key) @@ -665,9 +670,7 @@ def xml(self) -> str: """ return serialize_for_reading(self) - def xpath( # pyright: ignore[reportIncompatibleMethodOverride] - self, xpath_str: str - ) -> Any: + def xpath(self, xpath_str: str) -> Any: """Override of `lxml` _Element.xpath() method. Provides standard Open XML namespace mapping (`nsmap`) in centralized location. diff --git a/src/docx/section.py b/src/docx/section.py index ccae91975..61bdbb2f1 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -1,37 +1,17 @@ """The |Section| object and related proxy classes.""" -from collections.abc import Sequence +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence from docx.blkcntnr import BlockItemContainer from docx.enum.section import WD_HEADER_FOOTER from docx.shared import lazyproperty - -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.oxml.section import CT_SectPr + from docx.parts.document import DocumentPart + from docx.shared import Length class Section(object): @@ -40,23 +20,26 @@ class Section(object): 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 @@ -65,7 +48,7 @@ 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 @@ -114,30 +97,32 @@ 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. + def footer_distance(self) -> Length | None: + """Distance from bottom edge of page to bottom edge of the footer. - |None| if no setting is present in the XML. + 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. + 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. - The page gutter is extra spacing added to the `inner` margin to ensure even - margins after page binding. """ return self._sectPr.gutter @gutter.setter - def gutter(self, value): + def gutter(self, value: int | Length | None): self._sectPr.gutter = value @lazyproperty @@ -150,7 +135,7 @@ def header(self): return _Header(self._sectPr, self._document_part, WD_HEADER_FOOTER.PRIMARY) @property - def header_distance(self): + def header_distance(self) -> Length | None: """|Length| object representing the distance from the top edge of the page to the top edge of the header. @@ -159,17 +144,17 @@ def header_distance(self): 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): + 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 @@ -184,9 +169,10 @@ def orientation(self, value): 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. + 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. @@ -243,6 +229,33 @@ def top_margin(self, value): self._sectPr.top_margin = value +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, 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) + + class _BaseHeaderFooter(BlockItemContainer): """Base class for header and footer classes.""" diff --git a/src/docx/text/font.py b/src/docx/text/font.py index 7a15eb7c1..c7c514d40 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -1,7 +1,8 @@ """Font-related proxy objects.""" -from ..dml.color import ColorFormat -from ..shared import ElementProxy +from docx.dml.color import ColorFormat +from docx.enum.text import WD_UNDERLINE +from docx.shared import ElementProxy class Font(ElementProxy): @@ -359,7 +360,8 @@ def underline(self): rPr = self._element.rPr if rPr is None: return None - return rPr.u_val + val = rPr.u_val + return None if val == WD_UNDERLINE.INHERITED else val @underline.setter def underline(self, value): diff --git a/tests/test_enum.py b/tests/test_enum.py index 51f95f2d8..1b8a14f5b 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -5,94 +5,90 @@ assertions on those. """ +import enum + import pytest -from docx.enum.base import ( - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, - alias, -) +from docx.enum.base import BaseXmlEnum + + +class SomeXmlAttr(BaseXmlEnum): + """SomeXmlAttr docstring.""" + + FOO = (1, "foo", "Do foo instead of bar.") + """Do foo instead of bar.""" + BAR = (2, "bar", "Do bar instead of foo.") + """Do bar instead of foo.""" -@alias("BARFOO") -class FOOBAR(Enumeration): - """ - Enumeration docstring - """ + BAZ = (3, None, "Maps to the value assumed when the attribute is omitted.") + """Maps to the value assumed when the attribute is omitted.""" - __ms_name__ = "MsoFoobar" - __url__ = "http://msdn.microsoft.com/foobar.aspx" +class DescribeBaseXmlEnum: + """Unit-test suite for `docx.enum.base.BaseXmlEnum`.""" - __members__ = ( - EnumMember(None, None, "No setting/remove setting"), - EnumMember("READ_WRITE", 1, "Readable and settable"), - ReturnValueOnlyEnumMember("READ_ONLY", -2, "Return value only"), - ) + def it_is_an_instance_of_EnumMeta_just_like_a_regular_Enum(self): + assert type(SomeXmlAttr) is enum.EnumMeta + def it_has_the_same_repr_as_a_regular_Enum(self): + assert repr(SomeXmlAttr) == "" -@alias("XML-FU") -class XMLFOO(XmlEnumeration): - """ - XmlEnumeration docstring - """ + 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__}" - __ms_name__ = "MsoXmlFoobar" + def it_knows_the_XML_value_for_each_member_by_the_member_instance(self): + assert SomeXmlAttr.to_xml(SomeXmlAttr.FOO) == "foo" - __url__ = "http://msdn.microsoft.com/msoxmlfoobar.aspx" + def it_knows_the_XML_value_for_each_member_by_the_member_value(self): + assert SomeXmlAttr.to_xml(2) == "bar" - __members__ = ( - XmlMappedEnumMember(None, None, None, "No setting"), - XmlMappedEnumMember("XML_RW", 42, "attrVal", "Read/write setting"), - ReturnValueOnlyEnumMember("RO", -2, "Return value only;"), - ) + 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_can_find_the_member_from_the_XML_attr_value(self): + assert SomeXmlAttr.from_xml("bar") == SomeXmlAttr.BAR -class DescribeEnumeration(object): - def it_has_the_right_metaclass(self): - assert type(FOOBAR).__name__ == "MetaEnumeration" + def and_it_can_find_the_member_from_None_when_a_member_maps_that(self): + assert SomeXmlAttr.from_xml(None) == SomeXmlAttr.BAZ - 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 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_provides_the_enumeration_value_for_each_named_member(self): - assert FOOBAR.READ_WRITE == 1 - assert FOOBAR.READ_ONLY == -2 - def it_knows_if_a_setting_is_valid(self): - FOOBAR.validate(None) - FOOBAR.validate(FOOBAR.READ_WRITE) - with pytest.raises(ValueError, match="foobar not a member of FOOBAR enumerat"): - FOOBAR.validate("foobar") - with pytest.raises(ValueError, match=r"READ_ONLY \(-2\) not a member of FOOB"): - FOOBAR.validate(FOOBAR.READ_ONLY) +class DescribeBaseXmlEnumMembers: + """Unit-test suite for `docx.enum.base.BaseXmlEnum`.""" - def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): - assert BARFOO is FOOBAR # noqa + def it_is_an_instance_of_its_XmlEnum_subtype_class(self): + assert type(SomeXmlAttr.FOO) is SomeXmlAttr + def it_has_the_default_Enum_repr(self): + assert repr(SomeXmlAttr.BAR) == "" -class DescribeEnumValue(object): - def it_provides_its_symbolic_name_as_its_string_value(self): - assert ("%s" % FOOBAR.READ_WRITE) == "READ_WRITE (1)" + def but_its_str_value_is_customized(self): + assert str(SomeXmlAttr.FOO) == "FOO (1)" - def it_provides_its_description_as_its_docstring(self): - assert FOOBAR.READ_ONLY.__doc__ == "Return value only" + def its_value_is_the_same_int_as_its_corresponding_MS_API_enum_member(self): + assert SomeXmlAttr.FOO.value == 1 + def its_name_is_its_member_name_the_same_as_a_regular_Enum(self): + assert SomeXmlAttr.FOO.name == "FOO" -class DescribeXmlEnumeration(object): - 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, match=r"value 'RO \(-2\)' not in enumeration "): - 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/text/test_run.py b/tests/text/test_run.py index 6705595a0..3ab71f482 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -101,7 +101,7 @@ def it_can_change_its_underline_type(self, underline_set_fixture): def it_raises_on_assign_invalid_underline_type(self, underline_raise_fixture): run, underline = underline_raise_fixture - with pytest.raises(ValueError, match="' not in enumeration WD_UNDERLINE"): + with pytest.raises(ValueError, match=" is not a valid WD_UNDERLINE"): run.underline = underline def it_provides_access_to_its_font(self, font_fixture): From 58c24536fed3461e7cf52b828c18dd2316122c7a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 7 Oct 2023 23:37:56 -0700 Subject: [PATCH 049/131] rfctr: Section type-checks clean --- docs/user/sections.rst | 17 +- pyproject.toml | 2 +- src/docx/opc/part.py | 2 +- src/docx/oxml/section.py | 213 +++++++---- src/docx/oxml/shared.py | 11 +- src/docx/oxml/text/font.py | 39 +- src/docx/oxml/xmlchemy.py | 4 +- src/docx/parts/document.py | 6 +- src/docx/section.py | 121 ++++--- src/docx/text/font.py | 11 +- src/docx/text/run.py | 2 +- tests/test_section.py | 724 +++++++++++++++++++------------------ tests/text/test_font.py | 34 +- tests/text/test_run.py | 16 +- 14 files changed, 669 insertions(+), 533 deletions(-) diff --git a/docs/user/sections.rst b/docs/user/sections.rst index 502ea584a..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/pyproject.toml b/pyproject.toml index 1722272e1..dc9884a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ norecursedirs = [ ] python_files = ["test_*.py"] python_classes = ["Test", "Describe"] -python_functions = ["it_", "they_", "and_it_", "but_it_"] +python_functions = ["it_", "its_", "they_", "and_", "but_"] [tool.ruff] exclude = [] diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index e8cf4a3d3..655ea5fa7 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -55,7 +55,7 @@ def content_type(self): """Content type of this part.""" return self._content_type - def drop_rel(self, rId): + def drop_rel(self, rId: str): """Remove the relationship identified by `rId` if its reference count is less than 2. diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index 2937880ba..394033e18 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -3,9 +3,10 @@ from __future__ import annotations from copy import deepcopy -from typing import TYPE_CHECKING +from typing import Callable from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START +from docx.oxml.shared import CT_OnOff from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString from docx.oxml.xmlchemy import ( BaseOxmlElement, @@ -14,9 +15,7 @@ ZeroOrMore, ZeroOrOne, ) - -if TYPE_CHECKING: - from docx.shared import Length +from docx.shared import Length class CT_HdrFtr(BaseOxmlElement): @@ -29,35 +28,70 @@ class CT_HdrFtr(BaseOxmlElement): class CT_HdrFtrRef(BaseOxmlElement): """`w:headerReference` and `w:footerReference` elements.""" - type_ = RequiredAttribute("w:type", WD_HEADER_FOOTER) - rId = RequiredAttribute("r:id", XsdString) + 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 = 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) + 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 = OptionalAttribute("w:w", ST_TwipsMeasure) - h = OptionalAttribute("w:h", ST_TwipsMeasure) - orient = OptionalAttribute( - "w:orient", WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT + 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", @@ -82,15 +116,21 @@ class CT_SectPr(BaseOxmlElement): ) 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:]) + 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 = ZeroOrOne("w:titlePg", successors=_tag_seq[14:]) + titlePg: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:titlePg", successors=_tag_seq[14:] + ) del _tag_seq - def add_footerReference(self, type_, rId): + 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`. @@ -100,7 +140,7 @@ def add_footerReference(self, type_, rId): footerReference.rId = rId return footerReference - def add_headerReference(self, type_, rId): + 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`. @@ -122,36 +162,43 @@ def bottom_margin(self) -> Length | None: return pgMar.bottom @bottom_margin.setter - def bottom_margin(self, value): + def bottom_margin(self, value: int | Length | None): pgMar = self.get_or_add_pgMar() - pgMar.bottom = value + pgMar.bottom = ( + value if value is None or isinstance(value, Length) else Length(value) + ) - def clone(self): + 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. """ - clone_sectPr = deepcopy(self) - clone_sectPr.attrib.clear() - return clone_sectPr + cloned_sectPr = deepcopy(self) + cloned_sectPr.attrib.clear() + return cloned_sectPr @property - def footer(self): - """The value of the ``w:footer`` attribute in the ```` child element, + 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.""" + present. + """ pgMar = self.pgMar if pgMar is None: return None return pgMar.footer @footer.setter - def footer(self, value): + def footer(self, value: int | Length | None): pgMar = self.get_or_add_pgMar() - pgMar.footer = value + pgMar.footer = ( + value if value is None or isinstance(value, Length) else Length(value) + ) - def get_footerReference(self, type_): + 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) @@ -159,7 +206,7 @@ def get_footerReference(self, type_): return None return footerReferences[0] - def get_headerReference(self, type_): + 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_) @@ -179,24 +226,30 @@ def gutter(self) -> Length | None: return pgMar.gutter @gutter.setter - def gutter(self, value): + def gutter(self, value: int | Length | None): pgMar = self.get_or_add_pgMar() - pgMar.gutter = value + pgMar.gutter = ( + value if value is None or isinstance(value, Length) else Length(value) + ) @property def header(self) -> Length | None: - """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.""" + """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): + def header(self, value: int | Length | None): pgMar = self.get_or_add_pgMar() - pgMar.header = value + pgMar.header = ( + value if value is None or isinstance(value, Length) else Length(value) + ) @property def left_margin(self) -> Length | None: @@ -209,76 +262,90 @@ def left_margin(self) -> Length | None: return pgMar.left @left_margin.setter - def left_margin(self, value): + def left_margin(self, value: int | Length | None): pgMar = self.get_or_add_pgMar() - pgMar.left = value + pgMar.left = ( + value if value is None or isinstance(value, Length) else Length(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.""" + 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): + def orientation(self, value: WD_ORIENTATION | None): pgSz = self.get_or_add_pgSz() - pgSz.orient = value + pgSz.orient = value if value else WD_ORIENTATION.PORTRAIT @property - def page_height(self): - """Value in EMU of the ``h`` attribute of the ```` child element, or - |None| if not present.""" + 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): + def page_height(self, value: Length | None): 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.""" + 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): + def page_width(self, value: Length | None): pgSz = self.get_or_add_pgSz() pgSz.w = value @property - def preceding_sectPr(self): + 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--- + # -- [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_): + 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_): + 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): + 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.""" @@ -288,12 +355,12 @@ def right_margin(self): return pgMar.right @right_margin.setter - def right_margin(self, value): + def right_margin(self, value: Length | None): pgMar = self.get_or_add_pgMar() pgMar.right = value @property - def start_type(self): + 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.""" @@ -303,7 +370,7 @@ def start_type(self): return type.val @start_type.setter - def start_type(self, value): + 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 @@ -312,21 +379,21 @@ def start_type(self, value): @property def titlePg_val(self) -> bool: - """Value of `w:titlePg/@val` or |None| if not present.""" + """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): + def titlePg_val(self, value: bool | None): if value in [None, False]: self._remove_titlePg() else: - self.get_or_add_titlePg().val = value + self.get_or_add_titlePg().val = True @property - def top_margin(self): + 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.""" @@ -336,7 +403,7 @@ def top_margin(self): return pgMar.top @top_margin.setter - def top_margin(self, value): + def top_margin(self, value: Length | None): pgMar = self.get_or_add_pgMar() pgMar.top = value @@ -344,4 +411,6 @@ def top_margin(self, value): class CT_SectType(BaseOxmlElement): """```` element, defining the section start type.""" - val = OptionalAttribute("w:val", WD_SECTION_START) + val: WD_SECTION_START | None = ( # pyright: ignore[reportGeneralTypeIssues] + OptionalAttribute("w:val", WD_SECTION_START) + ) diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index d2b2aa24a..d979c638b 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -21,10 +21,15 @@ def new(cls, nsptagname, 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'.""" + """Used for `w:b`, `w:i` elements and others. - val = OptionalAttribute("w:val", ST_OnOff, default=True) + 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): diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 78b395d03..c6d9e93b4 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -1,5 +1,9 @@ """Custom element classes related to run properties (font).""" +from __future__ import annotations + +from typing import Callable + from docx.enum.dml import MSO_THEME_COLOR from docx.enum.text import WD_COLOR, WD_UNDERLINE from docx.oxml.ns import nsdecls, qn @@ -48,6 +52,9 @@ class CT_HpsMeasure(BaseOxmlElement): class CT_RPr(BaseOxmlElement): """```` element, containing the properties for a run.""" + _add_u: Callable[[], CT_Underline] + _remove_u: Callable[[], None] + _tag_seq = ( "w:rStyle", "w:rFonts", @@ -110,7 +117,9 @@ class CT_RPr(BaseOxmlElement): 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:]) + u: CT_Underline | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "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:]) @@ -265,15 +274,19 @@ def sz_val(self, value): sz.val = value @property - def u_val(self): - """Value of `w:u/@val`, or None if not present.""" + def u_val(self) -> bool | 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): + def u_val(self, value: bool | WD_UNDERLINE | None): self._remove_u() if value is not None: self._add_u().val = value @@ -298,18 +311,22 @@ class CT_Underline(BaseOxmlElement): """```` element, specifying the underlining style for a run.""" @property - def val(self): + def val(self) -> bool | WD_UNDERLINE | None: """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 + return ( + None + if underline == WD_UNDERLINE.INHERITED + else True + if underline == WD_UNDERLINE.SINGLE + else False + if underline == WD_UNDERLINE.NONE + else underline + ) @val.setter - def val(self, value): + def val(self, value: bool | WD_UNDERLINE | None): # 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. diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 0e97ce965..a0d870409 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -87,7 +87,7 @@ class MetaOxmlElement(type): """Metaclass for BaseOxmlElement.""" def __new__( - cls: Type[_T],clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any] + cls: Type[_T], clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any] ) -> _T: bases = (*bases, etree.ElementBase) return super().__new__(cls, clsname, bases, namespace) @@ -623,9 +623,11 @@ class BaseOxmlElement(metaclass=MetaOxmlElement): Adds standardized behavior to all classes in one place. """ + attrib: Dict[str, str] append: Callable[[ElementBase], 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] diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index c877e5a57..ec3bf3785 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -43,11 +43,11 @@ def document(self): """A |Document| object providing access to the content of this document.""" return Document(self._element, self) - def drop_header_part(self, 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): + def footer_part(self, rId: str): """Return |FooterPart| related by `rId`.""" return self.related_parts[rId] @@ -69,7 +69,7 @@ def get_style_id(self, style_or_name, style_type): """ return self.styles.get_style_id(style_or_name, style_type) - def header_part(self, rId): + def header_part(self, rId: str): """Return |HeaderPart| related by `rId`.""" return self.related_parts[rId] diff --git a/src/docx/section.py b/src/docx/section.py index 61bdbb2f1..7e96dacd4 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -2,13 +2,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING, Iterator, List, Sequence, overload from docx.blkcntnr import BlockItemContainer from docx.enum.section import WD_HEADER_FOOTER +from docx.parts.hdrftr import FooterPart, HeaderPart from docx.shared import lazyproperty 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.shared import Length @@ -52,7 +55,7 @@ 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 @@ -61,7 +64,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 @@ -70,7 +73,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 @@ -79,7 +82,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 @@ -88,7 +91,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 @@ -126,7 +129,7 @@ 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 @@ -136,10 +139,10 @@ def header(self): @property def header_distance(self) -> Length | None: - """|Length| object representing the distance from the top edge of the page to - the top edge of the header. + """Distance from top edge of page to top edge of header. - |None| if no setting is present in the XML. + Read/write. |None| if no setting is present in the XML. Assigning |None| causes + default value to be used. """ return self._sectPr.header @@ -158,14 +161,15 @@ 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 @@ -174,58 +178,62 @@ def page_height(self) -> Length | None: 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. + 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. + def page_width(self) -> Length | None: + """Total page width used for this section. - Page orientation is taken into account, so for example, its expected value would - be ``Inches(11)`` for letter-sized paper when orientation is landscape. + 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): + 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): + 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 @@ -235,12 +243,20 @@ class Sections(Sequence[Section]): Supports ``len()``, iteration, and indexed access. """ - def __init__(self, document_elm, document_part): + def __init__(self, document_elm: CT_Document, document_part: DocumentPart): super(Sections, self).__init__() self._document_elm = document_elm self._document_part = document_part - def __getitem__(self, key): + @overload + def __getitem__(self, key: int) -> Section: + ... + + @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) @@ -248,24 +264,29 @@ def __getitem__(self, key): ] return Section(self._document_elm.sectPr_lst[key], self._document_part) - def __iter__(self): + def __iter__(self) -> Iterator[Section]: for sectPr in self._document_elm.sectPr_lst: yield Section(sectPr, self._document_part) - def __len__(self): + 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, document_part, header_footer_index): + 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. @@ -279,7 +300,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: @@ -290,7 +311,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 @@ -300,16 +321,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") @@ -318,7 +339,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 @@ -339,12 +360,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. @@ -362,7 +383,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) @@ -372,6 +393,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): @@ -416,6 +439,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): diff --git a/src/docx/text/font.py b/src/docx/text/font.py index c7c514d40..728f2331c 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -1,5 +1,7 @@ """Font-related proxy objects.""" +from __future__ import annotations + from docx.dml.color import ColorFormat from docx.enum.text import WD_UNDERLINE from docx.shared import ElementProxy @@ -348,9 +350,10 @@ def superscript(self, value): rPr.superscript = value @property - def underline(self): - """The underline style for this |Font|, one of |None|, |True|, |False|, or a - value from :ref:`WdUnderline`. + 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 @@ -364,7 +367,7 @@ def underline(self): return None if val == WD_UNDERLINE.INHERITED else val @underline.setter - def underline(self, value): + def underline(self, value: bool | WD_UNDERLINE | None): rPr = self._element.get_or_add_rPr() rPr.u_val = value diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 4a01aebc5..55ad85cdc 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -31,7 +31,7 @@ def __init__(self, r: CT_R, parent: t.StoryChild): super(Run, self).__init__(parent) self._r = self._element = self.element = r - def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): # pyright: ignore + 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 diff --git a/tests/test_section.py b/tests/test_section.py index a810eb418..4eb1f9192 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -1,28 +1,49 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for the docx.section module.""" +from __future__ import annotations + +from typing import cast + import pytest -from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENT, WD_SECTION +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 Section, Sections, _BaseHeaderFooter, _Footer, _Header -from docx.shared import Inches +from docx.shared import Inches, Length 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.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_) @@ -36,10 +57,13 @@ def it_can_iterate_over_its_Section_instances( 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_ @@ -55,10 +79,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_ @@ -75,43 +102,65 @@ def it_can_access_its_Section_instances_by_slice( # 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_) @@ -123,9 +172,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_) @@ -137,9 +186,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_) @@ -151,9 +200,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_) @@ -165,9 +214,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_) @@ -179,9 +228,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_) @@ -192,118 +241,178 @@ 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) + @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 + assert start_type is expected_value - 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) + @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 - - def it_can_change_its_orientation(self, orientation_set_fixture): - sectPr, new_orientation, expected_xml = orientation_set_fixture - section = Section(sectPr, None) - - section.orientation = new_orientation - - 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) - - value = getattr(section, margin_prop_name) + assert orientation is expected_value - 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( + ("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_) - setattr(section, margin_prop_name, new_value) + section.orientation = 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=[ + @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), @@ -313,15 +422,25 @@ def diff_first_header_set_fixture(self, request): ("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 + 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 - @pytest.fixture( - params=[ + @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}"), @@ -351,185 +470,102 @@ def margins_get_fixture(self, request): 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") + 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) - return sectPr, new_page_width, expected_xml + section = Section(sectPr, document_part_) - @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 + setattr(section, margin_prop_name, value) - @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 + assert section._sectPr.xml == 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 @@ -537,12 +573,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 @@ -550,11 +588,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() @@ -562,15 +602,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() @@ -579,77 +621,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): - def it_can_add_a_footer_part_to_help(self, document_part_, footer_part_): +class Describe_Footer: + """Unit-test suite for `docx.section._Footer`.""" + + 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) @@ -663,7 +692,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_ @@ -674,7 +703,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) @@ -683,16 +712,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] @@ -708,7 +743,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) @@ -716,36 +751,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) @@ -759,7 +783,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_ @@ -770,7 +794,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) @@ -779,16 +803,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] @@ -812,26 +842,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/text/test_font.py b/tests/text/test_font.py index d37c4252c..a17b2f1bd 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -1,5 +1,7 @@ """Test suite for the docx.text.run module.""" +from __future__ import annotations + import pytest from docx.dml.color import ColorFormat @@ -63,8 +65,21 @@ def it_can_change_whether_it_is_superscript(self, superscript_set_fixture): 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 + ): + font = Font(element(r_cxml), None) assert font.underline is expected_value def it_can_change_its_underline_type(self, underline_set_fixture): @@ -381,21 +396,6 @@ def superscript_set_fixture(self, request): 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}"), diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 3ab71f482..775d0b424 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, cast +from typing import Any, List, cast import pytest @@ -99,10 +99,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 + @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 = underline + run.underline = invalid_value def it_provides_access_to_its_font(self, font_fixture): run, Font_, font_ = font_fixture @@ -369,12 +371,6 @@ def underline_set_fixture(self, request): 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 From b61b63b8267d3e51cc069cacd274dd23d309bf77 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 8 Oct 2023 12:28:28 -0700 Subject: [PATCH 050/131] rfctr: Paragraph type-checks clean --- src/docx/opc/part.py | 26 ++++++++++++++++----- src/docx/oxml/__init__.py | 7 +++--- src/docx/oxml/shared.py | 18 ++++++++++----- src/docx/oxml/styles.py | 40 +++++++++++++++++++-------------- src/docx/oxml/text/paragraph.py | 12 ++++++---- src/docx/oxml/text/parfmt.py | 27 +++++++++++++++------- src/docx/oxml/xmlchemy.py | 1 + src/docx/parts/document.py | 18 +++++++++++---- src/docx/parts/story.py | 21 +++++++++++++---- src/docx/parts/styles.py | 8 ++++++- src/docx/shared.py | 26 ++++++++++++++++++--- src/docx/styles/__init__.py | 10 ++++++--- src/docx/styles/style.py | 22 ++++++++++++------ src/docx/styles/styles.py | 29 ++++++++++++++++-------- src/docx/text/paragraph.py | 12 +++++----- 15 files changed, 198 insertions(+), 79 deletions(-) diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index 655ea5fa7..a7cd19d61 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -1,11 +1,18 @@ """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.opc.package import OpcPackage + class Part(object): """Base class for package parts. @@ -14,7 +21,13 @@ class Part(object): to implement specific part behaviors. """ - def __init__(self, partname, content_type, blob=None, package=None): + def __init__( + self, + partname: str, + content_type: str, + blob: bytes | None = None, + package: OpcPackage | None = None, + ): super(Part, self).__init__() self._partname = partname self._content_type = content_type @@ -96,7 +109,7 @@ def partname(self, partname): raise TypeError(tmpl % type(partname).__name__) self._partname = partname - def part_related_by(self, reltype): + 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 @@ -105,9 +118,12 @@ def part_related_by(self, reltype): """ 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.""" + 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: diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 53cbd5601..e72e4c5cd 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -21,6 +21,7 @@ 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 ( @@ -68,13 +69,13 @@ register_element_cls("w:t", CT_Text) # --------------------------------------------------------------------------- -# other custom element class mappings - -from .shared import CT_DecimalNumber, CT_OnOff, CT_String # noqa +# 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 diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index d979c638b..1774560ac 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -1,5 +1,9 @@ """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 @@ -33,15 +37,19 @@ class CT_OnOff(BaseOxmlElement): class CT_String(BaseOxmlElement): - """Used for ```` and ```` elements and others, containing a - style name in its ``val`` attribute.""" + """Used for `w:pStyle` and `w:tblStyle` elements and others. + + In those cases, it containing a style name in its `val` attribute. + """ - val = RequiredAttribute("w:val", ST_String) + val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_String + ) @classmethod - def new(cls, nsptagname, val): + def new(cls, nsptagname: str, val: str): """Return a new ``CT_String`` element with tagname `nsptagname` and ``val`` attribute set to `val`.""" - elm = OxmlElement(nsptagname) + elm = cast(CT_String, OxmlElement(nsptagname)) elm.val = val return elm diff --git a/src/docx/oxml/styles.py b/src/docx/oxml/styles.py index 486110848..e0a3eaeaf 100644 --- a/src/docx/oxml/styles.py +++ b/src/docx/oxml/styles.py @@ -1,5 +1,7 @@ """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 ( @@ -126,8 +128,14 @@ class CT_Style(BaseOxmlElement): rPr = ZeroOrOne("w:rPr", successors=_tag_seq[18:]) del _tag_seq - type = OptionalAttribute("w:type", WD_STYLE_TYPE) - styleId = OptionalAttribute("w:styleId", ST_String) + 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) @@ -293,23 +301,21 @@ def default_for(self, style_type): # 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_id(self, styleId: str) -> CT_Style | None: + """`w:style` child where @styleId = `styleId`. - def get_by_name(self, name): - """Return the ```` child element having ```` child element with - value `name`, or |None| if not found.""" + |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 - try: - return self.xpath(xpath)[0] - except IndexError: - return None + return next(iter(self.xpath(xpath)), None) def _iter_styles(self): """Generate each of the `w:style` child elements in document order.""" diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index 21384285f..f771dd74f 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -1,14 +1,17 @@ +# pyright: reportPrivateUsage=false + """Custom element classes related to paragraphs (CT_P).""" from __future__ import annotations -from typing import TYPE_CHECKING, Callable, List +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 @@ -18,6 +21,7 @@ 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] @@ -28,7 +32,7 @@ class CT_P(BaseOxmlElement): def add_p_before(self) -> CT_P: """Return a new `` element inserted directly prior to this one.""" - new_p = OxmlElement("w:p") + new_p = cast(CT_P, OxmlElement("w:p")) self.addprevious(new_p) return new_p @@ -66,7 +70,7 @@ def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]: "./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak" ) - def set_sectPr(self, sectPr): + 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() @@ -84,7 +88,7 @@ def style(self) -> str | None: return pPr.style @style.setter - def style(self, style): + def style(self, style: str | None): pPr = self.get_or_add_pPr() pPr.style = style diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index 74c2504ab..49ea01003 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Callable + from docx.enum.text import ( WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, @@ -18,6 +20,10 @@ ) 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.""" @@ -37,6 +43,11 @@ class CT_Jc(BaseOxmlElement): 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", @@ -75,7 +86,9 @@ class CT_PPr(BaseOxmlElement): "w:sectPr", "w:pPrChange", ) - pStyle = ZeroOrOne("w:pStyle", successors=_tag_seq[1:]) + 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:]) @@ -273,20 +286,18 @@ def spacing_lineRule(self, value): self.get_or_add_spacing().lineRule = value @property - def style(self): - """String contained in child, or None if that element is not - present.""" + 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): - """Set val attribute of child element to `style`, adding a new - element if necessary. + def style(self, style: str | None): + """Set `./w:pStyle/@val` `style`, adding a new element if necessary. - If `style` is |None|, remove the element if present. + If `style` is |None|, remove `./w:pStyle` when present. """ if style is None: self._remove_pStyle() diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index a0d870409..350e72aa8 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -623,6 +623,7 @@ class BaseOxmlElement(metaclass=MetaOxmlElement): Adds standardized behavior to all classes in one place. """ + addprevious: Callable[[BaseOxmlElement], None] attrib: Dict[str, str] append: Callable[[ElementBase], None] find: Callable[[str], ElementBase | None] diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index ec3bf3785..a157764b9 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -1,6 +1,11 @@ """|DocumentPart| and closely related objects.""" +from __future__ import annotations + +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 @@ -10,6 +15,9 @@ from docx.shape import InlineShapes from docx.shared import lazyproperty +if TYPE_CHECKING: + from docx.styles.style import BaseStyle + class DocumentPart(StoryPart): """Main document part of a WordprocessingML (WML) package, aka a .docx file. @@ -51,7 +59,7 @@ def footer_part(self, rId: str): """Return |FooterPart| related by `rId`.""" return self.related_parts[rId] - def get_style(self, style_id, 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 @@ -124,14 +132,16 @@ def _settings_part(self): return settings_part @property - def _styles_part(self): + 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/src/docx/parts/story.py b/src/docx/parts/story.py index 41d365c49..012c812f6 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -1,10 +1,19 @@ """|StoryPart| and related objects.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + 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 +if TYPE_CHECKING: + from docx.enum.style import WD_STYLE_TYPE + from docx.parts.document import DocumentPart + from docx.styles.style import BaseStyle + class StoryPart(XmlPart): """Base class for story parts. @@ -26,7 +35,7 @@ def get_or_add_image(self, image_descriptor): rId = self.relate_to(image_part, RT.IMAGE) return rId, image_part.image - def get_style(self, style_id, 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 @@ -34,7 +43,9 @@ def get_style(self, style_id, style_type): """ return self._document_part.get_style(style_id, style_type) - def get_style_id(self, style_or_name, 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 @@ -69,6 +80,8 @@ def next_id(self): return max(used_ids) + 1 @lazyproperty - def _document_part(self): + def _document_part(self) -> DocumentPart: """|DocumentPart| object for this package.""" - return self.package.main_document_part + 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 index 9016163ab..dffa762ef 100644 --- a/src/docx/parts/styles.py +++ b/src/docx/parts/styles.py @@ -1,6 +1,9 @@ """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 @@ -8,13 +11,16 @@ 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): + 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 diff --git a/src/docx/shared.py b/src/docx/shared.py index 83e064098..40dfb369d 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from docx.oxml.xmlchemy import BaseOxmlElement + from docx.parts.story import StoryPart class Length(int): @@ -267,7 +268,7 @@ class ElementProxy(object): common type of class in python-docx other than custom element (oxml) classes. """ - def __init__(self, element: "BaseOxmlElement", parent=None): + def __init__(self, element: BaseOxmlElement, parent=None): self._element = element self._parent = parent @@ -299,7 +300,7 @@ def part(self): return self._parent.part -class Parented(object): +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. @@ -308,7 +309,6 @@ class Parented(object): """ def __init__(self, parent): - super(Parented, self).__init__() self._parent = parent @property @@ -317,6 +317,26 @@ def part(self): 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). These 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(). diff --git a/src/docx/styles/__init__.py b/src/docx/styles/__init__.py index b6b09e0ca..514eee908 100644 --- a/src/docx/styles/__init__.py +++ b/src/docx/styles/__init__.py @@ -1,5 +1,9 @@ """Sub-package module for docx.styles sub-package.""" +from __future__ import annotations + +from typing import Dict + class BabelFish(object): """Translates special-case style names from UI name (e.g. Heading 1) to @@ -20,17 +24,17 @@ class BabelFish(object): ("Heading 9", "heading 9"), ) - internal_style_names = dict(style_aliases) + 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): + 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): + 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/style.py b/src/docx/styles/style.py index 6ca4ba8df..aa175ea80 100644 --- a/src/docx/styles/style.py +++ b/src/docx/styles/style.py @@ -1,16 +1,20 @@ """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): - """Return a style object of the appropriate |BaseStyle| subclass, according to the - type of `style_elm`.""" - style_cls = { +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, @@ -27,6 +31,10 @@ class BaseStyle(ElementProxy): 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. @@ -117,13 +125,13 @@ def quick_style(self, value): self._element.qFormat_val = value @property - def style_id(self): + 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._element.styleId + return self._style_elm.styleId @style_id.setter def style_id(self, value): @@ -133,7 +141,7 @@ def style_id(self, value): def type(self): """Member of :ref:`WdStyleType` corresponding to the type of this style, e.g. ``WD_STYLE_TYPE.PARAGRAPH``.""" - type = self._element.type + type = self._style_elm.type if type is None: return WD_STYLE_TYPE.PARAGRAPH return type diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py index c9b8d47fb..98a56e520 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -1,7 +1,11 @@ """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 @@ -15,12 +19,16 @@ class Styles(ElementProxy): 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): + 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 @@ -59,7 +67,7 @@ def add_style(self, name, style_type, builtin=False): style = self._element.add_style_of_type(style_name, style_type, builtin) return StyleFactory(style) - def default(self, style_type): + 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) @@ -67,7 +75,7 @@ def default(self, style_type): return None return StyleFactory(style) - def get_by_id(self, style_id, style_type): + 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 @@ -99,18 +107,20 @@ def latent_styles(self): 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): + 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) + 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, style_type): + 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 @@ -119,9 +129,10 @@ def _get_style_id_from_name(self, style_name, 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`. + 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`. """ diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 72b0174d0..2425f1d6e 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -2,16 +2,15 @@ from __future__ import annotations -from typing import Iterator, List +from typing import Iterator, List, cast from typing_extensions import Self -from docx import types as t 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 Parented +from docx.shared import StoryChild from docx.styles.style import CharacterStyle, ParagraphStyle from docx.text.hyperlink import Hyperlink from docx.text.pagebreak import RenderedPageBreak @@ -19,10 +18,10 @@ from docx.text.run import Run -class Paragraph(Parented): +class Paragraph(StoryChild): """Proxy object wrapping a `` element.""" - def __init__(self, p: CT_P, parent: t.StoryChild): + def __init__(self, p: CT_P, parent: StoryChild): super(Paragraph, self).__init__(parent) self._p = self._element = p @@ -141,7 +140,8 @@ def style(self) -> ParagraphStyle | None: 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 = 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): From ac26854ca7e8689fdcaed00e69181af09b4c9a10 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 8 Oct 2023 17:41:47 -0700 Subject: [PATCH 051/131] rfctr: Run type-checks clean --- src/docx/image/image.py | 62 +++++++++++++--------- src/docx/opc/part.py | 4 +- src/docx/oxml/__init__.py | 2 + src/docx/oxml/shape.py | 23 +++++++-- src/docx/oxml/simpletypes.py | 42 ++++++++------- src/docx/oxml/text/font.py | 34 ++++++++----- src/docx/oxml/text/run.py | 28 ++++++---- src/docx/oxml/xmlchemy.py | 99 +++++++++++++++++++++++------------- src/docx/package.py | 10 ++-- src/docx/parts/image.py | 8 ++- src/docx/parts/story.py | 20 +++++--- src/docx/shared.py | 8 +-- src/docx/text/font.py | 14 ++++- src/docx/text/run.py | 79 +++++++++++++++++----------- src/docx/types.py | 19 +++++++ tests/text/test_run.py | 49 +++++++++--------- 16 files changed, 322 insertions(+), 179 deletions(-) diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 2e5286f6f..2d2f2c97d 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -4,26 +4,31 @@ 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, lazyproperty +from docx.shared import Emu, Inches, Length, lazyproperty -class Image(object): +class Image: """Graphical image stream such as JPEG, PNG, or GIF with properties and methods required by ImagePart.""" - def __init__(self, blob, filename, image_header): + 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): + 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) @@ -73,17 +78,17 @@ def filename(self): return self._filename @property - def px_width(self): + def px_width(self) -> int: """The horizontal pixel dimension of the image.""" return self._image_header.px_width @property - def px_height(self): + def px_height(self) -> int: """The vertical pixel dimension of the image.""" return self._image_header.px_height @property - def horz_dpi(self): + 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. @@ -91,7 +96,7 @@ def horz_dpi(self): return self._image_header.horz_dpi @property - def vert_dpi(self): + 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. @@ -99,34 +104,40 @@ def vert_dpi(self): return self._image_header.vert_dpi @property - def width(self): + 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): + 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=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. + 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) @@ -142,7 +153,12 @@ def sha1(self): return hashlib.sha1(self._blob).hexdigest() @classmethod - def _from_stream(cls, stream, blob, filename=None): + 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) diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index a7cd19d61..ed52f54cb 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -11,7 +11,7 @@ from docx.oxml.parser import parse_xml if TYPE_CHECKING: - from docx.opc.package import OpcPackage + from docx.package import Package class Part(object): @@ -26,7 +26,7 @@ def __init__( partname: str, content_type: str, blob: bytes | None = None, - package: OpcPackage | None = None, + package: Package | None = None, ): super(Part, self).__init__() self._partname = partname diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index e72e4c5cd..bf8d00962 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -8,6 +8,7 @@ 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, @@ -48,6 +49,7 @@ 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) diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index f9ae2d9af..05c96697a 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -2,6 +2,8 @@ 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 ( @@ -20,6 +22,13 @@ 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 @@ -49,14 +58,14 @@ class CT_GraphicalObjectData(BaseOxmlElement): class CT_Inline(BaseOxmlElement): - """```` element, container for an inline shape.""" + """`` 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): + 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()) @@ -71,9 +80,13 @@ def new(cls, cx, cy, shape_id, 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.""" + 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) diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index e0c609959..1d63dfa38 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -5,14 +5,23 @@ schema. """ -from ..exceptions import InvalidXmlError -from ..shared import Emu, Pt, RGBColor, Twips +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from docx.exceptions import InvalidXmlError +from docx.shared import Emu, Pt, RGBColor, Twips + +if TYPE_CHECKING: + from docx import types as t class BaseSimpleType(object): + """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): @@ -20,6 +29,10 @@ 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): @@ -35,15 +48,10 @@ def validate_int_in_range(cls, value, min_inclusive, max_inclusive): ) @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): @@ -62,15 +70,15 @@ 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) @@ -160,7 +168,7 @@ def validate(cls, value): 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") if value not in valid_values: diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index c6d9e93b4..0ca22576a 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Callable +from typing import TYPE_CHECKING, Callable from docx.enum.dml import MSO_THEME_COLOR from docx.enum.text import WD_COLOR, WD_UNDERLINE @@ -21,6 +21,9 @@ ZeroOrOne, ) +if TYPE_CHECKING: + from docx.oxml.shared import CT_OnOff, CT_String + class CT_Color(BaseOxmlElement): """`w:color` element, specifying the color of a font and perhaps other objects.""" @@ -53,7 +56,9 @@ class CT_RPr(BaseOxmlElement): """```` element, containing the properties for a run.""" _add_u: Callable[[], CT_Underline] + _add_rStyle: Callable[..., CT_String] _remove_u: Callable[[], None] + _remove_rStyle: Callable[[], None] _tag_seq = ( "w:rStyle", @@ -96,9 +101,13 @@ class CT_RPr(BaseOxmlElement): "w:specVanish", "w:oMath", ) - rStyle = ZeroOrOne("w:rStyle", successors=_tag_seq[1:]) + rStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:rStyle", successors=_tag_seq[1:] + ) rFonts = ZeroOrOne("w:rFonts", successors=_tag_seq[2:]) - b = ZeroOrOne("w:b", successors=_tag_seq[3:]) + 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:]) @@ -185,20 +194,18 @@ def rFonts_hAnsi(self, value): rFonts.hAnsi = value @property - def style(self): - """String contained in child, or None if that element is not - present.""" + 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): - """Set val attribute of child element to `style`, adding a new - element if necessary. + 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 the element if present. + If `style` is |None|, remove `w:rStyle` element if present. """ if style is None: self._remove_rStyle() @@ -291,15 +298,14 @@ def u_val(self, value: bool | WD_UNDERLINE | None): 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'.""" + 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, value): + def _set_bool_val(self, name: str, value: bool | None): if value is None: getattr(self, "_remove_%s" % name)() return diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index c995bfbbb..66c3537e1 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -12,7 +12,9 @@ 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 @@ -22,10 +24,12 @@ 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 = ZeroOrOne("w:rPr") + rPr: CT_RPr | None = ZeroOrOne("w:rPr") # pyright: ignore[reportGeneralTypeIssues] br = ZeroOrMore("w:br") cr = ZeroOrMore("w:cr") drawing = ZeroOrMore("w:drawing") @@ -39,7 +43,7 @@ def add_t(self, text: str) -> CT_Text: t.set(qn("xml:space"), "preserve") return t - def add_drawing(self, inline_or_anchor): + 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. @@ -48,11 +52,11 @@ def add_drawing(self, inline_or_anchor): drawing.append(inline_or_anchor) return drawing - def clear_content(self): + def clear_content(self) -> None: """Remove all child elements except a `w:rPr` 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) + # -- 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]: @@ -100,7 +104,7 @@ def style(self) -> str | None: return rPr.style @style.setter - def style(self, style): + def style(self, style: str | None): """Set character style of this `w:r` element to `style`. If `style` is None, remove the style element. @@ -120,7 +124,7 @@ def text(self) -> str: for e in self.xpath("w:br | w:cr | w:noBreakHyphen | w:ptab | w:t | w:tab") ) - @text.setter + @text.setter # pyright: ignore[reportIncompatibleVariableOverride] def text(self, text: str): self.clear_content() _RunContentAppender.append_to_run_from_text(self, text) @@ -140,7 +144,9 @@ class CT_Br(BaseOxmlElement): type: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] "w:type", ST_BrType, default="textWrapping" ) - clear = OptionalAttribute("w:clear", ST_BrClear) + 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. @@ -236,7 +242,7 @@ class _RunContentAppender(object): def __init__(self, r: CT_R): self._r = r - self._bfr = [] + self._bfr: List[str] = [] @classmethod def append_to_run_from_text(cls, r: CT_R, text: str): @@ -270,4 +276,4 @@ def flush(self): text = "".join(self._bfr) if text: self._r.add_t(text) - del self._bfr[:] + self._bfr.clear() diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 350e72aa8..075a4c842 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from typing import Any, Callable, Dict, List, Tuple, Type, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Type, TypeVar from lxml import etree from lxml.etree import ElementBase @@ -12,6 +12,9 @@ from docx.oxml.ns import NamespacePrefixedTag, nsmap, qn from docx.shared import lazyproperty +if TYPE_CHECKING: + from docx import types as t + def serialize_for_reading(element): """Serialize `element` to human-readable XML suitable for tests. @@ -108,15 +111,19 @@ def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, An class BaseAttribute(object): - """Base class for OptionalAttribute and RequiredAttribute, providing common - methods.""" + """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[t.AbstractSimpleType]): super(BaseAttribute, self).__init__() self._attr_name = attr_name self._simple_type = simple_type - def populate_class_members(self, element_cls, prop_name): + 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 @@ -137,24 +144,53 @@ def _clark_name(self): 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. + assigned |None|, the attribute is removed, but still returns the default value when + one is specified. """ - def __init__(self, attr_name, simple_type, default=None): + def __init__( + self, + attr_name: str, + simple_type: Type[t.AbstractSimpleType], + default: t.AbstractSimpleTypeMember | 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 _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." + ) - def get_attr_value(obj): + @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 @@ -164,22 +200,12 @@ def get_attr_value(obj): 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] @@ -200,6 +226,15 @@ class RequiredAttribute(BaseAttribute): 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 @@ -217,15 +252,6 @@ def get_attr_value(obj): 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 @@ -625,13 +651,14 @@ class BaseOxmlElement(metaclass=MetaOxmlElement): addprevious: Callable[[BaseOxmlElement], None] attrib: Dict[str, str] - append: Callable[[ElementBase], None] + 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 diff --git a/src/docx/package.py b/src/docx/package.py index 27a1da937..89ae47a09 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -1,5 +1,9 @@ """WordprocessingML Package class and related objects.""" +from __future__ import annotations + +from typing import IO + from docx.image.image import Image from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.package import OpcPackage @@ -18,7 +22,7 @@ def after_unmarshal(self): """ self._gather_image_parts() - def get_or_add_image_part(self, 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 @@ -27,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() @@ -61,7 +65,7 @@ def __len__(self): def append(self, item): self._image_parts.append(item) - def get_or_add_image_part(self, 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 diff --git a/src/docx/parts/image.py b/src/docx/parts/image.py index d93c47922..e4580df74 100644 --- a/src/docx/parts/image.py +++ b/src/docx/parts/image.py @@ -1,5 +1,7 @@ """The proxy class for an image part, and related objects.""" +from __future__ import annotations + import hashlib from docx.image.image import Image @@ -13,7 +15,9 @@ class ImagePart(Part): Corresponds to the target part of a relationship with type RELATIONSHIP_TYPE.IMAGE. """ - def __init__(self, partname, content_type, blob, image=None): + 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 @@ -54,7 +58,7 @@ def from_image(cls, image, partname): return ImagePart(partname, image.content_type, image.blob, image) @property - def image(self): + def image(self) -> Image: if self._image is None: self._image = Image.from_blob(self.blob) return self._image diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py index 012c812f6..b5c8ac882 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -2,15 +2,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING +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 lazyproperty +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 @@ -23,7 +24,7 @@ class StoryPart(XmlPart): `.add_paragraph()`, `.add_table()` etc. """ - def get_or_add_image(self, image_descriptor): + 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 @@ -31,7 +32,9 @@ def get_or_add_image(self, image_descriptor): `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) + 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 @@ -54,7 +57,12 @@ def get_style_id( """ return self._document_part.get_style_id(style_or_name, style_type) - def new_pic_inline(self, image_descriptor, width, height): + 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 @@ -66,7 +74,7 @@ def new_pic_inline(self, image_descriptor, width, height): return CT_Inline.new_pic_inline(shape_id, rId, filename, cx, cy) @property - def next_id(self): + 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 diff --git a/src/docx/shared.py b/src/docx/shared.py index 40dfb369d..53a000d6f 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -268,7 +268,7 @@ class ElementProxy(object): common type of class in python-docx other than custom element (oxml) classes. """ - def __init__(self, element: BaseOxmlElement, parent=None): + def __init__(self, element: BaseOxmlElement, parent: Any | None = None): self._element = element self._parent = parent @@ -321,9 +321,9 @@ class StoryChild: """A document element within a story part. Story parts include DocumentPart and Header/FooterPart and can contain block items - (paragraphs and tables). These 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. + (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. """ diff --git a/src/docx/text/font.py b/src/docx/text/font.py index 728f2331c..8b4470a34 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -2,15 +2,25 @@ 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 +if TYPE_CHECKING: + from docx.oxml.text.run import CT_R + class Font(ElementProxy): - """Proxy object wrapping the parent of a ```` element and providing access to + """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): """Read/write. @@ -384,7 +394,7 @@ def web_hidden(self): def web_hidden(self, value): self._set_bool_prop("webHidden", value) - def _get_bool_prop(self, name): + 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: diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 55ad85cdc..d79b583aa 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -2,23 +2,26 @@ from __future__ import annotations -from typing import IO, Iterator +from typing import IO, TYPE_CHECKING, Iterator, cast -from docx import types as t 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.oxml.text.run import CT_R, CT_Text from docx.shape import InlineShape -from docx.shared import Length, Parented +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(Parented): + +class Run(StoryChild): """Proxy object wrapping `` element. Several of the properties on Run take a tri-state value, |True|, |False|, or |None|. @@ -27,8 +30,8 @@ class Run(Parented): the style hierarchy. """ - def __init__(self, r: CT_R, parent: t.StoryChild): - super(Run, self).__init__(parent) + 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): @@ -58,11 +61,14 @@ def add_picture( width: Length | None = None, height: Length | None = None, ) -> InlineShape: - """Return an |InlineShape| instance containing the image identified by - `image_path_or_stream`, added to the end of this run. + """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 + 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- @@ -73,10 +79,10 @@ def add_picture( self._r.add_drawing(inline) return InlineShape(inline) - def add_tab(self): + 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() + self._r.add_tab() def add_text(self, text: str): """Returns a newly appended |_Text| object (corresponding to a new ```` @@ -89,15 +95,17 @@ def add_text(self, text: str): return _Text(t) @property - def bold(self) -> bool: - """Read/write. + def bold(self) -> bool | None: + """Read/write tri-state value. - Causes the text of the run to appear in bold. + 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): + def bold(self, value: bool | None): self.font.bold = value def clear(self): @@ -122,21 +130,23 @@ def contains_page_break(self) -> bool: return bool(self._r.lastRenderedPageBreaks) @property - def font(self): + 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: + def italic(self) -> bool | None: """Read/write tri-state value. - When |True|, causes the text of the run to appear in italics. + 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): + def italic(self, value: bool | None): self.font.italic = value def iter_inner_content(self) -> Iterator[str | Drawing | RenderedPageBreak]: @@ -165,16 +175,18 @@ def iter_inner_content(self) -> Iterator[str | Drawing | RenderedPageBreak]: yield Drawing(item, self) @property - def style(self) -> CharacterStyle | None: + def style(self) -> CharacterStyle: """Read/write. - A |_CharacterStyle| object representing the character style applied to this run. + 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) + return cast( + CharacterStyle, self.part.get_style(style_id, WD_STYLE_TYPE.CHARACTER) + ) @style.setter def style(self, style_or_name: str | CharacterStyle | None): @@ -204,17 +216,22 @@ def text(self, text: str): self._r.text = text @property - def underline(self) -> bool: - """The underline style for this |Run|, one of |None|, |True|, |False|, or a - value from :ref:`WdUnderline`. + 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. + |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 diff --git a/src/docx/types.py b/src/docx/types.py index 6097f740c..9f5b7dae7 100644 --- a/src/docx/types.py +++ b/src/docx/types.py @@ -7,6 +7,25 @@ from docx.parts.story import StoryPart +class AbstractSimpleTypeMember(Protocol): + """A simple-type member is a valid value for a simple-type. + + The name *simple-type* comes from the ISO spec which refers to XML attribute value + types as simple types and gives those the prefix `ST_` in the XML Schema. Many are + enumerations but that is not strictly required. + """ + + +class AbstractSimpleType(Protocol): + """A simple-type can provide XML attribute value mapping.""" + + @classmethod + def from_xml(cls, xml_value: str) -> AbstractSimpleTypeMember: ... + + @classmethod + def to_xml(cls, value: AbstractSimpleTypeMember) -> str: ... + + class StoryChild(Protocol): """An object that can fulfill the `parent` role in a `Parented` class. diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 775d0b424..68976e874 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Test suite for the docx.text.run module.""" from __future__ import annotations @@ -157,11 +159,31 @@ 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() + @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) + + cleared_run = run.clear() + assert run._r.xml == expected_xml - assert _run is run + assert cleared_run is run @pytest.mark.parametrize( ("r_cxml", "expected_text"), @@ -260,25 +282,6 @@ def bool_prop_set_fixture(self, request): 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) From 44f9ede23ab62c10dd0c5a2e51b52fb132a7b9bc Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 8 Oct 2023 21:05:51 -0700 Subject: [PATCH 052/131] rfctr: Font type-checks clean --- src/docx/oxml/simpletypes.py | 10 +- src/docx/oxml/text/font.py | 153 +++++++------ src/docx/oxml/xmlchemy.py | 10 +- src/docx/text/font.py | 165 +++++++------ src/docx/types.py | 19 -- tests/text/test_font.py | 432 ++++++++++++++++++----------------- 6 files changed, 409 insertions(+), 380 deletions(-) diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index 1d63dfa38..10e683f15 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from docx import types as t + from docx.shared import Length class BaseSimpleType(object): @@ -245,13 +246,13 @@ class ST_HpsMeasure(XsdUnsignedLong): """Half-point measure, e.g. 24.0 represents 12.0 points.""" @classmethod - def convert_from_xml(cls, 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) @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) @@ -343,7 +344,7 @@ 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 = { @@ -354,8 +355,7 @@ def convert_from_xml(cls, str_value): "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): diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 0ca22576a..0e183cf65 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Callable from docx.enum.dml import MSO_THEME_COLOR -from docx.enum.text import WD_COLOR, WD_UNDERLINE -from docx.oxml.ns import nsdecls, qn +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, @@ -23,6 +23,7 @@ if TYPE_CHECKING: from docx.oxml.shared import CT_OnOff, CT_String + from docx.shared import Length class CT_Color(BaseOxmlElement): @@ -33,32 +34,50 @@ class CT_Color(BaseOxmlElement): class CT_Fonts(BaseOxmlElement): - """```` element, specifying typeface name for the various language - types.""" + """`` element. - ascii = OptionalAttribute("w:ascii", ST_String) - hAnsi = OptionalAttribute("w:hAnsi", ST_String) + 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 = RequiredAttribute("w:val", WD_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.""" + """Used for `` element and others, specifying font size in half-points.""" - val = RequiredAttribute("w:val", ST_HpsMeasure) + val: Length = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_HpsMeasure + ) class CT_RPr(BaseOxmlElement): - """```` element, containing the properties for a run.""" + """`` element, containing the properties for a run.""" - _add_u: Callable[[], CT_Underline] + 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] - _remove_u: Callable[[], None] + _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", @@ -104,7 +123,9 @@ class CT_RPr(BaseOxmlElement): rStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] "w:rStyle", successors=_tag_seq[1:] ) - rFonts = ZeroOrOne("w:rFonts", successors=_tag_seq[2:]) + 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:] ) @@ -124,12 +145,22 @@ class CT_RPr(BaseOxmlElement): 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:]) + 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 = ZeroOrOne("w:vertAlign", successors=_tag_seq[32:]) + 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:]) @@ -141,16 +172,18 @@ def _new_color(self): 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.""" + 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): + def highlight_val(self, value: WD_COLOR_INDEX | None) -> None: if value is None: self._remove_highlight() return @@ -158,7 +191,7 @@ def highlight_val(self, value): highlight.val = value @property - def rFonts_ascii(self): + 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 @@ -171,7 +204,7 @@ def rFonts_ascii(self): return rFonts.ascii @rFonts_ascii.setter - def rFonts_ascii(self, value): + def rFonts_ascii(self, value: str | None) -> None: if value is None: self._remove_rFonts() return @@ -179,7 +212,7 @@ def rFonts_ascii(self, value): rFonts.ascii = value @property - def rFonts_hAnsi(self): + 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: @@ -187,7 +220,7 @@ def rFonts_hAnsi(self): return rFonts.hAnsi @rFonts_hAnsi.setter - def rFonts_hAnsi(self, value): + def rFonts_hAnsi(self, value: str | None): if value is None and self.rFonts is None: return rFonts = self.get_or_add_rFonts() @@ -215,8 +248,8 @@ def style(self, style: str | None) -> None: self.rStyle.val = style @property - def subscript(self): - """|True| if `w:vertAlign/@w:val` is 'subscript'. + 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. @@ -229,18 +262,20 @@ def subscript(self): return False @subscript.setter - def subscript(self, value): + 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 - elif self.vertAlign is None: - return - elif self.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): + 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 @@ -254,18 +289,20 @@ def superscript(self): return False @superscript.setter - def superscript(self, value): + 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 - elif self.vertAlign is None: - return - elif self.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): + 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: @@ -273,7 +310,7 @@ def sz_val(self): return sz.val @sz_val.setter - def sz_val(self, value): + def sz_val(self, value: Length | None): if value is None: self._remove_sz() return @@ -281,7 +318,7 @@ def sz_val(self, value): sz.val = value @property - def u_val(self) -> bool | WD_UNDERLINE | None: + 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 @@ -293,7 +330,7 @@ def u_val(self) -> bool | WD_UNDERLINE | None: return u.val @u_val.setter - def u_val(self, value: bool | WD_UNDERLINE | None): + def u_val(self, value: WD_UNDERLINE | None): self._remove_u() if value is not None: self._add_u().val = value @@ -314,38 +351,18 @@ def _set_bool_val(self, name: str, value: bool | None): class CT_Underline(BaseOxmlElement): - """```` element, specifying the underlining style for a run.""" + """`` element, specifying the underlining style for a run.""" - @property - def val(self) -> bool | WD_UNDERLINE | None: - """The underline type corresponding to the ``w:val`` attribute value.""" - val = self.get(qn("w:val")) - underline = WD_UNDERLINE.from_xml(val) - return ( - None - if underline == WD_UNDERLINE.INHERITED - else True - if underline == WD_UNDERLINE.SINGLE - else False - if underline == WD_UNDERLINE.NONE - else underline + val: WD_UNDERLINE | None = ( + OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", WD_UNDERLINE ) - - @val.setter - def val(self, value: bool | WD_UNDERLINE | None): - # 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.""" + """`` element, specifying subscript or superscript.""" - val = RequiredAttribute("w:val", ST_VerticalAlignRun) + val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_VerticalAlignRun + ) diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 075a4c842..2d4150de6 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -14,6 +14,8 @@ 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): @@ -116,7 +118,9 @@ class BaseAttribute(object): Provides common methods. """ - def __init__(self, attr_name: str, simple_type: Type[t.AbstractSimpleType]): + 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 @@ -166,8 +170,8 @@ class OptionalAttribute(BaseAttribute): def __init__( self, attr_name: str, - simple_type: Type[t.AbstractSimpleType], - default: t.AbstractSimpleTypeMember | None = None, + simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType], + default: BaseXmlEnum | BaseSimpleType | None = None, ): super(OptionalAttribute, self).__init__(attr_name, simple_type) self._default = default diff --git a/src/docx/text/font.py b/src/docx/text/font.py index 8b4470a34..acd60795b 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -6,10 +6,12 @@ from docx.dml.color import ColorFormat from docx.enum.text import WD_UNDERLINE -from docx.shared import ElementProxy +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): @@ -22,7 +24,7 @@ def __init__(self, r: CT_R, parent: Any | None = None): self._r = r @property - def all_caps(self): + def all_caps(self) -> bool | None: """Read/write. Causes text in this font to appear in capital letters. @@ -30,11 +32,11 @@ def all_caps(self): return self._get_bool_prop("caps") @all_caps.setter - def all_caps(self, value): + def all_caps(self, value: bool | None) -> None: self._set_bool_prop("caps", value) @property - def bold(self): + def bold(self) -> bool | None: """Read/write. Causes text in this font to appear in bold. @@ -42,7 +44,7 @@ def bold(self): return self._get_bool_prop("b") @bold.setter - def bold(self, value): + def bold(self, value: bool | None) -> None: self._set_bool_prop("b", value) @property @@ -52,7 +54,7 @@ def color(self): return ColorFormat(self._element) @property - def complex_script(self): + 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 @@ -61,11 +63,11 @@ def complex_script(self): return self._get_bool_prop("cs") @complex_script.setter - def complex_script(self, value): + def complex_script(self, value: bool | None) -> None: self._set_bool_prop("cs", value) @property - def cs_bold(self): + 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 @@ -74,11 +76,11 @@ def cs_bold(self): return self._get_bool_prop("bCs") @cs_bold.setter - def cs_bold(self, value): + def cs_bold(self, value: bool | None) -> None: self._set_bool_prop("bCs", value) @property - def cs_italic(self): + 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 @@ -87,11 +89,11 @@ def cs_italic(self): return self._get_bool_prop("iCs") @cs_italic.setter - def cs_italic(self, value): + def cs_italic(self, value: bool | None) -> None: self._set_bool_prop("iCs", value) @property - def double_strike(self): + def double_strike(self) -> bool | None: """Read/write tri-state value. When |True|, causes the text in the run to appear with double strikethrough. @@ -99,11 +101,11 @@ def double_strike(self): return self._get_bool_prop("dstrike") @double_strike.setter - def double_strike(self, value): + def double_strike(self, value: bool | None) -> None: self._set_bool_prop("dstrike", value) @property - def emboss(self): + 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 @@ -112,11 +114,11 @@ def emboss(self): return self._get_bool_prop("emboss") @emboss.setter - def emboss(self, value): + def emboss(self, value: bool | None) -> None: self._set_bool_prop("emboss", value) @property - def hidden(self): + def hidden(self) -> bool | None: """Read/write tri-state value. When |True|, causes the text in the run to be hidden from display, unless @@ -125,25 +127,24 @@ def hidden(self): return self._get_bool_prop("vanish") @hidden.setter - def hidden(self, value): + def hidden(self, value: bool | None) -> None: 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.""" + 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): + def highlight_color(self, value: WD_COLOR_INDEX | None): rPr = self._element.get_or_add_rPr() rPr.highlight_val = value @property - def italic(self): + def italic(self) -> bool | None: """Read/write tri-state value. When |True|, causes the text of the run to appear in italics. |None| indicates @@ -152,11 +153,11 @@ def italic(self): return self._get_bool_prop("i") @italic.setter - def italic(self, value): + def italic(self, value: bool | None) -> None: self._set_bool_prop("i", value) @property - def imprint(self): + 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. @@ -164,11 +165,11 @@ def imprint(self): return self._get_bool_prop("imprint") @imprint.setter - def imprint(self, value): + def imprint(self, value: bool | None) -> None: self._set_bool_prop("imprint", value) @property - def math(self): + def math(self) -> bool | None: """Read/write tri-state value. When |True|, specifies this run contains WML that should be handled as though it @@ -177,15 +178,15 @@ def math(self): return self._get_bool_prop("oMath") @math.setter - def math(self, value): + def math(self, value: bool | None) -> None: 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. + def name(self) -> str | None: + """The typeface name for this |Font|. - |None| indicates the typeface is inherited from the style hierarchy. + 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: @@ -193,13 +194,13 @@ def name(self): return rPr.rFonts_ascii @name.setter - def name(self, value): + 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): + def no_proof(self) -> bool | None: """Read/write tri-state value. When |True|, specifies that the contents of this run should not report any @@ -208,11 +209,11 @@ def no_proof(self): return self._get_bool_prop("noProof") @no_proof.setter - def no_proof(self, value): + def no_proof(self, value: bool | None) -> None: self._set_bool_prop("noProof", value) @property - def outline(self): + 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 @@ -222,11 +223,11 @@ def outline(self): return self._get_bool_prop("outline") @outline.setter - def outline(self, value): + def outline(self, value: bool | None) -> None: self._set_bool_prop("outline", value) @property - def rtl(self): + def rtl(self) -> bool | None: """Read/write tri-state value. When |True| causes the text in the run to have right-to-left characteristics. @@ -234,11 +235,11 @@ def rtl(self): return self._get_bool_prop("rtl") @rtl.setter - def rtl(self, value): + def rtl(self, value: bool | None) -> None: self._set_bool_prop("rtl", value) @property - def shadow(self): + 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 @@ -247,18 +248,24 @@ def shadow(self): return self._get_bool_prop("shadow") @shadow.setter - def shadow(self, value): + def shadow(self, value: bool | None) -> None: 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 + 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: @@ -266,12 +273,12 @@ def size(self): return rPr.sz_val @size.setter - def size(self, emu): + def size(self, emu: int | Length | None) -> None: rPr = self._element.get_or_add_rPr() - rPr.sz_val = emu + rPr.sz_val = None if emu is None else Emu(emu) @property - def small_caps(self): + def small_caps(self) -> bool | None: """Read/write tri-state value. When |True| causes the lowercase characters in the run to appear as capital @@ -280,11 +287,11 @@ def small_caps(self): return self._get_bool_prop("smallCaps") @small_caps.setter - def small_caps(self, value): + def small_caps(self, value: bool | None) -> None: self._set_bool_prop("smallCaps", value) @property - def snap_to_grid(self): + 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 @@ -293,11 +300,11 @@ def snap_to_grid(self): return self._get_bool_prop("snapToGrid") @snap_to_grid.setter - def snap_to_grid(self, value): + def snap_to_grid(self, value: bool | None) -> None: self._set_bool_prop("snapToGrid", value) @property - def spec_vanish(self): + 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 @@ -308,11 +315,11 @@ def spec_vanish(self): return self._get_bool_prop("specVanish") @spec_vanish.setter - def spec_vanish(self, value): + def spec_vanish(self, value: bool | None) -> None: self._set_bool_prop("specVanish", value) @property - def strike(self): + 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 @@ -321,11 +328,11 @@ def strike(self): return self._get_bool_prop("strike") @strike.setter - def strike(self, value): + def strike(self, value: bool | None) -> None: self._set_bool_prop("strike", value) @property - def subscript(self): + 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 @@ -337,12 +344,12 @@ def subscript(self): return rPr.subscript @subscript.setter - def subscript(self, value): + def subscript(self, value: bool | None) -> None: rPr = self._element.get_or_add_rPr() rPr.subscript = value @property - def superscript(self): + def superscript(self) -> bool | None: """Boolean indicating whether the characters in this |Font| appear as superscript. @@ -355,7 +362,7 @@ def superscript(self): return rPr.superscript @superscript.setter - def superscript(self, value): + def superscript(self, value: bool | None) -> None: rPr = self._element.get_or_add_rPr() rPr.superscript = value @@ -374,15 +381,33 @@ def underline(self) -> bool | WD_UNDERLINE | None: if rPr is None: return None val = rPr.u_val - return None if val == WD_UNDERLINE.INHERITED else 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): + def underline(self, value: bool | WD_UNDERLINE | None) -> None: rPr = self._element.get_or_add_rPr() - rPr.u_val = 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. + 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): + 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 @@ -391,7 +416,7 @@ def web_hidden(self): return self._get_bool_prop("webHidden") @web_hidden.setter - def web_hidden(self, value): + def web_hidden(self, value: bool | None) -> None: self._set_bool_prop("webHidden", value) def _get_bool_prop(self, name: str) -> bool | None: @@ -399,9 +424,9 @@ def _get_bool_prop(self, name: str) -> bool | None: rPr = self._element.rPr if rPr is None: return None - return rPr._get_bool_val(name) + return rPr._get_bool_val(name) # pyright: ignore[reportPrivateUsage] - def _set_bool_prop(self, name, value): + 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) + rPr._set_bool_val(name, value) # pyright: ignore[reportPrivateUsage] diff --git a/src/docx/types.py b/src/docx/types.py index 9f5b7dae7..6097f740c 100644 --- a/src/docx/types.py +++ b/src/docx/types.py @@ -7,25 +7,6 @@ from docx.parts.story import StoryPart -class AbstractSimpleTypeMember(Protocol): - """A simple-type member is a valid value for a simple-type. - - The name *simple-type* comes from the ISO spec which refers to XML attribute value - types as simple types and gives those the prefix `ST_` in the XML Schema. Many are - enumerations but that is not strictly required. - """ - - -class AbstractSimpleType(Protocol): - """A simple-type can provide XML attribute value mapping.""" - - @classmethod - def from_xml(cls, xml_value: str) -> AbstractSimpleTypeMember: ... - - @classmethod - def to_xml(cls, value: AbstractSimpleTypeMember) -> str: ... - - class StoryChild(Protocol): """An object that can fulfill the `parent` role in a `Parented` class. diff --git a/tests/text/test_font.py b/tests/text/test_font.py index a17b2f1bd..fa927b6c8 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -1,105 +1,114 @@ +# pyright: reportPrivateUsage=false + """Test suite for the docx.text.run module.""" 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 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): - def it_provides_access_to_its_color_object(self, color_fixture): - font, color_, ColorFormat_ = color_fixture + """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) + 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 - font.name = value - assert font._element.xml == expected_xml - - def it_knows_its_size(self, size_get_fixture): - font, expected_value = size_get_fixture - assert font.size == expected_value - - def it_can_change_its_size(self, size_set_fixture): - font, value, expected_xml = size_set_fixture - 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 - - 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 - assert font.subscript == expected_value - - def it_can_change_whether_it_is_subscript(self, subscript_set_fixture): - font, value, expected_xml = subscript_set_fixture - font.subscript = value - assert font._element.xml == expected_xml + @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) - def it_knows_whether_it_is_superscript(self, superscript_get_fixture): - font, expected_value = superscript_get_fixture - assert font.superscript == expected_value + font.name = value - def it_can_change_whether_it_is_superscript(self, superscript_set_fixture): - font, value, expected_xml = superscript_set_fixture - font.superscript = value assert font._element.xml == expected_xml @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), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:sz{w:val=28}", Pt(14)), ], ) - def it_knows_its_underline_type( - self, r_cxml: str, expected_value: WD_UNDERLINE | bool | None - ): - font = Font(element(r_cxml), None) - assert font.underline is expected_value + 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_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", 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) - def it_knows_its_highlight_color(self, highlight_get_fixture): - font, expected_value = highlight_get_fixture - assert font.highlight_color is expected_value + font.size = 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=[ + @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), @@ -124,15 +133,18 @@ def it_can_change_its_highlight_color(self, highlight_set_fixture): ("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 + 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.fixture( - params=[ + @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}"), @@ -189,129 +201,39 @@ def bool_prop_get_fixture(self, request): 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.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) + 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) - 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 + setattr(font, prop_name, 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 + assert font._element.xml == expected_xml - @pytest.fixture( - params=[ + @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 subscript_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml)) - return font, expected_value + 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 - @pytest.fixture( - params=[ + @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"), @@ -338,30 +260,39 @@ def subscript_get_fixture(self, request): 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)) + 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) - return font, value, expected_xml - @pytest.fixture( - params=[ + font.subscript = value + + assert font._element.xml == expected_xml + + @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 superscript_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml)) - return font, expected_value + 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 - @pytest.fixture( - params=[ + @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"), @@ -388,16 +319,40 @@ def superscript_get_fixture(self, request): 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)) + 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) - return font, value, expected_xml - @pytest.fixture( - params=[ + font.superscript = value + + assert font._element.xml == expected_xml + + @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 + + @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"), @@ -416,20 +371,67 @@ def superscript_set_fixture(self, request): 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 + 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) + + font.underline = value + + assert font._element.xml == 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.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) + + font.highlight_color = value + + assert font._element.xml == expected_xml - # fixture components --------------------------------------------- + # -- fixtures ---------------------------------------------------- @pytest.fixture - def color_(self, request): + def color_(self, request: FixtureRequest): return instance_mock(request, ColorFormat) @pytest.fixture - def ColorFormat_(self, request, color_): + def ColorFormat_(self, request: FixtureRequest, color_: Mock): return class_mock(request, "docx.text.font.ColorFormat", return_value=color_) From 12a6bb8d32f35fe34f02b713cf7380752fd748a8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 8 Oct 2023 21:17:15 -0700 Subject: [PATCH 053/131] rfctr: remove obsolete inherit-from-`object` --- src/docx/enum/__init__.py | 2 +- src/docx/image/constants.py | 10 +++++----- src/docx/image/helpers.py | 2 +- src/docx/image/image.py | 2 +- src/docx/image/jpeg.py | 8 ++++---- src/docx/image/png.py | 8 ++++---- src/docx/image/tiff.py | 8 ++++---- src/docx/opc/constants.py | 8 ++++---- src/docx/opc/coreprops.py | 2 +- src/docx/opc/package.py | 4 ++-- src/docx/opc/part.py | 4 ++-- src/docx/opc/phys_pkg.py | 4 ++-- src/docx/opc/pkgreader.py | 10 +++++----- src/docx/opc/pkgwriter.py | 4 ++-- src/docx/opc/rel.py | 2 +- src/docx/oxml/simpletypes.py | 2 +- src/docx/oxml/text/run.py | 2 +- src/docx/oxml/xmlchemy.py | 4 ++-- src/docx/package.py | 2 +- src/docx/parts/numbering.py | 2 +- src/docx/section.py | 2 +- src/docx/shape.py | 2 +- src/docx/shared.py | 2 +- src/docx/styles/__init__.py | 2 +- src/docx/text/run.py | 2 +- tests/dml/test_color.py | 2 +- tests/image/test_bmp.py | 2 +- tests/image/test_gif.py | 2 +- tests/image/test_helpers.py | 2 +- tests/image/test_image.py | 6 +++--- tests/image/test_jpeg.py | 22 +++++++++++----------- tests/image/test_png.py | 16 ++++++++-------- tests/image/test_tiff.py | 20 ++++++++++---------- tests/opc/parts/test_coreprops.py | 2 +- tests/opc/test_coreprops.py | 2 +- tests/opc/test_oxml.py | 10 +++++----- tests/opc/test_package.py | 4 ++-- tests/opc/test_packuri.py | 2 +- tests/opc/test_part.py | 8 ++++---- tests/opc/test_phys_pkg.py | 8 ++++---- tests/opc/test_pkgreader.py | 10 +++++----- tests/opc/test_pkgwriter.py | 4 ++-- tests/opc/test_rel.py | 4 ++-- tests/opc/unitdata/rels.py | 4 ++-- tests/oxml/parts/test_document.py | 2 +- tests/oxml/test__init__.py | 8 ++++---- tests/oxml/test_ns.py | 2 +- tests/oxml/test_styles.py | 2 +- tests/oxml/test_table.py | 4 ++-- tests/oxml/test_xmlchemy.py | 22 +++++++++++----------- tests/parts/test_document.py | 2 +- tests/parts/test_hdrftr.py | 4 ++-- tests/parts/test_image.py | 2 +- tests/parts/test_numbering.py | 4 ++-- tests/parts/test_settings.py | 2 +- tests/parts/test_story.py | 2 +- tests/parts/test_styles.py | 2 +- tests/styles/test_latent.py | 4 ++-- tests/styles/test_style.py | 8 ++++---- tests/styles/test_styles.py | 2 +- tests/test_api.py | 2 +- tests/test_blkcntnr.py | 2 +- tests/test_document.py | 4 ++-- tests/test_package.py | 4 ++-- tests/test_settings.py | 2 +- tests/test_shape.py | 4 ++-- tests/test_shared.py | 6 +++--- tests/test_table.py | 12 ++++++------ tests/text/test_font.py | 2 +- tests/text/test_paragraph.py | 2 +- tests/text/test_parfmt.py | 2 +- tests/text/test_run.py | 4 ++-- tests/text/test_tabstops.py | 4 ++-- tests/unitdata.py | 2 +- tests/unitutil/cxml.py | 2 +- 75 files changed, 178 insertions(+), 178 deletions(-) diff --git a/src/docx/enum/__init__.py b/src/docx/enum/__init__.py index c0ba1a5a3..bfab52d36 100644 --- a/src/docx/enum/__init__.py +++ b/src/docx/enum/__init__.py @@ -1,7 +1,7 @@ """Enumerations used in python-docx.""" -class Enumeration(object): +class Enumeration: @classmethod def from_xml(cls, xml_val): return cls._xml_to_idx[xml_val] diff --git a/src/docx/image/constants.py b/src/docx/image/constants.py index 65286c647..729a828b2 100644 --- a/src/docx/image/constants.py +++ b/src/docx/image/constants.py @@ -1,7 +1,7 @@ """Constants specific the the image sub-package.""" -class JPEG_MARKER_CODE(object): +class JPEG_MARKER_CODE: """JPEG marker codes.""" TEM = b"\x01" @@ -97,7 +97,7 @@ def is_standalone(cls, marker_code): return marker_code in cls.STANDALONE_MARKERS -class MIME_TYPE(object): +class MIME_TYPE: """Image content types.""" BMP = "image/bmp" @@ -107,7 +107,7 @@ class MIME_TYPE(object): TIFF = "image/tiff" -class PNG_CHUNK_TYPE(object): +class PNG_CHUNK_TYPE: """PNG chunk type names.""" IHDR = "IHDR" @@ -115,7 +115,7 @@ class PNG_CHUNK_TYPE(object): IEND = "IEND" -class TIFF_FLD_TYPE(object): +class TIFF_FLD_TYPE: """Tag codes for TIFF Image File Directory (IFD) entries.""" BYTE = 1 @@ -136,7 +136,7 @@ class TIFF_FLD_TYPE(object): TIFF_FLD = TIFF_FLD_TYPE -class TIFF_TAG(object): +class TIFF_TAG: """Tag codes for TIFF Image File Directory (IFD) entries.""" IMAGE_WIDTH = 0x0100 diff --git a/src/docx/image/helpers.py b/src/docx/image/helpers.py index 8532a2e58..647b30851 100644 --- a/src/docx/image/helpers.py +++ b/src/docx/image/helpers.py @@ -6,7 +6,7 @@ LITTLE_ENDIAN = "<" -class StreamReader(object): +class StreamReader: """Wraps a file-like object to provide access to structured data from a binary file. Byte-order is configurable. `base_offset` is added to any base value provided to diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 2d2f2c97d..945432872 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -185,7 +185,7 @@ def read_32(stream): raise UnrecognizedImageError -class BaseImageHeader(object): +class BaseImageHeader: """Base class for image header subclasses like |Jpeg| and |Tiff|.""" def __init__(self, px_width, px_height, horz_dpi, vert_dpi): diff --git a/src/docx/image/jpeg.py b/src/docx/image/jpeg.py index 92770e948..b0114a998 100644 --- a/src/docx/image/jpeg.py +++ b/src/docx/image/jpeg.py @@ -61,7 +61,7 @@ def from_stream(cls, stream): return cls(px_width, px_height, horz_dpi, vert_dpi) -class _JfifMarkers(object): +class _JfifMarkers: """Sequence of markers in a JPEG file, perhaps truncated at first SOS marker for performance reasons.""" @@ -125,7 +125,7 @@ def sof(self): raise KeyError("no start of frame (SOFn) marker in image") -class _MarkerParser(object): +class _MarkerParser: """Service class that knows how to parse a JFIF stream and iterate over its markers.""" @@ -152,7 +152,7 @@ def iter_markers(self): start = segment_offset + marker.segment_length -class _MarkerFinder(object): +class _MarkerFinder: """Service class that knows how to find the next JFIF marker in a stream.""" def __init__(self, stream): @@ -239,7 +239,7 @@ def _MarkerFactory(marker_code, stream, offset): return marker_cls.from_stream(stream, marker_code, offset) -class _Marker(object): +class _Marker: """Base class for JFIF marker classes. Represents a marker and its segment occuring in a JPEG byte stream. diff --git a/src/docx/image/png.py b/src/docx/image/png.py index bfbbaf30d..dd3cf819e 100644 --- a/src/docx/image/png.py +++ b/src/docx/image/png.py @@ -32,7 +32,7 @@ def from_stream(cls, stream): return cls(px_width, px_height, horz_dpi, vert_dpi) -class _PngParser(object): +class _PngParser: """Parses a PNG image stream to extract the image properties found in its chunks.""" def __init__(self, chunks): @@ -89,7 +89,7 @@ def _dpi(units_specifier, px_per_unit): return 72 -class _Chunks(object): +class _Chunks: """Collection of the chunks parsed from a PNG image stream.""" def __init__(self, chunk_iterable): @@ -126,7 +126,7 @@ def _find_first(self, match): return None -class _ChunkParser(object): +class _ChunkParser: """Extracts chunks from a PNG image stream.""" def __init__(self, stream_rdr): @@ -176,7 +176,7 @@ def _ChunkFactory(chunk_type, stream_rdr, offset): return chunk_cls.from_offset(chunk_type, stream_rdr, offset) -class _Chunk(object): +class _Chunk: """Base class for specific chunk types. Also serves as the default chunk type. diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py index 4e09db15d..b84d9f10f 100644 --- a/src/docx/image/tiff.py +++ b/src/docx/image/tiff.py @@ -34,7 +34,7 @@ def from_stream(cls, stream): return cls(px_width, px_height, horz_dpi, vert_dpi) -class _TiffParser(object): +class _TiffParser: """Parses a TIFF image stream to extract the image properties found in its main image file directory (IFD)""" @@ -119,7 +119,7 @@ def _make_stream_reader(cls, stream): return StreamReader(stream, endian) -class _IfdEntries(object): +class _IfdEntries: """Image File Directory for a TIFF image, having mapping (dict) semantics allowing "tag" values to be retrieved by tag code.""" @@ -149,7 +149,7 @@ def get(self, tag_code, default=None): return self._entries.get(tag_code, default) -class _IfdParser(object): +class _IfdParser: """Service object that knows how to extract directory entries from an Image File Directory (IFD)""" @@ -186,7 +186,7 @@ def _IfdEntryFactory(stream_rdr, offset): return EntryCls.from_stream(stream_rdr, offset) -class _IfdEntry(object): +class _IfdEntry: """Base class for IFD entry classes. Subclasses are differentiated by value type, e.g. ASCII, long int, etc. diff --git a/src/docx/opc/constants.py b/src/docx/opc/constants.py index 48e32e0ce..89d3c16cc 100644 --- a/src/docx/opc/constants.py +++ b/src/docx/opc/constants.py @@ -4,7 +4,7 @@ """ -class CONTENT_TYPE(object): +class CONTENT_TYPE: """Content type URIs (like MIME-types) that specify a part's format.""" BMP = "image/bmp" @@ -251,7 +251,7 @@ class CONTENT_TYPE(object): X_WMF = "image/x-wmf" -class NAMESPACE(object): +class NAMESPACE: """Constant values for OPC XML namespaces.""" DML_WORDPROCESSING_DRAWING = ( @@ -265,14 +265,14 @@ class NAMESPACE(object): WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" -class RELATIONSHIP_TARGET_MODE(object): +class RELATIONSHIP_TARGET_MODE: """Open XML relationship target modes.""" EXTERNAL = "External" INTERNAL = "Internal" -class RELATIONSHIP_TYPE(object): +class RELATIONSHIP_TYPE: AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" A_F_CHUNK = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" diff --git a/src/docx/opc/coreprops.py b/src/docx/opc/coreprops.py index 26e73fef6..2fd9a75c8 100644 --- a/src/docx/opc/coreprops.py +++ b/src/docx/opc/coreprops.py @@ -4,7 +4,7 @@ """ -class CoreProperties(object): +class CoreProperties: """Corresponds to part named ``/docProps/core.xml``, containing the core document properties for this document package.""" diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index 671c585de..b5bdc0e7c 100644 --- a/src/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -10,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 @@ -164,7 +164,7 @@ 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 diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index ed52f54cb..ad9abf7c9 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -14,7 +14,7 @@ from docx.package import Package -class Part(object): +class Part: """Base class for package parts. Provides common properties and methods, but intended to be subclassed in client code @@ -154,7 +154,7 @@ def _rel_ref_count(self, rId): return len([_rId for _rId in rIds if _rId == rId]) -class PartFactory(object): +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. diff --git a/src/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py index 71c096278..5ec32237c 100644 --- a/src/docx/opc/phys_pkg.py +++ b/src/docx/opc/phys_pkg.py @@ -7,7 +7,7 @@ from docx.opc.packuri import CONTENT_TYPES_URI -class PhysPkgReader(object): +class PhysPkgReader: """Factory for physical package reader objects.""" def __new__(cls, pkg_file): @@ -25,7 +25,7 @@ def __new__(cls, pkg_file): return super(PhysPkgReader, cls).__new__(reader_cls) -class PhysPkgWriter(object): +class PhysPkgWriter: """Factory for physical package writer objects.""" def __new__(cls, pkg_file): diff --git a/src/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py index b5459a61e..f00e7b5f0 100644 --- a/src/docx/opc/pkgreader.py +++ b/src/docx/opc/pkgreader.py @@ -7,7 +7,7 @@ from docx.opc.shared import CaseInsensitiveDict -class PackageReader(object): +class PackageReader: """Provides access to the contents of a zip-format OPC package via its :attr:`serialized_parts` and :attr:`pkg_srels` attributes.""" @@ -87,7 +87,7 @@ def _walk_phys_parts(phys_reader, srels, visited_partnames=None): yield (partname, blob, reltype, srels) -class _ContentTypeMap(object): +class _ContentTypeMap: """Value type providing dictionary semantics for looking up content type by part name, e.g. ``content_type = cti['/ppt/presentation.xml']``.""" @@ -131,7 +131,7 @@ def _add_override(self, partname, content_type): self._overrides[partname] = content_type -class _SerializedPart(object): +class _SerializedPart: """Value object for an OPC package part. Provides access to the partname, content type, blob, and serialized relationships @@ -168,7 +168,7 @@ def srels(self): return self._srels -class _SerializedRelationship(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 @@ -231,7 +231,7 @@ def target_partname(self): return self._target_partname -class _SerializedRelationships(object): +class _SerializedRelationships: """Read-only sequence of |_SerializedRelationship| instances corresponding to the relationships item XML passed to constructor.""" diff --git a/src/docx/opc/pkgwriter.py b/src/docx/opc/pkgwriter.py index 5506373be..75af6ac75 100644 --- a/src/docx/opc/pkgwriter.py +++ b/src/docx/opc/pkgwriter.py @@ -12,7 +12,7 @@ from docx.opc.spec import default_content_types -class PackageWriter(object): +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. @@ -52,7 +52,7 @@ def _write_pkg_rels(phys_writer, pkg_rels): phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) -class _ContentTypesItem(object): +class _ContentTypesItem: """Service class that composes a content types item ([Content_Types].xml) based on a list of parts. diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index 3155e2c6b..efac5e06b 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -108,7 +108,7 @@ def _next_rId(self): return rId_candidate -class _Relationship(object): +class _Relationship: """Value object for relationship to part.""" def __init__(self, rId: str, reltype, target, baseURI, external=False): diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index 10e683f15..4e8d91cba 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -17,7 +17,7 @@ from docx.shared import Length -class BaseSimpleType(object): +class BaseSimpleType: """Base class for simple-types.""" @classmethod diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 66c3537e1..f17d33845 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -231,7 +231,7 @@ def __str__(self) -> str: # Utility -class _RunContentAppender(object): +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. diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 2d4150de6..2ea985abc 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -112,7 +112,7 @@ def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, An value.populate_class_members(cls, key) -class BaseAttribute(object): +class BaseAttribute: """Base class for OptionalAttribute and RequiredAttribute. Provides common methods. @@ -268,7 +268,7 @@ def set_attr_value(obj, value): return set_attr_value -class _BaseChildElement(object): +class _BaseChildElement: """Base class for the child element classes corresponding to varying cardinalities, such as ZeroOrOne and ZeroOrMore.""" diff --git a/src/docx/package.py b/src/docx/package.py index 89ae47a09..12a166bf3 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -47,7 +47,7 @@ def _gather_image_parts(self): self.image_parts.append(rel.target_part) -class ImageParts(object): +class ImageParts: """Collection of |ImagePart| objects corresponding to images in the package.""" def __init__(self): diff --git a/src/docx/parts/numbering.py b/src/docx/parts/numbering.py index 31ba05e99..54a430c1b 100644 --- a/src/docx/parts/numbering.py +++ b/src/docx/parts/numbering.py @@ -21,7 +21,7 @@ def numbering_definitions(self): return _NumberingDefinitions(self._element) -class _NumberingDefinitions(object): +class _NumberingDefinitions: """Collection of |_NumberingDefinition| instances corresponding to the ```` elements in a numbering part.""" diff --git a/src/docx/section.py b/src/docx/section.py index 7e96dacd4..08cc36e9a 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -17,7 +17,7 @@ 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. diff --git a/src/docx/shape.py b/src/docx/shape.py index bc7818693..b91ecbf64 100644 --- a/src/docx/shape.py +++ b/src/docx/shape.py @@ -38,7 +38,7 @@ def _inline_lst(self): return body.xpath(xpath) -class InlineShape(object): +class InlineShape: """Proxy for an ```` element, representing the container for an inline graphical object.""" diff --git a/src/docx/shared.py b/src/docx/shared.py index 53a000d6f..304fce4a4 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -260,7 +260,7 @@ def write_only_property(f): return property(fset=f, doc=docstring) -class ElementProxy(object): +class ElementProxy: """Base class for lxml element proxy classes. An element proxy class is one whose primary responsibilities are fulfilled by diff --git a/src/docx/styles/__init__.py b/src/docx/styles/__init__.py index 514eee908..6358baf33 100644 --- a/src/docx/styles/__init__.py +++ b/src/docx/styles/__init__.py @@ -5,7 +5,7 @@ from typing import Dict -class BabelFish(object): +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.""" diff --git a/src/docx/text/run.py b/src/docx/text/run.py index d79b583aa..ec0e6c757 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -240,7 +240,7 @@ def underline(self, value: bool): self.font.underline = value -class _Text(object): +class _Text: """Proxy object wrapping `` element.""" def __init__(self, t_elm: CT_Text): diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index a461650f5..ea848e7d6 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -9,7 +9,7 @@ from ..unitutil.cxml import element, xml -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 diff --git a/tests/image/test_bmp.py b/tests/image/test_bmp.py index ed49db386..15b322b66 100644 --- a/tests/image/test_bmp.py +++ b/tests/image/test_bmp.py @@ -10,7 +10,7 @@ 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_ = ( diff --git a/tests/image/test_gif.py b/tests/image/test_gif.py index 3af8cfd9f..a533da04d 100644 --- a/tests/image/test_gif.py +++ b/tests/image/test_gif.py @@ -10,7 +10,7 @@ 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" diff --git a/tests/image/test_helpers.py b/tests/image/test_helpers.py index 8d3db4760..9192564dc 100644 --- a/tests/image/test_helpers.py +++ b/tests/image/test_helpers.py @@ -8,7 +8,7 @@ from docx.image.helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader -class DescribeStreamReader(object): +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) diff --git a/tests/image/test_image.py b/tests/image/test_image.py index addbe61fc..bd5ed0903 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -26,7 +26,7 @@ ) -class DescribeImage(object): +class DescribeImage: def it_can_construct_from_an_image_blob( self, blob_, BytesIO_, _from_stream_, stream_, image_ ): @@ -266,7 +266,7 @@ def width_prop_(self, request): return property_mock(request, Image, "width") -class Describe_ImageHeaderFactory(object): +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) @@ -300,7 +300,7 @@ def call_fixture(self, request): 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 d18fbc234..a558e1d4e 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -31,7 +31,7 @@ ) -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 @@ -40,7 +40,7 @@ def it_knows_its_default_ext(self): jpeg = Jpeg(None, None, None, None) assert jpeg.default_ext == "jpg" - class DescribeExif(object): + 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 @@ -54,7 +54,7 @@ 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 jfif = Jfif.from_stream(stream_) @@ -102,7 +102,7 @@ def stream_(self, request): 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_ ): @@ -228,7 +228,7 @@ def stream_(self, request): 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 @@ -256,7 +256,7 @@ 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" marker_code, offset, length = JPEG_MARKER_CODE.APP0, 0, 16 @@ -294,7 +294,7 @@ def dpi_fixture(self, request): 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_ ): @@ -388,7 +388,7 @@ def _tiff_from_exif_segment_(self, request, 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" marker_code, offset, length = JPEG_MARKER_CODE.SOF0, 0, 17 @@ -414,7 +414,7 @@ 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_) @@ -478,7 +478,7 @@ def stream_(self, request): 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_) @@ -520,7 +520,7 @@ def stream_(self, request): 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_ ): diff --git a/tests/image/test_png.py b/tests/image/test_png.py index dce3aec95..61e7fdbed 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -29,7 +29,7 @@ ) -class DescribePng(object): +class DescribePng: def it_can_construct_from_a_png_stream( self, stream_, _PngParser_, png_parser_, Png__init__ ): @@ -76,7 +76,7 @@ def stream_(self, request): 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_ ): @@ -156,7 +156,7 @@ def stream_(self, request): 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_ ): @@ -234,7 +234,7 @@ def stream_(self, request): 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_ ): @@ -328,7 +328,7 @@ 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) @@ -389,7 +389,7 @@ 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 = _Chunk.from_offset(chunk_type, None, None) @@ -397,7 +397,7 @@ def it_can_construct_from_a_stream_and_offset(self): 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) @@ -415,7 +415,7 @@ def from_offset_fixture(self): 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] diff --git a/tests/image/test_tiff.py b/tests/image/test_tiff.py index 08027ac1d..b7f37afe5 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -31,7 +31,7 @@ ) -class DescribeTiff(object): +class DescribeTiff: def it_can_construct_from_a_tiff_stream( self, stream_, _TiffParser_, tiff_parser_, Tiff__init_ ): @@ -79,7 +79,7 @@ def stream_(self, request): 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_, @@ -201,7 +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_, @@ -261,7 +261,7 @@ def stream_(self, request): return instance_mock(request, io.BytesIO) -class Describe_IfdParser(object): +class Describe_IfdParser: def it_can_iterate_through_the_directory_entries_in_an_IFD(self, iter_fixture): ( ifd_parser, @@ -304,7 +304,7 @@ def iter_fixture(self, _IfdEntryFactory_, ifd_entry_, ifd_entry_2_): 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) @@ -385,7 +385,7 @@ 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_ ): @@ -422,7 +422,7 @@ 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(io.BytesIO(bytes_), BIG_ENDIAN) @@ -430,7 +430,7 @@ def it_can_parse_an_ascii_string_IFD_entry(self): 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(io.BytesIO(bytes_), BIG_ENDIAN) @@ -438,7 +438,7 @@ def it_can_parse_a_short_int_IFD_entry(self): 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(io.BytesIO(bytes_), BIG_ENDIAN) @@ -446,7 +446,7 @@ def it_can_parse_a_long_int_IFD_entry(self): 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(io.BytesIO(bytes_), BIG_ENDIAN) diff --git a/tests/opc/parts/test_coreprops.py b/tests/opc/parts/test_coreprops.py index 0f528829a..1db650353 100644 --- a/tests/opc/parts/test_coreprops.py +++ b/tests/opc/parts/test_coreprops.py @@ -11,7 +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 diff --git a/tests/opc/test_coreprops.py b/tests/opc/test_coreprops.py index 73b9a1280..2978ad5ae 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -8,7 +8,7 @@ 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) diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index 2fc2a22db..0b3e5e36f 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -19,7 +19,7 @@ ) -class DescribeCT_Default(object): +class DescribeCT_Default: def it_provides_read_access_to_xml_values(self): default = a_Default().element assert default.extension == "xml" @@ -31,7 +31,7 @@ def it_can_construct_a_new_default_element(self): 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" @@ -43,7 +43,7 @@ def it_can_construct_a_new_override_element(self): 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" @@ -69,7 +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 = ( @@ -98,7 +98,7 @@ def it_can_generate_rels_file_xml(self): 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 diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 7bb6ae62e..7fdeaa422 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -24,7 +24,7 @@ ) -class DescribeOpcPackage(object): +class DescribeOpcPackage: def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): # mockery ---------------------- pkg_file = Mock(name="pkg_file") @@ -292,7 +292,7 @@ def Unmarshaller_(self, request): return class_mock(request, "docx.opc.package.Unmarshaller") -class DescribeUnmarshaller(object): +class DescribeUnmarshaller: def it_can_unmarshal_from_a_pkg_reader( self, pkg_reader_, diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index 272175795..987da69c4 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -5,7 +5,7 @@ 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 diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index f9b5f8732..163912154 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -21,7 +21,7 @@ ) -class DescribePart(object): +class DescribePart: def it_can_be_constructed_by_PartFactory( self, partname_, content_type_, blob_, package_, __init_ ): @@ -116,7 +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 @@ -263,7 +263,7 @@ def url_(self, request): return instance_mock(request, str) -class DescribePartFactory(object): +class DescribePartFactory: def it_constructs_part_from_selector_if_defined(self, cls_selector_fixture): # fixture ---------------------- ( @@ -419,7 +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_ ): diff --git a/tests/opc/test_phys_pkg.py b/tests/opc/test_phys_pkg.py index e33d5cf5a..6de0d868b 100644 --- a/tests/opc/test_phys_pkg.py +++ b/tests/opc/test_phys_pkg.py @@ -24,7 +24,7 @@ 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) @@ -63,13 +63,13 @@ 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") -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) @@ -125,7 +125,7 @@ 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) diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py index 10f3cf0b1..8e14f0e01 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -29,7 +29,7 @@ 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 ): @@ -276,7 +276,7 @@ def _walk_phys_parts(self, request): 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 ct_map = _ContentTypeMap.from_xml(content_types_xml) @@ -380,7 +380,7 @@ 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" @@ -398,7 +398,7 @@ 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( @@ -468,7 +468,7 @@ def it_raises_on_target_partname_when_external(self): 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 = ( diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py index 77d44ea63..747300f82 100644 --- a/tests/opc/test_pkgwriter.py +++ b/tests/opc/test_pkgwriter.py @@ -20,7 +20,7 @@ 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") @@ -127,7 +127,7 @@ def xml_for_(self, request): 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 diff --git a/tests/opc/test_rel.py b/tests/opc/test_rel.py index 65d756ec0..7b7a98dfe 100644 --- a/tests/opc/test_rel.py +++ b/tests/opc/test_rel.py @@ -12,7 +12,7 @@ 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" @@ -48,7 +48,7 @@ def it_should_have_relative_ref_for_internal_rel(self): 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", diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index 3b4e8fa4d..268216afe 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -6,7 +6,7 @@ from docx.opc.rel import Relationships -class BaseBuilder(object): +class BaseBuilder: """ Provides common behavior for all data builders. """ @@ -22,7 +22,7 @@ def with_indent(self, indent): return self -class RelationshipsBuilder(object): +class RelationshipsBuilder: """Builder class for test Relationships""" partname_tmpls = { diff --git a/tests/oxml/parts/test_document.py b/tests/oxml/parts/test_document.py index ec275941c..90b587674 100644 --- a/tests/oxml/parts/test_document.py +++ b/tests/oxml/parts/test_document.py @@ -5,7 +5,7 @@ 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() diff --git a/tests/oxml/test__init__.py b/tests/oxml/test__init__.py index 9331780b0..5f392df38 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -8,7 +8,7 @@ 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") assert isinstance(element, etree._Element) @@ -32,7 +32,7 @@ def it_adds_additional_namespace_declarations_when_supplied(self): 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) @@ -48,7 +48,7 @@ def whitespace_fixture(self): 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) @@ -83,7 +83,7 @@ def xml_bytes(self): ).encode("utf-8") -class DescribeRegisterElementCls(object): +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) diff --git a/tests/oxml/test_ns.py b/tests/oxml/test_ns.py index 7e1d659f9..cd493e6ca 100644 --- a/tests/oxml/test_ns.py +++ b/tests/oxml/test_ns.py @@ -5,7 +5,7 @@ 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 -" diff --git a/tests/oxml/test_styles.py b/tests/oxml/test_styles.py index a6748b4f0..7677a8a9e 100644 --- a/tests/oxml/test_styles.py +++ b/tests/oxml/test_styles.py @@ -7,7 +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) diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 2cddf925b..ecd0cf9d7 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -11,7 +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() @@ -46,7 +46,7 @@ def tc_raise_fixture(self, request): 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_ ): diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index 3242223d7..fca309851 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -24,7 +24,7 @@ from .unitdata.text import a_b, a_u, an_i, an_rPr -class DescribeBaseOxmlElement(object): +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 @@ -116,7 +116,7 @@ def rPr_bldr(self, children): 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) @@ -145,7 +145,7 @@ def element(self): 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 @@ -236,7 +236,7 @@ def xml_line_case(self, request): return line, other, differs -class DescribeChoice(object): +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 @@ -334,7 +334,7 @@ def parent_bldr(self, choice_tag=None): 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 @@ -348,7 +348,7 @@ def getter_fixture(self): return parent, oooChild -class DescribeOneOrMore(object): +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 @@ -433,7 +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 @@ -466,7 +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 @@ -518,7 +518,7 @@ def setter_fixture(self): return parent, value, expected_xml -class DescribeZeroOrMore(object): +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 @@ -604,7 +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 @@ -695,7 +695,7 @@ 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 diff --git a/tests/parts/test_document.py b/tests/parts/test_document.py index b9b50676a..3a86b5168 100644 --- a/tests/parts/test_document.py +++ b/tests/parts/test_document.py @@ -19,7 +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" diff --git a/tests/parts/test_hdrftr.py b/tests/parts/test_hdrftr.py index d153ad348..ee0cc7134 100644 --- a/tests/parts/test_hdrftr.py +++ b/tests/parts/test_hdrftr.py @@ -12,7 +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_ ): @@ -80,7 +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 3b6424fe4..acf0b0727 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -14,7 +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_ ): diff --git a/tests/parts/test_numbering.py b/tests/parts/test_numbering.py index 2783b9eea..7655206ec 100644 --- a/tests/parts/test_numbering.py +++ b/tests/parts/test_numbering.py @@ -9,7 +9,7 @@ from ..unitutil.mock import class_mock, instance_mock -class DescribeNumberingPart(object): +class DescribeNumberingPart: def it_provides_access_to_the_numbering_definitions(self, num_defs_fixture): ( numbering_part, @@ -54,7 +54,7 @@ def numbering_elm_(self, request): return instance_mock(request, CT_Numbering) -class Describe_NumberingDefinitions(object): +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 20ec8cdff..581cc6173 100644 --- a/tests/parts/test_settings.py +++ b/tests/parts/test_settings.py @@ -13,7 +13,7 @@ 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_ ): diff --git a/tests/parts/test_story.py b/tests/parts/test_story.py index 50bbf0953..b65abe8b7 100644 --- a/tests/parts/test_story.py +++ b/tests/parts/test_story.py @@ -16,7 +16,7 @@ from ..unitutil.mock import instance_mock, method_mock, property_mock -class DescribeStoryPart(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" diff --git a/tests/parts/test_styles.py b/tests/parts/test_styles.py index faad075b5..d0f63cfbb 100644 --- a/tests/parts/test_styles.py +++ b/tests/parts/test_styles.py @@ -11,7 +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 diff --git a/tests/styles/test_latent.py b/tests/styles/test_latent.py index 5ab235445..9479d6b44 100644 --- a/tests/styles/test_latent.py +++ b/tests/styles/test_latent.py @@ -7,7 +7,7 @@ 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() @@ -129,7 +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 diff --git a/tests/styles/test_style.py b/tests/styles/test_style.py index ffd9baf22..b24e02733 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -18,7 +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) @@ -94,7 +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 @@ -396,7 +396,7 @@ def unhide_set_fixture(self, request): return style, value, expected_xml -class DescribeCharacterStyle(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 base_style = style.base_style @@ -476,7 +476,7 @@ def StyleFactory_(self, request): return function_mock(request, "docx.styles.style.StyleFactory") -class DescribeParagraphStyle(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 diff --git a/tests/styles/test_styles.py b/tests/styles/test_styles.py index bdee6d2b5..ea9346bdc 100644 --- a/tests/styles/test_styles.py +++ b/tests/styles/test_styles.py @@ -12,7 +12,7 @@ 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 diff --git a/tests/test_api.py b/tests/test_api.py index acd3606d5..b6e6818b5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -9,7 +9,7 @@ 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) diff --git a/tests/test_blkcntnr.py b/tests/test_blkcntnr.py index 9da168e1d..6f1d28a46 100644 --- a/tests/test_blkcntnr.py +++ b/tests/test_blkcntnr.py @@ -12,7 +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_ diff --git a/tests/test_document.py b/tests/test_document.py index b5d58aefd..12e3361db 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -20,7 +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_ @@ -354,7 +354,7 @@ 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() diff --git a/tests/test_package.py b/tests/test_package.py index 978670b1d..eda5f0132 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -11,7 +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_ ): @@ -46,7 +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_ ): diff --git a/tests/test_settings.py b/tests/test_settings.py index a4dc5d786..9f430822d 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -7,7 +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 ): diff --git a/tests/test_shape.py b/tests/test_shape.py index 647ae683c..da307e48f 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -19,7 +19,7 @@ from .unitutil.mock import loose_mock -class DescribeInlineShapes(object): +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 @@ -70,7 +70,7 @@ def inline_shapes_with_parent_(self, request): 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 diff --git a/tests/test_shared.py b/tests/test_shared.py index 7a026c456..3fbe54b07 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -9,7 +9,7 @@ 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 @@ -63,7 +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) @@ -109,7 +109,7 @@ def units_fixture(self, request): 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, match=r"RGBColor\(\) takes three integer valu"): diff --git a/tests/test_table.py b/tests/test_table.py index e5f8a3c31..0ef273e3f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -23,7 +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() @@ -346,7 +346,7 @@ 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 @@ -585,7 +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 @@ -681,7 +681,7 @@ def table_prop_(self, request, 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 @@ -735,7 +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 @@ -900,7 +900,7 @@ def table_prop_(self, request, 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 diff --git a/tests/text/test_font.py b/tests/text/test_font.py index fa927b6c8..6a9da0223 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -19,7 +19,7 @@ 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): diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index ff980aa0d..a5db30da8 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -18,7 +18,7 @@ from ..unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock -class DescribeParagraph(object): +class DescribeParagraph: """Unit-test suite for `docx.text.run.Paragraph`.""" @pytest.mark.parametrize( diff --git a/tests/text/test_parfmt.py b/tests/text/test_parfmt.py index 5f9da996b..be31329e9 100644 --- a/tests/text/test_parfmt.py +++ b/tests/text/test_parfmt.py @@ -11,7 +11,7 @@ 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 diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 68976e874..3d5e82cd9 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -21,7 +21,7 @@ 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): @@ -171,7 +171,7 @@ def it_can_add_a_picture(self, add_picture_fixture): '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 diff --git a/tests/text/test_tabstops.py b/tests/text/test_tabstops.py index ca2122206..79d920c24 100644 --- a/tests/text/test_tabstops.py +++ b/tests/text/test_tabstops.py @@ -10,7 +10,7 @@ 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 @@ -144,7 +144,7 @@ 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 diff --git a/tests/unitdata.py b/tests/unitdata.py index c894c4796..31f0044d8 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -4,7 +4,7 @@ from docx.oxml.parser import parse_xml -class BaseBuilder(object): +class BaseBuilder: """ Provides common behavior for all data builders. """ diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index b88e67bd4..c7b7d172c 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -54,7 +54,7 @@ def nsdecls(*nspfxs): 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. From 9c9106f88abdf32daac2b93ccf7ef4b5622e4713 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Oct 2023 21:45:10 -0700 Subject: [PATCH 054/131] section: Section.iter_inner_content() * clean up section (more needs doing in feature/steps and tests) * acceptance test * unit test --- features/sct-section.feature | 5 + features/steps/section.py | 79 +++++++---- .../steps/test_files/sct-inner-content.docx | Bin 0 -> 12051 bytes pyrightconfig.json | 2 +- src/docx/oxml/document.py | 21 ++- src/docx/oxml/section.py | 133 +++++++++++++++++- src/docx/section.py | 20 +++ src/docx/table.py | 22 ++- tests/test_files/sct-inner-content.docx | Bin 0 -> 12051 bytes tests/test_section.py | 48 +++++++ typings/behave/__init__.pyi | 13 +- 11 files changed, 295 insertions(+), 48 deletions(-) create mode 100644 features/steps/test_files/sct-inner-content.docx create mode 100644 tests/test_files/sct-inner-content.docx 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/section.py b/features/steps/section.py index 4e0621056..6291d756e 100644 --- a/features/steps/section.py +++ b/features/steps/section.py @@ -1,6 +1,7 @@ """Step implementations for section-related features.""" from behave import given, then, when +from behave.runner import Context from docx import Document from docx.enum.section import WD_ORIENT, WD_SECTION @@ -13,36 +14,43 @@ @given("a Section object as section") -def given_a_Section_object_as_section(context): +def given_a_Section_object_as_section(context: Context): context.section = Document(test_docx("sct-section-props")).sections[-1] +@given("a Section object of a multi-section document as section") +def given_a_Section_object_of_a_multi_section_document_as_section(context: Context): + context.section = Document(test_docx("sct-inner-content")).sections[1] + + @given("a Section object {with_or_without} a distinct first-page header as section") -def given_a_Section_object_with_or_without_first_page_header(context, with_or_without): +def given_a_Section_object_with_or_without_first_page_header( + context: Context, with_or_without: str +): section_idx = {"with": 1, "without": 0}[with_or_without] context.section = Document(test_docx("sct-first-page-hdrftr")).sections[section_idx] @given("a section collection containing 3 sections") -def given_a_section_collection_containing_3_sections(context): +def given_a_section_collection_containing_3_sections(context: Context): document = Document(test_docx("doc-access-sections")) context.sections = document.sections @given("a section having known page dimension") -def given_a_section_having_known_page_dimension(context): +def given_a_section_having_known_page_dimension(context: Context): document = Document(test_docx("sct-section-props")) context.section = document.sections[-1] @given("a section having known page margins") -def given_a_section_having_known_page_margins(context): +def given_a_section_having_known_page_margins(context: Context): document = Document(test_docx("sct-section-props")) context.section = document.sections[0] @given("a section having start type {start_type}") -def given_a_section_having_start_type(context, start_type): +def given_a_section_having_start_type(context: Context, start_type: str): section_idx = { "CONTINUOUS": 0, "NEW_PAGE": 1, @@ -55,7 +63,7 @@ def given_a_section_having_start_type(context, start_type): @given("a section known to have {orientation} orientation") -def given_a_section_having_known_orientation(context, orientation): +def given_a_section_having_known_orientation(context: Context, orientation: str): section_idx = {"landscape": 0, "portrait": 1}[orientation] document = Document(test_docx("sct-section-props")) context.section = document.sections[section_idx] @@ -65,12 +73,14 @@ def given_a_section_having_known_orientation(context, orientation): @when("I assign {bool_val} to section.different_first_page_header_footer") -def when_I_assign_value_to_section_different_first_page_hdrftr(context, bool_val): +def when_I_assign_value_to_section_different_first_page_hdrftr( + context: Context, bool_val: str +): context.section.different_first_page_header_footer = eval(bool_val) @when("I set the {margin_side} margin to {inches} inches") -def when_I_set_the_margin_side_length(context, margin_side, inches): +def when_I_set_the_margin_side_length(context: Context, margin_side: str, inches: str): prop_name = { "left": "left_margin", "right": "right_margin", @@ -85,7 +95,7 @@ def when_I_set_the_margin_side_length(context, margin_side, inches): @when("I set the section orientation to {orientation}") -def when_I_set_the_section_orientation(context, orientation): +def when_I_set_the_section_orientation(context: Context, orientation: str): new_orientation = { "WD_ORIENT.PORTRAIT": WD_ORIENT.PORTRAIT, "WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE, @@ -95,17 +105,17 @@ def when_I_set_the_section_orientation(context, orientation): @when("I set the section page height to {y} inches") -def when_I_set_the_section_page_height_to_y_inches(context, y): +def when_I_set_the_section_page_height_to_y_inches(context: Context, y: str): context.section.page_height = Inches(float(y)) @when("I set the section page width to {x} inches") -def when_I_set_the_section_page_width_to_x_inches(context, x): +def when_I_set_the_section_page_width_to_x_inches(context: Context, x: str): context.section.page_width = Inches(float(x)) @when("I set the section start type to {start_type}") -def when_I_set_the_section_start_type_to_start_type(context, start_type): +def when_I_set_the_section_start_type_to_start_type(context: Context, start_type: str): new_start_type = { "None": None, "CONTINUOUS": WD_SECTION.CONTINUOUS, @@ -121,7 +131,7 @@ def when_I_set_the_section_start_type_to_start_type(context, start_type): @then("I can access a section by index") -def then_I_can_access_a_section_by_index(context): +def then_I_can_access_a_section_by_index(context: Context): sections = context.sections for idx in range(3): section = sections[idx] @@ -129,7 +139,7 @@ def then_I_can_access_a_section_by_index(context): @then("I can iterate over the sections") -def then_I_can_iterate_over_the_sections(context): +def then_I_can_iterate_over_the_sections(context: Context): sections = context.sections actual_count = 0 for section in sections: @@ -139,13 +149,13 @@ def then_I_can_iterate_over_the_sections(context): @then("len(sections) is 3") -def then_len_sections_is_3(context): +def then_len_sections_is_3(context: Context): sections = context.sections assert len(sections) == 3, "expected len(sections) of 3, got %s" % len(sections) @then("section.different_first_page_header_footer is {bool_val}") -def then_section_different_first_page_header_footer_is(context, bool_val): +def then_section_different_first_page_header_footer_is(context: Context, bool_val: str): actual = context.section.different_first_page_header_footer expected = eval(bool_val) assert actual == expected, ( @@ -154,49 +164,58 @@ def then_section_different_first_page_header_footer_is(context, bool_val): @then("section.even_page_footer is a _Footer object") -def then_section_even_page_footer_is_a_Footer_object(context): +def then_section_even_page_footer_is_a_Footer_object(context: Context): actual = type(context.section.even_page_footer).__name__ expected = "_Footer" assert actual == expected, "section.even_page_footer is a %s object" % actual @then("section.even_page_header is a _Header object") -def then_section_even_page_header_is_a_Header_object(context): +def then_section_even_page_header_is_a_Header_object(context: Context): actual = type(context.section.even_page_header).__name__ expected = "_Header" assert actual == expected, "section.even_page_header is a %s object" % actual @then("section.first_page_footer is a _Footer object") -def then_section_first_page_footer_is_a_Footer_object(context): +def then_section_first_page_footer_is_a_Footer_object(context: Context): actual = type(context.section.first_page_footer).__name__ expected = "_Footer" assert actual == expected, "section.first_page_footer is a %s object" % actual @then("section.first_page_header is a _Header object") -def then_section_first_page_header_is_a_Header_object(context): +def then_section_first_page_header_is_a_Header_object(context: Context): actual = type(context.section.first_page_header).__name__ expected = "_Header" assert actual == expected, "section.first_page_header is a %s object" % actual @then("section.footer is a _Footer object") -def then_section_footer_is_a_Footer_object(context): +def then_section_footer_is_a_Footer_object(context: Context): actual = type(context.section.footer).__name__ expected = "_Footer" assert actual == expected, "section.footer is a %s object" % actual @then("section.header is a _Header object") -def then_section_header_is_a_Header_object(context): +def then_section_header_is_a_Header_object(context: Context): actual = type(context.section.header).__name__ expected = "_Header" assert actual == expected, "section.header is a %s object" % actual +@then("section.iter_inner_content() produces the paragraphs and tables in section") +def step_impl(context: Context): + actual = [type(item).__name__ for item in context.section.iter_inner_content()] + expected = ["Table", "Paragraph", "Paragraph"] + assert actual == expected, f"expected: {expected}, got: {actual}" + + @then("section.{propname}.is_linked_to_previous is True") -def then_section_hdrftr_prop_is_linked_to_previous_is_True(context, propname): +def then_section_hdrftr_prop_is_linked_to_previous_is_True( + context: Context, propname: str +): actual = getattr(context.section, propname).is_linked_to_previous expected = True assert actual == expected, "section.%s.is_linked_to_previous is %s" % ( @@ -206,7 +225,7 @@ def then_section_hdrftr_prop_is_linked_to_previous_is_True(context, propname): @then("the reported {margin_side} margin is {inches} inches") -def then_the_reported_margin_is_inches(context, margin_side, inches): +def then_the_reported_margin_is_inches(context: Context, margin_side: str, inches: str): prop_name = { "left": "left_margin", "right": "right_margin", @@ -222,7 +241,9 @@ def then_the_reported_margin_is_inches(context, margin_side, inches): @then("the reported page orientation is {orientation}") -def then_the_reported_page_orientation_is_orientation(context, orientation): +def then_the_reported_page_orientation_is_orientation( + context: Context, orientation: str +): expected_value = { "WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE, "WD_ORIENT.PORTRAIT": WD_ORIENT.PORTRAIT, @@ -231,17 +252,17 @@ def then_the_reported_page_orientation_is_orientation(context, orientation): @then("the reported page width is {x} inches") -def then_the_reported_page_width_is_width(context, x): +def then_the_reported_page_width_is_width(context: Context, x: str): assert context.section.page_width == Inches(float(x)) @then("the reported page height is {y} inches") -def then_the_reported_page_height_is_11_inches(context, y): +def then_the_reported_page_height_is_11_inches(context: Context, y: str): assert context.section.page_height == Inches(float(y)) @then("the reported section start type is {start_type}") -def then_the_reported_section_start_type_is_type(context, start_type): +def then_the_reported_section_start_type_is_type(context: Context, start_type: str): expected_start_type = { "CONTINUOUS": WD_SECTION.CONTINUOUS, "EVEN_PAGE": WD_SECTION.EVEN_PAGE, diff --git a/features/steps/test_files/sct-inner-content.docx b/features/steps/test_files/sct-inner-content.docx new file mode 100644 index 0000000000000000000000000000000000000000..a94165f8d01889e70eee54627486286a5352cdad GIT binary patch literal 12051 zcmeHtg;!k3_H`3nf(D1+!Ce9b3GS}Jo!|r-w*+^02yVe8jk~+MyIXMQ>&$!eCX+Yo z`v-pSuGOczWS?7IYoA+H`yP2INT^o;7yujq03ZRZ|$$dug&0WWl5X`^@1t`@B-Zbf7k!v8TcMMVBOAyDsmlnhZ0qzVz8T4Nb@3u zFP>g;4;IJmh3aE;PvcWF#tQ`%h$sY0QVQ1VWmeU0pRxI5OL#Pp6U6~GZ~T`S9kaKz zb2FRtUPicY)?%5K1^PHXTe3Aa;YxErFp+^eqKA|aOfNTo1z1^gFPWsozmdxO7wxy-U;*lZQRyf@*cogzMMIs$uXmGPdi zcYER9>IM=3cz%Wi$p0-QUq|89oPc>I3ywHMa7gOf8C%*jGW@jv3&sD#@%PJLFOF_A z?_ffDf8z7#GuEUq--eSd%V;>dfHMaRrzs(gu`p{s^Yq9wJNrU=PhV_cd?IGl)jn0! zVKr9!6fa)oWpESx^sP>Z=6$0xAkMEbm)UvRb{#2Y`^x08pG<(a z;!rEb7LD-LRz9K`DfP&tA$e7H+9#En>qJl1j3nl%X>)N+H`qd6k!F9w`e9s81QY&> z1XlXUi1y`5?M~(Wm|B)FbLvYvJWD1E!(F0(@fS=udPdOlp02PpyX#x4ep*qBc4gWG!&5a;_epu8eeH+w%pc*jO*I%1>AO@Ua(u40;H}Jm zvu0P|c)JzM7*`Ac01ZrrKUniCUk(!$BIY?!dsgK3o!+~wyp(_||CqV&xX>i229G)K zRvc;=9mrX22`R<@7WmuM`eTeFCz4LSj_ zi4-nL;Y}mw*J?s#WSBG<%H`3aO@vX)2Yp5S)-~C_YwQ%c`+%(2Z2BHUMy22gcir_+ zdGa!|aOfDzZ>7xHe3}gQ(hu|)K}3!|r7r8<`lJ+t*I04*^9HjaN{36s8z3 ze8%w9S)B^Avzj4x%D1{lkD!aF%QEX>q0~yx13O(pWRtCCV(3oqIF^Ci=5{kpJRDZo zIi8xpm-LIZ^eD1)2}Fc)*Br6zr>|~R9?bOP_~nr5DGvih4|!#yPJy!dzJgfO7&Qn( z0=7OFD5$V-_@DWP<)qDe%Ex>!Kkq<@MV~WObo3IINO=rQBp46)=vnAYoF$C$O@dmc zTG~y^3ya?sL*244S0R1kM5lV~Aujc1Gz%aSDMTpl6Fvvw;itW~n|1!?w6I*SPjvgxi_O(&awBL9<3C@Y(2K9xAOkP__*~uNvU>tr&R)MwBJ6nap z$x;AB_Dh=XWvvi5)JS4a<0pJ+c@AyflKD^a;nTrKLtf0)cEd}{_^g&mh~M~_PZ5As z?^TL8nvJ`X_h%?god`|D`<@zounRm6Lad-&6`Kkme`pkXO0OX!6@g}mI#pN4tV3+m zBnkX$5saqTu~(qg^xaCfdsJ+ZT!_*)(dx_!zsNcM*n2f{vd+U(83OY#G0CaxR>k^w zBB&bjP?lv7Q@CVeU-TZmIB&$)5yo)|f3a)L%}x3gcs0bp2-lHDoq&|KLBf;rVs%d} zCn~tD%x1JoNI1=;(3&ApF?9OKn+~NIR{;Kt454qeJFXJ!5U@nLDjB&-P_S$h4&z-n zup=OX;qB0{6w44MOq{}sx<`#|$}-2RW7H7OC85JN!P@Q&?i#pfT{QYF+HVANe2qx@ zUgd7eqJFPLOB;0K=j&mRf6t)3p1pV0K+LEX`G_du41t;7a&LmbhofKMnE>+hrhvJXC6=5K`S1svr_`=VmmKVYx&qQPlox30C^c&IoNE_y%tk z{^r7``oj8pHv6^p$3$+iM8x*!a7|vdPlTzVvx(&TJxv}=bRaD;@z_u_WBB7*M7qV0 zcj;b^5ML9}!Yd%pf;c0%t;x3z-RtN?nL<0cI6B}5WpE;*d>K#%48y|(Y@`Q3lDP`u zXcAhK0?Y+X%t?`Gj>xf+PNuo;@7Hum{qS%Un#+_8g#-3?N@ z$JctDdmv9vE)84^ezG5Uhuv>MLW5{zZzuFv6RU|3n$J$()6oIS*tyloZkR5EP`2KH z84@rYRX>;ju;I1IDly~@o6w)e&k|5sX#lTN9$lwecP?gXPPKJCkQ!lfWo!`n31E_5 z+Gl*an> z{f)i&D=O{?1zB!Fyr2@2t{yZpS(C~Q9q1Bk&UigObiO#T z111I#MdBdtaPv7@SKx4b`~V<2yN=-uMD3FT#>i(tQR5=I5|crCczmI%Ty>rOzCnn* z*ACOj)?$jf9C4$Cj(Do^3cr=N`(wnXgS66bpflA(%1n<>d?OpyBtRq0ceAHwdVD^$ zlpJvazAuyCQr8mrakh;s*jolyh#WGI_-kMH2C}xj8LyK{eNCzwLl7xMmduhlKn~MF zX9?A|;%{|z%*iWiwSRxpE8IT!elibTsUtLDR4Z$?AE%2gVo`*pGHOxxG)mu=q-}yE zZxA=M0+k~P-(7#S3C67Z_=>kaP=-9g$Xw`{wEL`=F9ilI*i?0xJm_qe)&PMdI5kCD zLxSntw}9^kg2L~(S*U#NxF-E*A=3X4BFkiZ(UF{qb!5X^7PXrCFLR+?YY%C=} z^y7(v^cXXq;o)MlK}S`R{(0!d6kKJ$v1QeeicxprKBfBmlAK`t5W^rQK}Vv1;?EI=fg6p9@E?8G;x!`2vOZjnBts%GkTD^+%g4a2iNY<3SeIctMjF}l&J z8Xq4c&9DUQ>r%fUa*bOBu--yPjd}W-luN6Wp$6yf%X6~6$*>*oP+cT_>~|+{5d1mXg9yE@OB&q)F=*TTl%D)z zSk_Nup*<^i61#F#SE{UaYm@j=eDPXQ6xAAa+;A&j*>h`2=^7ZtJY+RPw_rhAn!jP@ zBh0WUjb6tTx_UNCz-O#m!TsF+&q%xe1e;$h(6xthEHoPR4dni*eHj zHRKF)=i9Fw=r7Sj*q58hPhK@x8|guw^(rra{IN&a;8-4AsCZ}TeN3n(Xj<9rLAVb@ zo1V6Hp|;%T%+f~ofhpQSpFzdoD=&$%5c68y z>Tq#wRB61}nDa_Bp@Og}n#MBKtx;OLgPROx4ayMEUd_a-E~!JFYp=8Bc)@Q{n86H7 zi+F$f`%XlJQ{RrdvCE2HT;B1J){M?peU|3>CbnR>Ms=cq40E;ofT)alzB=k=yk>yO z7~!Y+nl_WkLPP@wSmEANw0&$CyJISjb&OxRC<(6x zx=vgLhEOEq;`ymu9Lf~UZi65RtU+-=HwYL=IJ ziWKh`1G|j#4pu9%rcaKGKSSl_*39STsTDainbfeK)}tg;`+qHKR@*LGc9)R7UgUM+StfkETEZ3;gQdenbKi%0+sxYD zh)S`x@a^2C%8qha+^kbd7`jJk$2ac$_;^Nl`L&WC2!@sY1wyH!s`S^d(+>MCg|qtp zlVJtSu;u8{BrntL4w%WWWsb1Cr^I7Y<+J2;cJB@I^U;*@qj-*}wL}r9V+()>(ZPkJ z(8tdoP+VjdhmEO&{q;*_gyjqnxZp}1b5!HQ9(6eYPVt+k*+K+3Ajh88mi1aOl7Xro z_B{|X=oPdo0z;re3h|oI>J9D~*VW6;x=W4%T*PgbnWKAE>P zQ22xscmprEP4|VHPn_xHgZX`R&{dRxcY)Q;40t%)66O1PTABX)FZ)^P&Gk5md#^bT zn?eYq3r5vV*lAXWvAr#*3qY)!h>2YA^kZ&`@B}%YaZ_iTC}~Ey+k&Dw0~=Mx71*%>s&8s>?bF9~4Czv%=j7h6uRo5%FsI#FbE2zX6V*bx8yjR3)Q zn(qN+d$A6?i%An_aXTX1)pURln*(0Iy)?U41?~~0kw(|7!LjG?ooW_~#R8yALEC95 z*2d?XP@iMIPc;Oadz<89N`yQ9mHNl>@{U1Rn)#S74va|kqx%epq(vbx+LjJKGNg(b zaDj$J_%-!}hbR*8D?)~v6;o>7ik~{SIzIC&U6uhD;1-=74Q|VuY`8YAM+|7}A$3t`;mSnz5Cget~S( z_Nc4f8<_tH+dE@Hg;`{KprvMlTOLn~fHCpu$Olvr%4n}zN?=v+e zrbuMhGk^PI2+8Y}LNgELkDN_-CL&m>VF2Qr>J6NbFHyJsE1a!8`rBei1h2fh#4FA! z#=6JDF34?Oo-JjDHiYLK&~4BG)jDVL7qJA!AYXe@HFGgBh0Tz)E9A| zG5EwpGV|OTu?8s8{YN%5O@_5lg4qxO){S5N$p$B51EpUc__5J_56Zna}>X-)Pn6VUAEFOIj-;-j~3U(X#zI;`X# zcOpB}Zks3|lZcgzxlwFZ;!B>pmRqpvR%L99=;ppKEKzrd@%bXjt7wsXc&T8gC=LGt z+hgZIGX#X0;~BYb5UrYG+ugqiNGd zQjmU~jZAe1DyS;NorC@isRSsw;2PBu?-WzS6C$Bb5n5H-i-L1b!O#08VU@=}huK6} ze}mLU@$7A=$ebSSdTe+6n>fT0@k#&Wy25ev8Ch%1??FQ$>(Og`nJNWuJc$S0x>L08 z7{A$7PGGO}`%v;BQ&cYp`aXPHU{l&sM6`UL36w}wtm$QhdyVIe#xCj!Q_Q%!1dVyi zRo?trh0HrL=xpQMLN6Q3y6LS(uyEU%7`;zOspQrJfKKO*B6h4xl!F1x;E^uAy4=(qsE1W8kF3E5Y57i+=YV%vD&_ z(OTQ7+&?}7)>asoO~6c@0W%ftSEd@;*!{5{{Ew>Oc>`ae(QRO=BKe<)HwwkMrF@5m zQdyAr>}Y_FE<|ii@}s((BPiNZdnK)SiJSYVb?EB(KIVu#{cxXV*1JNm??o4y0EBp6 zZREge{LG4kIZ6Dr>(i@^ zn3*r%j=KD1Z#RnlEZDWazwT;lsxn~WkilR#Grc_Txv{@Gn=ZjasB6K5RNhWCmaRqT zCrB40nj5-CVVX$@=8f@7X z1bA7xwYv-@AX#=j!NyAL;EX@m4tu^{{lYIaO>|V?o+CGMPECUmoue-;c*Z0*9=d4c z>wYALcbWeAQvb6ThRsNz*!z}=8`}65@C^UAB-i3nqn`tQ!1mY!04U%de<<+wKo?8n zKh7eJUuEnUIMMx2G(pc_awm>SDjvS(bh8@2>zfCCTF2#zVHF1J!{nLJeY0u0EdX(e zjFgO7y)woikwT)+yPo(TJ@*n;(kj{R`cz1aJUQmP-hCD3i7+NTyRYiIZWysymdVO8 za%opgIm;CxeEXPXvd`l$|k+T~D z8L&!@1t0!toayR&ZU;3dvVCLss}WqUWZ@4FYj=DJ!9z+i<3bpF*&%eZ4;Fa$DzIXt z8xihph2QB0#T<%~BZesNS4@)qlF6fK5#!r({3@@-76X^cf;m&8brLq*mItln0+emcC;EbvFx zEy~QEG2gub;c>f^_{)NePGwV(T@U>D=q~N)<|OSV00-=zY*DrzG7h$$Yzpuo5_7=c_ODQ9k|hr%w@eJ17neO<#&Ak*qPrz+PVLQ`OCA%5}bc|v}mL$eW1JPm>UU+hbbtZHB!TzRicuU2RqX9~$)h48` zWkQl38{Y{Qi+Ajtk5B4GX$d2K(xujpjX2^Q@j6pZ=s}Z0A0mUI(NXPZB_&}lx$&k= z>q%uP-^=*c>k_LrmrBf`c9<60b?)nYGc6pQQx5-8HBO2QT>y0wHzbZ=g}tx$tOHvi zl!z8Gh@p^eOSrgY;Wihy_HwtnF}!!tdmiBx%p*4&*mk=QDMMVDhXt8%`3fk*;v-QXS|` z5JzdJ|oW(6K(d|P&*^SVs5fP*WPvp=r(et7$ZU9@d4>5#AvR`v!|i-o#=1A z{*Fb?9_NyML4mCCCHo?yd=EyzAm+n40{f=Q|$bmU)GY@xj_-%=oA)bCY)-A2tdaG!t< zpTtTR+K+)Hx&?!nefGsKid*l2kQ>?Z!Rn`J;${#12*27|upSD$qpa`W0RI|n|Lb#j zYS@X5*aVlCVn9HyGRP%fAM}RNr8+wJn=^_=N7M zsm(Z|N@p!Q;WOajBkSggc0It7O#jyOUVE?)n7>u8XOnbfU2ZWbhwM6N?Mzx|xukHz z)YZ3UYluHazj4XR$lJXTCp>WGoIzoEZd9}77`Ir-L--MX!Ts&%`2Kg}x%_|#X>DMcfvF_Tcx#sut>W+S^X1cFHIp|+|D(W-A`S|<>w#z@rU?_?C4dmaPC1)!}r?YUry+!khi6#0ntZFOTYE1hvVnB72 zk6cqgQoI_k0^yVlh7tAw?z=q63;IbkT4??k6v}$9$(6+g5HU$d1^vJ+YcG%}l|M#c z(0oThqsj4w!&LlNJs6=J`2LS>FSQojhJ1Vxs6S>3LTMDBe!(i#W(-HA`Q8J5e;oiu z2>#keBb?!ffcCSiWI-#BctNB$)}K|=5qSQHnDZOH2uDI-2#KKIt3QVJ=Z$`jhvdJ9 zc+}O7Cg^v?nJIXWnkgt5C{F_YpRxT5^0Nq!nFtI2pV4PA@})?{HZL4r|1jlkaymq5 z0Zsw;QLYuNu^l)GK*w?E><=G&5vhr?hG9`qteTjSNVmU5UcCbCRMroGYJ@Ww+&vo( zz1n+nBi#2C&*^-dOb}MZgy`G2;ypIG*RcchtB7LKf zV#ds~UH;@2UTA5TGUH*dYnAXoER*+84D4Qw-l2|LtH2X{hg$Q#19L6VNu8v!dyz@l@L=%w_nSp(rc*G^O|- z6TJey>$icwncoHh%$9*_qs=hERWcU7Zmy+OrIX@#+VWko??Oy^+LZ9+n{4Wtwoy&u zIJ9ZoJ4v{EKV&iR)igBv4pj1T)bj`T+(UINGY4;Q^2&la$3pnq8TA9K5)w-qo&D7ykzVi>ooVuvM`Ui@|~l~YO;lb4_*zp^GD88D;8m@HZ4*Zxx60L3XAVV9Ww5@ zZ<-a3OJDCl4Nlzc%w7=9xMV>@4B5C(@yevBhmGGex+E``-!0f}J)8Gq8#g(5nXj;( z8ay|88mGugU{8XKg^y>q;mg&`mmH*jd({^o(nw1AA$5(_rBK zsP^OF`9B+ah!^x=ap|A4hrylxcKt&xvAopZ9sE7l@mF*V_`St1nUB8%|DFW)E3gXu z*8l&P685{N-xGuW^0fy}k^ZkVq2J-ZCmQ?(pGEpN{P(nj-(CFvn&vMTOWc61#iOsH~ep!@bCEFb;G~#1GxXj|E41T?&0qO*yjny+8(1Xaq5E0-f9-;rJOBUy literal 0 HcmV?d00001 diff --git a/pyrightconfig.json b/pyrightconfig.json index 7808af007..161e49d2b 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -16,6 +16,6 @@ "reportUnnecessaryTypeIgnoreComment": true, "stubPath": "./typings", "typeCheckingMode": "strict", - "useLibraryCodeForTypes": false, + "useLibraryCodeForTypes": true, "verboseOutput": true } diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index f84c0f7cc..c4894f601 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -1,5 +1,10 @@ """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 @@ -9,10 +14,18 @@ class CT_Document(BaseOxmlElement): 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") + 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): diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index 394033e18..d1dc33ce2 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -3,11 +3,17 @@ from __future__ import annotations from copy import deepcopy -from typing import Callable +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, @@ -15,7 +21,9 @@ ZeroOrMore, ZeroOrOne, ) -from docx.shared import Length +from docx.shared import Length, lazyproperty + +BlockElement: TypeAlias = Union[CT_P, CT_Tbl] class CT_HdrFtr(BaseOxmlElement): @@ -251,6 +259,14 @@ def header(self, value: int | Length | None): 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 @@ -414,3 +430,116 @@ class CT_SectType(BaseOxmlElement): 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/section.py b/src/docx/section.py index 08cc36e9a..f72b60867 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -6,14 +6,18 @@ from docx.blkcntnr import BlockItemContainer 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 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 @@ -150,6 +154,18 @@ def header_distance(self) -> Length | None: def header_distance(self, value: int | Length | None): self._sectPr.header = value + 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 @@ -203,6 +219,10 @@ def page_width(self) -> Length | None: def page_width(self, value: Length | None): self._sectPr.page_width = value + @property + 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 diff --git a/src/docx/table.py b/src/docx/table.py index b05bffefc..13cc5b7bf 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -1,5 +1,9 @@ """The |Table| object and related proxy classes.""" +from __future__ import annotations + +from typing import List, Tuple, overload + from docx.blkcntnr import BlockItemContainer from docx.enum.style import WD_STYLE_TYPE from docx.oxml.simpletypes import ST_Merge @@ -85,7 +89,7 @@ def row_cells(self, row_idx): return self._cells[start:end] @lazyproperty - def rows(self): + def rows(self) -> _Rows: """|_Rows| instance containing the sequence of rows in this table.""" return _Rows(self._tbl, self) @@ -221,7 +225,7 @@ def tables(self): return super(_Cell, self).tables @property - def text(self): + 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 @@ -349,7 +353,7 @@ def __init__(self, tr, parent): self._tr = self._element = tr @property - def cells(self): + def cells(self) -> Tuple[_Cell]: """Sequence of |_Cell| instances corresponding to cells in this row.""" return tuple(self.table.row_cells(self._index)) @@ -394,8 +398,16 @@ 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): diff --git a/tests/test_files/sct-inner-content.docx b/tests/test_files/sct-inner-content.docx new file mode 100644 index 0000000000000000000000000000000000000000..a94165f8d01889e70eee54627486286a5352cdad GIT binary patch literal 12051 zcmeHtg;!k3_H`3nf(D1+!Ce9b3GS}Jo!|r-w*+^02yVe8jk~+MyIXMQ>&$!eCX+Yo z`v-pSuGOczWS?7IYoA+H`yP2INT^o;7yujq03ZRZ|$$dug&0WWl5X`^@1t`@B-Zbf7k!v8TcMMVBOAyDsmlnhZ0qzVz8T4Nb@3u zFP>g;4;IJmh3aE;PvcWF#tQ`%h$sY0QVQ1VWmeU0pRxI5OL#Pp6U6~GZ~T`S9kaKz zb2FRtUPicY)?%5K1^PHXTe3Aa;YxErFp+^eqKA|aOfNTo1z1^gFPWsozmdxO7wxy-U;*lZQRyf@*cogzMMIs$uXmGPdi zcYER9>IM=3cz%Wi$p0-QUq|89oPc>I3ywHMa7gOf8C%*jGW@jv3&sD#@%PJLFOF_A z?_ffDf8z7#GuEUq--eSd%V;>dfHMaRrzs(gu`p{s^Yq9wJNrU=PhV_cd?IGl)jn0! zVKr9!6fa)oWpESx^sP>Z=6$0xAkMEbm)UvRb{#2Y`^x08pG<(a z;!rEb7LD-LRz9K`DfP&tA$e7H+9#En>qJl1j3nl%X>)N+H`qd6k!F9w`e9s81QY&> z1XlXUi1y`5?M~(Wm|B)FbLvYvJWD1E!(F0(@fS=udPdOlp02PpyX#x4ep*qBc4gWG!&5a;_epu8eeH+w%pc*jO*I%1>AO@Ua(u40;H}Jm zvu0P|c)JzM7*`Ac01ZrrKUniCUk(!$BIY?!dsgK3o!+~wyp(_||CqV&xX>i229G)K zRvc;=9mrX22`R<@7WmuM`eTeFCz4LSj_ zi4-nL;Y}mw*J?s#WSBG<%H`3aO@vX)2Yp5S)-~C_YwQ%c`+%(2Z2BHUMy22gcir_+ zdGa!|aOfDzZ>7xHe3}gQ(hu|)K}3!|r7r8<`lJ+t*I04*^9HjaN{36s8z3 ze8%w9S)B^Avzj4x%D1{lkD!aF%QEX>q0~yx13O(pWRtCCV(3oqIF^Ci=5{kpJRDZo zIi8xpm-LIZ^eD1)2}Fc)*Br6zr>|~R9?bOP_~nr5DGvih4|!#yPJy!dzJgfO7&Qn( z0=7OFD5$V-_@DWP<)qDe%Ex>!Kkq<@MV~WObo3IINO=rQBp46)=vnAYoF$C$O@dmc zTG~y^3ya?sL*244S0R1kM5lV~Aujc1Gz%aSDMTpl6Fvvw;itW~n|1!?w6I*SPjvgxi_O(&awBL9<3C@Y(2K9xAOkP__*~uNvU>tr&R)MwBJ6nap z$x;AB_Dh=XWvvi5)JS4a<0pJ+c@AyflKD^a;nTrKLtf0)cEd}{_^g&mh~M~_PZ5As z?^TL8nvJ`X_h%?god`|D`<@zounRm6Lad-&6`Kkme`pkXO0OX!6@g}mI#pN4tV3+m zBnkX$5saqTu~(qg^xaCfdsJ+ZT!_*)(dx_!zsNcM*n2f{vd+U(83OY#G0CaxR>k^w zBB&bjP?lv7Q@CVeU-TZmIB&$)5yo)|f3a)L%}x3gcs0bp2-lHDoq&|KLBf;rVs%d} zCn~tD%x1JoNI1=;(3&ApF?9OKn+~NIR{;Kt454qeJFXJ!5U@nLDjB&-P_S$h4&z-n zup=OX;qB0{6w44MOq{}sx<`#|$}-2RW7H7OC85JN!P@Q&?i#pfT{QYF+HVANe2qx@ zUgd7eqJFPLOB;0K=j&mRf6t)3p1pV0K+LEX`G_du41t;7a&LmbhofKMnE>+hrhvJXC6=5K`S1svr_`=VmmKVYx&qQPlox30C^c&IoNE_y%tk z{^r7``oj8pHv6^p$3$+iM8x*!a7|vdPlTzVvx(&TJxv}=bRaD;@z_u_WBB7*M7qV0 zcj;b^5ML9}!Yd%pf;c0%t;x3z-RtN?nL<0cI6B}5WpE;*d>K#%48y|(Y@`Q3lDP`u zXcAhK0?Y+X%t?`Gj>xf+PNuo;@7Hum{qS%Un#+_8g#-3?N@ z$JctDdmv9vE)84^ezG5Uhuv>MLW5{zZzuFv6RU|3n$J$()6oIS*tyloZkR5EP`2KH z84@rYRX>;ju;I1IDly~@o6w)e&k|5sX#lTN9$lwecP?gXPPKJCkQ!lfWo!`n31E_5 z+Gl*an> z{f)i&D=O{?1zB!Fyr2@2t{yZpS(C~Q9q1Bk&UigObiO#T z111I#MdBdtaPv7@SKx4b`~V<2yN=-uMD3FT#>i(tQR5=I5|crCczmI%Ty>rOzCnn* z*ACOj)?$jf9C4$Cj(Do^3cr=N`(wnXgS66bpflA(%1n<>d?OpyBtRq0ceAHwdVD^$ zlpJvazAuyCQr8mrakh;s*jolyh#WGI_-kMH2C}xj8LyK{eNCzwLl7xMmduhlKn~MF zX9?A|;%{|z%*iWiwSRxpE8IT!elibTsUtLDR4Z$?AE%2gVo`*pGHOxxG)mu=q-}yE zZxA=M0+k~P-(7#S3C67Z_=>kaP=-9g$Xw`{wEL`=F9ilI*i?0xJm_qe)&PMdI5kCD zLxSntw}9^kg2L~(S*U#NxF-E*A=3X4BFkiZ(UF{qb!5X^7PXrCFLR+?YY%C=} z^y7(v^cXXq;o)MlK}S`R{(0!d6kKJ$v1QeeicxprKBfBmlAK`t5W^rQK}Vv1;?EI=fg6p9@E?8G;x!`2vOZjnBts%GkTD^+%g4a2iNY<3SeIctMjF}l&J z8Xq4c&9DUQ>r%fUa*bOBu--yPjd}W-luN6Wp$6yf%X6~6$*>*oP+cT_>~|+{5d1mXg9yE@OB&q)F=*TTl%D)z zSk_Nup*<^i61#F#SE{UaYm@j=eDPXQ6xAAa+;A&j*>h`2=^7ZtJY+RPw_rhAn!jP@ zBh0WUjb6tTx_UNCz-O#m!TsF+&q%xe1e;$h(6xthEHoPR4dni*eHj zHRKF)=i9Fw=r7Sj*q58hPhK@x8|guw^(rra{IN&a;8-4AsCZ}TeN3n(Xj<9rLAVb@ zo1V6Hp|;%T%+f~ofhpQSpFzdoD=&$%5c68y z>Tq#wRB61}nDa_Bp@Og}n#MBKtx;OLgPROx4ayMEUd_a-E~!JFYp=8Bc)@Q{n86H7 zi+F$f`%XlJQ{RrdvCE2HT;B1J){M?peU|3>CbnR>Ms=cq40E;ofT)alzB=k=yk>yO z7~!Y+nl_WkLPP@wSmEANw0&$CyJISjb&OxRC<(6x zx=vgLhEOEq;`ymu9Lf~UZi65RtU+-=HwYL=IJ ziWKh`1G|j#4pu9%rcaKGKSSl_*39STsTDainbfeK)}tg;`+qHKR@*LGc9)R7UgUM+StfkETEZ3;gQdenbKi%0+sxYD zh)S`x@a^2C%8qha+^kbd7`jJk$2ac$_;^Nl`L&WC2!@sY1wyH!s`S^d(+>MCg|qtp zlVJtSu;u8{BrntL4w%WWWsb1Cr^I7Y<+J2;cJB@I^U;*@qj-*}wL}r9V+()>(ZPkJ z(8tdoP+VjdhmEO&{q;*_gyjqnxZp}1b5!HQ9(6eYPVt+k*+K+3Ajh88mi1aOl7Xro z_B{|X=oPdo0z;re3h|oI>J9D~*VW6;x=W4%T*PgbnWKAE>P zQ22xscmprEP4|VHPn_xHgZX`R&{dRxcY)Q;40t%)66O1PTABX)FZ)^P&Gk5md#^bT zn?eYq3r5vV*lAXWvAr#*3qY)!h>2YA^kZ&`@B}%YaZ_iTC}~Ey+k&Dw0~=Mx71*%>s&8s>?bF9~4Czv%=j7h6uRo5%FsI#FbE2zX6V*bx8yjR3)Q zn(qN+d$A6?i%An_aXTX1)pURln*(0Iy)?U41?~~0kw(|7!LjG?ooW_~#R8yALEC95 z*2d?XP@iMIPc;Oadz<89N`yQ9mHNl>@{U1Rn)#S74va|kqx%epq(vbx+LjJKGNg(b zaDj$J_%-!}hbR*8D?)~v6;o>7ik~{SIzIC&U6uhD;1-=74Q|VuY`8YAM+|7}A$3t`;mSnz5Cget~S( z_Nc4f8<_tH+dE@Hg;`{KprvMlTOLn~fHCpu$Olvr%4n}zN?=v+e zrbuMhGk^PI2+8Y}LNgELkDN_-CL&m>VF2Qr>J6NbFHyJsE1a!8`rBei1h2fh#4FA! z#=6JDF34?Oo-JjDHiYLK&~4BG)jDVL7qJA!AYXe@HFGgBh0Tz)E9A| zG5EwpGV|OTu?8s8{YN%5O@_5lg4qxO){S5N$p$B51EpUc__5J_56Zna}>X-)Pn6VUAEFOIj-;-j~3U(X#zI;`X# zcOpB}Zks3|lZcgzxlwFZ;!B>pmRqpvR%L99=;ppKEKzrd@%bXjt7wsXc&T8gC=LGt z+hgZIGX#X0;~BYb5UrYG+ugqiNGd zQjmU~jZAe1DyS;NorC@isRSsw;2PBu?-WzS6C$Bb5n5H-i-L1b!O#08VU@=}huK6} ze}mLU@$7A=$ebSSdTe+6n>fT0@k#&Wy25ev8Ch%1??FQ$>(Og`nJNWuJc$S0x>L08 z7{A$7PGGO}`%v;BQ&cYp`aXPHU{l&sM6`UL36w}wtm$QhdyVIe#xCj!Q_Q%!1dVyi zRo?trh0HrL=xpQMLN6Q3y6LS(uyEU%7`;zOspQrJfKKO*B6h4xl!F1x;E^uAy4=(qsE1W8kF3E5Y57i+=YV%vD&_ z(OTQ7+&?}7)>asoO~6c@0W%ftSEd@;*!{5{{Ew>Oc>`ae(QRO=BKe<)HwwkMrF@5m zQdyAr>}Y_FE<|ii@}s((BPiNZdnK)SiJSYVb?EB(KIVu#{cxXV*1JNm??o4y0EBp6 zZREge{LG4kIZ6Dr>(i@^ zn3*r%j=KD1Z#RnlEZDWazwT;lsxn~WkilR#Grc_Txv{@Gn=ZjasB6K5RNhWCmaRqT zCrB40nj5-CVVX$@=8f@7X z1bA7xwYv-@AX#=j!NyAL;EX@m4tu^{{lYIaO>|V?o+CGMPECUmoue-;c*Z0*9=d4c z>wYALcbWeAQvb6ThRsNz*!z}=8`}65@C^UAB-i3nqn`tQ!1mY!04U%de<<+wKo?8n zKh7eJUuEnUIMMx2G(pc_awm>SDjvS(bh8@2>zfCCTF2#zVHF1J!{nLJeY0u0EdX(e zjFgO7y)woikwT)+yPo(TJ@*n;(kj{R`cz1aJUQmP-hCD3i7+NTyRYiIZWysymdVO8 za%opgIm;CxeEXPXvd`l$|k+T~D z8L&!@1t0!toayR&ZU;3dvVCLss}WqUWZ@4FYj=DJ!9z+i<3bpF*&%eZ4;Fa$DzIXt z8xihph2QB0#T<%~BZesNS4@)qlF6fK5#!r({3@@-76X^cf;m&8brLq*mItln0+emcC;EbvFx zEy~QEG2gub;c>f^_{)NePGwV(T@U>D=q~N)<|OSV00-=zY*DrzG7h$$Yzpuo5_7=c_ODQ9k|hr%w@eJ17neO<#&Ak*qPrz+PVLQ`OCA%5}bc|v}mL$eW1JPm>UU+hbbtZHB!TzRicuU2RqX9~$)h48` zWkQl38{Y{Qi+Ajtk5B4GX$d2K(xujpjX2^Q@j6pZ=s}Z0A0mUI(NXPZB_&}lx$&k= z>q%uP-^=*c>k_LrmrBf`c9<60b?)nYGc6pQQx5-8HBO2QT>y0wHzbZ=g}tx$tOHvi zl!z8Gh@p^eOSrgY;Wihy_HwtnF}!!tdmiBx%p*4&*mk=QDMMVDhXt8%`3fk*;v-QXS|` z5JzdJ|oW(6K(d|P&*^SVs5fP*WPvp=r(et7$ZU9@d4>5#AvR`v!|i-o#=1A z{*Fb?9_NyML4mCCCHo?yd=EyzAm+n40{f=Q|$bmU)GY@xj_-%=oA)bCY)-A2tdaG!t< zpTtTR+K+)Hx&?!nefGsKid*l2kQ>?Z!Rn`J;${#12*27|upSD$qpa`W0RI|n|Lb#j zYS@X5*aVlCVn9HyGRP%fAM}RNr8+wJn=^_=N7M zsm(Z|N@p!Q;WOajBkSggc0It7O#jyOUVE?)n7>u8XOnbfU2ZWbhwM6N?Mzx|xukHz z)YZ3UYluHazj4XR$lJXTCp>WGoIzoEZd9}77`Ir-L--MX!Ts&%`2Kg}x%_|#X>DMcfvF_Tcx#sut>W+S^X1cFHIp|+|D(W-A`S|<>w#z@rU?_?C4dmaPC1)!}r?YUry+!khi6#0ntZFOTYE1hvVnB72 zk6cqgQoI_k0^yVlh7tAw?z=q63;IbkT4??k6v}$9$(6+g5HU$d1^vJ+YcG%}l|M#c z(0oThqsj4w!&LlNJs6=J`2LS>FSQojhJ1Vxs6S>3LTMDBe!(i#W(-HA`Q8J5e;oiu z2>#keBb?!ffcCSiWI-#BctNB$)}K|=5qSQHnDZOH2uDI-2#KKIt3QVJ=Z$`jhvdJ9 zc+}O7Cg^v?nJIXWnkgt5C{F_YpRxT5^0Nq!nFtI2pV4PA@})?{HZL4r|1jlkaymq5 z0Zsw;QLYuNu^l)GK*w?E><=G&5vhr?hG9`qteTjSNVmU5UcCbCRMroGYJ@Ww+&vo( zz1n+nBi#2C&*^-dOb}MZgy`G2;ypIG*RcchtB7LKf zV#ds~UH;@2UTA5TGUH*dYnAXoER*+84D4Qw-l2|LtH2X{hg$Q#19L6VNu8v!dyz@l@L=%w_nSp(rc*G^O|- z6TJey>$icwncoHh%$9*_qs=hERWcU7Zmy+OrIX@#+VWko??Oy^+LZ9+n{4Wtwoy&u zIJ9ZoJ4v{EKV&iR)igBv4pj1T)bj`T+(UINGY4;Q^2&la$3pnq8TA9K5)w-qo&D7ykzVi>ooVuvM`Ui@|~l~YO;lb4_*zp^GD88D;8m@HZ4*Zxx60L3XAVV9Ww5@ zZ<-a3OJDCl4Nlzc%w7=9xMV>@4B5C(@yevBhmGGex+E``-!0f}J)8Gq8#g(5nXj;( z8ay|88mGugU{8XKg^y>q;mg&`mmH*jd({^o(nw1AA$5(_rBK zsP^OF`9B+ah!^x=ap|A4hrylxcKt&xvAopZ9sE7l@mF*V_`St1nUB8%|DFW)E3gXu z*8l&P685{N-xGuW^0fy}k^ZkVq2J-ZCmQ?(pGEpN{P(nj-(CFvn&vMTOWc61#iOsH~ep!@bCEFb;G~#1GxXj|E41T?&0qO*yjny+8(1Xaq5E0-f9-;rJOBUy literal 0 HcmV?d00001 diff --git a/tests/test_section.py b/tests/test_section.py index 4eb1f9192..333e755b7 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -8,6 +8,7 @@ import pytest +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 @@ -15,8 +16,11 @@ from docx.parts.hdrftr import FooterPart, HeaderPart 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.file import test_file from .unitutil.mock import ( FixtureRequest, Mock, @@ -241,6 +245,50 @@ def it_provides_access_to_its_default_header( ) assert header is header_ + 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"), [ diff --git a/typings/behave/__init__.pyi b/typings/behave/__init__.pyi index efc9b36ad..f8ffc2058 100644 --- a/typings/behave/__init__.pyi +++ b/typings/behave/__init__.pyi @@ -2,16 +2,15 @@ from __future__ import annotations from typing import Callable -from typing_extensions import Concatenate, ParamSpec, TypeAlias +from typing_extensions import TypeAlias from .runner import Context -_P = ParamSpec("_P") - -_ArgsStep: TypeAlias = Callable[Concatenate[Context, _P], None] -_NoArgsStep: TypeAlias = Callable[[Context], None] - -_Step: TypeAlias = _NoArgsStep | _ArgsStep[str] +_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]: ... From 7ab9d31390dca989355dca4f859a7891d0761336 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 10 Oct 2023 13:38:00 -0700 Subject: [PATCH 055/131] docs: small docs fixes --- docs/api/style.rst | 8 ++++---- docs/conf.py | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) 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/conf.py b/docs/conf.py index db96ecf52..06b428064 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -79,7 +79,9 @@ .. |_Cell| replace:: :class:`._Cell` -.. |_CharacterStyle| replace:: :class:`._CharacterStyle` +.. |_CharacterStyle| replace:: :class:`.CharacterStyle` + +.. |CharacterStyle| replace:: :class:`.CharacterStyle` .. |Cm| replace:: :class:`.Cm` @@ -147,7 +149,9 @@ .. |ParagraphFormat| replace:: :class:`.ParagraphFormat` -.. |_ParagraphStyle| replace:: :class:`._ParagraphStyle` +.. |_ParagraphStyle| replace:: :class:`.ParagraphStyle` + +.. |ParagraphStyle| replace:: :class:`.ParagraphStyle` .. |Part| replace:: :class:`.Part` From 6df147ebbc7d46f80273fd5819f35d0668ded98c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 10 Oct 2023 16:46:40 -0700 Subject: [PATCH 056/131] release: prepare v1.0.0 release --- HISTORY.rst | 1 + Makefile | 2 +- src/docx/__init__.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c465665d1..e6c5bd333 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,7 @@ Release History * 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/Makefile b/Makefile index 6d5dd85b8..0478b2bce 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ test: test-upload: sdist wheel $(TWINE) upload --repository testpypi dist/* -upload: sdist wheel +upload: clean sdist wheel $(TWINE) upload dist/* wheel: diff --git a/src/docx/__init__.py b/src/docx/__init__.py index 19fea99eb..f6f7b5b5a 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -1,6 +1,6 @@ from docx.api import Document # noqa -__version__ = "1.0.0rc1" +__version__ = "1.0.0" # register custom Part classes with opc package reader From 532ddd5c053d6c94f3b73b9ac5c65434e79931df Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 10 Oct 2023 17:05:45 -0700 Subject: [PATCH 057/131] docs: add .readthedocs.yaml --- .readthedocs.yaml | 19 +++++++++++++++++++ requirements-docs.txt | 1 + 2 files changed, 20 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..125538586 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +version: 2 + +# -- set the OS, Python version and other tools you might need -- +build: + os: ubuntu-22.04 + tools: + python: "3.9" + +# -- build documentation in the "docs/" directory with Sphinx -- +sphinx: + configuration: docs/conf.py + # -- fail on all warnings to avoid broken references -- + # fail_on_warning: true + +# -- package versions required to build your documentation -- +# -- see https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -- +python: + install: + - requirements: requirements-docs.txt diff --git a/requirements-docs.txt b/requirements-docs.txt index 357592950..11f9d2cd2 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,3 +1,4 @@ Sphinx==1.8.6 Jinja2==2.11.3 MarkupSafe==0.23 +-e . From 129dd83f4aef7991a6d21da892f2b4aa2e55de34 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 11 Oct 2023 09:41:01 -0700 Subject: [PATCH 058/131] fix: #1256 republish parse_xml() at docx.oxml Multiple downstream "extension" packages expect to find `OxmlElement` and `parse_xml()` at `docx.oxml`. Refactoring in v1.0.0 moved those to `docx.oxml.parser`, but republishing them at `docx.oxml` does not cause any immediate breakage. Republish those two callables at `docx.oxml` for v1.0.1. --- src/docx/oxml/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index bf8d00962..621ef279a 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -6,7 +6,7 @@ from __future__ import annotations from docx.oxml.drawing import CT_Drawing -from docx.oxml.parser import register_element_cls +from docx.oxml.parser import OxmlElement, parse_xml, register_element_cls from docx.oxml.shape import ( CT_Anchor, CT_Blip, @@ -34,6 +34,11 @@ CT_Text, ) +# -- `OxmlElement` and `parse_xml()` are not used in this module but several downstream +# -- "extension" packages expect to find them here and there's no compelling reason +# -- not to republish them here so those keep working. +__all__ = ["OxmlElement", "parse_xml"] + # --------------------------------------------------------------------------- # DrawingML-related elements From 96f80f792a3378dbc08ef9bf696e80756cf5f976 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 12 Oct 2023 15:05:08 -0700 Subject: [PATCH 059/131] hlink: add Hyperlink.fragment and .url --- docs/dev/analysis/features/text/hyperlink.rst | 253 ++++++++++-------- features/hlk-props.feature | 19 ++ features/steps/hyperlink.py | 44 +++ .../steps/test_files/par-hlink-frags.docx | Bin 0 -> 12071 bytes src/docx/oxml/text/hyperlink.py | 10 +- src/docx/text/hyperlink.py | 53 +++- tests/text/test_hyperlink.py | 52 +++- 7 files changed, 310 insertions(+), 121 deletions(-) create mode 100644 features/steps/test_files/par-hlink-frags.docx diff --git a/docs/dev/analysis/features/text/hyperlink.rst b/docs/dev/analysis/features/text/hyperlink.rst index 4dff91d20..cfd451fe1 100644 --- a/docs/dev/analysis/features/text/hyperlink.rst +++ b/docs/dev/analysis/features/text/hyperlink.rst @@ -2,31 +2,45 @@ Hyperlink ========= -Word allows hyperlinks to be placed in a document wherever paragraphs can appear. +Word allows a hyperlink to be placed in a document wherever a paragraph can appear. The +actual hyperlink element is a peer of |Run|. -The target (URL) of a hyperlink may be external, such as a web site, or internal, to -another location in the document. +The link may be to an external resource such as a web site, or internal, to another +location in the document. The link may also be a `mailto:` URI or a reference to a file +on an accessible local or network filesystem. 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. +applies the built-in `Hyperlink` character style to a newly inserted hyperlink. Like +other text, the hyperlink text may often be broken into multiple runs as a result of +edits in different "revision-save" editing sessions (between "Save" commands). Note that rendered page-breaks can occur in the middle of a hyperlink. A |Hyperlink| is a child of |Paragraph|, a peer of |Run|. +TODO: What about URL-encoding/decoding (like %20) behaviors, if any? + + 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 -("#"). +only an anchor. An anchor is more precisely known as a *URI fragment* in a web URL and +follows a hash mark ("#"). The fragment-separator hash character is not stored in the +XML. + +Note that the anchor and address are stored in two distinct attributes, so you need to +concatenate `.address` and `.anchor` like `f"{address}#{anchor}"` if you want the whole +thing. -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. +Also note that Word does not rigorously separate a fragment in a web URI so it may +appear as part of the address or separately in the anchor attribute, depending on how +the hyperlink was authored. Hyperlinks inserted using the dialog-box seem to separate it +and addresses typed into the document directly don't, based on my limited experience. .. highlight:: python @@ -49,6 +63,16 @@ concatenate `.address` and `.anchor` if you want the whole thing. >>> hyperlink.address 'https://google.com/' +**Access hyperlink fragment**:: + + >>> hyperlink.fragment + 'introduction' + +**Access hyperlink history (visited or not, True means not visited yet)**:: + + >>> hyperlink.history + True + **Access hyperlinks runs**:: >>> hyperlink.runs @@ -58,6 +82,11 @@ concatenate `.address` and `.anchor` if you want the whole thing. ] +**Access hyperlink URL**:: + + >>> hyperlink.url + 'https://us.com#introduction' + **Determine whether a hyperlink contains a rendered page-break**:: >>> hyperlink.contains_page_break @@ -68,29 +97,31 @@ concatenate `.address` and `.anchor` if you want the whole thing. >>> hyperlink.text 'an excellent Wikipedia article on ferrets' -**Add an external hyperlink**:: +**Add an external hyperlink** (not yet implemented):: >>> hyperlink = paragraph.add_hyperlink( - 'About', address='http://us.com', anchor='about' - ) + ... 'About', address='http://us.com', fragment='about' + ... ) >>> hyperlink >>> hyperlink.text 'About' >>> hyperlink.address 'http://us.com' - >>> hyperlink.anchor + >>> hyperlink.fragment 'about' + >>> hyperlink.url + 'http://us.com#about' **Add an internal hyperlink (to a bookmark)**:: - >>> hyperlink = paragraph.add_hyperlink('Section 1', anchor='Section_1') + >>> hyperlink = paragraph.add_hyperlink('Section 1', fragment='Section_1') >>> hyperlink.text 'Section 1' - >>> hyperlink.anchor + >>> hyperlink.fragment 'Section_1' >>> hyperlink.address - None + '' **Modify hyperlink properties**:: @@ -183,8 +214,8 @@ file, keyed by the w:hyperlink@r:id attribute:: -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 can contain multiple runs of text (and a whole lot of other stuff, at least +as far as the schema indicates):: @@ -256,97 +287,97 @@ Schema excerpt :: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/hlk-props.feature b/features/hlk-props.feature index 5472f49a3..a315318e0 100644 --- a/features/hlk-props.feature +++ b/features/hlk-props.feature @@ -19,6 +19,11 @@ Feature: Access hyperlink properties | one | True | + Scenario: Hyperlink.fragment has the URI fragment of the hyperlink + Given a hyperlink having a URI fragment + Then hyperlink.fragment is the URI fragment of the hyperlink + + Scenario Outline: Hyperlink.runs contains Run for each run in hyperlink Given a hyperlink having runs Then hyperlink.runs has length @@ -33,3 +38,17 @@ Feature: Access hyperlink properties Scenario: Hyperlink.text has the visible text of the hyperlink Given a hyperlink Then hyperlink.text is the visible text of the hyperlink + + + Scenario Outline: Hyperlink.url is the full URL of an internet hyperlink + Given a hyperlink having address
and fragment + Then hyperlink.url is + + Examples: Hyperlink.url cases + | address | fragment | url | + | '' | linkedBookmark | '' | + | https://foo.com | '' | https://foo.com | + | https://foo.com?q=bar | '' | https://foo.com?q=bar | + | http://foo.com/ | intro | http://foo.com/#intro | + | https://foo.com?q=bar#baz | '' | https://foo.com?q=bar#baz | + | court-exif.jpg | '' | court-exif.jpg | diff --git a/features/steps/hyperlink.py b/features/steps/hyperlink.py index 0596a3cd6..2bba31ed8 100644 --- a/features/steps/hyperlink.py +++ b/features/steps/hyperlink.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Dict, Tuple + from behave import given, then from behave.runner import Context @@ -18,6 +20,30 @@ def given_a_hyperlink(context: Context): context.hyperlink = document.paragraphs[1].hyperlinks[0] +@given("a hyperlink having a URI fragment") +def given_a_hyperlink_having_a_uri_fragment(context: Context): + document = Document(test_docx("par-hlink-frags")) + context.hyperlink = document.paragraphs[1].hyperlinks[0] + + +@given("a hyperlink having address {address} and fragment {fragment}") +def given_a_hyperlink_having_address_and_fragment( + context: Context, address: str, fragment: str +): + paragraph_idxs: Dict[Tuple[str, str], int] = { + ("''", "linkedBookmark"): 1, + ("https://foo.com", "''"): 2, + ("https://foo.com?q=bar", "''"): 3, + ("http://foo.com/", "intro"): 4, + ("https://foo.com?q=bar#baz", "''"): 5, + ("court-exif.jpg", "''"): 7, + } + paragraph_idx = paragraph_idxs[(address, fragment)] + document = Document(test_docx("par-hlink-frags")) + paragraph = document.paragraphs[paragraph_idx] + context.hyperlink = paragraph.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 = { @@ -61,6 +87,15 @@ def then_hyperlink_contains_page_break_is_value(context: Context, value: str): ), f"expected: {expected_value}, got: {actual_value}" +@then("hyperlink.fragment is the URI fragment of the hyperlink") +def then_hyperlink_fragment_is_the_URI_fragment_of_the_hyperlink(context: Context): + actual_value = context.hyperlink.fragment + expected_value = "linkedBookmark" + 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] @@ -86,3 +121,12 @@ def then_hyperlink_text_is_the_visible_text_of_the_hyperlink(context: Context): assert ( actual_value == expected_value ), f"expected: {expected_value}, got: {actual_value}" + + +@then("hyperlink.url is {value}") +def then_hyperlink_url_is_value(context: Context, value: str): + actual_value = context.hyperlink.url + expected_value = "" if value == "''" else value + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" diff --git a/features/steps/test_files/par-hlink-frags.docx b/features/steps/test_files/par-hlink-frags.docx new file mode 100644 index 0000000000000000000000000000000000000000..461d78ff9d743b5805de9151128fba97bc17bb12 GIT binary patch literal 12071 zcmeHtg%raK8b9X?yjn?w_hbWC}=DIEZ`Xc03Zb{<&0TtK>z?GFaQ81;2EU0n4PVY zsjZW~s++y3qb`%HjWtO&G$hR@03`VR|NH$9k3dyizim4Un#5K7EoyYFn$dQ45glZh zU;?Ac4%~AONcD%9?xx2UOh{!lh-d_BGD^0qMK<*=ztOoAYk2f(7s@?cfdrpeJxeb7 z+39sgZxcMKl{l6~kzURZ*6dBqc=DVOEacT4F@vuWUZs6b^TXD>21L-sN2*h}e`F)_ zzsF%)zG_~CN?fG0r+rlo_X0)l69!^z#WcrzTWPZACbN#?n&$EH{svW8A-kSC6Pa3h1!@>~oH!%96>b=>O9au^in36{^*aw)5B z=NQGAA)<#CQnJ5lW`Q!EXyEcbLNxJ9tj!am(nc@i1NgQS{jueRN+nPFn=o^-7ebQo z>-Z(DK=9EPP+4x5!<#d6X`wkoZ_gntKd%XOEtXn;9kN^ou!wOinGeHV*nAQu z?Ae^Zv$=)>0G^(p080NbNup@{+9U8fQv_QaBG@GL9ZanqnVEjb|1-t^Vf*{rrx(Y3 z19q?=2_N}A_>DFz&wYFTS&`XzWd8Xq+%s(%dCd74;Pm4I|I7@e?v9~U|JZo!h`VE& zr1Nr|?lFFX8gghe{M3zJhxT2QD%(^pAeY@d=@C#QhY+8%I40un8OgbhP@UI=DF z<%w*JQIYM76}sP5cVp{begx89FyLFWFjJ;Q8xq6XHA~eN&~sV)r6VSL6&Fn10y@035PrKFMi>)JrEzf=j$$0RRH>pM{5outZ zg`MxS^%_!kH%AFC-(_^tKt4nw3qu(Qlg9LI?AX)W&H*Y{xVCpw!Y0H@IbocjT+n!+ zkATilK-bTKzK}Csm@6fGQglch)_VJn+$@D~q8g)OQZ5fZ-z)_>PP`xAYrJ&4D#|`< zqX{xVpP++*NzMfx13nhQp&>?)~*a>w(youT-yh9N0721+l40}80fO2Ni z0!MO%QYG?6^;E;6s@1FAUE>uk>cfG47+>X8spS(}pB1RcfSn?LG5| zH(J$j2vdt>FebY~#h}>yr^N?AN;V@IMsG|N_SGKW#AFRYDTQ&Wb!Iy$Pf(DfS(j>w zoqp`h99kibIoo`rlT8@QfLNb8+udGbcUYkf?MKaEqoJOGMGVuW#ryklXMBHxQq1-$ z7&Lj9!ZtR(F)iqb&-UUNIZT+Llp3Ce%&e~nT69$8)8hI^PIQL{B}E2pscY%jKCC5p@|_K~oo2!eed2D%tu z`m>j`6cg8B&^lK%)k74lLXM?SjwCPTAoP|mi>8i4G6`~?4i9_`v(;OOHb~2cPpp&L zZBE^kNZI?jaOB5DXfYx)c!sMK{1hi749eF7dU7~ekGS!zSw!oPota}|HTB+y4*Kjt z`9Ke93f$PaMDt${^gbKaPpD3nmaMgO5AZ$qz4{1(-UK zn-MzcHhcQ!w7Ot)+s)#Y^Fe;`JPhc>usDD>a)4hHQcKj=FCTjigVD$Un#V*0Uo#m} zy(Hf>bSSXf*YumC;kORP@Q?Ac?Xpm=@BF<7t*0MMS&luoqdF%9-efx$5B5#^@e%57 z&;;ZchYw6}dB1cxND`^;Ep$mPcRgE%Y+3q7mF`Of_3UqQAX(nSg(3C@8+xg2I zER@D)*W2}vBM+~j*(Xas-6y7{`aTKmUiD0$>fA^Q1$x$c593I;GzNDxh6vU1Q5ltw ziHSGxG{ROA$5%9~2R|QUF~&qD zhr;l3eO60Ug{czDruWDv0K0LBL9k8>)d_niq7+o5nQxA>$H(uM$WA5XWkW=C2Pbd= zaiKdy?|X3uAF@?uYu4Nt{lvZSnOXhJ+du<>b}iM+Cgxe!=bhtEE#G%tOd>{f+HP5v z=oGr9HhrxIW9K4z9n3k%XegqrFooE%)b=`!8eJ8Wk2Ns(*qSl7TJPF{U zNY|5rNc(VW?uj94L>XiTX4%*7oE@i6hX#*`|J(%CoSP?&fgOA=2>^f%cGf?gypx5g zjVbd_$@;^^@4t}?dm)9^0(VLrSl{nHxNTU>F(!R#HO;wL?HAdsVwLwb!*UcfSIrV= zkm&#qofajARKI~Hip2VTJ%-r0OeT#udY4Fsjq&1pq^=))qpzA!OVMLPQ9}c}!>lq|j!oTxycEArPJdcd_XF}~aj))MklkcyV%~g&-m(igXVJ&E8O^!l; zhY~02VxH?MyrNGQfd5>%rBu~eOw_o0yyzAK5nbt!fOPJyVLB_HVB1yho)QIxywgJH zqhmiGZl4t?9ioY&gV;lDoHjyuJ_loWM@RLitsA}1jZ>u%szqFKe79?Vx;l+)eLu*ArWR?qXh{_aS9!CUA0%bEq#IDfZ#E}{2l!{f~9 zt=@MhgRyo8uF>7Cj~id6z3*@IcXLJd8qG^Hg*+}EroA8Tze655I3)!UM?FW};uU;n zTmGEW;thcK^eUD+7_C>%DOTwdG%X&YI|(_|`}aOH70d23RcnMOJMFMd?5*Z#i;>ql z7>LJOSTEWHx-25!?q!q|R-dXTQD?n>D>%GnOX_5z%{Oy=Y9Q!WN6i^8;*XrdMO#M@ z!2NAZ+0i<*Twu=N!V`HIuRFpMM|84h`kr^PM=nUbtcYIFKaMuEC;c`~nm2$KUXI3@{KC_4 zq#4$->+n*bAy|PT(F7=VNY-`QBbW+{9%`;WL=kd2LvMsY8k&|WuO-8B=3gLq4M7=j z*dkW8a#)*rFdr4T50P!Qx$usHg>87vR}rm-_ULp*x?ESWcyu&1NYdiSNPd(R-*|ta z#i*k)+3+m4FB*}bFeqV=>*d_s zP9P;i=s1^i=>zRtsbV`3A2%%8L_HGji7UDGZ3E)@L;-pMR%tvx2EozPlzaz4Sp? zn?!xZI5S@8vfmb~$}7=pQrSY=Zllftv1WX_gUj)r{gtgzofN}JWy)r8-p$f9yTsN_ zaSYEFlOa01#vZ8xlZsUdAxJHO?NP1O_IyE1(VZ3@r6zi(S@Pxm$RL&iJ}rxfC`%j> z$NDrMM4mC5AhsKr=ut0!vod+LQnb+AT_tWds!#S~9qJ&U@6eh6r=rxC$w@~AG~t&o zg4(S(P)t+grCto~Lo8}F9|qit?1g@ac~6W{-zksbj2N;BJZ7XgA5sjEm~YR{oxrUa z(U&W2+gK;LkY2b_kwmja8#CU>SM}Ney}ojaW*xMdW|+64FUeoC^b==VkjJQJ312>) zArLgxuVqWbKaZoWv1HdK?wFtHZqRg&uT;z4h-?4Gxs) zChX145~8RasEc|}k&Ts?KlacqZgi-GAy&Kv5{?bmuAWl$xEJrOrq9gSIM-P0b$$7b z{0&Qtvmuk3QMv%Brt=Je^pVt>@mAE(4I^~mX{wrO8!Nhl%^>Sa{qj(8U35u;)TrxH z456~PIl9&&&5cP$yR(M^buH?klcR>2cYSh)63@=}+Qa#P32`P%937I~sj98WNSEF% zO;fiegZR9|L7i#6jfQOP)pcCa2(6kV5e3#7rG7~TV7?~WdV+S4*(l-Lx!P}L6Gezd zY8tzXQ(-pmTv5qdQ73oPWU~s}OK~E6C+U0HF}Fw6-qkaw^H39BiF6*hiwvU5#wQ5T zxH*@q0B=C-L;FL?k_5L3&EYHd3(zh zI8#T5U#eGKu23NQ`7I^_58NB4X{z@?hP``NTDk8=w&QO3d%JcnH&5e>bF*13$8iH{VohLrX^Y0@WYfDxF#Pr_)AARHxt!b?DX|Kc%m(PLG(6%`laB0q0lvi3+~18< zww=pytKWS`F+F-LI58L@VrvMUe(PcIsI{4o0>KK|3zBlMSODxNHH;^vVnYQW9riT(`oPn>p9T{LTJ}X>gHm!-f&)e&jJ>-VjhN3%FQR{2qB+**NFTXUJ)hc zO!H2tU}@6Ukx1J3`lI@MHW$yI(Liey%FcqkYU3Jn+dSEO)@}|R>l;_PmUK=)3ltjf{)2fi>oH8}Uh;JXZIvcJLZDa5ELn z+DRJ7fViDj&vyB~+sa4fp7NYfnM0!cm4)By(Y6s8Up2Tj1P;|wP+KOp@~tRk}WIRdR`WBRBaZU=eRGg-#{kSMhms;mPT^u z8Rfj78Gix8##Nq^qlZo#;JHp&^(_^KTrGeSnnZdHy9zaW)nNDjgEG9_)5ld#geT-K z7T_93)dmx`7UHFSajnvxexuCz(W2X3`?wk%y(f-nru@VF3HLK<-8@EK5)u5M{%uMM z+0u@;w-_%Qukug!DNTac(oYR#?npdi)Pbq)-+3cpjOr;Ys(_hDrPVzfOq2_&Jc03k z!_mdPjN1)_pPpws!EaQ5phcFGogsCpd-+nAv>wmZ1MwyshjSnZQ=>7h?EZCV-rc3+5;fwH%lg} zW;TAy*hPCK!>0-?E;O^&&LcH}fy|l-d}q;i?L>&kIPa2fge5#)mK_F&;A(-AK{LBu zeoPe&79*YNxTp;wlL}fO9L}&D|D#VEUuSmdMGCo>PYv5L3vaI=aG`Da`6@hT_B043 zLwxaySKvoPwf0f{V9=WwI?37Tr6q-06cb|umXPhyQuD=9Sp&wry3|6km^=m=y3+TA zR@gNc7MG+BubGe+-e55&)OsNZ8s zRwi^dBG0i8Dkd@UfeU{?>YS)HxKB0Y>#PpDNZuI3{1~Y2WS?`9nRU@+%V+f*?pT06 z*)x5VAnZMZ3g zM1bYpDP4YAKH4?*@tndA`4p7(48_CMnqvTVS56^*xFKJ~x;WS;0tm%_RW1;ZO$GBN zwtlIDi+sFnje(*fL2DTz216#ss>q+8Ky4dF7{RH%{IcMv5PJpn^)vA`D888}geCOW z&>#*)S-q7vXg!nDBDC$=mdJhQ&Fy5=pi=T6i6QL>Z#FrUG@cVOEV%Olk8==Bk&t~3 z3F6WBg*)`LX;y|;Pq?nC!`F0CH@8eZHQL0|gUjJmq zciZ3L#B0{+dGK*mK5(gOx0bBW?>X4h!%!bs9rTD%UbQ)}wzi~Hn<9R}m8Q)!iN+qL zOp&!!LC96H=KPQngw?h+7)Z7o*HNcluy)6Zgk(6>#H&{g-s_);HnDzWZu6>ffHdf zn+JT#K&B-Pe!+7vVAoFHC8s2oni7k_TZvHl! zm}OJW%N=rWnLjXx)lA%Qjm%Evlq+0f)_{K1zbk<%9`UR6L|{sN(HO>2+P%dn)jK^b zw!k}o^(@{qD8ogz#r-jFnfTcBmTPq){*$*f%*YDWST@R11N-Cx%RjQ2``1? zkv6S~=QmiI`fXD%*f*ZA#ia(5!6YWxfSXQ^g9!J!i$x1s42k~CDkP=`I#XtS_exvh znd@g!LCAfPpT>P~+qxL}fpS;+5POIiIO2rPyzJQ?uH=| z$k->%-5UB0jZDF7eRlRH4qskZo`}Zy_uq8R99lrW5LJ$T6ln*vK|Mqp@OsTva}*u1 zdBf>`uzMI^AD_+nbVOfn7w(B_7w8RMj{ofiysqLpOu#=kPNm{NHN* z!%D+rzJY;!q`)KTCb4*r)G8SNqMBI1!FLLZK;Z?^RU1Pa49Z#ArHm>NFYiIy;N{a@ z>;XmQ!-QHHoN+)5Oenc8WR(0{(rFlX?V%12EKMoO{P-B9PdE%I@zWYS<9M%O^}@#t z(f}`9W2*4RVdl8=*}@s;g%xbp3CF_dO@X_fz$wkwHMv%8@Zb#(tQ3zkmP?ybq_~r? z11H(VkgC83HYDr`lJw5ESZlGMt;mz^ z)thR44y=ksY$(;uG*iVogg$~yQQ}$9G#3k3T=)0x0f2KKQhU-hn_ZSo+{zSv?7%#? zrkVe{$CJPonQx8sKu~+@rYykQ0=&QZTd4WX-e02oNqwyUg8@+Y8!oh7p2+KE3?UMPM{Bbwf23h$1;Ny zqe98gzfaEkY8)3p3xA4%g}ZP|SuTEvO7iXrk-_fv;A%@%vei}ZS$ulL+`mj-a z@cxR^`P$x0 z!^Sr6jPEq)xIP#YECT>k<7H4H>{~RWKb1y zBAF@-&Ppx!D^ViEJ8?aS=-%LL8ha&j5vRVoY^9HnmpDHU!7fL;YG6xALO^a6>_=c7 z)8?k{s}wDp`iOWYwUHDe6IGb_ZG=b(rt7BdNMZSBfhVf@8ji8e%_1ML}Eq%57o>od1j>d1txYGSdq!0q*~__xRE$ZrOoDoLX$@9S~MtYMX0-tq&* z9r=yK9Qn0G9r;y69QkF19r=ACHeFYr51#nb`o1oj!3|I>BX-YjPHpsvc(!(!fze*q zX#Roa@X(*Q3zWXpQP()`_T0tg5@qdBamv-Uc_P%dYM{cQtZ!2`KAFx<^>k~6J;5^J zYl(%c+1MISuLy?0?!c7%cRx$k%1jrjU~J24PrZ~|dX+N^CZZx8+zm0lp-O$7-BWHO znQS-zwzNdGL%isyq@kO%E&ebbQ-g7l+#c6BwViMh&cuD6@Xktq*K2k0n4B^c3-x$J zatsl)3ED76jwY+n=#8M#?B{~&uo&+PbJoR+?bu1p-gH<&h2iPpYYrUuwHXEC!bS`i z1QukWe^eDC$1e+%WVdzpRQae4zN!6jgKTqM2MSY}!#XPAy}Zk8jS}|HiVY=%^`r^eb~ki~+iZo&>ScR`^4KV3YfaCi@k~oGRsgR**p3!_ zuiixOwHhSHm`UN%F{*V>w#KoTX1^GyN{RoPU7e$so&NI8B;wo((0Jqc+5EF**>#v? zq`7iv^!q^u^N)PAFPHKXJNRl0krOBPipA%eVP!_-3Rk64*gYRA*6W%2iQr;O;^KBg zsGYuBCV%0b(&;7EOgxXdH*L@<6P?krL>%=l($|%Vw4HT0h5*S>y~J|uhZEjOJ3K!u;u-LZHwVxMbtMl^(09qz!=$J zY4>E>mm)8Nv=)VK`sK^{v$8-NO)YM@>*C*O^}4hdJ0Ius^DWh`{BG@r-6Ub-Bg86!pYW#ZAG*Hz+?Jd z@~k-%tjmgGMG|wIyz3zkRqNN1#C$m%uKb;E8!dYI0^oc~0#5;VukwM(?B$|mJ>5+0 zz&oxP&dpdqluGT4U^-oAEJ1lzo_#xOa`%4MivhWy6En(kQ@4e2j2fOPIBb642Ek>keRaI){n`v4=o-EGEP{BH58?*B`DOGpL43@8++uxi zNu`Tp@7tRV{h^EKe|+@ci`g{rg&A~xBG zXuz{7r33f3AC$5L6bnWK!5SE3e=j42WK<0n2EVE#3bQF98h}-$^yi~$uL%~Nf)OHW zRSROU3OqVp&L3U-v!S2;`R^_QnQ>6i=&HKmvFC!GVXKJxBhdb*Z-1!#xRotv+o$PavS!<-w-cGY|>r?KRyw~0hkq(Uo9ghWSrT4{;!(^VyyR5+X$Hya6FCIvY zr~)eT3j`7;bxWElA)Qyw?%b|01)1J2%O$3mlkRi81_4^2HZz_U^V6}wc;CVaxVc$RmDZz4y z=l*jdDJq`U8o@&z#n_97+z%tz5hyF)Uu`4EZGTbY4Q$`uV;9OQy}I{ru|3CBdOSBe zj=1fX<3n^*OBmnae7>w|aLp-1!pGp$^3}SEhczvH5x*p%4 zT^CvTTS;I6i$+Hw(jHK{tpOjStb~Z0)Pr>e{MbN4Of~7t$|}l#iUR#fG@#_`zL3(* zOrl|L0+lE~>mtKzYKTEjO-7-u13$pKaEQY+;%7JL*bHY|*W+&(b`#V-b3}TJSz#P3 zHxywgT)nOdr!I#=TBhaOqIE-{!C0?&hI#8g{AznoQV3$>6*KISrVzq`p%C4`WF4?@ zaB@ZT0<)~7OHEEsW34Dx@h?Hy`sMAUO6nh?BepUV+`LZp#Li!0-G!q>3+VVuT(rT@ zhJpK6qP6P;7N?E)8jRKT{}67vAHO8<{zu&ZXw~MQ;-8(!)wUsfrU${H@J3J4=_stx ztTWrBLtpWlyx%QrojF5%TMXnecRNTo9{9k>-Rxb6I?+_K;MPS`s6RgkIEdity{*j+ zFW*s~xL;I1t3Ggb{8()}ir=2M8emIrtH-@zS>*P1|F)OYsL$U$s@)6KK%;nRe3)`f7fJFqP;EE|2A2oLyf7cp?$NaY!3KwWE* ziEF6cgF}O*SDjR?C0E=T1R3+ARYFbXjz>Ed<)Beub*)>u6PSBtx0wZN9^0ZmyLUlS zO~G9>I`@@!VOgQ8JF$BU*`u5@oYMTV;9w}9Ts{7>7HLy-_iN%Et_CjdfpOjKm+N~W z@HYOBBoqRY5ez&1bqMiKhVUO5*Dv`md|@TIe^&5MKI1P{03a58w(&Q1;}1n5s2_mW zzY)Ej|J3{e&E)u0f|&t7Q;z>H?F;4)+ShLkqrbQC-*lzFx9|!~kN(-hKgmnrYX60} z^!pe8PG0!+%}GdqR{J}Z;rA+jKl=GqMLOo6Rs8)R=y&zs@#J6XL11Rq&ky?ty8OHT z?+D~C{dl}T^?$=7f3M-6P}?tk0N@ZGe4_Om@bT+%jpN#77=g)##%@ie%$>(qzNse literal 0 HcmV?d00001 diff --git a/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py index 76733457b..77d409f6a 100644 --- a/src/docx/oxml/text/hyperlink.py +++ b/src/docx/oxml/text/hyperlink.py @@ -4,12 +4,11 @@ from typing import TYPE_CHECKING, List -from docx.oxml.simpletypes import ST_OnOff, XsdString +from docx.oxml.simpletypes import ST_OnOff, ST_String, XsdString from docx.oxml.text.run import CT_R from docx.oxml.xmlchemy import ( BaseOxmlElement, OptionalAttribute, - RequiredAttribute, ZeroOrMore, ) @@ -22,7 +21,12 @@ class CT_Hyperlink(BaseOxmlElement): r_lst: List[CT_R] - rId = RequiredAttribute("r:id", XsdString) + rId: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "r:id", XsdString + ) + anchor: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:anchor", ST_String + ) history = OptionalAttribute("w:history", ST_OnOff, default=True) r = ZeroOrMore("w:r") diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py index 6082a53d1..a8551c39b 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -34,9 +34,12 @@ def address(self) -> str: 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. + within the document. When this hyperlink is an internal "jump" to for example a + heading from the table-of-contents (TOC), the address is blank. The bookmark + reference (like "_Toc147925734") is stored in the `.fragment` property. """ - return self._parent.part.rels[self._hyperlink.rId].target_ref + rId = self._hyperlink.rId + return self._parent.part.rels[rId].target_ref if rId else "" @property def contains_page_break(self) -> bool: @@ -50,6 +53,30 @@ def contains_page_break(self) -> bool: """ return bool(self._hyperlink.lastRenderedPageBreaks) + @property + def fragment(self) -> str: + """Reference like `#glossary` at end of URL that refers to a sub-resource. + + Note that this value does not include the fragment-separator character ("#"). + + This value is known as a "named anchor" in an HTML context and "anchor" in the + MS API, but an "anchor" element (``) represents a full hyperlink in HTML so + we avoid confusion by using the more precise RFC 3986 naming "URI fragment". + + These are also used to refer to bookmarks within the same document, in which + case the `.address` value with be blank ("") and this property will hold a + value like "_Toc147925734". + + To reliably get an entire web URL you will need to concatenate this with the + `.address` value, separated by "#" when both are present. Consider using the + `.url` property for that purpose. + + Word sometimes stores a fragment in this property (an XML attribute) and + sometimes with the address, depending on how the URL is inserted, so don't + depend on this field being empty to indicate no fragment is present. + """ + return self._hyperlink.anchor or "" + @property def runs(self) -> List[Run]: """List of |Run| instances in this hyperlink. @@ -59,7 +86,7 @@ def runs(self) -> List[Run]: 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] + return [Run(r, self._parent) for r in self._hyperlink.r_lst] @property def text(self) -> str: @@ -70,3 +97,23 @@ def text(self) -> str: they are not reflected in this text. """ return self._hyperlink.text + + @property + def url(self) -> str: + """Convenience property to get web URLs from hyperlinks that contain them. + + This value is the empty string ("") when there is no address portion, so its + boolean value can also be used to distinguish external URIs from internal "jump" + hyperlinks like those found in a table-of-contents. + + Note that this value may also be a link to a file, so if you only want web-urls + you'll need to check for a protocol prefix like `https://`. + + When both an address and fragment are present, the return value joins the two + separated by the fragment-separator hash ("#"). Otherwise this value is the same + as that of the `.address` property. + """ + address, fragment = self.address, self.fragment + if not address: + return "" + return f"{address}#{fragment}" if fragment else address diff --git a/tests/text/test_hyperlink.py b/tests/text/test_hyperlink.py index 484196902..b954d3ea9 100644 --- a/tests/text/test_hyperlink.py +++ b/tests/text/test_hyperlink.py @@ -17,12 +17,21 @@ 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)) + @pytest.mark.parametrize( + ("hlink_cxml", "expected_value"), + [ + ('w:hyperlink{r:id=rId6}/w:r/w:t"post"', "https://google.com/"), + ("w:hyperlink{w:anchor=_Toc147925734}", ""), + ("w:hyperlink", ""), + ], + ) + def it_knows_the_hyperlink_address( + self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild + ): + hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) - assert hyperlink.address == "https://google.com/" + assert hyperlink.address == expected_value @pytest.mark.parametrize( ("hlink_cxml", "expected_value"), @@ -42,6 +51,21 @@ def it_knows_whether_it_contains_a_page_break( assert hyperlink.contains_page_break is expected_value + @pytest.mark.parametrize( + ("hlink_cxml", "expected_value"), + [ + ("w:hyperlink{r:id=rId6}", ""), + ("w:hyperlink{w:anchor=intro}", "intro"), + ], + ) + def it_knows_the_link_fragment_when_there_is_one( + self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild + ): + hlink = cast(CT_Hyperlink, element(hlink_cxml)) + hyperlink = Hyperlink(hlink, fake_parent) + + assert hyperlink.fragment == expected_value + @pytest.mark.parametrize( ("hlink_cxml", "count"), [ @@ -85,6 +109,26 @@ def it_knows_the_visible_text_of_the_link( assert text == expected_text + @pytest.mark.parametrize( + ("hlink_cxml", "expected_value"), + [ + ("w:hyperlink", ""), + ("w:hyperlink{w:anchor=_Toc147925734}", ""), + ('w:hyperlink{r:id=rId6}/w:r/w:t"post"', "https://google.com/"), + ( + 'w:hyperlink{r:id=rId6,w:anchor=foo}/w:r/w:t"post"', + "https://google.com/#foo", + ), + ], + ) + def it_knows_the_full_url_for_web_addresses( + self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild + ): + hlink = cast(CT_Hyperlink, element(hlink_cxml)) + hyperlink = Hyperlink(hlink, fake_parent) + + assert hyperlink.url == expected_value + # -- fixtures -------------------------------------------------------------------- @pytest.fixture From b56f51616d61f85ff5e512006c398d77965f9715 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 12 Oct 2023 16:00:56 -0700 Subject: [PATCH 060/131] rfctr: improve typing --- src/docx/__init__.py | 22 +++++++++++++++++++--- src/docx/opc/part.py | 21 ++++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/docx/__init__.py b/src/docx/__init__.py index f6f7b5b5a..89c66bfe4 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -1,9 +1,25 @@ -from docx.api import Document # noqa +"""Initialize `docx` package. + +Export the `Document` constructor function and establish the mapping of part-type to +the part-classe that implements that type. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Type + +from docx.api import Document + +if TYPE_CHECKING: + from docx.opc.part import Part __version__ = "1.0.0" -# register custom Part classes with opc package reader +__all__ = ["Document"] + + +# -- register custom Part classes with opc package reader -- from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.constants import RELATIONSHIP_TYPE as RT @@ -17,7 +33,7 @@ from docx.parts.styles import StylesPart -def part_class_selector(content_type, reltype): +def part_class_selector(content_type: str, reltype: str) -> Type[Part] | None: if reltype == RT.IMAGE: return ImagePart return None diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index ad9abf7c9..a4ad3e7b2 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Dict, Type from docx.opc.oxml import serialize_part_xml from docx.opc.packuri import PackURI @@ -78,7 +78,7 @@ def drop_rel(self, rId: str): del self.rels[rId] @classmethod - def load(cls, partname, content_type, blob, package): + def load(cls, partname: str, content_type: str, blob: bytes, package: Package): return cls(partname, content_type, blob, package) def load_rel(self, reltype, target, rId, is_external=False): @@ -167,12 +167,19 @@ class PartFactory: the part, which is by default ``opc.package.Part``. """ - part_class_selector = None - part_type_for = {} + part_class_selector: Callable[[str, str], Type[Part] | None] | None + part_type_for: Dict[str, Type[Part]] = {} default_part_type = Part - def __new__(cls, partname, content_type, reltype, blob, package): - PartClass = None + def __new__( + cls, + partname: str, + content_type: str, + reltype: str, + blob: bytes, + package: Package, + ): + PartClass: Type[Part] | None = 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) @@ -181,7 +188,7 @@ def __new__(cls, partname, content_type, reltype, blob, package): return PartClass.load(partname, content_type, blob, package) @classmethod - def _part_cls_for(cls, content_type): + def _part_cls_for(cls, content_type: str): """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: From 8e05650be5da0820a6591698e675164fe70d50a8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 12 Oct 2023 15:56:57 -0700 Subject: [PATCH 061/131] release: prepare v1.0.1 release --- HISTORY.rst | 35 +++++++++++++++++++++-------------- src/docx/__init__.py | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e6c5bd333..d57bf5696 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,24 +3,31 @@ Release History --------------- +1.0.1 (2023-10-12) +++++++++++++++++++ + +- Fix #1256: parse_xml() and OxmlElement moved. +- Add Hyperlink.fragment and .url + + 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() +- 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/src/docx/__init__.py b/src/docx/__init__.py index 89c66bfe4..a518501a5 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from docx.opc.part import Part -__version__ = "1.0.0" +__version__ = "1.0.1" __all__ = ["Document"] From e45fd78a04cf91a9f90ac33b8a3f4d9432483f12 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Beasley" Date: Fri, 13 Oct 2023 09:18:17 -0400 Subject: [PATCH 062/131] fix: add build-backend in pyproject.toml This defaults to the right value on most platforms but must be explicit on Fedora. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index dc9884a9b..74149e24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" [project] name = "python-docx" From 8995a40d58013962b2662ff70b325ba24c704faa Mon Sep 17 00:00:00 2001 From: "Benjamin A. Beasley" Date: Fri, 13 Oct 2023 09:40:57 -0400 Subject: [PATCH 063/131] fix: include requirements files in PyPI sdist These are needed to run tests via tox.ini. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index c75168672..b2d3fadcf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include HISTORY.rst LICENSE README.rst tox.ini +include requirements*.txt graft src/docx/templates graft features graft tests From e441969102a9ddfbf04dc7b2f8e374239207a9d8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 22:35:35 -0700 Subject: [PATCH 064/131] dev: set looponfailroots and filterwarnings - Avoid rerunning tests when in "-f" follow mode every time Neovim updates the session file. - Suppress warnings related to "-f" flag deprecation but make all other warnings into errors. --- pyproject.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 74149e24d..d35c790c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,17 @@ Repository = "https://github.com/python-openxml/python-docx" target-version = ["py37", "py38", "py39", "py310", "py311"] [tool.pytest.ini_options] +filterwarnings = [ + # -- exit on any warning not explicitly ignored here -- + "error", + + # -- pytest-xdist plugin may warn about `looponfailroots` deprecation -- + "ignore::DeprecationWarning:xdist", + + # -- pytest complains when pytest-xdist is not installed -- + "ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning", +] +looponfailroots = ["src", "tests"] norecursedirs = [ "doc", "docx", From a1c6b4f183f8801666bd0c9cbbeb12bf0d88bd6f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 14:56:34 -0700 Subject: [PATCH 065/131] xml: BaseOxmlElement subclasses etree.ElementBase BaseOxmlElement inherits fine from `etree.ElementBase`, it's just the lxml stubs that says it doesn't, so ignore that error and we've got full access to `lxml` methods and properties on all `BaseOxmlElement` instances. --- src/docx/oxml/xmlchemy.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 2ea985abc..80c8c0e4a 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Type, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Type, TypeVar from lxml import etree from lxml.etree import ElementBase @@ -91,12 +91,6 @@ def _parse_line(cls, line): class MetaOxmlElement(type): """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, @@ -647,25 +641,15 @@ def _remove_choice_group_method_name(self): return "_remove_%s" % self._prop_name -class BaseOxmlElement(metaclass=MetaOxmlElement): +# -- lxml typing isn't quite right here, just ignore this error on _Element -- +class BaseOxmlElement( # pyright: ignore[reportGeneralTypeIssues] + etree.ElementBase, metaclass=MetaOxmlElement +): """Effective base class for all custom element classes. Adds standardized behavior to all classes in one place. """ - 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__, From 523328cd78e1a0c8b35d75a26747251c9bb3e45a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 22:29:03 -0700 Subject: [PATCH 066/131] rfctr: improve xmlchemy typing --- src/docx/enum/base.py | 4 +- src/docx/oxml/simpletypes.py | 93 ++++++++++++-------- src/docx/oxml/xmlchemy.py | 164 ++++++++++++++++++++++------------- src/docx/shared.py | 24 +++-- 4 files changed, 180 insertions(+), 105 deletions(-) diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index 4c20af644..e37e74299 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -67,7 +67,9 @@ def from_xml(cls, xml_value: str | None) -> Self: @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 + # -- presence of multi-arg `__new__()` method fools type-checker, but getting a + # -- member by its value using EnumCls(val) works as usual. + return cls(value).xml_value # pyright: ignore[reportGeneralTypeIssues] class DocsPageFormatter: diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index 4e8d91cba..debb5dc3c 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -1,3 +1,5 @@ +# pyright: reportImportCycles=false + """Simple-type classes, corresponding to ST_* schema items. These provide validation and format translation for values stored in XML element @@ -7,13 +9,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Tuple from docx.exceptions import InvalidXmlError from docx.shared import Emu, Pt, RGBColor, Twips if TYPE_CHECKING: - from docx import types as t from docx.shared import Length @@ -21,26 +22,36 @@ class BaseSimpleType: """Base class for simple-types.""" @classmethod - def from_xml(cls, xml_value: str): + def from_xml(cls, xml_value: str) -> Any: return cls.convert_from_xml(xml_value) @classmethod - def to_xml(cls, value): + def to_xml(cls, value: Any) -> str: cls.validate(value) str_value = cls.convert_to_xml(value) return str_value @classmethod - def convert_from_xml(cls, str_value: str) -> t.AbstractSimpleTypeMember: + def convert_from_xml(cls, str_value: str) -> Any: return int(str_value) @classmethod - def validate_int(cls, value): + def convert_to_xml(cls, value: Any) -> str: + ... + + @classmethod + def validate(cls, value: Any) -> None: + ... + + @classmethod + def validate_int(cls, value: object): if not isinstance(value, int): raise TypeError("value must be , got %s" % type(value)) @classmethod - def validate_int_in_range(cls, value, min_inclusive, max_inclusive): + def validate_int_in_range( + cls, value: int, min_inclusive: int, max_inclusive: int + ) -> None: cls.validate_int(value) if value < min_inclusive or value > max_inclusive: raise ValueError( @@ -57,15 +68,15 @@ def validate_string(cls, value: Any) -> str: class BaseIntType(BaseSimpleType): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> int: return int(str_value) @classmethod - def convert_to_xml(cls, value): + def convert_to_xml(cls, value: int) -> str: return str(value) @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int(value) @@ -84,22 +95,26 @@ def validate(cls, value: str): class BaseStringEnumerationType(BaseStringType): + _members: Tuple[str, ...] + @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_string(value) if value not in cls._members: 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 regex in the spec this is supposed to meet... + + but current assessment is that 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): + def convert_from_xml(cls, str_value: str) -> bool: 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 @@ -107,11 +122,11 @@ def convert_from_xml(cls, str_value): return str_value in ("1", "true") @classmethod - def convert_to_xml(cls, value): + def convert_to_xml(cls, value: bool) -> str: return {True: "1", False: "0"}[value] @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: if value not in (True, False): raise TypeError( "only True or False (and possibly None) may be assigned, got" @@ -130,13 +145,13 @@ class XsdId(BaseStringType): class XsdInt(BaseIntType): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, -2147483648, 2147483647) class XsdLong(BaseIntType): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, -9223372036854775808, 9223372036854775807) @@ -157,13 +172,13 @@ class XsdToken(BaseStringType): class XsdUnsignedInt(BaseIntType): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, 0, 4294967295) class XsdUnsignedLong(BaseIntType): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, 0, 18446744073709551615) @@ -178,7 +193,7 @@ def validate(cls, value: str) -> None: class ST_BrType(XsdString): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_string(value) valid_values = ("page", "column", "textWrapping") if value not in valid_values: @@ -187,19 +202,19 @@ def validate(cls, value): class ST_Coordinate(BaseIntType): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> Length: 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)) @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: ST_CoordinateUnqualified.validate(value) class ST_CoordinateUnqualified(XsdLong): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, -27273042329600, 27273042316900) @@ -213,19 +228,23 @@ class ST_DrawingElementId(XsdUnsignedInt): class ST_HexColor(BaseStringType): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml( # pyright: ignore[reportIncompatibleMethodOverride] + cls, str_value: str + ) -> RGBColor | str: if str_value == "auto": return ST_HexColorAuto.AUTO return RGBColor.from_string(str_value) @classmethod - def convert_to_xml(cls, value): + def convert_to_xml( # pyright: ignore[reportIncompatibleMethodOverride] + cls, value: RGBColor + ) -> str: """Keep alpha hex numerals all uppercase just for consistency.""" # expecting 3-tuple of ints in range 0-255 return "%02X%02X%02X" % value @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: # must be an RGBColor object --- if not isinstance(value, RGBColor): raise ValueError( @@ -269,7 +288,7 @@ class ST_Merge(XsdStringEnumeration): class ST_OnOff(XsdBoolean): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> bool: 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" @@ -280,11 +299,11 @@ def convert_from_xml(cls, str_value): class ST_PositiveCoordinate(XsdLong): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> Length: return Emu(int(str_value)) @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, 0, 27273042316900) @@ -294,13 +313,13 @@ class ST_RelationshipId(XsdString): class ST_SignedTwipsMeasure(XsdInt): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> Length: 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)) @classmethod - def convert_to_xml(cls, value): + def convert_to_xml(cls, value: int | Length) -> str: emu = Emu(value) twips = emu.twips return str(twips) @@ -312,7 +331,7 @@ class ST_String(XsdString): class ST_TblLayoutType(XsdString): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_string(value) valid_values = ("fixed", "autofit") if value not in valid_values: @@ -321,7 +340,7 @@ def validate(cls, value): class ST_TblWidth(XsdString): @classmethod - def validate(cls, value): + def validate(cls, value: Any) -> None: cls.validate_string(value) valid_values = ("auto", "dxa", "nil", "pct") if value not in valid_values: @@ -330,13 +349,13 @@ def validate(cls, value): class ST_TwipsMeasure(XsdUnsignedLong): @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> Length: 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)) @classmethod - def convert_to_xml(cls, value): + def convert_to_xml(cls, value: int | Length) -> str: emu = Emu(value) twips = emu.twips return str(twips) diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 80c8c0e4a..d075f88f1 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -1,24 +1,35 @@ +# pyright: reportImportCycles=false + """Enabling declarative definition of lxml custom element classes.""" from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Type, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Sequence, + Tuple, + Type, + TypeVar, +) from lxml import etree -from lxml.etree import ElementBase +from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage] 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): +def serialize_for_reading(element: ElementBase): """Serialize `element` to human-readable XML suitable for tests. No XML declaration. @@ -39,7 +50,9 @@ class XmlString(str): _xml_elm_line_patt = re.compile(r"( *)([^<]*)?$") - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, str): + return False lines = self.splitlines() lines_other = other.splitlines() if len(lines) != len(lines_other): @@ -49,10 +62,10 @@ def __eq__(self, other): return False return True - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not self.__eq__(other) - def _attr_seq(self, attrs): + def _attr_seq(self, attrs: str) -> List[str]: """Return a sequence of attribute strings parsed from `attrs`. Each attribute string is stripped of whitespace on both ends. @@ -61,7 +74,7 @@ def _attr_seq(self, attrs): attr_lst = attrs.split() return sorted(attr_lst) - def _eq_elm_strs(self, line, line_2): + def _eq_elm_strs(self, line: str, line_2: str): """Return True if the element in `line_2` is XML equivalent to the element in `line`.""" front, attrs, close, text = self._parse_line(line) @@ -77,10 +90,11 @@ def _eq_elm_strs(self, line, line_2): return True @classmethod - def _parse_line(cls, line): - """Return front, attrs, close, text 4-tuple result of parsing XML element string - `line`.""" + def _parse_line(cls, line: str) -> Tuple[str, str, str, str]: + """(front, attrs, close, text) 4-tuple result of parsing XML element `line`.""" match = cls._xml_elm_line_patt.match(line) + if match is None: + return "", "", "", "" front, attrs, close, text = [match.group(n) for n in range(1, 5)] return front, attrs, close, text @@ -120,7 +134,7 @@ def __init__( self._simple_type = simple_type def populate_class_members( - self, element_cls: Type[BaseOxmlElement], prop_name: str + self, element_cls: MetaOxmlElement, prop_name: str ) -> None: """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls @@ -129,11 +143,13 @@ def populate_class_members( 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. + + The property 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 + # -- assign unconditionally to overwrite element name definition -- setattr(self._element_cls, self._prop_name, property_) @property @@ -143,13 +159,13 @@ def _clark_name(self): return self._attr_name @property - def _getter(self) -> Callable[[BaseOxmlElement], t.AbstractSimpleTypeMember]: + def _getter(self) -> Callable[[BaseOxmlElement], Any | None]: ... @property def _setter( self, - ) -> Callable[[BaseOxmlElement, t.AbstractSimpleTypeMember | None], None]: + ) -> Callable[[BaseOxmlElement, Any | None], None]: ... @@ -183,12 +199,12 @@ def _docstring(self): @property def _getter( self, - ) -> Callable[[BaseOxmlElement], str | bool | t.AbstractSimpleTypeMember]: + ) -> Callable[[BaseOxmlElement], Any | None]: """Function suitable for `__get__()` method on attribute property descriptor.""" def get_attr_value( obj: BaseOxmlElement, - ) -> str | bool | t.AbstractSimpleTypeMember: + ) -> Any | None: attr_str_value = obj.get(self._clark_name) if attr_str_value is None: return self._default @@ -198,17 +214,19 @@ def get_attr_value( return get_attr_value @property - def _setter(self) -> Callable[[BaseOxmlElement, t.AbstractSimpleTypeMember], None]: + def _setter(self) -> Callable[[BaseOxmlElement, Any], None]: """Function suitable for `__set__()` method on attribute property descriptor.""" - def set_attr_value( - obj: BaseOxmlElement, value: t.AbstractSimpleTypeMember | None - ): + def set_attr_value(obj: BaseOxmlElement, value: Any | 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) + if str_value is None: + if self._clark_name in obj.attrib: + del obj.attrib[self._clark_name] + return obj.set(self._clark_name, str_value) return set_attr_value @@ -234,11 +252,10 @@ def _docstring(self): ) @property - def _getter(self): - """Return a function object suitable for the "get" side of the attribute - property descriptor.""" + def _getter(self) -> Callable[[BaseOxmlElement], Any]: + """function object suitable for "get" side of attr property descriptor.""" - def get_attr_value(obj): + def get_attr_value(obj: BaseOxmlElement) -> Any | None: attr_str_value = obj.get(self._clark_name) if attr_str_value is None: raise InvalidXmlError( @@ -251,27 +268,33 @@ def get_attr_value(obj): return get_attr_value @property - def _setter(self): - """Return a function object suitable for the "set" side of the attribute - property descriptor.""" + def _setter(self) -> Callable[[BaseOxmlElement, Any], None]: + """function object suitable for "set" side of attribute property descriptor.""" - def set_attr_value(obj, value): + def set_attr_value(obj: BaseOxmlElement, value: Any): str_value = self._simple_type.to_xml(value) + if str_value is None: + raise ValueError(f"cannot assign {value} to this required attribute") obj.set(self._clark_name, str_value) return set_attr_value class _BaseChildElement: - """Base class for the child element classes corresponding to varying cardinalities, - such as ZeroOrOne and ZeroOrMore.""" + """Base class for the child-element classes. - def __init__(self, nsptagname, successors=()): + The child-element sub-classes correspond to varying cardinalities, such as ZeroOrOne + and ZeroOrMore. + """ + + def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()): super(_BaseChildElement, self).__init__() self._nsptagname = nsptagname self._successors = successors - def populate_class_members(self, element_cls, prop_name): + def populate_class_members( + self, element_cls: MetaOxmlElement, prop_name: str + ) -> None: """Baseline behavior for adding the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._prop_name = prop_name @@ -279,7 +302,7 @@ def populate_class_members(self, element_cls, prop_name): def _add_adder(self): """Add an ``_add_x()`` method to the element class for this child element.""" - def _add_child(obj, **attrs): + def _add_child(obj: BaseOxmlElement, **attrs: Any): new_method = getattr(obj, self._new_method_name) child = new_method() for key, value in attrs.items(): @@ -308,13 +331,13 @@ def _add_getter(self): """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 + # -- 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.""" - def _insert_child(obj, child): + def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement): obj.insert_element_before(child, *self._successors) return child @@ -338,7 +361,7 @@ def _add_method_name(self): def _add_public_adder(self): """Add a public ``add_x()`` method to the parent element class.""" - def add_child(obj): + def add_child(obj: BaseOxmlElement): private_add_method = getattr(obj, self._add_method_name) child = private_add_method() return child @@ -349,7 +372,7 @@ def add_child(obj): ) self._add_to_class(self._public_add_method_name, add_child) - def _add_to_class(self, name, method): + def _add_to_class(self, name: str, method: Callable[..., Any]): """Add `method` to the target class as `name`, unless `name` is already defined on the class.""" if hasattr(self._element_cls, name): @@ -357,11 +380,11 @@ def _add_to_class(self, name, method): setattr(self._element_cls, name, method) @property - def _creator(self): + def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]: """Callable that creates an empty element of the right type, with no attrs.""" from docx.oxml.parser import OxmlElement - def new_child_element(obj): + def new_child_element(obj: BaseOxmlElement): return OxmlElement(self._nsptagname) return new_child_element @@ -375,7 +398,7 @@ def _getter(self): if not present. """ - def get_child_element(obj): + def get_child_element(obj: BaseOxmlElement): return obj.find(qn(self._nsptagname)) get_child_element.__doc__ = ( @@ -392,7 +415,7 @@ def _list_getter(self): """Return a function object suitable for the "get" side of a list property descriptor.""" - def get_child_element_list(obj): + def get_child_element_list(obj: BaseOxmlElement): return obj.findall(qn(self._nsptagname)) get_child_element_list.__doc__ = ( @@ -428,7 +451,12 @@ class Choice(_BaseChildElement): def nsptagname(self): return self._nsptagname - def populate_class_members(self, element_cls, group_prop_name, successors): + def populate_class_members( # pyright: ignore[reportIncompatibleMethodOverride] + self, + element_cls: MetaOxmlElement, + group_prop_name: str, + successors: Tuple[str, ...], + ) -> None: """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._group_prop_name = group_prop_name @@ -444,7 +472,7 @@ def _add_get_or_change_to_method(self): """Add a ``get_or_change_to_x()`` method to the element class for this child element.""" - def get_or_change_to_child(obj): + def get_or_change_to_child(obj: BaseOxmlElement): child = getattr(obj, self._prop_name) if child is not None: return child @@ -477,10 +505,12 @@ def _remove_group_method_name(self): class OneAndOnlyOne(_BaseChildElement): """Defines a required child element for MetaOxmlElement.""" - def __init__(self, nsptagname): - super(OneAndOnlyOne, self).__init__(nsptagname, None) + def __init__(self, nsptagname: str): + super(OneAndOnlyOne, self).__init__(nsptagname, ()) - def populate_class_members(self, element_cls, prop_name): + def populate_class_members( + self, element_cls: MetaOxmlElement, prop_name: str + ) -> None: """Add the appropriate methods to `element_cls`.""" super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @@ -490,7 +520,7 @@ def _getter(self): """Return a function object suitable for the "get" side of the property descriptor.""" - def get_child_element(obj): + def get_child_element(obj: BaseOxmlElement): child = obj.find(qn(self._nsptagname)) if child is None: raise InvalidXmlError( @@ -508,7 +538,9 @@ class OneOrMore(_BaseChildElement): """Defines a repeating child element for MetaOxmlElement that must appear at least once.""" - def populate_class_members(self, element_cls, prop_name): + def populate_class_members( + self, element_cls: MetaOxmlElement, prop_name: str + ) -> None: """Add the appropriate methods to `element_cls`.""" super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() @@ -522,7 +554,9 @@ def populate_class_members(self, element_cls, prop_name): class ZeroOrMore(_BaseChildElement): """Defines an optional repeating child element for MetaOxmlElement.""" - def populate_class_members(self, element_cls, prop_name): + def populate_class_members( + self, element_cls: MetaOxmlElement, prop_name: str + ) -> None: """Add the appropriate methods to `element_cls`.""" super(ZeroOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() @@ -536,7 +570,9 @@ def populate_class_members(self, element_cls, prop_name): class ZeroOrOne(_BaseChildElement): """Defines an optional child element for MetaOxmlElement.""" - def populate_class_members(self, element_cls, prop_name): + def populate_class_members( + self, element_cls: MetaOxmlElement, prop_name: str + ) -> None: """Add the appropriate methods to `element_cls`.""" super(ZeroOrOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @@ -550,7 +586,7 @@ def _add_get_or_adder(self): """Add a ``get_or_add_x()`` method to the element class for this child element.""" - def get_or_add_child(obj): + def get_or_add_child(obj: BaseOxmlElement): child = getattr(obj, self._prop_name) if child is None: add_method = getattr(obj, self._add_method_name) @@ -565,7 +601,7 @@ def get_or_add_child(obj): def _add_remover(self): """Add a ``_remove_x()`` method to the element class for this child element.""" - def _remove_child(obj): + def _remove_child(obj: BaseOxmlElement): obj.remove_all(self._nsptagname) _remove_child.__doc__ = ( @@ -582,11 +618,13 @@ class ZeroOrOneChoice(_BaseChildElement): """Correspondes to an ``EG_*`` element group where at most one of its members may appear as a child.""" - def __init__(self, choices, successors=()): + def __init__(self, choices: Sequence[Choice], successors: Tuple[str, ...] = ()): self._choices = choices self._successors = successors - def populate_class_members(self, element_cls, prop_name): + def populate_class_members( + self, element_cls: MetaOxmlElement, prop_name: str + ) -> None: """Add the appropriate methods to `element_cls`.""" super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name) self._add_choice_getter() @@ -607,7 +645,7 @@ def _add_group_remover(self): """Add a ``_remove_eg_x()`` method to the element class for this choice group.""" - def _remove_choice_group(obj): + def _remove_choice_group(obj: BaseOxmlElement): for tagname in self._member_nsptagnames: obj.remove_all(tagname) @@ -621,7 +659,7 @@ def _choice_getter(self): """Return a function object suitable for the "get" side of the property descriptor.""" - def get_group_member_element(obj): + def get_group_member_element(obj: BaseOxmlElement): return obj.first_child_found_in(*self._member_nsptagnames) get_group_member_element.__doc__ = ( @@ -657,7 +695,7 @@ def __repr__(self): id(self), ) - def first_child_found_in(self, *tagnames: str) -> ElementBase | None: + def first_child_found_in(self, *tagnames: str) -> _Element | None: """First child with tag in `tagnames`, or None if not found.""" for tagname in tagnames: child = self.find(qn(tagname)) @@ -688,7 +726,9 @@ def xml(self) -> str: """ return serialize_for_reading(self) - def xpath(self, xpath_str: str) -> Any: + def xpath( # pyright: ignore[reportIncompatibleMethodOverride] + self, xpath_str: str + ) -> Any: """Override of `lxml` _Element.xpath() method. Provides standard Open XML namespace mapping (`nsmap`) in centralized location. diff --git a/src/docx/shared.py b/src/docx/shared.py index 304fce4a4..0f91b9cdd 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -3,7 +3,17 @@ from __future__ import annotations import functools -from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, List, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generic, + Iterator, + List, + Tuple, + TypeVar, + cast, +) if TYPE_CHECKING: from docx.oxml.xmlchemy import BaseOxmlElement @@ -109,13 +119,17 @@ def __new__(cls, twips): return Length.__new__(cls, emu) -class RGBColor(tuple): +class RGBColor(Tuple[int, int, int]): """Immutable value object defining a particular RGB color.""" - def __new__(cls, r, g, b): + def __new__(cls, r: int, g: int, b: int): 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: + if ( + not isinstance(val, int) # pyright: ignore[reportUnnecessaryIsInstance] + or val < 0 + or val > 255 + ): raise ValueError(msg) return super(RGBColor, cls).__new__(cls, (r, g, b)) @@ -127,7 +141,7 @@ def __str__(self): return "%02X%02X%02X" % self @classmethod - def from_string(cls, rgb_hex_str): + def from_string(cls, rgb_hex_str: str) -> RGBColor: """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) From b7f590391b8dda42c69bb0aa1dead7a476ffad08 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 22:45:03 -0700 Subject: [PATCH 067/131] rfctr: resolve StoryChild conflation We need one type for the base-class role and a different one for the parameter-type role. The base-class is concrete, the parameter-type is abstract. Introduce `docx.types.ProvidesStoryPart` for the parameter-type role. Add `ProvidesXmlPart` while we're at it for use by `ElementProxy`, which probably needs a little more thought too. --- src/docx/drawing/__init__.py | 2 +- src/docx/text/hyperlink.py | 2 +- src/docx/text/pagebreak.py | 4 +++- src/docx/text/paragraph.py | 3 ++- src/docx/text/run.py | 3 ++- src/docx/types.py | 23 ++++++++++++++++++++--- tests/conftest.py | 15 ++++++++++----- tests/text/test_hyperlink.py | 14 +++++++------- tests/text/test_pagebreak.py | 16 ++++++++-------- tests/text/test_paragraph.py | 8 ++++---- tests/text/test_run.py | 2 +- 11 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/docx/drawing/__init__.py b/src/docx/drawing/__init__.py index 71bda0413..03c9c5ab8 100644 --- a/src/docx/drawing/__init__.py +++ b/src/docx/drawing/__init__.py @@ -10,7 +10,7 @@ class Drawing(Parented): """Container for a DrawingML object.""" - def __init__(self, drawing: CT_Drawing, parent: t.StoryChild): + def __init__(self, drawing: CT_Drawing, parent: t.ProvidesStoryPart): super().__init__(parent) self._parent = parent self._drawing = self._element = drawing diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py index a8551c39b..705a97ee4 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -23,7 +23,7 @@ class Hyperlink(Parented): stored. """ - def __init__(self, hyperlink: CT_Hyperlink, parent: t.StoryChild): + def __init__(self, hyperlink: CT_Hyperlink, parent: t.ProvidesStoryPart): super().__init__(parent) self._parent = parent self._hyperlink = self._element = hyperlink diff --git a/src/docx/text/pagebreak.py b/src/docx/text/pagebreak.py index f3c16bc5c..a5e68b5aa 100644 --- a/src/docx/text/pagebreak.py +++ b/src/docx/text/pagebreak.py @@ -36,7 +36,9 @@ class RenderedPageBreak(Parented): """ def __init__( - self, lastRenderedPageBreak: CT_LastRenderedPageBreak, parent: t.StoryChild + self, + lastRenderedPageBreak: CT_LastRenderedPageBreak, + parent: t.ProvidesStoryPart, ): super().__init__(parent) self._element = lastRenderedPageBreak diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 2425f1d6e..fb1067c16 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -6,6 +6,7 @@ from typing_extensions import Self +from docx import types as t from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.oxml.text.paragraph import CT_P @@ -21,7 +22,7 @@ class Paragraph(StoryChild): """Proxy object wrapping a `` element.""" - def __init__(self, p: CT_P, parent: StoryChild): + def __init__(self, p: CT_P, parent: t.ProvidesStoryPart): super(Paragraph, self).__init__(parent) self._p = self._element = p diff --git a/src/docx/text/run.py b/src/docx/text/run.py index ec0e6c757..44c41c0fe 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -4,6 +4,7 @@ from typing import IO, TYPE_CHECKING, Iterator, cast +from docx import types as t from docx.drawing import Drawing from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_BREAK @@ -30,7 +31,7 @@ class Run(StoryChild): the style hierarchy. """ - def __init__(self, r: CT_R, parent: StoryChild): + def __init__(self, r: CT_R, parent: t.ProvidesStoryPart): super().__init__(parent) self._r = self._element = self.element = r diff --git a/src/docx/types.py b/src/docx/types.py index 6097f740c..00bc100a1 100644 --- a/src/docx/types.py +++ b/src/docx/types.py @@ -2,13 +2,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from typing_extensions import Protocol -from docx.parts.story import StoryPart +if TYPE_CHECKING: + from docx.opc.part import XmlPart + from docx.parts.story import StoryPart -class StoryChild(Protocol): - """An object that can fulfill the `parent` role in a `Parented` class. +class ProvidesStoryPart(Protocol): + """An object that provides access to the StoryPart. This type is for objects that have a story part like document or header as their root part. @@ -17,3 +21,16 @@ class StoryChild(Protocol): @property def part(self) -> StoryPart: ... + + +class ProvidesXmlPart(Protocol): + """An object that provides access to its XmlPart. + + This type is for objects that need access to their part but it either isn't a + StoryPart or they don't care, possibly because they just need access to the package + or related parts. + """ + + @property + def part(self) -> XmlPart: + ... diff --git a/tests/conftest.py b/tests/conftest.py index 6503efb3b..2abfcc969 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,21 @@ """pytest fixtures that are shared across test modules.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import pytest -from docx import types as t -from docx.parts.story import StoryPart +if TYPE_CHECKING: + from docx import types as t + from docx.parts.story import StoryPart @pytest.fixture -def fake_parent() -> t.StoryChild: - class StoryChild: +def fake_parent() -> t.ProvidesStoryPart: + class ProvidesStoryPart: @property def part(self) -> StoryPart: raise NotImplementedError - return StoryChild() + return ProvidesStoryPart() diff --git a/tests/text/test_hyperlink.py b/tests/text/test_hyperlink.py index b954d3ea9..0cb977156 100644 --- a/tests/text/test_hyperlink.py +++ b/tests/text/test_hyperlink.py @@ -26,7 +26,7 @@ class DescribeHyperlink: ], ) def it_knows_the_hyperlink_address( - self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild + self, hlink_cxml: str, expected_value: str, fake_parent: t.ProvidesStoryPart ): hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) @@ -44,7 +44,7 @@ def it_knows_the_hyperlink_address( ], ) def it_knows_whether_it_contains_a_page_break( - self, hlink_cxml: str, expected_value: bool, fake_parent: t.StoryChild + self, hlink_cxml: str, expected_value: bool, fake_parent: t.ProvidesStoryPart ): hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) @@ -59,7 +59,7 @@ def it_knows_whether_it_contains_a_page_break( ], ) def it_knows_the_link_fragment_when_there_is_one( - self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild + self, hlink_cxml: str, expected_value: str, fake_parent: t.ProvidesStoryPart ): hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) @@ -78,7 +78,7 @@ def it_knows_the_link_fragment_when_there_is_one( ], ) def it_provides_access_to_the_runs_it_contains( - self, hlink_cxml: str, count: int, fake_parent: t.StoryChild + self, hlink_cxml: str, count: int, fake_parent: t.ProvidesStoryPart ): hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) @@ -100,7 +100,7 @@ def it_provides_access_to_the_runs_it_contains( ], ) def it_knows_the_visible_text_of_the_link( - self, hlink_cxml: str, expected_text: str, fake_parent: t.StoryChild + self, hlink_cxml: str, expected_text: str, fake_parent: t.ProvidesStoryPart ): hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) @@ -122,7 +122,7 @@ def it_knows_the_visible_text_of_the_link( ], ) def it_knows_the_full_url_for_web_addresses( - self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild + self, hlink_cxml: str, expected_value: str, fake_parent: t.ProvidesStoryPart ): hlink = cast(CT_Hyperlink, element(hlink_cxml)) hyperlink = Hyperlink(hlink, fake_parent) @@ -132,7 +132,7 @@ def it_knows_the_full_url_for_web_addresses( # -- fixtures -------------------------------------------------------------------- @pytest.fixture - def fake_parent(self, story_part: Mock, rel: Mock) -> t.StoryChild: + def fake_parent(self, story_part: Mock, rel: Mock) -> t.ProvidesStoryPart: class StoryChild: @property def part(self) -> StoryPart: diff --git a/tests/text/test_pagebreak.py b/tests/text/test_pagebreak.py index 6bb770619..c7494dca2 100644 --- a/tests/text/test_pagebreak.py +++ b/tests/text/test_pagebreak.py @@ -17,7 +17,7 @@ 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 + self, fake_parent: t.ProvidesStoryPart ): p_cxml = 'w:p/(w:r/(w:t"abc",w:lastRenderedPageBreak,w:lastRenderedPageBreak))' p = cast(CT_P, element(p_cxml)) @@ -28,7 +28,7 @@ def it_raises_on_preceding_fragment_when_page_break_is_not_first_in_paragrah( page_break.preceding_paragraph_fragment def it_produces_None_for_preceding_fragment_when_page_break_is_leading( - self, fake_parent: t.StoryChild + self, fake_parent: t.ProvidesStoryPart ): """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"))' @@ -41,7 +41,7 @@ def it_produces_None_for_preceding_fragment_when_page_break_is_leading( assert preceding_fragment is None def it_can_split_off_the_preceding_paragraph_content_when_in_a_run( - self, fake_parent: t.StoryChild + self, fake_parent: t.ProvidesStoryPart ): p_cxml = ( "w:p/(" @@ -61,7 +61,7 @@ def it_can_split_off_the_preceding_paragraph_content_when_in_a_run( 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 + self, fake_parent: t.ProvidesStoryPart ): p_cxml = ( "w:p/(" @@ -81,7 +81,7 @@ def and_it_can_split_off_the_preceding_paragraph_content_when_in_a_hyperlink( 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 + self, fake_parent: t.ProvidesStoryPart ): p_cxml = 'w:p/(w:r/(w:lastRenderedPageBreak,w:lastRenderedPageBreak,w:t"abc"))' p = cast(CT_P, element(p_cxml)) @@ -92,7 +92,7 @@ def it_raises_on_following_fragment_when_page_break_is_not_first_in_paragrah( page_break.following_paragraph_fragment def it_produces_None_for_following_fragment_when_page_break_is_trailing( - self, fake_parent: t.StoryChild + self, fake_parent: t.ProvidesStoryPart ): """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))' @@ -105,7 +105,7 @@ def it_produces_None_for_following_fragment_when_page_break_is_trailing( assert following_fragment is None def it_can_split_off_the_following_paragraph_content_when_in_a_run( - self, fake_parent: t.StoryChild + self, fake_parent: t.ProvidesStoryPart ): p_cxml = ( "w:p/(" @@ -125,7 +125,7 @@ def it_can_split_off_the_following_paragraph_content_when_in_a_run( 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 + self, fake_parent: t.ProvidesStoryPart ): p_cxml = ( "w:p/(" diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index a5db30da8..c1451c3c1 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -31,7 +31,7 @@ class DescribeParagraph: ], ) def it_knows_whether_it_contains_a_page_break( - self, p_cxml: str, expected_value: bool, fake_parent: t.StoryChild + self, p_cxml: str, expected_value: bool, fake_parent: t.ProvidesStoryPart ): p = cast(CT_P, element(p_cxml)) paragraph = Paragraph(p, fake_parent) @@ -50,7 +50,7 @@ def it_knows_whether_it_contains_a_page_break( ], ) def it_provides_access_to_the_hyperlinks_it_contains( - self, p_cxml: str, count: int, fake_parent: t.StoryChild + self, p_cxml: str, count: int, fake_parent: t.ProvidesStoryPart ): p = cast(CT_P, element(p_cxml)) paragraph = Paragraph(p, fake_parent) @@ -72,7 +72,7 @@ def it_provides_access_to_the_hyperlinks_it_contains( ], ) def it_can_iterate_its_inner_content_items( - self, p_cxml: str, expected: List[str], fake_parent: t.StoryChild + self, p_cxml: str, expected: List[str], fake_parent: t.ProvidesStoryPart ): p = cast(CT_P, element(p_cxml)) paragraph = Paragraph(p, fake_parent) @@ -120,7 +120,7 @@ def it_can_change_its_paragraph_style(self, style_set_fixture): ], ) def it_provides_access_to_the_rendered_page_breaks_it_contains( - self, p_cxml: str, count: int, fake_parent: t.StoryChild + self, p_cxml: str, count: int, fake_parent: t.ProvidesStoryPart ): p = cast(CT_P, element(p_cxml)) paragraph = Paragraph(p, fake_parent) diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 3d5e82cd9..772c5ad82 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -70,7 +70,7 @@ def it_knows_whether_it_contains_a_page_break( ], ) def it_can_iterate_its_inner_content_items( - self, r_cxml: str, expected: List[str], fake_parent: t.StoryChild + self, r_cxml: str, expected: List[str], fake_parent: t.ProvidesStoryPart ): r = cast(CT_R, element(r_cxml)) run = Run(r, fake_parent) From cf178112cc3ed86db9154356552ee5ac1590c555 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 22:13:07 -0700 Subject: [PATCH 068/131] rfctr: docx.shared type-checks strict --- src/docx/shared.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/docx/shared.py b/src/docx/shared.py index 0f91b9cdd..7b696202f 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -16,6 +16,8 @@ ) if TYPE_CHECKING: + from docx import types as t + from docx.opc.part import XmlPart from docx.oxml.xmlchemy import BaseOxmlElement from docx.parts.story import StoryPart @@ -34,7 +36,7 @@ class Length(int): _EMUS_PER_PT = 12700 _EMUS_PER_TWIP = 635 - def __new__(cls, emu): + def __new__(cls, emu: int): return int.__new__(cls, emu) @property @@ -71,7 +73,7 @@ def twips(self): class Inches(Length): """Convenience constructor for length in inches, e.g. ``width = Inches(0.5)``.""" - def __new__(cls, inches): + def __new__(cls, inches: float): emu = int(inches * Length._EMUS_PER_INCH) return Length.__new__(cls, emu) @@ -79,7 +81,7 @@ def __new__(cls, inches): class Cm(Length): """Convenience constructor for length in centimeters, e.g. ``height = Cm(12)``.""" - def __new__(cls, cm): + def __new__(cls, cm: float): emu = int(cm * Length._EMUS_PER_CM) return Length.__new__(cls, emu) @@ -88,14 +90,14 @@ class Emu(Length): """Convenience constructor for length in English Metric Units, e.g. ``width = Emu(457200)``.""" - def __new__(cls, emu): + def __new__(cls, emu: int): 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): + def __new__(cls, mm: float): emu = int(mm * Length._EMUS_PER_MM) return Length.__new__(cls, emu) @@ -103,7 +105,7 @@ def __new__(cls, mm): class Pt(Length): """Convenience value class for specifying a length in points.""" - def __new__(cls, points): + def __new__(cls, points: float): emu = int(points * Length._EMUS_PER_PT) return Length.__new__(cls, emu) @@ -114,7 +116,7 @@ class Twips(Length): A twip is a twentieth of a point, 635 EMU. """ - def __new__(cls, twips): + def __new__(cls, twips: float): emu = int(twips * Length._EMUS_PER_TWIP) return Length.__new__(cls, emu) @@ -263,7 +265,7 @@ def __set__(self, obj: Any, value: Any) -> None: raise AttributeError("can't set attribute") -def write_only_property(f): +def write_only_property(f: Callable[[Any, Any], None]): """@write_only_property decorator. Creates a property (descriptor attribute) that accepts assignment, but not getattr @@ -282,11 +284,13 @@ class ElementProxy: common type of class in python-docx other than custom element (oxml) classes. """ - def __init__(self, element: BaseOxmlElement, parent: Any | None = None): + def __init__( + self, element: BaseOxmlElement, parent: t.ProvidesXmlPart | None = None + ): self._element = element self._parent = parent - def __eq__(self, other): + def __eq__(self, other: object): """Return |True| if this proxy object refers to the same oxml element as does `other`. @@ -298,7 +302,7 @@ def __eq__(self, other): return False return self._element is other._element - def __ne__(self, other): + def __ne__(self, other: object): if not isinstance(other, ElementProxy): return True return self._element is not other._element @@ -309,8 +313,10 @@ def element(self): return self._element @property - def part(self): + def part(self) -> XmlPart: """The package part containing this object.""" + if self._parent is None: + raise ValueError("part is not accessible from this element") return self._parent.part @@ -322,7 +328,7 @@ class Parented: Provides ``self._parent`` attribute to subclasses. """ - def __init__(self, parent): + def __init__(self, parent: t.ProvidesXmlPart): self._parent = parent @property @@ -342,7 +348,7 @@ class StoryChild: Provides `self._parent` attribute to subclasses. """ - def __init__(self, parent: StoryChild): + def __init__(self, parent: t.ProvidesStoryPart): self._parent = parent @property From e31513946633a62e9006c6438c3da85d06ac4568 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 16:20:31 -0700 Subject: [PATCH 069/131] rfctr: improve typing local to BlockItemContainer --- features/steps/block.py | 13 ++++---- src/docx/blkcntnr.py | 35 ++++++++++++++++----- src/docx/document.py | 58 ++++++++++++++++++++++++---------- src/docx/oxml/document.py | 25 ++++++++++----- src/docx/oxml/section.py | 23 +++++++------- src/docx/oxml/table.py | 28 +++++++++++++---- src/docx/table.py | 64 ++++++++++++++++++++++---------------- src/docx/text/paragraph.py | 11 ++++--- tests/oxml/test_table.py | 2 ++ tests/test_blkcntnr.py | 2 ++ tests/test_document.py | 7 +++++ 11 files changed, 182 insertions(+), 86 deletions(-) diff --git a/features/steps/block.py b/features/steps/block.py index a091694ad..fea1bfb4a 100644 --- a/features/steps/block.py +++ b/features/steps/block.py @@ -1,6 +1,7 @@ """Step implementations for block content containers.""" from behave import given, then, when +from behave.runner import Context from docx import Document from docx.table import Table @@ -11,12 +12,12 @@ @given("a document containing a table") -def given_a_document_containing_a_table(context): +def given_a_document_containing_a_table(context: Context): context.document = Document(test_docx("blk-containing-table")) @given("a paragraph") -def given_a_paragraph(context): +def given_a_paragraph(context: Context): context.document = Document() context.paragraph = context.document.add_paragraph() @@ -25,13 +26,13 @@ def given_a_paragraph(context): @when("I add a paragraph") -def when_add_paragraph(context): +def when_add_paragraph(context: Context): document = context.document context.p = document.add_paragraph() @when("I add a table") -def when_add_table(context): +def when_add_table(context: Context): rows, cols = 2, 2 context.document.add_table(rows, cols) @@ -40,12 +41,12 @@ def when_add_table(context): @then("I can access the table") -def then_can_access_table(context): +def then_can_access_table(context: Context): table = context.document.tables[-1] assert isinstance(table, Table) @then("the new table appears in the document") -def then_new_table_appears_in_document(context): +def then_new_table_appears_in_document(context: Context): table = context.document.tables[-1] assert isinstance(table, Table) diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index 81166556a..d1df8e70a 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -1,15 +1,34 @@ +# pyright: reportImportCycles=false + """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 annotations + +from typing import TYPE_CHECKING + +from typing_extensions import TypeAlias + from docx.oxml.table import CT_Tbl -from docx.shared import Parented +from docx.shared import StoryChild from docx.text.paragraph import Paragraph +if TYPE_CHECKING: + from docx import types as t + from docx.oxml.document import CT_Body + from docx.oxml.section import CT_HdrFtr + from docx.oxml.table import CT_Tc + from docx.shared import Length + from docx.styles.style import ParagraphStyle + from docx.table import Table -class BlockItemContainer(Parented): +BlockItemElement: TypeAlias = "CT_Body | CT_HdrFtr | CT_Tc" + + +class BlockItemContainer(StoryChild): """Base class for proxy objects that can contain block items. These containers include _Body, _Cell, header, footer, footnote, endnote, comment, @@ -17,11 +36,13 @@ class BlockItemContainer(Parented): paragraph or table. """ - def __init__(self, element, parent): + def __init__(self, element: BlockItemElement, parent: t.ProvidesStoryPart): super(BlockItemContainer, self).__init__(parent) self._element = element - def add_paragraph(self, text="", style=None): + def add_paragraph( + self, text: str = "", style: str | ParagraphStyle | None = None + ) -> Paragraph: """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 @@ -37,7 +58,7 @@ def add_paragraph(self, text="", style=None): paragraph.style = style return paragraph - def add_table(self, rows, cols, width): + def add_table(self, rows: int, cols: int, width: Length) -> Table: """Return table of `width` having `rows` rows and `cols` columns. The table is appended appended at the end of the content in this container. @@ -47,7 +68,7 @@ def add_table(self, rows, cols, width): from docx.table import Table tbl = CT_Tbl.new_tbl(rows, cols, width) - self._element._insert_tbl(tbl) + self._element._insert_tbl(tbl) # # pyright: ignore[reportPrivateUsage] return Table(tbl, self) @property @@ -64,7 +85,7 @@ def tables(self): Read-only. """ - from .table import Table + from docx.table import Table return [Table(tbl, self) for tbl in self._element.tbl_lst] diff --git a/src/docx/document.py b/src/docx/document.py index 07751f155..8a72cd7f9 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -1,11 +1,27 @@ +# pyright: reportImportCycles=false +# pyright: reportPrivateUsage=false + """|Document| and closely related objects.""" +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, List + 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 + +if TYPE_CHECKING: + from docx import types as t + from docx.oxml.document import CT_Body, CT_Document + from docx.parts.document import DocumentPart + from docx.settings import Settings + from docx.shared import Length + from docx.styles.style import ParagraphStyle, _TableStyle + from docx.table import Table + from docx.text.paragraph import Paragraph class Document(ElementProxy): @@ -15,12 +31,13 @@ class Document(ElementProxy): a document. """ - def __init__(self, element, part): + def __init__(self, element: CT_Document, part: DocumentPart): super(Document, self).__init__(element) + self._element = element self._part = part self.__body = None - def add_heading(self, text="", level=1): + def add_heading(self, text: str = "", level: int = 1): """Return a heading paragraph newly added to the end of the document. The heading paragraph will contain `text` and have its paragraph style @@ -39,7 +56,9 @@ def add_page_break(self): paragraph.add_run().add_break(WD_BREAK.PAGE) return paragraph - def add_paragraph(self, text: str = "", style=None) -> Paragraph: + def add_paragraph( + self, text: str = "", style: str | ParagraphStyle | None = None + ) -> Paragraph: """Return paragraph newly added to the end of the document. The paragraph is populated with `text` and having paragraph style `style`. @@ -51,7 +70,12 @@ def add_paragraph(self, text: str = "", style=None) -> Paragraph: """ return self._body.add_paragraph(text, style) - def add_picture(self, image_path_or_stream, width=None, height=None): + def add_picture( + self, + image_path_or_stream: str | IO[bytes], + width: int | Length | None = None, + height: int | Length | None = 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 @@ -65,7 +89,7 @@ def add_picture(self, image_path_or_stream, width=None, height=None): 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): + def add_section(self, start_type: WD_SECTION = 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` @@ -75,7 +99,7 @@ def add_section(self, start_type=WD_SECTION.NEW_PAGE): new_sectPr.start_type = start_type return Section(new_sectPr, self._part) - def add_table(self, rows, cols, style=None): + def add_table(self, rows: int, cols: int, style: str | _TableStyle | None = 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|, @@ -92,7 +116,7 @@ def core_properties(self): @property def inline_shapes(self): - """The |InlineShapes| collectoin for this document. + """The |InlineShapes| collection 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 @@ -101,7 +125,7 @@ def inline_shapes(self): return self._part.inline_shapes @property - def paragraphs(self): + def paragraphs(self) -> List[Paragraph]: """The |Paragraph| instances in the document, in document order. Note that paragraphs within revision marks such as ```` or ```` do @@ -110,11 +134,11 @@ def paragraphs(self): return self._body.paragraphs @property - def part(self): + def part(self) -> DocumentPart: """The |DocumentPart| object of this document.""" return self._part - def save(self, path_or_stream): + def save(self, path_or_stream: str | IO[bytes]): """Save this document to `path_or_stream`. `path_or_stream` can be either a path to a filesystem location (a string) or a @@ -123,12 +147,12 @@ def save(self, path_or_stream): self._part.save(path_or_stream) @property - def sections(self): + def sections(self) -> Sections: """|Sections| object providing access to each section in this document.""" return Sections(self._element, self._part) @property - def settings(self): + def settings(self) -> Settings: """A |Settings| object providing access to the document-level settings.""" return self._part.settings @@ -138,7 +162,7 @@ def styles(self): return self._part.styles @property - def tables(self): + def tables(self) -> List[Table]: """All |Table| instances in the document, in document order. Note that only tables appearing at the top level of the document appear in this @@ -149,13 +173,13 @@ def tables(self): return self._body.tables @property - def _block_width(self): + def _block_width(self) -> Length: """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): + def _body(self) -> _Body: """The |_Body| instance containing the content for this document.""" if self.__body is None: self.__body = _Body(self._element.body, self) @@ -168,7 +192,7 @@ class _Body(BlockItemContainer): It's primary role is a container for document content. """ - def __init__(self, body_elm, parent): + def __init__(self, body_elm: CT_Body, parent: t.ProvidesStoryPart): super(_Body, self).__init__(body_elm, parent) self._body = body_elm diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index c4894f601..ebbfa1888 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -2,11 +2,15 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING, Callable, List from docx.oxml.section import CT_SectPr from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne +if TYPE_CHECKING: + from docx.oxml.table import CT_Tbl + from docx.oxml.text.paragraph import CT_P + class CT_Document(BaseOxmlElement): """```` element, the root element of a document.xml file.""" @@ -29,14 +33,22 @@ def sectPr_lst(self) -> List[CT_SectPr]: class CT_Body(BaseOxmlElement): - """````, the container element for the main document story in - ``document.xml``.""" + """`w:body`, the container element for the main document story in `document.xml`.""" + + add_p: Callable[[], CT_P] + get_or_add_sectPr: Callable[[], CT_SectPr] + p_lst: List[CT_P] + tbl_lst: List[CT_Tbl] + + _insert_tbl: Callable[[CT_Tbl], CT_Tbl] p = ZeroOrMore("w:p", successors=("w:sectPr",)) tbl = ZeroOrMore("w:tbl", successors=("w:sectPr",)) - sectPr = ZeroOrOne("w:sectPr", successors=()) + sectPr: CT_SectPr | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:sectPr", successors=() + ) - def add_section_break(self): + def add_section_break(self) -> CT_SectPr: """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 @@ -63,6 +75,5 @@ def clear_content(self): 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: + for content_elm in self.xpath("./*[not(self::w:sectPr)]"): self.remove(content_elm) diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index d1dc33ce2..4de4caa41 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -3,7 +3,7 @@ from __future__ import annotations from copy import deepcopy -from typing import Callable, Iterator, Sequence, Union, cast +from typing import Callable, Iterator, List, Sequence, cast from lxml import etree from typing_extensions import TypeAlias @@ -23,12 +23,18 @@ ) from docx.shared import Length, lazyproperty -BlockElement: TypeAlias = Union[CT_P, CT_Tbl] +BlockElement: TypeAlias = "CT_P | CT_Tbl" class CT_HdrFtr(BaseOxmlElement): """`w:hdr` and `w:ftr`, the root element for header and footer part respectively.""" + add_p: Callable[[], CT_P] + p_lst: List[CT_P] + tbl_lst: List[CT_Tbl] + + _insert_tbl: Callable[[CT_Tbl], CT_Tbl] + p = ZeroOrMore("w:p", successors=()) tbl = ZeroOrMore("w:tbl", successors=()) @@ -487,13 +493,8 @@ def _blocks_in_and_above_section(self, sectPr: CT_SectPr) -> Sequence[BlockEleme 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] - ) + # -- XPath callable results are Any (basically), so need a cast. -- + return cast(Sequence[BlockElement], xpath(sectPr)) @lazyproperty def _blocks_in_and_above_section_xpath(self) -> str: @@ -533,9 +534,7 @@ def _count_of_blocks_in_and_above_section(self, sectPr: CT_SectPr) -> int: ) 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] - ) + return int(cast(float, xpath(sectPr))) @lazyproperty def _sectPrs(self) -> Sequence[CT_SectPr]: diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index cefc545bf..c15c92907 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -1,5 +1,9 @@ """Custom element classes for tables.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, List + 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 @@ -22,6 +26,10 @@ ) from docx.shared import Emu, Twips +if TYPE_CHECKING: + from docx.oxml.text.paragraph import CT_P + from docx.shared import Length + class CT_Height(BaseOxmlElement): """Used for ```` to specify a row height and row height rule.""" @@ -140,9 +148,11 @@ def iter_tcs(self): yield tc @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.""" + def new_tbl(cls, rows: int, cols: int, width: Length) -> CT_Tbl: + """Return a new `w:tbl` element having `rows` rows and `cols` columns. + + `width` is distributed evenly between the columns. + """ return parse_xml(cls._tbl_xml(rows, cols, width)) @property @@ -167,7 +177,7 @@ def tblStyle_val(self, styleId): tblPr._add_tblStyle().val = styleId @classmethod - def _tbl_xml(cls, rows, cols, width): + def _tbl_xml(cls, rows: int, cols: int, width: Length) -> str: col_width = Emu(width / cols) if cols > 0 else Emu(0) return ( "\n" @@ -295,7 +305,7 @@ def alignment(self, value): jc.val = value @property - def autofit(self): + def autofit(self) -> bool: """|False| when there is a `w:tblLayout` child with `@w:type="fixed"`. Otherwise |True|. @@ -304,7 +314,7 @@ def autofit(self): return True if tblLayout is None else tblLayout.type != "fixed" @autofit.setter - def autofit(self, value): + def autofit(self, value: bool): tblLayout = self.get_or_add_tblLayout() tblLayout.type = "autofit" if value else "fixed" @@ -352,6 +362,12 @@ def width(self, value): class CT_Tc(BaseOxmlElement): """`w:tc` table cell element.""" + add_p: Callable[[], CT_P] + p_lst: List[CT_P] + tbl_lst: List[CT_Tbl] + + _insert_tbl: Callable[[CT_Tbl], CT_Tbl] + tcPr = ZeroOrOne("w:tcPr") # bunches of successors, overriding insert p = OneOrMore("w:p") tbl = OneOrMore("w:tbl") diff --git a/src/docx/table.py b/src/docx/table.py index 13cc5b7bf..31372284c 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -2,22 +2,29 @@ from __future__ import annotations -from typing import List, Tuple, overload +from typing import TYPE_CHECKING, List, Tuple, overload 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 +if TYPE_CHECKING: + from docx import types as t + from docx.enum.table import WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION + from docx.oxml.table import CT_Tbl, CT_TblPr + from docx.shared import Length + from docx.styles.style import _TableStyle # pyright: ignore[reportPrivateUsage] + class Table(Parented): """Proxy class for a WordprocessingML ```` element.""" - def __init__(self, tbl, parent): + def __init__(self, tbl: CT_Tbl, parent: t.StoryChild): super(Table, self).__init__(parent) self._element = self._tbl = tbl - def add_column(self, width): + def add_column(self, width: Length): """Return a |_Column| object of `width`, newly added rightmost to the table.""" tblGrid = self._tbl.tblGrid gridCol = tblGrid.add_gridCol() @@ -37,7 +44,7 @@ def add_row(self): return _Row(tr, self) @property - def alignment(self): + def alignment(self) -> WD_TABLE_ALIGNMENT | None: """Read/write. A member of :ref:`WdRowAlignment` or None, specifying the positioning of this @@ -47,11 +54,11 @@ def alignment(self): return self._tblPr.alignment @alignment.setter - def alignment(self, value): + def alignment(self, value: WD_TABLE_ALIGNMENT | None): self._tblPr.alignment = value @property - def autofit(self): + def autofit(self) -> bool: """|True| if column widths can be automatically adjusted to improve the fit of cell contents. @@ -61,16 +68,18 @@ def autofit(self): return self._tblPr.autofit @autofit.setter - def autofit(self, value): + def autofit(self, value: bool): 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.""" + def cell(self, row_idx: int, col_idx: int) -> _Cell: + """|_Cell| at `row_idx`, `col_idx` intersection. + + (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): + def column_cells(self, column_idx: int) -> List[_Cell]: """Sequence of cells in the column at `column_idx` in this table.""" cells = self._cells idxs = range(column_idx, len(cells), self._column_count) @@ -81,7 +90,7 @@ def columns(self): """|_Columns| instance representing the sequence of columns in this table.""" return _Columns(self._tbl, self) - def row_cells(self, row_idx): + def row_cells(self, row_idx: int) -> List[_Cell]: """Sequence of cells in the row at `row_idx` in this table.""" column_count = self._column_count start = row_idx * column_count @@ -94,22 +103,23 @@ def rows(self) -> _Rows: 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 + def style(self) -> _TableStyle | None: + """|_TableStyle| object representing the style applied to this table. + + Read/write. 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. + default table style of the document. - displayed in the user interface; a hyphen, if it appears, must be removed. For - example, `Light Shading - Accent 1` becomes `Light Shading Accent 1`. + 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): + def style(self, style_or_name: _TableStyle | None): style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.TABLE) self._tbl.tblStyle_val = style_id @@ -124,20 +134,20 @@ def table(self): 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`. + def table_direction(self) -> WD_TABLE_DIRECTION | None: + """Member of :ref:`WdTableDirection` indicating cell-ordering direction. - |None| indicates the value is inherited from the style hierarchy. + For example: `WD_TABLE_DIRECTION.LTR`. |None| indicates the value is inherited + from the style hierarchy. """ return self._element.bidiVisual_val @table_direction.setter - def table_direction(self, value): + def table_direction(self, value: WD_TABLE_DIRECTION | None): self._element.bidiVisual_val = value @property - def _cells(self): + def _cells(self) -> List[_Cell]: """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 @@ -161,7 +171,7 @@ def _column_count(self): return self._tbl.col_count @property - def _tblPr(self): + def _tblPr(self) -> CT_TblPr: return self._tbl.tblPr diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index fb1067c16..0a5d67674 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -2,22 +2,25 @@ from __future__ import annotations -from typing import Iterator, List, cast +from typing import TYPE_CHECKING, Iterator, List, cast from typing_extensions import Self from docx import types as t 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.styles.style import 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 +if TYPE_CHECKING: + from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + from docx.oxml.text.paragraph import CT_P + from docx.styles.style import CharacterStyle + class Paragraph(StoryChild): """Proxy object wrapping a `` element.""" diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index ecd0cf9d7..07a42876b 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -1,5 +1,7 @@ """Test suite for the docx.oxml.text module.""" +from __future__ import annotations + import pytest from docx.exceptions import InvalidSpanError diff --git a/tests/test_blkcntnr.py b/tests/test_blkcntnr.py index 6f1d28a46..dfd4aa933 100644 --- a/tests/test_blkcntnr.py +++ b/tests/test_blkcntnr.py @@ -13,6 +13,8 @@ class DescribeBlockItemContainer: + """Unit-test suite for `docx.blkcntnr.BlockItemContainer`.""" + 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_ diff --git a/tests/test_document.py b/tests/test_document.py index 12e3361db..adb667893 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1,5 +1,10 @@ +# pyright: reportPrivateUsage=false +# pyright: reportUnknownMemberType=false + """Unit test suite for the docx.document module.""" +from __future__ import annotations + import pytest from docx.document import Document, _Body @@ -21,6 +26,8 @@ class DescribeDocument: + """Unit-test suite for `docx.Document`.""" + def it_can_add_a_heading(self, add_heading_fixture, add_paragraph_, paragraph_): level, style = add_heading_fixture add_paragraph_.return_value = paragraph_ From 3c1669156c70a0516c67adeee1d3c17198d3c2f9 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 3 Nov 2023 13:09:52 -0700 Subject: [PATCH 070/131] xfail: for BlockItemContainer.iter_inner_content() --- features/blk-iter-inner-content.feature | 28 ++++++++++ features/steps/block.py | 50 ++++++++++++++++++ .../test_files/blk-paras-and-tables.docx | Bin 0 -> 15649 bytes 3 files changed, 78 insertions(+) create mode 100644 features/blk-iter-inner-content.feature create mode 100644 features/steps/test_files/blk-paras-and-tables.docx diff --git a/features/blk-iter-inner-content.feature b/features/blk-iter-inner-content.feature new file mode 100644 index 000000000..0ad2f44a8 --- /dev/null +++ b/features/blk-iter-inner-content.feature @@ -0,0 +1,28 @@ +Feature: Iterate paragraphs and tables in document-order + In order to access paragraphs and tables in the same order they appear in the document + As a developer using python-docx + I need the ability to iterate the inner-content of a block-item-container + + + @wip + Scenario: Document.iter_inner_content() + Given a Document object with paragraphs and tables + Then document.iter_inner_content() produces the block-items in document order + + + @wip + Scenario: Header.iter_inner_content() + Given a Header object with paragraphs and tables + Then header.iter_inner_content() produces the block-items in document order + + + @wip + Scenario: Footer.iter_inner_content() + Given a Footer object with paragraphs and tables + Then footer.iter_inner_content() produces the block-items in document order + + + @wip + Scenario: _Cell.iter_inner_content() + Given a _Cell object with paragraphs and tables + Then cell.iter_inner_content() produces the block-items in document order diff --git a/features/steps/block.py b/features/steps/block.py index fea1bfb4a..c365c9510 100644 --- a/features/steps/block.py +++ b/features/steps/block.py @@ -11,11 +11,33 @@ # given =================================================== +@given("a _Cell object with paragraphs and tables") +def given_a_cell_with_paragraphs_and_tables(context: Context): + context.cell = ( + Document(test_docx("blk-paras-and-tables")).tables[1].rows[0].cells[0] + ) + + +@given("a Document object with paragraphs and tables") +def given_a_document_with_paragraphs_and_tables(context: Context): + context.document = Document(test_docx("blk-paras-and-tables")) + + @given("a document containing a table") def given_a_document_containing_a_table(context: Context): context.document = Document(test_docx("blk-containing-table")) +@given("a Footer object with paragraphs and tables") +def given_a_footer_with_paragraphs_and_tables(context: Context): + context.footer = Document(test_docx("blk-paras-and-tables")).sections[0].footer + + +@given("a Header object with paragraphs and tables") +def given_a_header_with_paragraphs_and_tables(context: Context): + context.header = Document(test_docx("blk-paras-and-tables")).sections[0].header + + @given("a paragraph") def given_a_paragraph(context: Context): context.document = Document() @@ -40,6 +62,34 @@ def when_add_table(context: Context): # then ===================================================== +@then("cell.iter_inner_content() produces the block-items in document order") +def then_cell_iter_inner_content_produces_the_block_items(context: Context): + actual = [type(item).__name__ for item in context.cell.iter_inner_content()] + expected = ["Paragraph", "Table", "Paragraph"] + assert actual == expected, f"expected: {expected}, got: {actual}" + + +@then("document.iter_inner_content() produces the block-items in document order") +def then_document_iter_inner_content_produces_the_block_items(context: Context): + actual = [type(item).__name__ for item in context.document.iter_inner_content()] + expected = ["Table", "Paragraph", "Table", "Paragraph", "Table", "Paragraph"] + assert actual == expected, f"expected: {expected}, got: {actual}" + + +@then("footer.iter_inner_content() produces the block-items in document order") +def then_footer_iter_inner_content_produces_the_block_items(context: Context): + actual = [type(item).__name__ for item in context.footer.iter_inner_content()] + expected = ["Paragraph", "Table", "Paragraph"] + assert actual == expected, f"expected: {expected}, got: {actual}" + + +@then("header.iter_inner_content() produces the block-items in document order") +def then_header_iter_inner_content_produces_the_block_items(context: Context): + actual = [type(item).__name__ for item in context.header.iter_inner_content()] + expected = ["Table", "Paragraph"] + assert actual == expected, f"expected: {expected}, got: {actual}" + + @then("I can access the table") def then_can_access_table(context: Context): table = context.document.tables[-1] diff --git a/features/steps/test_files/blk-paras-and-tables.docx b/features/steps/test_files/blk-paras-and-tables.docx new file mode 100644 index 0000000000000000000000000000000000000000..85553c08ab5a13fe35ab2bf6c9b4adb3999e2b54 GIT binary patch literal 15649 zcmeIZ1$P|DvNhUbW@fO!lErL`EM{hAW@ZM9nVFd-i$jzX_tkIvY5f8ys@IzA%4$?-lNjo-1)p?qZYasE zQBmbj-TAAun)4wCkth@sw?QIL1CjvJ+a80F23G`efW?}P8C1P2E}2OWCl~& zY45Nm4(&JJP7~KC4{LS%EbqLafw&kHb^YE7l36l6WSuh=T`V)tR0Q49y4-) z$2tG&X&@lqU4Mbog>;7){eJTX#(cZ}HaGcs={~}96zvN1k=V3?lO#r3za%@kx_@#{ zk<3ZD!^uzD`7OHkPQ;}!%1jrE@aB-WQi|+n_Cgvj4DK z!dTpcP&J zrZ@PujJ*g2tsy3fw!C2e_> z1G4)3EIXwicPZY?IjM|u^X8%&9#Cc6LY=_`t&=$3u*ST#$;`Ad(YyZn<99c`#FfS!+UQW=1z#xXN2SZ4ah2kJuqrV&O2C+_auSk z3(@CTeKbMDr~3RcZ9iXN+U)2j+crdqv@d`}`k%uv4MR5*H#h*0i2(qh0NL|9&HhfX z;}kiYEe>Sgb(z*T^h#-&%{1$q9_s~a;e1iiH(RN2w1UD~Jg!BzPzTa4lmlm;#W z2~QV$KXiC1gloG$zSHFN-7s$$Jzvl=&5Y!d*@Vy&)nW=OlqH=`R%MVOgBj@8fWh(L z<~<0Ii%UQ$m_waJnI7Oc*nWO?>2G4tD=$i`V*j7IGefc1-7Qh7X)5vDFTKRR+rS-!W> z#k(83umA4uoq>H%%cPI25y?h(yj;PhCmZy2Q#yAM#`@bOpcK#nN!;6DB&Gt7C#8yH zdTNXQ5rMgc!MyLNvU7BAUui4=-A#G+z^!4({j7Ri;~rdD&8dx(FLQs~(T|A(6`PQI zvL(he$zMcP05QQ&!La+8Um3sta0RcKdR(xbu`;XB=!$IYOX*rc4uiYU;er=q4HOK_ zjrfHMGGA)nSMb(!c$!?z@XGso=Baz*MFn$I5?R?!nX)LkiOGE#p^lUGBA zQE5Kg!C_@torRx343;#VwcE{cP3A%b->@D-f&JV_@57WcGl!9H_8OSdokF7Q&~~*M zPs_z>VOEwLylrZT9@_}ivr!$Q)XpE3{K&{@x#h->no#U6B7gXb1Sw@e`w}7kj=Iqr zH>YhlTN5kj+2&j4j+x2u=ZZ7->>#pnzb5<14AlbuPhsXzjCno|4DB^A^Md;`e2fz% zy)`ccpzLX5y_vmKKhokXJ>@oGw4ptLK`6#7V}9FgM>^~KDFbmJx~4O% z&dCgl1#t;h%yiv%C*MLOl|E{JtV)g0Vnux?m60VLAJ##ZhE=7Sh7?;GOph(gp8Nri zY+=dZ?hMK<{CtL@IPEEPXgPI$`M4wg;R$JycKQSVJs#`Yks~)dt8sF!All-bA9M6w z=mt1vSWSc|B_DRDFoqH${#lDX>gXZ7da!(+J$in2@#CFG8IkmzkDJ1Kd1E>d84*%O zh!&iiCp~mQAsS5Z0YRSlK+)b>szm#T`I4W*TRE}Oc7|4D{0GjkD#|43GF^)RC|ezB z(sti{5PZ71K;Bq8b+~(ZBz{hznFml$gkbOKOqfcD@&PI_=K^OGgS#@5@)}#a<;?xo zgQUG0QH+Q(cs>1n^uF5eq7?Xa`%6JqNctx^B@4EOf&3TY%cD0Xn|}}aKJSz$3IM~s zP8a|H_0O@!*v7`u+Q!ky;Wuwqk+d9@&VU?z_Th#g<_t|hI$emQylk=Zw(6)6X@iwQ zH5{o4sEOn zK`i4)f+5N$hVZQUY~IQ+2DwqO-IN<&5-U|x+}!UU3G^+^M!{l+L6U^BA8F)ac$? zv)G*8Yv>X^4BdxUD0^pO-*ZbB2xibcirza+35WX9Sh5qMr`2yja?{5^G8_Uh6YeAr zgXKifo*4C0~*CR%qB)0FSwU?LZN2ksaDh%c`Rs1_-#x=xQ zG-3RMt2IjD-KIXH?8HEsZ75O7Hri5at2vr_-Bxd4=~wUhfs|_oQtrR*&B)sDABi>S&0`UCHaCzDp8cs~Dw*ggg`6QiLL&F-tO- zL?MaHiJ5_GG&dhvimGbWl);qo0c|^!=zcezEsV*cXI+K#u0Ib~1nI@JmgFNgRyR%& z3}Y05y09&?VHD9t9b6hbfG;pUCp>K}LVbf@35o>|98`&5#uNLH-!@#YKrH#1A%f*3 zE~!s6(;jsdR3%MJb%pv}NPtRxpMOfUWHg{6bo-2J_Y_*vXId9JnFtKsZ!VO$=wc!e zibipK#%x7aF_7t9-;%du9eu%^eh+upkEfb(idgm{4 zzGslLAGrCk_=-W*JcDj6BKWX>HI+RX#vxSzHQy2j=V#(w_BABn^JW4=L6llBxr%3LG@}WK4jr?kzyBB+ zcA%W9H_Qm59IU2Rj%PrEjh*XqWRj?&Z!Jg^Xa;HNNTZ%0l)zw*8G}^-B=_f-O=9!> zV5ocwztA!W+#66nMcSEWqQk@;iWkuUw!;Dg?aTQaN43N{ z5NDY6`Rr-x%ezpy1w9e?yv766UWsfvR7kA_j#H(yjxAvVdSoc<1^`-ylcm{ENg2$8 z?=GQ8Y#)Qc7dg&({2TrXo_X3Ian(WsV?4rbR+HX(uf9mSi&e|KFh>SGTm_=tiQTrI zZKExMGU%lKgqJx8?e4r8Ny45L>K7(Th$lEa;2NmmOOuZuSsAIhCp zCXO~getoT_l@_b|8E+ZuV1O~OOoy3l7&@=`wkfLGhn4OiAj@B1q!CFS^FZJ?24h>` zs6jm}J^^{Tp4Sxj)Q3vFrL#~uYMFSXQm=_UqBFxH@YUGkB^#s58BXTx;onDEgv%GG zV<7S6;QvPa-<rdF?sdu&PU1BMfmd1~sWMuvHgS)VC2J4oi1-?PyhRGK zb#D^`Vgf6n5{eTDgZ0%ZmwnDHP0draqmvTF&h!Zy4^e$$a#8j_7f4@MzQS2SYfM~3Zhaa=?bk4(tP|t3lf}phD5yn z8qd*s5MSnsc%?XsN5HtQx}I%sw!161GOJ(YRy<5LG8&_lh@B9M18*{y{cXU~S~Fqd zt#t{FVe-91{0BaJ;g-hOcVY^|K^x}jhE898Bfj;A;nPZDXPV0EFq-29x@xbis!>I@ zZo1R9=lX{0XDfYdM-G&L657`0bI++!G)Scd%asqTA|nW#-k+(vYamkM3Jk0+drg{@Yg&A^~t5Hy0pYEnVkl}r12XwLyZw*9TZ zv-Kr(EQ|atldwKgf$L-?bEis&%D48$suq2bk0QOYF-tY5RldUJ&F!<$b-H3XTNGA) zdmrAXjgJ~~5AGSp?|*0pJZ}pN-XisEA^mF%LZY4*s1FGMEE50#z*+Wxrr3_AMpj1j zzsukK!m*l!4Vnmkm&%a~_6fmr2tgb))Ib!16|`Vz?iR-y!5L!es@gCNNd)Dk^=uHU7Z<2LKP!{Y)hF(`$df)dLwdazeqJdwBu5{DV%TuqG zC>9`%scP>~iRs!53q@c^3aP??^FVeomVYmwn-h;P+sXwW>b~8x74`CeOe<{qsJCWx zp7_2L%aa0;#X>%hVPT`D$Y)2@IpwoR%2%!!wDWDAk8*_is?-anZc0}d)al6|KB6eV znZh}8UyU3v`ES%%9|&>`!&r0-J{F#2bb&CiM*nnf%e1E5Hvuf%^~je?pe!F$&Ml#3 zLBtSDUY#nY&k5t`Y;-Yw(XnY-p146>^F6L%{GwA{#nyO4-VU;;=b-&|t6W^K-isGQ zP&K*fkvy-0XVeZN6hZOD!IWT%i|M1`@FFXvC&GqBJa|eTbJ^ku%SBzt+e`73_tT98yDWy^RHyg# zP`t=g=fm?95h2Hh1}IY4IZtG$Ap0=m(mo^h9`a|%t-1G)C`CeHm1GGVJ!yk}r)4F9 zos5IIT9zQPdeZk#52?=5yXF}3*tAtk%IT5wvmpry2 z>E!FfoCGO_JPVfK<_IlV{&tDZ#g53OjFttP_wsoSOSO#|G_isL;6`NvaJd!-uWB7eZ|_ zJ~;abbC!PfwD+8CGZ}vL@T_s!0GIyZekadrp&!m>RbX_%50}|qEA5rGr#%NM z+*m*4Qo69eO1x13C9^=bGdj7IyziNHA;TQM`%{H6AW6~khdt9UoGBS^g$ z>fA>4raTfkhIf9qZAJ_*V;b;s#$Q>r({GBtcxE)VldL4^Vi$MI97zYLr8IEqsDVvY4d8_TSXEOhm!98;rwbJg zQe0LQP8zi|5~N8z(y?J{AetNkh%l>3hbMkr>M99(pbAcc|I)7lr(85^86Ay|0wre~ z0clBg%XqL_vxp%JlexB&mIXa*RS#BgziIrH@oAKz9A|t6+GMdxXF`&u!IpW_iJ{)# z{lQZB?gxvI%ClRlyyW&{iuf-!|*T}L)r8(C|~zwLQ!=O zKo?R^U?Hv9(Q>|%%5ARTI{Nt$4woH+CtX|S$&)BA`qF`rk5v1cmXwKJCkG(&BrmZ@ zO%2B84KraZ)6Th;V{&@&f!us>ksj;radJ|aTnFoFc=Kq~GgKLNio}@hP6VB31!8ge z+?841S*VDiK7d;fp72~Z10l76w&#=n%#2!{OmGM!Wqi$~E6YcuUqge%Nck1%qZtvd1qm$vcLj+BKy{)GiYX54^WZqPQ;IgHlJTVpLn$TN^fOx|( znHHJW4teh0cm@vQ{+3L3w&~)s9tn(z0cG=fCZX|1dg$ed%xWY!mxJp9{)*4!Ca%Z$ zHI6(wua9HJUevYnITRkBdkn>|YU`EC#JOemG$do&H!zF=RV_K9J;e*I=%GWc_72h? z3Pgp)^ANQUo0&fxj=L9^AeLnQT>!s?z%}BANC@;PjQ+J#QQ4uu|k36L{gQ`(buLX9I^= z*asDDJuUJd*e2We8&fdod9p4tBxom6^=2<06(!nJ)^owCl^oauD$?vg(c$GpHy1-x zFF_v(Bq7|MG%Ud4Gl{L^@5|mHZp*}g@*WgxlnR7m#jIdF1XETtR~C|Xn?erA2%hHy z7-qA&KG$;NQ@3}E&YedP?Tjmq=8?_CLH7(Ge8@rM9mi3tt_47WQ?&;CkN(J86v`i$e=@dmKEavezAx_*?kBKg647uVR>W*@w&ChBpI_G!Q5NTi-20#m90 z_XN3%h@9^d7q$PDxYZru!KTr0E&IOjN6aQE?!+>VeN@C=Kjq&8!fNPx*I+&WEd@g*6HLcd$Qbm*T~NX?PzuvD zOhIMlcD8-{(Eg%rE7tyU`IO=&5jt**+Iy|@R`YKipOq9ImB~Z$=HnQgL`o9JK53|Z zfMk9HFH^fPqLj>Py*^feC^QrvCSW2rb7XYrs&YipBbyUMWpRELK0Wc$7${UdgzcA5 zRFWbwN!LA2FsxVdV^cU+>b9Z0inZ#+ghRk&OL_R*(n(|5^hFv|eKAyfee^Mtrmn6e zen1)1rLKPzm3F!J0F_)X`kTUS$*zTVl4o@IPedOQbM~Ap$#`|UwxLroYl>XHT}&m{ zc^a`2ku~V0N-B>wbl#p%0}yiO0bfZWgk;W3yiMIJ=IACSKhDmjC|6D5z!u1I8niPT z%%V-{Pmgg@%6655O|)iuPHUEvq3$U_{M4}1t6hi36tq2jhEzwA7S1Ajc_q#okG9?3 z3C=^vQYuhr=)&{A7e!$ocHs@}D*{*vFrYvr=M zexCdWoC^NW<+weiWH?mdh;!fs0Q?p#|GwMg;OJ)gpXJ}3ObLf&4%DGDjj!u|g|nw5 z7t2u!+mee>9nMl2uELW$@UbyLtYW4QAtw*l;ZWp!=KlHt9i7MIwW7C3(Wkt=kU~ev zIy!YMk0aso!_Srtcit}O7hFl9!$ecU>qJ%SUf4oL1DAn?O#3GJD%w%rFV}ZnFPG1? z71|2g!mu&PMD_QUEahbnLz$UOSr-wBXS*5uM>~p`IOX4mw(KYk1{An`u*NJWsTom9 zOSD~XU0k9?5Q+{2VrNT39!zk4;7olm=u$XWL^0By>~Ginmi6>4%P4jn4(F;)F>+FV zY%FCsIi*7YFe<1yEH7PtSH>t62``>{c9Par=Lo^k3vvsx!GQ@AWw*lMoKV#Lr!FFfTpI%STnqg7ToZivTtNvYF8UeMwqX>& zi{onPVKa4k&jmf$U7qk!23L8=1MQlo0D3DYVBXj+Pxpktri?FkP0?$>rU2F~i)8ib z$~ZM`1m#7=QR-xjXzEv{j6wSWCKyZv85#F>Vly(3!6?fKL9P@`<|{XkK7=3W+2lG-Bw{$g1aMbX?r1sIw5+m;U>V#qgG&OsVY&cb(Ws zLMA?XkQrQXk(_oy#2U(~Lb8;Xr(%W@Lmx#91mSD5jtmceq0hr#Q^7^m`ffHFs;Xng zKVB2O4++W4#mbPfnd;XAF9VOCE$1XB@b0KISopghLS)IhrKkLj2C$(C*kFTT<=Vv^ zLd`1B-gdO`v&+RLRlI|orVOvY&dRAc}~Q8zmv^^ z!KET*`zro@$szBA1gs4mJU)VaBrv2PDU}q9%%NoSWl)DQ4g&_^q;uo;=>rBOX_OjU zQ#c~1RjJ*DS|l%cIZ5ABJO=+6oLuOUANZanUee9ZIu{|&cdta0k;!jOtxQkR#-$b& zhqismFc!_565!iXtlS6pGW?`gbmj3T6cwiTw6775ncE(-iWm@zd!UuVC=$$;?!93-ZN$EKBucT^ni7H zfSI0PuspFpZC6f4LUr%Ld2Ei_ZQ`<^n4IQHKt>{emUWDd+tYYJ!Gbu$abSvR=n8yY zOa+}bQX~8XwxG~R13Jr5XgWe>JtE^dVXgJ@T`Tx4 z)Em|HcoYtcl-rn%j3uUvF6MNH@Z`FM+1<$xYmOVAlAp@78JBS)l^FrA>{au^I6l0$ zgx1d?Yuiyo(;k4!ckYpb+791ic8aeF(}B$a2}FK`l^YShc$GQa)(!r*B>5Nxff34J z${^l?XS=>cewfsPIYFOv3pYe^0%6Ak0%564bNRv5OOSzQG<`P%VNdM%gKkOqgA$N) z`Cc5bpsL#bwMEX-&OwcKta$6vVMPm#@4O+7NNtZv=>6}AG1%9FRx8iY2q)N zJfkOhh>!W0UoYz)dBbfYqCk|yk(lZ@0B ztoUVnU4!=+SB}8n$Ks{N;xsxftXf zb6*%2RzZk1+`L{*gxmOURl(pD>O$ft_ntXpgPaGM^u2 z!+SXAqxn4+qG26a=`_q8oSJ1{fX{5V824kY*|K!@Pcd?4uG#o~?{{%@-m@qJd?7lG zBga3*WgBiX?bnvSg!@L|@c{6RlWCn=f#N|cax7^{^BSr92iW1V+yMWaid*#|q`uwLXtoDRMnJmGBqEoKw7vWgCzUSxU z)=qhZx$08qEGn^Mi%)ntE$3`%uSr0sz#5jeu40au9>#w6-7<6#853hlQO)kkpF~D( zBAh>2rZwe9pf%>PTY5?9Fts_R3cFU@5xvK%x{cF(+l<_joC(@Mrk5eus!GqSaLclJ z$q}>I3B7G8ul1E6NLKo~V;!rMc8XBxRWg^ON84V@W#kpwQy5FN zm)@WGujz{Wie~jX_=@5KJ*z|t z?;g$G*gC3$z}aHRT%u<97vg233UD=gQmSFqowDfr35F})zHuvA`nE2ah-!qkF;(+5 z;Wix6iD>Hvh*&&c_ZfBkrUD7pE$dnSF@}0VYoaP7nd&s9o^}{EwpzVGitwM;?N%bU zEi^Z2M*;?0ng;mo?K{`q-a^tY$(tuE&sof(n#t4)Z;+u3YGZ_Sg@IkqNKK%=ohr_OaT}h8$ z(HF-BW;wb7Cl}~{P9I#1^cDWLZjmrEAq~oi40YZXdiL!aQ7Z(wUN1R(+Le*w2rS{0 zhQ;|)y0qQq34!wYJME2`2DAMiujy(uF4*6p$ZpX;aN}U9^ErOMZ?3vt{`qpC{9%%f zjZHRC{LJsBuCr79+6bu}YgkRX4wc+Kv}z_jqiqJ;8SnF$!4dJOu>_1&l94)I!=cx5 zB)*eF1sCfnRRzbuRa-?8- zq{`^^@tr-Qz%_Dx^DEU}d>s~9c^;}@oM0kdXB>tXsPRl2I_5o*KG9wJFPlup&iby= zHM6i<56w{t$J8NYI{9RoxEp=0%@s+AwIs4{&)2#~K@YR^H1e-KKAQ1W79)f7&rkt6J!nJW00+QlWSkFuiM#|H_S6Kv6Ffp9kL*w}TM_#$~84P7|c z%BkLlCDJZpSF;UbcS%n$OS+9kk;HpvILj(s-jMf(MSzFBhz%vAA0*hcLPe$z^tAah zonr%iZR`4JP>npG4FUm|{ zK<;h(Ai$pMSJ+E}RTTTA?b>un0qz9!!At(!lEld=kjxK+A6473nya@(_Z%^x{ zY?=}ai=SHD#2(5}`<|wKq1RUkg6^E+L|@O?@QbOwc;*7%J zQ%#?T)`S=xAd9RAt=$W z+*IZGFkXRMHEFjbgQX-pdpdgV%EDnz_Rw7gR5VQ#CBGMbCQv)-bq7&row!Eo&W{%k z4j(SPlN6ss)wZ%c`0(;(@J=={C~?vSi8NZwwwF5Kl|bW;c=IrVQ}f}=>id{zC5!v? zD=IhjdL==|HOCz(x;ma*b7$WUAavK9;3+-HygyXUr ztz{w81&S#WpG(ij((tj5GMQ9LX<{fOU3(;7U`a(JBi>!>edDzt5y7Jo=1BAxHsxb& zJ?`{&V*J@JkA2eZp39jb_@0sCXLA(=%&zJWLtzLqgHjGQ21SEpgB!cJOq3C3%Y~T2 zahjFxE8LO6K$6lrm<#d94ws`XdDzTDzS!NkH?x@s- z;rq=bWD`L#S_6du{ZTYED%Deu#-UkW(3@Fse_B6Cz5H1g+`?eE4Tj2%D} zJM7rbxN&02Z|cE{`wsdY;eW_@+e>$nu;HkW>c%&|VIZOowj%B0H$M`v4x5E`^TTp? zI>YbG{o(6_{f<3b6t#k7LMSX$8zqX-+7>bfcJ}r`jaaw?$ zzlpFRXI(_7KUy|Lq*sb;_>Ni~=i?hEe?=6*qg7j&YUTv~!YnPcWo)y@*$N{cs+rox z_Z0zGuY>&|o2h8tjN+;6tWHnb@TEKqKp|TVtgkAZ>q*hvl34f{ic*{HfTgs^P>s3f zP1;NPt5cBDm_IaPjlYBwbvekkBIljsUVFWa8*g+kx6{Gd_C?$&OqKNqL3d=cMt+|$ z?vuV#s^P4iEcO|)i03};^4+Pq%O6LZdS9PArv!e?3rx%iJXx>kA;FkIn)5@2M-ZNr z){V1Pw_jr77Zxax9}uHJm%6EUl|GQ)GEBMzL!e8KH`ai(G%ol#u z@XxsTFFXLi!3Y5S8YTaV{%2(Jcl0&$-_U=?DZj#h^%;LDNM-xu@r{Ay&HwcvvJwzL T%lK_C1qsj&G%lHszy14vez(MZ literal 0 HcmV?d00001 From f46751fd9df36e500ef2193c245bd4dec75f9649 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 3 Nov 2023 13:22:57 -0700 Subject: [PATCH 071/131] blk: add Document.iter_inner_content() --- src/docx/blkcntnr.py | 6 +++++- src/docx/document.py | 6 +++++- tests/test_document.py | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index d1df8e70a..367121be8 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterator from typing_extensions import TypeAlias @@ -71,6 +71,10 @@ def add_table(self, rows: int, cols: int, width: Length) -> Table: self._element._insert_tbl(tbl) # # pyright: ignore[reportPrivateUsage] return Table(tbl, self) + def iter_inner_content(self) -> Iterator[Paragraph | Table]: + """Generate each `Paragraph` or `Table` in this container in document order.""" + raise NotImplementedError + @property def paragraphs(self): """A list containing the paragraphs in this container, in document order. diff --git a/src/docx/document.py b/src/docx/document.py index 8a72cd7f9..4deb8aa8e 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import IO, TYPE_CHECKING, List +from typing import IO, TYPE_CHECKING, Iterator, List from docx.blkcntnr import BlockItemContainer from docx.enum.section import WD_SECTION @@ -124,6 +124,10 @@ def inline_shapes(self): """ return self._part.inline_shapes + def iter_inner_content(self) -> Iterator[Paragraph | Table]: + """Generate each `Paragraph` or `Table` in this document in document order.""" + return self._body.iter_inner_content() + @property def paragraphs(self) -> List[Paragraph]: """The |Paragraph| instances in the document, in document order. diff --git a/tests/test_document.py b/tests/test_document.py index adb667893..6a2c5af88 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -5,12 +5,15 @@ from __future__ import annotations +from typing import cast + import pytest 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 +from docx.oxml.document import CT_Document from docx.parts.document import DocumentPart from docx.section import Section, Sections from docx.settings import Settings @@ -22,7 +25,7 @@ from docx.text.run import Run from .unitutil.cxml import element, xml -from .unitutil.mock import class_mock, instance_mock, method_mock, property_mock +from .unitutil.mock import Mock, class_mock, instance_mock, method_mock, property_mock class DescribeDocument: @@ -104,6 +107,16 @@ def it_provides_access_to_its_inline_shapes(self, inline_shapes_fixture): document, inline_shapes_ = inline_shapes_fixture assert document.inline_shapes is inline_shapes_ + def it_can_iterate_the_inner_content_of_the_document( + self, body_prop_: Mock, body_: Mock, document_part_: Mock + ): + document_elm = cast(CT_Document, element("w:document")) + body_prop_.return_value = body_ + body_.iter_inner_content.return_value = iter((1, 2, 3)) + document = Document(document_elm, document_part_) + + assert list(document.iter_inner_content()) == [1, 2, 3] + def it_provides_access_to_its_paragraphs(self, paragraphs_fixture): document, paragraphs_ = paragraphs_fixture paragraphs = document.paragraphs From 24e4c1b44c0793fa6162cd910a1808487f1d398e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 3 Nov 2023 14:39:44 -0700 Subject: [PATCH 072/131] oxml: add .inner_content_elements props --- src/docx/oxml/document.py | 9 +++++++++ src/docx/oxml/section.py | 9 +++++++++ src/docx/oxml/table.py | 9 +++++++++ tests/oxml/test_document.py | 19 +++++++++++++++++++ tests/oxml/test_section.py | 19 +++++++++++++++++++ tests/oxml/test_table.py | 9 ++++++++- 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/oxml/test_document.py create mode 100644 tests/oxml/test_section.py diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index ebbfa1888..cc27f5aa9 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -77,3 +77,12 @@ def clear_content(self): """ for content_elm in self.xpath("./*[not(self::w:sectPr)]"): self.remove(content_elm) + + @property + def inner_content_elements(self) -> List[CT_P | CT_Tbl]: + """Generate all `w:p` and `w:tbl` elements in this document-body. + + Elements appear in document order. Elements shaded by nesting in a `w:ins` or + other "wrapper" element will not be included. + """ + return self.xpath("./w:p | ./w:tbl") diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index 4de4caa41..a4090898a 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -38,6 +38,15 @@ class CT_HdrFtr(BaseOxmlElement): p = ZeroOrMore("w:p", successors=()) tbl = ZeroOrMore("w:tbl", successors=()) + @property + def inner_content_elements(self) -> List[CT_P | CT_Tbl]: + """Generate all `w:p` and `w:tbl` elements in this header or footer. + + Elements appear in document order. Elements shaded by nesting in a `w:ins` or + other "wrapper" element will not be included. + """ + return self.xpath("./w:p | ./w:tbl") + class CT_HdrFtrRef(BaseOxmlElement): """`w:headerReference` and `w:footerReference` elements.""" diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index c15c92907..48a6d8c2f 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -415,6 +415,15 @@ def grid_span(self, value): tcPr = self.get_or_add_tcPr() tcPr.grid_span = value + @property + def inner_content_elements(self) -> List[CT_P | CT_Tbl]: + """Generate all `w:p` and `w:tbl` elements in this document-body. + + Elements appear in document order. Elements shaded by nesting in a `w:ins` or + other "wrapper" element will not be included. + """ + return self.xpath("./w:p | ./w:tbl") + def iter_block_items(self): """Generate a reference to each of the block-level content elements in this cell, in the order they appear.""" diff --git a/tests/oxml/test_document.py b/tests/oxml/test_document.py new file mode 100644 index 000000000..f7f99a524 --- /dev/null +++ b/tests/oxml/test_document.py @@ -0,0 +1,19 @@ +"""Unit-test suite for `docx.oxml.document` module.""" + +from __future__ import annotations + +from typing import cast + +from docx.oxml.document import CT_Body +from docx.oxml.table import CT_Tbl +from docx.oxml.text.paragraph import CT_P + +from ..unitutil.cxml import element + + +class DescribeCT_Body: + """Unit-test suite for selected units of `docx.oxml.document.CT_Body`.""" + + def it_knows_its_inner_content_block_item_elements(self): + body = cast(CT_Body, element("w:body/(w:tbl, w:p,w:p)")) + assert [type(e) for e in body.inner_content_elements] == [CT_Tbl, CT_P, CT_P] diff --git a/tests/oxml/test_section.py b/tests/oxml/test_section.py new file mode 100644 index 000000000..8cf0bd9b7 --- /dev/null +++ b/tests/oxml/test_section.py @@ -0,0 +1,19 @@ +"""Unit-test suite for `docx.oxml.section` module.""" + +from __future__ import annotations + +from typing import cast + +from docx.oxml.section import CT_HdrFtr +from docx.oxml.table import CT_Tbl +from docx.oxml.text.paragraph import CT_P + +from ..unitutil.cxml import element + + +class DescribeCT_HdrFtr: + """Unit-test suite for selected units of `docx.oxml.section.CT_HdrFtr`.""" + + def it_knows_its_inner_content_block_item_elements(self): + hdr = cast(CT_HdrFtr, element("w:hdr/(w:tbl,w:tbl,w:p)")) + assert [type(e) for e in hdr.inner_content_elements] == [CT_Tbl, CT_Tbl, CT_P] diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 07a42876b..395c812a6 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -2,11 +2,14 @@ from __future__ import annotations +from typing import cast + import pytest from docx.exceptions import InvalidSpanError from docx.oxml.parser import parse_xml -from docx.oxml.table import CT_Row, CT_Tc +from docx.oxml.table import CT_Row, CT_Tbl, CT_Tc +from docx.oxml.text.paragraph import CT_P from ..unitutil.cxml import element, xml from ..unitutil.file import snippet_seq @@ -102,6 +105,10 @@ def it_can_extend_its_horz_span_to_help_merge( ] assert tc.vMerge == vMerge + def it_knows_its_inner_content_block_item_elements(self): + tc = cast(CT_Tc, element("w:tc/(w:p,w:tbl,w:p)")) + assert [type(e) for e in tc.inner_content_elements] == [CT_P, CT_Tbl, CT_P] + def it_can_swallow_the_next_tc_help_merge(self, swallow_fixture): tc, grid_width, top_tc, tr, expected_xml = swallow_fixture tc._swallow_next_tc(grid_width, top_tc) From ee130dc275b53fbb21f519e32d9e763dc7c372a9 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 3 Nov 2023 14:43:50 -0700 Subject: [PATCH 073/131] blk: add BlockItemContainer.iter_inner_content() --- features/blk-iter-inner-content.feature | 4 ---- src/docx/blkcntnr.py | 10 +++++++++- tests/test_blkcntnr.py | 23 ++++++++++++++++++++++- tests/test_files/blk-inner-content.docx | Bin 0 -> 11949 bytes 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 tests/test_files/blk-inner-content.docx diff --git a/features/blk-iter-inner-content.feature b/features/blk-iter-inner-content.feature index 0ad2f44a8..047efb9ee 100644 --- a/features/blk-iter-inner-content.feature +++ b/features/blk-iter-inner-content.feature @@ -4,25 +4,21 @@ Feature: Iterate paragraphs and tables in document-order I need the ability to iterate the inner-content of a block-item-container - @wip Scenario: Document.iter_inner_content() Given a Document object with paragraphs and tables Then document.iter_inner_content() produces the block-items in document order - @wip Scenario: Header.iter_inner_content() Given a Header object with paragraphs and tables Then header.iter_inner_content() produces the block-items in document order - @wip Scenario: Footer.iter_inner_content() Given a Footer object with paragraphs and tables Then footer.iter_inner_content() produces the block-items in document order - @wip Scenario: _Cell.iter_inner_content() Given a _Cell object with paragraphs and tables Then cell.iter_inner_content() produces the block-items in document order diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index 367121be8..1327e6d08 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -13,6 +13,7 @@ from typing_extensions import TypeAlias from docx.oxml.table import CT_Tbl +from docx.oxml.text.paragraph import CT_P from docx.shared import StoryChild from docx.text.paragraph import Paragraph @@ -73,7 +74,14 @@ def add_table(self, rows: int, cols: int, width: Length) -> Table: def iter_inner_content(self) -> Iterator[Paragraph | Table]: """Generate each `Paragraph` or `Table` in this container in document order.""" - raise NotImplementedError + from docx.table import Table + + for element in self._element.inner_content_elements: + yield ( + Paragraph(element, self) + if isinstance(element, CT_P) + else Table(element, self) + ) @property def paragraphs(self): diff --git a/tests/test_blkcntnr.py b/tests/test_blkcntnr.py index dfd4aa933..1549bd8ea 100644 --- a/tests/test_blkcntnr.py +++ b/tests/test_blkcntnr.py @@ -2,13 +2,14 @@ import pytest +from docx import Document from docx.blkcntnr import BlockItemContainer from docx.shared import Inches from docx.table import Table from docx.text.paragraph import Paragraph from .unitutil.cxml import element, xml -from .unitutil.file import snippet_seq +from .unitutil.file import snippet_seq, test_file from .unitutil.mock import call, instance_mock, method_mock @@ -34,6 +35,26 @@ def it_can_add_a_table(self, add_table_fixture): assert table._element.xml == expected_xml assert table._parent is blkcntnr + def it_can_iterate_its_inner_content(self): + document = Document(test_file("blk-inner-content.docx")) + + inner_content = document.iter_inner_content() + + para = next(inner_content) + assert isinstance(para, Paragraph) + assert para.text == "P1" + # -- + t = next(inner_content) + assert isinstance(t, Table) + assert t.rows[0].cells[0].text == "T2" + # -- + para = next(inner_content) + assert isinstance(para, Paragraph) + assert para.text == "P3" + # -- + with pytest.raises(StopIteration): + next(inner_content) + def it_provides_access_to_the_paragraphs_it_contains(self, paragraphs_fixture): # test len(), iterable, and indexed access blkcntnr, expected_count = paragraphs_fixture diff --git a/tests/test_files/blk-inner-content.docx b/tests/test_files/blk-inner-content.docx new file mode 100644 index 0000000000000000000000000000000000000000..1edfb41977970ba897ade620b1361df205742a12 GIT binary patch literal 11949 zcmeHtg;$(O_I49of(ExV!GgO53GM`UhsNC@ND|!L-AT~k?(XjH4#AzTGrO~s$J^iL6o5-_80)^&*%z zj#hpT7U$y&<;SR=#;0bC7jjAvkq8zfWX#vg%*x%~WAjNC@Mu-eWCz$hai62LP1&jE zW;SWP3~|ZVVi=bB``A8NurxN|O0hvOys7Gl8d5;`kdmF^jj4JA2&0M(S0;4}WhU}@ zz+zs#Zd!(nUnaA8`=JUJA6YvC9WlCmhSkGLj0DGU&VEAGI9AF>xAIlszWd%}dW?I# zY=qr{w>{x$7#Qg=m7*SfMC8kq%x)eEIuvdX)8 zS|NIf$l=ALtYX#7rL<>C*t}4PMsDGa1wxd!k*j!scRMmZm{NSDB0mNiF>u zw-@d$Zy*7H=VwTO?B86HC=##c1Z-zAV2?utyQGdC$ikkU?x*~pEB+t$zhC}(QB<2* z2Lsak6Yodwu_n3sHk@o3dV|pgoHqyDlS4NNhZ^YmBdk3pw294msMBW4> z47FlxQ3+jbXpkxY>`eei+vi!HDmB zJTq-Xc>8j>R;S{AbS+b;+1pDRJPQVTvXn^uA_TnC#<6|3?3VnlKwld8O!z?hdv_yE zboHm5plw_}wiBRQrOui)eu@_VIe20EZ~vG!tdP3^cLpQadk6q<5YEf+!(_tXMYZqA0|LvzJY5;67;JMrP$veS=_ML~Uz@~)+aAEopAWXzpKm$K? zvDQ*5CVnugTYqX-l3+jbiEUw|Eyy`!@?(BRuN~@cNa@EJDwnTBrN{iJ<|^g~gj6_@ zx*FQr7HOkqGLn;*pM|nGsY2ozh;KAd&FKEHK09vnJ~8|zMj!#y`#b(xXQ=SLsx;7~(K zv7X2;Oq+;6AOmJ`10jt?bL`(e+^iW*pm1t4Mis!kMxR^Baak3T$n}_sYb8;#j#~6!LtAUYqkN8E_FZ zK~+Z}=Ae$eu)i;CYI16_K0F^*51_hdC(nMc>vcLt5CCFvN6MufQ>RSjer70-xcn+z zgj?m7JXxqp=T)C3LLLKM7Ig3WfXkYePVP2@K#_&^F_?;upwyH{FT%I0y-L6yRkL&* zS|7p_ezeWDE2bI=LwXiPmB@ya6J??rr_Mn5tMzynXzq;WhKknY2dFYW>x%tO`)5>{YZ)S;+B>$kb0(Yl?XFyD30!Oe5~aJzh(TyVFTH z9av-ztjaGv?|NiHdkKmQ#-oVfM2`mLcST|9msvcIi=LNMq|4(Pt> z&~%U#p_v|68>TZ}-ZiW_kX=`{0=M@1#ao$2XBkGwXIAT?;=MtMXRTfYif1K7 zDS0i2*^TT9zim{98Kf^(JwgsRjbkbNX!|OYLT%@WMHlqmUkN*PEBMlHdD|T9goG=ap0Q9{E05JdbQwI}}C5ZkH$@p__ zKU9+p#ur6xhCL(ps~d0|+SMy!9Tz(@pJ7|B@(yp3H_t0hGaXx+uVV1iO}B%GN{J9e zs@p;pKw|9Nj3PED6Hg(I+$R!eroHS8*YbvM_@czu{Ozg!TYWu?{o49t0+(n4VtZ7W z29Js@VM@qs0;yh4lLrG$m8PgzOb8kX{<8Xq_nQN$ z5Do3^1RrZ+G!R1aS!sJZI;t{uZnd);rb{6dtu|nW_zgx?4<-OCcx^HYba}%@w5M^i z1eBKQ4%f+#Zd0v07c(`dS~?y`jWD^=)`)xrFo`eiGb;D2*B-SSX4o&yW#=s{){P zvZv)~t8m8a;Z|oqm;a!_xHO&b%-^$Z% z5^j5tR`Rv#OgVuf)5Df`WWx&RV5sqK_Vi4b*SnU2EtcQsWfJ?_S^{5=wsARoi=gkq zhjc(ct?S+Z<~H*2I>{6w66I)u2*Ec=Oqm0uFfB9|P;D!ImRH9dJR+9+_cy&l?Q`!Z z^UxJKLgGg?vu68ox>&*&g_+7D7j;e}^=yD`6TrMd+>r06Y>D{pdZSG+rrpO^JoN$6 zr16Gkg2yD?XT7}1Fla%>%EP3AXS3A$2*99}WGQuVhI5|+-Wv!q-{WS%vbE!y^rM9c zze9*DqwPf}QU>OcjW05&)o)MEX2rg1$rO!^CHsq*oajr9G2$5+q+4mLM86i7cH9(uM@lO_uI>`y{p%8pF5w1t2y6Ks=h`{ zfP_-=qh0zEu=XS+L`Xr)R?{y%D41NA*(i6>N$P42T`6~eLtJ|G8DV1zCT z)@Vg8;XcEP=C|#Z%B&C@24{QNtR5^Mtn_O|X+|rOwu|y^S7um*cW#TKI13Gjsc;*5 zMGFke*M<3BsPpWOsjs)^^I{0>HEYT?QajAOSv?H*XDE25Zt@smip6hVm+~2rbKKIO z`4&2I%+trHOiHN~H7IvqmV=o*!)ClgdCBa{@CFZ?jA-H1l)W_SdnSDUc5_x_P?D4= z{?H-BvU=08?;Za^(5EO5V)VK$DRf7~z-_ZrTGESQ8DHUr_N?4V?DA0^$ceGgN^Vn=nZUJUckUnoF>(Z&(g$F>d;xhFoCoeEJmw{KR_*`*Jh+NGk?w zBRoj6UghPFKlTXeAIqW(7VRv(j}FnOnpXVyAkC3*~o~tOROhM6#YClCH^J{x4R@j#*>OK~X-7zJnI{H*j3c_ptt`j%@Ary(&I6g{O$5MH-+aV~yIYs`)GROc6w2Q(26Tb)4pz&trcaIwtJa;b zks$`SO(y-0+!|&ms}7d*`VOwubDc(aWA8#U=7_H#Tc(J39a7)CysE!!HAhm<6=8*w z(9X`+8GOA%ctw)Kk_Dd~oR@t6vbb9*7&1%F@mqrMSo`VtY1=Q@= zMFL5NKIk5JuxaeJZg`B&W_{6V?I6;4fk62h@<2+-sEwx)^ssjhe!-@DF8xx$&7W#}GD9~vTBI`~r7f}Gy(@;3Y-^8k2a0?`iv zL>@YyPo4t&5C%O)iiY2W>xiIo0~0oLTyT4Gc5uA6aKqc_vjb^Exfma&(BHa=o4wmj zGeqwzvAJ(7=dN3@2g->633^Z9-G&-rH=q0~SsizL@#i1MJv(&IM`EomkvXjmY^+~-ST#)RjyAlKS{-%z`ADp-^0XV>k=0!EI8rYduOrKIJP>)s8_Whg zI=O-|^%F@=DlK(_Z3`p&{K?j^Mi`{<-NSMmjreDd09~lL_+pLFQsb)fs7l4Qrqd3= zo1UvjteX|`2|Opnb@O#TTj}zXO8#ufos2CtqLGY!2)JvCVHoS@wm6QrwA z+-Mk7NA--n&#nwJmgr&BbV0^$nX$g!H09+c5nakyDCL2%(&}>%MqxQVeD9fOLDcln zxmj&pN1yo6=JJ6QHXD0^ppMR#eFlkF4#v>o-dN-MUO0o{Q39LJvPEV4+R(>V71tt@%5%VLpnk2r#L=}0q#&?h87je;}!Ao5$1!{UL8^QElcmB2Uxqw;rm>S46=`pEG{>h z2qTXJRTakEyD1|@W(gDWvRcyERoG)Pc8q#rS!S_q4V+{1NEv%=jD1vSNW z=id#AV+Ks?A`+&VJc^n$IW`Qd0ns{=tMFWsyg3cdx}e3pZLlnljMo@9LeZ&Wc4&?& z-JogmA%YNFC0ecU3|N`UWpDkw?eL6T+gIfVuq7%fTiCm~nw37kDgv%Kl78O6OD^u2 zd+lLbi>!vC**49O;IOC~;~GF%`;C%%){nLW?-@F|C)S8ju5vLmBmx-u(r$>gU_-o% z2-W{Rtac^EV=G}NJ~GgU9O|kTgy~@zf~muf&qe84tEGrFbDb^Usl9j4M0H-#o<)LA zL3sZ84f;+BhMSQ$B(@BzWVw7)(|+kFftV>P?fOEfnBU4+rzQul^i$XOOkEP zMpVG+$IKH{yO|>ST$&Prw{hAMnMaVFheduyOhAl=`F!g|Gq2NiZ2|H=_dcIQTBqGk zSfT1oYEC>X`A_VM8Z9N83kP;K)X)@1=10APWY?{Bj4jPcRUpJdY_YfLhLM;fWJwZM z@(8)|7HprA{9m>14Ed4l$8^*x7i`?KAtC9F_LUS>n3i@6GscRNYiG$$s6t*FR>Sc+ z-co%TAaPxuZKWt~5;A`~4UB@=)v=w!16XHQM$3 zEY7Fv4kHy>e2+Vk4W3_#{WFP~IT;&8XT?9~xodt5a@#78X%X4n7lI}3?f{)H0$)W5 z-@{7=IY+Ad6(A4HbDE{66WL`vRgFC${!_g?aJ2#QKtA((Gfy(kJmCs zR>dwA`L5PRL?_aa?Dxxt={Q+VPO&P&iN+?m3iH*44^_=NnpHOPEQHE5_UIyfh}2$uEg~7YTlOG6u$pBqe|2`mhClszNc0oBg`6i; zDsm$foBwISk%<9KuQIA7YV$j{WK@e>hT6QtMl^DgGU6B0CD-jy1j$t6AHz$Vot!&x z!>C*O)Vfn;mZlLoUFs6@Q(1K-X{gHiI1}JpGFH$AUw>$*7IsX!UBEfMW^0zz*1{t5 zSb=?K`|6_`!#^7-j;X0P2yCP?U?WBQ)kqDj?f#hZ|7WFO69qpZQPLm4cN=~uVh{dQ z*KB)KZHiT?P$m@QKt`x_EOcu`bKV%kgS(Ty`QmI7SgY!lnQ#k=uEW;{CMeVJ#S$^( zP{J7r+XBUs7u6{}3a>anVCGjT%VuE_Bq`OfLo;(uU}+bz={5ndp`+|LnaSo|tnESw z7e3|C!6RdK##Tq&Ma;9Z#aQm)o%%$)qiIu+;pr09wSXbv0&?O<7#SZRx8nNYky4)6 zB<0+!U+U&I%#XfpNs+&qZdE!^BUpr`fH9b05s!A9X7CE)!x|}F`u1J6M}zUbQk&Yb zZ)qmgDH&4af`

xreKmz#xrmsU6>s&qb1%UK=Hgvwi~i+5-Z-|n*68Jcr`8^O000F%1b=Yj_71KV zpg#^9jj7W13moWvCmL1HpK~XUf!`lebGn&9@A~GeY}au)qnU*Su%<~fq5EdjbXovn z;u*;qv%019fx_R2Kka(rn|SWUucVc;-1R8|4L#ZByxye>afgFQ&h9Jvt{Xn9egzTGCl+&W)wb8uraWiW#iro$rHCI)GOxao@@)7HO|_cy8r zDSgl5LC)lnh%JfN+z zqC07|l&M45Bxun*6Z${mmv5tUjibG@FXJR9yr-*UwyqB>xxLOkwy!$pSY{S0;1#Tm zcDij!Im_<6c5GCVKU~&mhlC<)5_VUpOS{qQxJUkg{9u&3Y|ECIly}j6GE*ZVmWyf6 z=K9<`$FY%Ei!R=7zY}~bPx{akwX=elW>NF}G$-Ow0gO{BdFJKjseY0O-keYvR1pz$ zvmPGY^Hf4iR1>uw<`rvpu}hDDXqp zDa_2CG26YV!sBu+_LBi8olB=8x*quO(Ouiq&4BGj07vYeY!Q~8H*73D*<|2vOz<}p zOAp4bNf<@g7{P}G5{}#u54l~a`%H@Y`?`jm0EXk^x4@8wk1%i>$1*p1i%YjWAr0?R z{cW9%Przk{p*z!(#k=YQH1wi{7C=zqK$Iw%7v2YhI-|M$AU|Vro)R(SD1d@-wGj!C zba3Kh<2!*Ov5uYd@kyOXO(8^Eowr&s;YS=JUT2E&J!q2XLvNs{w3YjrNq~&SH(#jJ zdQzCm_A=F6DvV z6u#r``koNW9Rc!@F~Y8tYM@;4fn|W^em0gXtLJ+S7Ajjy7ooHH(*UUIwb1z4P;K&L z=y^^E~}xd7Zcf_E09WVDY7+X!cUm>86~_YzrK`- zgp>KAnCj?Ov|+Mp+M)KxfDe|{1A3dA!Uq&`Pk7{NUwp4JT7s!I?YtsNpKEk8hVXrx z8SecJLY)Uz?nH^I0{MCfO~^hGo0p{c0*T6M+OkC7x6p{nw&aNE^?Fr_+TL;w+{dHC zCo2{SxL89>;8$A<)l2VF4RVUr2a?mfR!0SWH7422k6B^8XJgFr=Ml5-(Jd7 zLyLYynZ`TVq^gP`Qu|fl9E*WGN=Y};~PO*#S+=M3Z3-0WrR%MW8JkCbItGPRh@iSO?6&X<)Ekbe6LdnG%-std*17<|5z|k+|1YA z+{{OsjwK$WDC4{Npl9SHHC}mSd7K7tU7^c@1r4uItEJaiYuVsE$<9=1b8JC}b8-52 z&}&wehj5tvh?pd!0>0pqwHHVfiYDP0 zRFx=bR5?CynDYP12O|^%-v9C1OU;FkL*70JZ%r}PWsL_sUJSV6=WtUt4+!}0tOG3PgY5RL@F5TJnXt3R6dXGK5T1N`qM z9(A>&3HV-dWD4A)W(r6I$O57N)3$#>ekS2D6JX*0)A}rW-ek#`=7q!SAI3aQ&W9*1 z4pR>MDA#gU*p3_oRmZXEtPg5Fh;NCrhG9`qESngSNVdO5T)nE=DX$->su9YdbN6gG z^lI#K z!Z%xx;}YYp9sSNMH7t|Ck89`y&OQ&HUIG3iR19AePNTsiWu^aP0fUNQqMjBZogyE^ zfteVb9c!yk176k_t(S6oC4PZu;Jns)E^f|yc>WTY1RE7W#j-*m)!>v3RYyG@dRd7d zR8gU*Os}?7EjW923KetPthV;AA|SAL3V?BJ7J%TyD&$*-r`TtrBKTr@D&mtPD=AbR zo^Ct1qP7yh0$!Dc3QAg)Z0^H^4J^Gdpp}3Vl5O7{BT_*e!9lB89}r4ea#Y1Ued8P1 zjHze4?8z;>;LCeNWLSiK6ZV;z@ffhV|zTC=_bGtH{W|Asj~4cm+o(ZthCT}O8!40 zY6V>D*MYx@-x}eNEe+L1oneHl01~=xuBBF{kz{+?@>#L(LQH(x6!+nsZ0eb|R!(F) zv~JrwiNAY4WIm8;91?X`Rs3?)^9R@5Lv;)z8&6Qu%7Pi&LKypu>H$_U(1J>Pzj}S! zS0ODg3A@KC&Aq%d)MKt}=cv5;%|d~iR|D?+k;~MId8o2=izG%ak4LrK;yV$?jC-z| zX1U`MqW!1AiMyTI3*s5qEQs(SYxgN0=`_{Q@q2pLq~)@^1-q?hvwmz)le3rE3iGM{ zbE79HZAN=;aUc4y>Yaj@g{RvP^YW$SMRM@das&%}jzR4aA*F$++k2YIChOtHPs5XQ zL&x7lqfGDEvk#7}R0PXf4t5%pfFTt})q5994sK`P>*P;FI(-%4Rg^cXdXBD#k~5R( zo^y@i?|Xu}+|L*6xGvMARh9acxuNE)^vQbN2*vrkNoEW>OS==FagDTLFO6;*^xYp- zejGf5x9k5R7$IKJf@!6Hj_dt-uKM-)hbUiJ$-gW3dlcfY;OL(h6Mw}d{to&))mbAyZZo8f+dvBVG{!ZD5XMn)+wz{{bF Date: Fri, 3 Nov 2023 16:57:05 -0700 Subject: [PATCH 074/131] docs: update docs for .iter_inner_content() --- docs/api/table.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api/table.rst b/docs/api/table.rst index 215bf807c..6f27670fa 100644 --- a/docs/api/table.rst +++ b/docs/api/table.rst @@ -22,7 +22,9 @@ Table objects are constructed using the ``add_table()`` method on |Document|. ------------------------ .. autoclass:: _Cell + :inherited-members: :members: + :exclude-members: part |_Row| objects From 57d3b9ee9cb778e76258653c6dc464d5598ca80b Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 3 Nov 2023 17:15:44 -0700 Subject: [PATCH 075/131] release: prepare v1.1.0 release --- HISTORY.rst | 6 ++++++ src/docx/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index d57bf5696..8e0b1a588 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +1.1.0 (2023-11-03) +++++++++++++++++++ + +- Add BlockItemContainer.iter_inner_content() + + 1.0.1 (2023-10-12) ++++++++++++++++++ diff --git a/src/docx/__init__.py b/src/docx/__init__.py index a518501a5..b214045d1 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from docx.opc.part import Part -__version__ = "1.0.1" +__version__ = "1.1.0" __all__ = ["Document"] From 630ecbf0d8da2f413571243e22bc0be1fb1e5a57 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 29 Apr 2024 15:30:47 -0700 Subject: [PATCH 076/131] rfctr(lint): tune in ruff settings --- pyproject.toml | 8 ++++++-- src/docx/image/tiff.py | 6 +----- tests/unitutil/cxml.py | 6 ++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d35c790c7..8c0518a96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ Homepage = "https://github.com/python-openxml/python-docx" Repository = "https://github.com/python-openxml/python-docx" [tool.black] +line-length = 100 target-version = ["py37", "py38", "py39", "py310", "py311"] [tool.pytest.ini_options] @@ -69,6 +70,10 @@ python_functions = ["it_", "its_", "they_", "and_", "but_"] [tool.ruff] exclude = [] +line-length = 100 +target-version = "py38" + +[tool.ruff.lint] ignore = [ "COM812", # -- over-aggressively insists on trailing commas where not desired -- "PT001", # -- wants @pytest.fixture() instead of @pytest.fixture -- @@ -88,9 +93,8 @@ select = [ "UP032", # -- Use f-string instead of `.format()` call -- "UP034", # -- Avoid extraneous parentheses -- ] -target-version = "py37" -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["docx"] known-local-folder = ["helpers"] diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py index b84d9f10f..1194929af 100644 --- a/src/docx/image/tiff.py +++ b/src/docx/image/tiff.py @@ -98,11 +98,7 @@ def _dpi(self, resolution_tag): 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 - ) + resolution_unit = ifd_entries.get(TIFF_TAG.RESOLUTION_UNIT, 2) if resolution_unit == 1: # aspect ratio only return 72 diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index c7b7d172c..e76cabd74 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -89,7 +89,7 @@ def from_token(cls, token): Return an ``Element`` object constructed from a parser element token. """ tagname = token.tagname - attrs = [(name, value) for name, value in token.attr_list] + attrs = [tuple(a) for a in token.attr_list] text = token.text return cls(tagname, attrs, text) @@ -263,9 +263,7 @@ def grammar(): child_node_list << (open_paren + delimitedList(node) + close_paren | node) root_node = ( - element("element") - + Group(Optional(slash + child_node_list))("child_node_list") - + stringEnd + element("element") + Group(Optional(slash + child_node_list))("child_node_list") + stringEnd ).setParseAction(connect_root_node_children) return root_node From 5a22c521c5f749847c7b038cef0074b469a7994a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 5 Nov 2023 22:11:55 -0800 Subject: [PATCH 077/131] rfctr: improve typing for tables --- pyrightconfig.json | 2 +- requirements-dev.txt | 2 + requirements-test.txt | 1 + src/docx/opc/oxml.py | 6 +- src/docx/oxml/__init__.py | 2 + src/docx/oxml/document.py | 2 +- src/docx/oxml/parser.py | 4 +- src/docx/oxml/section.py | 66 ++--- src/docx/oxml/shared.py | 6 +- src/docx/oxml/table.py | 491 ++++++++++++++++++-------------- src/docx/oxml/text/hyperlink.py | 14 +- src/docx/oxml/text/paragraph.py | 6 +- src/docx/oxml/text/parfmt.py | 6 +- src/docx/oxml/text/run.py | 15 +- src/docx/oxml/xmlchemy.py | 67 ++--- src/docx/table.py | 121 ++++---- tests/test_table.py | 94 +++--- 17 files changed, 480 insertions(+), 425 deletions(-) diff --git a/pyrightconfig.json b/pyrightconfig.json index 161e49d2b..21afeb97b 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -6,7 +6,7 @@ "ignore": [ ], "include": [ - "src/docx/", + "src/docx", "tests" ], "pythonPlatform": "All", diff --git a/requirements-dev.txt b/requirements-dev.txt index 45e5f78c3..14d8740e3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,7 @@ -r requirements-test.txt build +ruff setuptools>=61.0.0 tox twine +types-lxml diff --git a/requirements-test.txt b/requirements-test.txt index 85d9f6ba3..9ee78b43f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,4 +2,5 @@ behave>=1.2.3 pyparsing>=2.0.1 pytest>=2.5 +pytest-xdist ruff diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index 570dcf413..0249de918 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Temporary stand-in for main oxml module. This module came across with the PackageReader transplant. Probably much will get @@ -27,7 +29,7 @@ # =========================================================================== -def parse_xml(text: str) -> etree._Element: # pyright: ignore[reportPrivateUsage] +def parse_xml(text: str) -> etree._Element: """`etree.fromstring()` replacement that uses oxml parser.""" return etree.fromstring(text, oxml_parser) @@ -44,7 +46,7 @@ def qn(tag): return "{%s}%s" % (uri, tagroot) -def serialize_part_xml(part_elm): +def serialize_part_xml(part_elm: etree._Element): """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 diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 621ef279a..a37ee9b8e 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -149,6 +149,7 @@ CT_TblGridCol, CT_TblLayoutType, CT_TblPr, + CT_TblPrEx, CT_TblWidth, CT_Tc, CT_TcPr, @@ -164,6 +165,7 @@ 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:tblPrEx", CT_TblPrEx) register_element_cls("w:tblStyle", CT_String) register_element_cls("w:tc", CT_Tc) register_element_cls("w:tcPr", CT_TcPr) diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index cc27f5aa9..ff3736f65 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -44,7 +44,7 @@ class CT_Body(BaseOxmlElement): p = ZeroOrMore("w:p", successors=("w:sectPr",)) tbl = ZeroOrMore("w:tbl", successors=("w:sectPr",)) - sectPr: CT_SectPr | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + sectPr: CT_SectPr | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:sectPr", successors=() ) diff --git a/src/docx/oxml/parser.py b/src/docx/oxml/parser.py index 7e6a0fb49..a38362676 100644 --- a/src/docx/oxml/parser.py +++ b/src/docx/oxml/parser.py @@ -1,3 +1,5 @@ +# pyright: reportImportCycles=false + """XML parser for python-docx.""" from __future__ import annotations @@ -43,7 +45,7 @@ def OxmlElement( nsptag_str: str, attrs: Dict[str, str] | None = None, nsdecls: Dict[str, str] | None = None, -) -> BaseOxmlElement: +) -> BaseOxmlElement | etree._Element: # pyright: ignore[reportPrivateUsage] """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`. diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py index a4090898a..71072e2df 100644 --- a/src/docx/oxml/section.py +++ b/src/docx/oxml/section.py @@ -51,38 +51,34 @@ def inner_content_elements(self) -> List[CT_P | CT_Tbl]: 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 + type_: WD_HEADER_FOOTER = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "w:type", WD_HEADER_FOOTER ) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] class CT_PageMar(BaseOxmlElement): """```` element, defining page margins.""" - top: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + top: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:top", ST_SignedTwipsMeasure ) - right: Length | None = OptionalAttribute( # pyright: ignore + right: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:right", ST_TwipsMeasure ) - bottom: Length | None = OptionalAttribute( # pyright: ignore + bottom: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:bottom", ST_SignedTwipsMeasure ) - left: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + left: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:left", ST_TwipsMeasure ) - header: Length | None = OptionalAttribute( # pyright: ignore + header: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:header", ST_TwipsMeasure ) - footer: Length | None = OptionalAttribute( # pyright: ignore + footer: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:footer", ST_TwipsMeasure ) - gutter: Length | None = OptionalAttribute( # pyright: ignore + gutter: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:gutter", ST_TwipsMeasure ) @@ -90,16 +86,14 @@ class CT_PageMar(BaseOxmlElement): class CT_PageSz(BaseOxmlElement): """```` element, defining page dimensions and orientation.""" - w: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + w: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:w", ST_TwipsMeasure ) - h: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + h: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:h", ST_TwipsMeasure ) - orient: WD_ORIENTATION = ( - OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] - "w:orient", WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT - ) + orient: WD_ORIENTATION = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:orient", WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT ) @@ -139,16 +133,16 @@ class CT_SectPr(BaseOxmlElement): ) headerReference = ZeroOrMore("w:headerReference", successors=_tag_seq) footerReference = ZeroOrMore("w:footerReference", successors=_tag_seq) - type: CT_SectType | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + type: CT_SectType | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:type", successors=_tag_seq[3:] ) - pgSz: CT_PageSz | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + pgSz: CT_PageSz | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:pgSz", successors=_tag_seq[4:] ) - pgMar: CT_PageMar | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + pgMar: CT_PageMar | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:pgMar", successors=_tag_seq[5:] ) - titlePg: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + titlePg: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:titlePg", successors=_tag_seq[14:] ) del _tag_seq @@ -187,9 +181,7 @@ def bottom_margin(self) -> Length | None: @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) - ) + 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 @@ -217,9 +209,7 @@ def footer(self) -> Length | None: @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) - ) + 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.""" @@ -251,9 +241,7 @@ def gutter(self) -> Length | None: @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) - ) + pgMar.gutter = value if value is None or isinstance(value, Length) else Length(value) @property def header(self) -> Length | None: @@ -270,9 +258,7 @@ def header(self) -> Length | None: @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) - ) + 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. @@ -295,9 +281,7 @@ def left_margin(self) -> Length | None: @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) - ) + pgMar.left = value if value is None or isinstance(value, Length) else Length(value) @property def orientation(self) -> WD_ORIENTATION: @@ -442,8 +426,8 @@ def top_margin(self, value: Length | None): class CT_SectType(BaseOxmlElement): """```` element, defining the section start type.""" - val: WD_SECTION_START | None = ( # pyright: ignore[reportGeneralTypeIssues] - OptionalAttribute("w:val", WD_SECTION_START) + val: WD_SECTION_START | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:val", WD_SECTION_START ) diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index 1774560ac..a74abc4ac 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -15,7 +15,7 @@ class CT_DecimalNumber(BaseOxmlElement): containing a text representation of a decimal number (e.g. 42) in its ``val`` attribute.""" - val = RequiredAttribute("w:val", ST_DecimalNumber) + val: int = RequiredAttribute("w:val", ST_DecimalNumber) # pyright: ignore[reportAssignmentType] @classmethod def new(cls, nsptagname, val): @@ -42,9 +42,7 @@ class CT_String(BaseOxmlElement): In those cases, it containing a style name in its `val` attribute. """ - val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] - "w:val", ST_String - ) + val: str = RequiredAttribute("w:val", ST_String) # pyright: ignore[reportGeneralTypeIssues] @classmethod def new(cls, nsptagname: str, val: str): diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index 48a6d8c2f..da3c6b51d 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -2,12 +2,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, List +from typing import TYPE_CHECKING, Callable, cast -from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE +from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE, WD_TABLE_DIRECTION from docx.exceptions import InvalidSpanError from docx.oxml.ns import nsdecls, qn from docx.oxml.parser import parse_xml +from docx.oxml.shared import CT_DecimalNumber from docx.oxml.simpletypes import ( ST_Merge, ST_TblLayoutType, @@ -15,6 +16,7 @@ ST_TwipsMeasure, XsdInt, ) +from docx.oxml.text.paragraph import CT_P from docx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, @@ -24,31 +26,43 @@ ZeroOrMore, ZeroOrOne, ) -from docx.shared import Emu, Twips +from docx.shared import Emu, Length, Twips if TYPE_CHECKING: - from docx.oxml.text.paragraph import CT_P - from docx.shared import Length + from docx.enum.table import WD_TABLE_ALIGNMENT + from docx.enum.text import WD_ALIGN_PARAGRAPH + from docx.oxml.shared import CT_OnOff, CT_String + from docx.oxml.text.parfmt import CT_Jc class CT_Height(BaseOxmlElement): - """Used for ```` to specify a row height and row height rule.""" + """Used for `w:trHeight` to specify a row height and row height rule.""" - val = OptionalAttribute("w:val", ST_TwipsMeasure) - hRule = OptionalAttribute("w:hRule", WD_ROW_HEIGHT_RULE) + val: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:val", ST_TwipsMeasure + ) + hRule: WD_ROW_HEIGHT_RULE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "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 + add_tc: Callable[[], CT_Tc] + get_or_add_trPr: Callable[[], CT_TrPr] + + tc_lst: list[CT_Tc] + # -- custom inserter below -- + tblPrEx: CT_TblPrEx | None = ZeroOrOne("w:tblPrEx") # pyright: ignore[reportAssignmentType] + # -- custom inserter below -- + trPr: CT_TrPr | None = ZeroOrOne("w:trPr") # pyright: ignore[reportAssignmentType] tc = ZeroOrMore("w:tc") - def tc_at_grid_col(self, idx): - """The ```` element appearing at grid column `idx`. + def tc_at_grid_col(self, idx: int) -> CT_Tc: + """`` element appearing at grid column `idx`. - Raises |ValueError| if no ``w:tc`` element begins at that grid column. + Raises |ValueError| if no `w:tc` element begins at that grid column. """ grid_col = 0 for tc in self.tc_lst: @@ -60,21 +74,21 @@ def tc_at_grid_col(self, idx): raise ValueError("index out of bounds") @property - def tr_idx(self): - """The index of this ```` element within its parent ```` - element.""" - return self.getparent().tr_lst.index(self) + def tr_idx(self) -> int: + """Index of this `w:tr` element within its parent `w:tbl` element.""" + tbl = cast(CT_Tbl, self.getparent()) + return tbl.tr_lst.index(self) @property - def trHeight_hRule(self): - """Return the value of `w:trPr/w:trHeight@w:hRule`, or |None| if not present.""" + def trHeight_hRule(self) -> WD_ROW_HEIGHT_RULE | None: + """The value of `./w:trPr/w:trHeight/@w:hRule`, or |None| if not present.""" trPr = self.trPr if trPr is None: return None return trPr.trHeight_hRule @trHeight_hRule.setter - def trHeight_hRule(self, value): + def trHeight_hRule(self, value: WD_ROW_HEIGHT_RULE | None): trPr = self.get_or_add_trPr() trPr.trHeight_hRule = value @@ -87,14 +101,14 @@ def trHeight_val(self): return trPr.trHeight_val @trHeight_val.setter - def trHeight_val(self, value): + def trHeight_val(self, value: Length | None): trPr = self.get_or_add_trPr() trPr.trHeight_val = value - def _insert_tblPrEx(self, tblPrEx): + def _insert_tblPrEx(self, tblPrEx: CT_TblPrEx): self.insert(0, tblPrEx) - def _insert_trPr(self, trPr): + def _insert_trPr(self, trPr: CT_TrPr): tblPrEx = self.tblPrEx if tblPrEx is not None: tblPrEx.addnext(trPr) @@ -108,13 +122,16 @@ def _new_tc(self): class CT_Tbl(BaseOxmlElement): """```` element.""" - tblPr = OneAndOnlyOne("w:tblPr") - tblGrid = OneAndOnlyOne("w:tblGrid") + add_tr: Callable[[], CT_Row] + tr_lst: list[CT_Row] + + tblPr: CT_TblPr = OneAndOnlyOne("w:tblPr") # pyright: ignore[reportAssignmentType] + tblGrid: CT_TblGrid = OneAndOnlyOne("w:tblGrid") # pyright: ignore[reportAssignmentType] tr = ZeroOrMore("w:tr") @property - def bidiVisual_val(self): - """Value of `w:tblPr/w:bidiVisual/@w:val` or |None| if not present. + def bidiVisual_val(self) -> bool | None: + """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. """ @@ -124,12 +141,12 @@ def bidiVisual_val(self): return bidiVisual.val @bidiVisual_val.setter - def bidiVisual_val(self, value): + def bidiVisual_val(self, value: WD_TABLE_DIRECTION | None): tblPr = self.tblPr if value is None: - tblPr._remove_bidiVisual() + tblPr._remove_bidiVisual() # pyright: ignore[reportPrivateUsage] else: - tblPr.get_or_add_bidiVisual().val = value + tblPr.get_or_add_bidiVisual().val = bool(value) @property def col_count(self): @@ -153,111 +170,118 @@ def new_tbl(cls, rows: int, cols: int, width: Length) -> CT_Tbl: `width` is distributed evenly between the columns. """ - return parse_xml(cls._tbl_xml(rows, cols, width)) + return cast(CT_Tbl, 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.""" + def tblStyle_val(self) -> str | None: + """`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 return tblStyle.val @tblStyle_val.setter - def tblStyle_val(self, styleId): + def tblStyle_val(self, styleId: str | None) -> None: """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() + tblPr._remove_tblStyle() # pyright: ignore[reportPrivateUsage] if styleId is None: return - tblPr._add_tblStyle().val = styleId + tblPr._add_tblStyle().val = styleId # pyright: ignore[reportPrivateUsage] @classmethod def _tbl_xml(cls, rows: int, cols: int, width: Length) -> str: - 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" - "%s" # tblGrid - "%s" # trs - "\n" - ) % ( - nsdecls("w"), - cls._tblGrid_xml(cols, col_width), - cls._trs_xml(rows, cols, col_width), + f"\n" + f" \n" + f' \n' + f' \n' + f" \n" + f"{cls._tblGrid_xml(cols, col_width)}" + f"{cls._trs_xml(rows, cols, col_width)}" + f"\n" ) @classmethod - def _tblGrid_xml(cls, col_count, col_width): + def _tblGrid_xml(cls, col_count: int, col_width: Length) -> str: xml = " \n" - for i in range(col_count): + for _ in range(col_count): xml += ' \n' % col_width.twips xml += " \n" return xml @classmethod - def _trs_xml(cls, row_count, col_count, col_width): - xml = "" - for i in range(row_count): - xml += (" \n" "%s" " \n") % cls._tcs_xml( - col_count, col_width - ) - return xml + def _trs_xml(cls, row_count: int, col_count: int, col_width: Length) -> str: + return f" \n{cls._tcs_xml(col_count, col_width)} \n" * row_count @classmethod - def _tcs_xml(cls, col_count, col_width): - xml = "" - for i in range(col_count): - xml += ( - " \n" - " \n" - ' \n' - " \n" - " \n" - " \n" - ) % col_width.twips - return xml + def _tcs_xml(cls, col_count: int, col_width: Length) -> str: + return ( + f" \n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + ) * col_count class CT_TblGrid(BaseOxmlElement): - """```` element, child of ````, holds ```` elements - that define column count, width, etc.""" + """`w:tblGrid` element. + + Child of `w:tbl`, holds `w:gridCol> elements that define column count, width, etc. + """ + + add_gridCol: Callable[[], CT_TblGridCol] + gridCol_lst: list[CT_TblGridCol] gridCol = ZeroOrMore("w:gridCol", successors=("w:tblGridChange",)) class CT_TblGridCol(BaseOxmlElement): - """```` element, child of ````, defines a table column.""" + """`w:gridCol` element, child of `w:tblGrid`, defines a table column.""" - w = OptionalAttribute("w:w", ST_TwipsMeasure) + w: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:w", ST_TwipsMeasure + ) @property - def gridCol_idx(self): - """The index of this ```` element within its parent ```` - element.""" - return self.getparent().gridCol_lst.index(self) + def gridCol_idx(self) -> int: + """Index of this `w:gridCol` element within its parent `w:tblGrid` element.""" + tblGrid = cast(CT_TblGrid, self.getparent()) + return tblGrid.gridCol_lst.index(self) class CT_TblLayoutType(BaseOxmlElement): - """```` element, specifying whether column widths are fixed or can be - automatically adjusted based on content.""" + """`w:tblLayout` element. - type = OptionalAttribute("w:type", ST_TblLayoutType) + Specifies whether column widths are fixed or can be automatically adjusted based on + content. + """ + + type: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:type", ST_TblLayoutType + ) class CT_TblPr(BaseOxmlElement): """```` element, child of ````, holds child elements that define table properties such as style and borders.""" + get_or_add_bidiVisual: Callable[[], CT_OnOff] + get_or_add_jc: Callable[[], CT_Jc] + get_or_add_tblLayout: Callable[[], CT_TblLayoutType] + _add_tblStyle: Callable[[], CT_String] + _remove_bidiVisual: Callable[[], None] + _remove_jc: Callable[[], None] + _remove_tblStyle: Callable[[], None] + _tag_seq = ( "w:tblStyle", "w:tblpPr", @@ -278,31 +302,35 @@ class CT_TblPr(BaseOxmlElement): "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: CT_String | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:tblStyle", successors=_tag_seq[1:] + ) + bidiVisual: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:bidiVisual", successors=_tag_seq[4:] + ) + jc: CT_Jc | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:jc", successors=_tag_seq[8:] + ) + tblLayout: CT_TblLayoutType | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "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. - """ + def alignment(self) -> WD_TABLE_ALIGNMENT | None: + """Horizontal alignment of table, |None| if `./w:jc` is not present.""" jc = self.jc if jc is None: return None - return jc.val + return cast("WD_TABLE_ALIGNMENT | None", jc.val) @alignment.setter - def alignment(self, value): + def alignment(self, value: WD_TABLE_ALIGNMENT | None): self._remove_jc() if value is None: return jc = self.get_or_add_jc() - jc.val = value + jc.val = cast("WD_ALIGN_PARAGRAPH", value) @property def autofit(self) -> bool: @@ -328,33 +356,40 @@ def style(self): return tblStyle.val @style.setter - def style(self, value): + def style(self, value: str | None): self._remove_tblStyle() if value is None: return - self._add_tblStyle(val=value) + self._add_tblStyle().val = value + + +class CT_TblPrEx(BaseOxmlElement): + """`w:tblPrEx` element, exceptions to table-properties. + + Applied at a lower level, like a `w:tr` to modify the appearance. Possibly used when + two tables are merged. For more see: + http://officeopenxml.com/WPtablePropertyExceptions.php + """ class CT_TblWidth(BaseOxmlElement): - """Used for ```` and ```` elements and many others, to specify a - table-related width.""" + """Used for `w:tblW` and `w:tcW` and others, specifies 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) + w: int = RequiredAttribute("w:w", XsdInt) # pyright: ignore[reportAssignmentType] 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.""" + def width(self) -> Length | None: + """EMU length indicated by the combined `w:w` and `w:type` attrs.""" if self.type != "dxa": return None return Twips(self.w) @width.setter - def width(self, value): + def width(self, value: Length): self.type = "dxa" self.w = Emu(value).twips @@ -363,17 +398,19 @@ class CT_Tc(BaseOxmlElement): """`w:tc` table cell element.""" add_p: Callable[[], CT_P] - p_lst: List[CT_P] - tbl_lst: List[CT_Tbl] - + get_or_add_tcPr: Callable[[], CT_TcPr] + p_lst: list[CT_P] + tbl_lst: list[CT_Tbl] _insert_tbl: Callable[[CT_Tbl], CT_Tbl] + _new_p: Callable[[], CT_P] - tcPr = ZeroOrOne("w:tcPr") # bunches of successors, overriding insert + # -- tcPr has many successors, `._insert_tcPr()` is overridden below -- + tcPr: CT_TcPr | None = ZeroOrOne("w:tcPr") # pyright: ignore[reportAssignmentType] p = OneOrMore("w:p") tbl = OneOrMore("w:tbl") @property - def bottom(self): + def bottom(self) -> int: """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 @@ -386,21 +423,18 @@ def bottom(self): return self._tr_idx + 1 def clear_content(self): - """Remove all content child elements, preserving the ```` element if - present. + """Remove all content elements, preserving `w:tcPr` 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. + Note that this leaves the `w:tc` 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 + `w:p`child element as the last content element. """ - new_children = [] - tcPr = self.tcPr - if tcPr is not None: - new_children.append(tcPr) - self[:] = new_children + # -- remove all cell inner-content except a `w:tcPr` when present. -- + for e in self.xpath("./*[not(self::w:tcPr)]"): + self.remove(e) @property - def grid_span(self): + def grid_span(self) -> int: """The integer number of columns this cell spans. Determined by ./w:tcPr/w:gridSpan/@val, it defaults to 1. @@ -411,12 +445,12 @@ def grid_span(self): return tcPr.grid_span @grid_span.setter - def grid_span(self, value): + def grid_span(self, value: int): tcPr = self.get_or_add_tcPr() tcPr.grid_span = value @property - def inner_content_elements(self) -> List[CT_P | CT_Tbl]: + def inner_content_elements(self) -> list[CT_P | CT_Tbl]: """Generate all `w:p` and `w:tbl` elements in this document-body. Elements appear in document order. Elements shaded by nesting in a `w:ins` or @@ -433,27 +467,28 @@ def iter_block_items(self): yield child @property - def left(self): + def left(self) -> int: """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.""" + def merge(self, other_tc: CT_Tc) -> CT_Tc: + """Return top-left `w:tc` element of a new span. + + Span is 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) return top_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")) + def new(cls) -> CT_Tc: + """A new `w:tc` element, containing an empty paragraph as the required EG_BlockLevelElt.""" + return cast(CT_Tc, parse_xml("\n" " \n" "" % nsdecls("w"))) @property - def right(self): + def right(self) -> int: """The grid column index that marks the right-side extent of the horizontal span of this cell. @@ -463,108 +498,118 @@ def right(self): return self._grid_col + self.grid_span @property - def top(self): + def top(self) -> int: """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.""" + def vMerge(self) -> str | None: + """Value of ./w:tcPr/w:vMerge/@val, |None| if w:vMerge is not present.""" tcPr = self.tcPr if tcPr is None: return None return tcPr.vMerge_val @vMerge.setter - def vMerge(self, value): + def vMerge(self, value: str | None): tcPr = self.get_or_add_tcPr() tcPr.vMerge_val = value @property - def width(self): - """Return the EMU length value represented in the ``./w:tcPr/w:tcW`` child - element or |None| if not present.""" + def width(self) -> Length | None: + """EMU length represented in `./w:tcPr/w:tcW` or |None| if not present.""" tcPr = self.tcPr if tcPr is None: return None return tcPr.width @width.setter - def width(self, value): + def width(self, value: Length): tcPr = self.get_or_add_tcPr() tcPr.width = value - def _add_width_of(self, other_tc): + def _add_width_of(self, other_tc: CT_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. """ if self.width and other_tc.width: - self.width += other_tc.width + self.width = Length(self.width + other_tc.width) @property - def _grid_col(self): + def _grid_col(self) -> int: """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.""" + def _grow_to(self, width: int, height: int, top_tc: CT_Tc | None = None): + """Grow this cell to `width` grid columns and `height` rows. - def vMerge_val(top_tc): - if top_tc is not self: - return ST_Merge.CONTINUE - if height == 1: - return None - return ST_Merge.RESTART + This is accomplished by expanding horizontal spans and creating continuation + cells to form vertical spans. + """ + + def vMerge_val(top_tc: CT_Tc): + return ( + ST_Merge.CONTINUE + if top_tc is not self + else None if height == 1 else ST_Merge.RESTART + ) 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) + tc_below = self._tc_below + assert tc_below is not None + 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.""" + def _insert_tcPr(self, tcPr: CT_TcPr) -> CT_TcPr: + """Override default `._insert_tcPr()`.""" + # -- `tcPr`` has a large number of successors, but always comes first if it appears, + # -- so just 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.""" + def _is_empty(self) -> bool: + """True if this cell contains only a single empty `w:p` element.""" block_items = list(self.iter_block_items()) if len(block_items) > 1: return False - p = block_items[0] # cell must include at least one element - if len(p.r_lst) == 0: + # -- cell must include at least one block item but can be a `w:tbl`, `w:sdt`, + # -- `w:customXml` or a `w:p` + only_item = block_items[0] + if isinstance(only_item, CT_P) and len(only_item.r_lst) == 0: return True 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.""" + def _move_content_to(self, other_tc: CT_Tc): + """Append the content of this cell to `other_tc`. + + Leaves this cell with a single empty ```` element. + """ if other_tc is self: return if self._is_empty: return other_tc._remove_trailing_empty_p() - # appending moves each element from self to other_tc + # -- appending moves each element from self to other_tc -- for block_element in self.iter_block_items(): other_tc.append(block_element) - # add back the required minimum single empty element + # -- add back the required minimum single empty element -- self.append(self._new_p()) - def _new_tbl(self): - return CT_Tbl.new() + def _new_tbl(self) -> None: + raise NotImplementedError( + "use CT_Tbl.new_tbl() to add a new table, specifying rows and columns" + ) @property - def _next_tc(self): + def _next_tc(self) -> CT_Tc | None: """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") @@ -572,32 +617,33 @@ def _next_tc(self): def _remove(self): """Remove this `w:tc` element from the XML tree.""" - self.getparent().remove(self) + parent_element = self.getparent() + assert parent_element is not None + parent_element.remove(self) def _remove_trailing_empty_p(self): - """Remove the last content element from this cell if it is an empty ```` - element.""" + """Remove last content element from this cell if it's an empty `w:p` element.""" block_items = list(self.iter_block_items()) last_content_elm = block_items[-1] - if last_content_elm.tag != qn("w:p"): + if not isinstance(last_content_elm, CT_P): return p = last_content_elm if len(p.r_lst) > 0: return self.remove(p) - def _span_dimensions(self, other_tc): + def _span_dimensions(self, other_tc: CT_Tc) -> tuple[int, int, int, int]: """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): + def raise_on_inverted_L(a: CT_Tc, b: CT_Tc): if a.top == b.top and a.bottom != b.bottom: raise InvalidSpanError("requested span not rectangular") if a.left == b.left and a.right != b.right: raise InvalidSpanError("requested span not rectangular") - def raise_on_tee_shaped(a, b): + def raise_on_tee_shaped(a: CT_Tc, b: CT_Tc): 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") @@ -616,9 +662,10 @@ 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`. + def _span_to_width(self, grid_width: int, top_tc: CT_Tc, vMerge: str | None): + """Incorporate `w:tc` elements to the right until this cell spans `grid_width`. + + Incorporated `w:tc` elements are removed (replaced by gridSpan value). 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 @@ -632,7 +679,7 @@ def _span_to_width(self, grid_width, top_tc, vMerge): self._swallow_next_tc(grid_width, top_tc) self.vMerge = vMerge - def _swallow_next_tc(self, grid_width, top_tc): + def _swallow_next_tc(self, grid_width: int, top_tc: CT_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. @@ -643,7 +690,7 @@ def _swallow_next_tc(self, grid_width, top_tc): than `grid_width` or if there is no next `` element in the row. """ - def raise_on_invalid_swallow(next_tc): + def raise_on_invalid_swallow(next_tc: CT_Tc | None): if next_tc is None: raise InvalidSpanError("not enough grid columns") if self.grid_span + next_tc.grid_span > grid_width: @@ -651,23 +698,24 @@ def raise_on_invalid_swallow(next_tc): next_tc = self._next_tc raise_on_invalid_swallow(next_tc) + assert next_tc is not None next_tc._move_content_to(top_tc) self._add_width_of(next_tc) self.grid_span += next_tc.grid_span next_tc._remove() @property - def _tbl(self): + def _tbl(self) -> CT_Tbl: """The tbl element this tc element appears in.""" - return self.xpath("./ancestor::w:tbl[position()=1]")[0] + return cast(CT_Tbl, self.xpath("./ancestor::w:tbl[position()=1]")[0]) @property - def _tc_above(self): + def _tc_above(self) -> CT_Tc: """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): + def _tc_below(self) -> CT_Tc | None: """The tc element immediately below this one in its grid column.""" tr_below = self._tr_below if tr_below is None: @@ -675,12 +723,12 @@ def _tc_below(self): return tr_below.tc_at_grid_col(self._grid_col) @property - def _tr(self): + def _tr(self) -> CT_Row: """The tr element this tc element appears in.""" - return self.xpath("./ancestor::w:tr[position()=1]")[0] + return cast(CT_Row, self.xpath("./ancestor::w:tr[position()=1]")[0]) @property - def _tr_above(self): + def _tr_above(self) -> CT_Row: """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. @@ -692,7 +740,7 @@ def _tr_above(self): return tr_lst[tr_idx - 1] @property - def _tr_below(self): + def _tr_below(self) -> CT_Row | None: """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 @@ -703,7 +751,7 @@ def _tr_below(self): return None @property - def _tr_idx(self): + def _tr_idx(self) -> int: """The row index of the tr element this tc element appears in.""" return self._tbl.tr_lst.index(self._tr) @@ -711,6 +759,14 @@ def _tr_idx(self): class CT_TcPr(BaseOxmlElement): """```` element, defining table cell properties.""" + get_or_add_gridSpan: Callable[[], CT_DecimalNumber] + get_or_add_tcW: Callable[[], CT_TblWidth] + get_or_add_vAlign: Callable[[], CT_VerticalJc] + _add_vMerge: Callable[[], CT_VMerge] + _remove_gridSpan: Callable[[], None] + _remove_vAlign: Callable[[], None] + _remove_vMerge: Callable[[], None] + _tag_seq = ( "w:cnfStyle", "w:tcW", @@ -731,14 +787,22 @@ class CT_TcPr(BaseOxmlElement): "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: CT_TblWidth | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:tcW", successors=_tag_seq[2:] + ) + gridSpan: CT_DecimalNumber | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:gridSpan", successors=_tag_seq[3:] + ) + vMerge: CT_VMerge | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:vMerge", successors=_tag_seq[5:] + ) + vAlign: CT_VerticalJc | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:vAlign", successors=_tag_seq[12:] + ) del _tag_seq @property - def grid_span(self): + def grid_span(self) -> int: """The integer number of columns this cell spans. Determined by ./w:gridSpan/@val, it defaults to 1. @@ -749,7 +813,7 @@ def grid_span(self): return gridSpan.val @grid_span.setter - def grid_span(self, value): + def grid_span(self, value: int): self._remove_gridSpan() if value > 1: self.get_or_add_gridSpan().val = value @@ -767,7 +831,7 @@ def vAlign_val(self): return vAlign.val @vAlign_val.setter - def vAlign_val(self, value): + def vAlign_val(self, value: WD_CELL_VERTICAL_ALIGNMENT | None): if value is None: self._remove_vAlign() return @@ -783,22 +847,21 @@ def vMerge_val(self): return vMerge.val @vMerge_val.setter - def vMerge_val(self, value): + def vMerge_val(self, value: str | None): self._remove_vMerge() if value is not None: self._add_vMerge().val = 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'.""" + def width(self) -> Length | None: + """EMU length in `./w:tcW` or |None| if not present or its type is not 'dxa'.""" tcW = self.tcW if tcW is None: return None return tcW.width @width.setter - def width(self, value): + def width(self, value: Length): tcW = self.get_or_add_tcW() tcW.width = value @@ -806,6 +869,8 @@ def width(self, value): class CT_TrPr(BaseOxmlElement): """```` element, defining table row properties.""" + get_or_add_trHeight: Callable[[], CT_Height] + _tag_seq = ( "w:cnfStyle", "w:divId", @@ -823,11 +888,13 @@ class CT_TrPr(BaseOxmlElement): "w:del", "w:trPrChange", ) - trHeight = ZeroOrOne("w:trHeight", successors=_tag_seq[8:]) + trHeight: CT_Height | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:trHeight", successors=_tag_seq[8:] + ) del _tag_seq @property - def trHeight_hRule(self): + def trHeight_hRule(self) -> WD_ROW_HEIGHT_RULE | None: """Return the value of `w:trHeight@w:hRule`, or |None| if not present.""" trHeight = self.trHeight if trHeight is None: @@ -835,7 +902,7 @@ def trHeight_hRule(self): return trHeight.hRule @trHeight_hRule.setter - def trHeight_hRule(self, value): + def trHeight_hRule(self, value: WD_ROW_HEIGHT_RULE | None): if value is None and self.trHeight is None: return trHeight = self.get_or_add_trHeight() @@ -850,7 +917,7 @@ def trHeight_val(self): return trHeight.val @trHeight_val.setter - def trHeight_val(self, value): + def trHeight_val(self, value: Length | None): if value is None and self.trHeight is None: return trHeight = self.get_or_add_trHeight() @@ -860,10 +927,14 @@ 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: WD_CELL_VERTICAL_ALIGNMENT = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "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) + val: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:val", ST_Merge, default=ST_Merge.CONTINUE # pyright: ignore[reportArgumentType] + ) diff --git a/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py index 77d409f6a..38a33ff15 100644 --- a/src/docx/oxml/text/hyperlink.py +++ b/src/docx/oxml/text/hyperlink.py @@ -21,13 +21,13 @@ class CT_Hyperlink(BaseOxmlElement): r_lst: List[CT_R] - rId: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] - "r:id", XsdString - ) - anchor: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + rId: str | None = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + anchor: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:anchor", ST_String ) - history = OptionalAttribute("w:history", ST_OnOff, default=True) + history: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:history", ST_OnOff, default=True + ) r = ZeroOrMore("w:r") @@ -36,8 +36,8 @@ 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: + @property + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] """The textual content of this hyperlink. `CT_Hyperlink` stores the hyperlink-text as one or more `w:r` children. diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py index f771dd74f..63e96f312 100644 --- a/src/docx/oxml/text/paragraph.py +++ b/src/docx/oxml/text/paragraph.py @@ -26,7 +26,7 @@ class CT_P(BaseOxmlElement): hyperlink_lst: List[CT_Hyperlink] r_lst: List[CT_R] - pPr: CT_PPr | None = ZeroOrOne("w:pPr") # pyright: ignore[reportGeneralTypeIssues] + pPr: CT_PPr | None = ZeroOrOne("w:pPr") # pyright: ignore[reportAssignmentType] hyperlink = ZeroOrMore("w:hyperlink") r = ZeroOrMore("w:r") @@ -92,8 +92,8 @@ def style(self, style: str | None): pPr = self.get_or_add_pPr() pPr.style = style - @property # pyright: ignore[reportIncompatibleVariableOverride] - def text(self): + @property + def text(self): # pyright: ignore[reportIncompatibleMethodOverride] """The textual content of this paragraph. Inner-content child elements like `w:r` and `w:hyperlink` are translated to diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index 49ea01003..94e802938 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -37,7 +37,9 @@ class CT_Ind(BaseOxmlElement): class CT_Jc(BaseOxmlElement): """```` element, specifying paragraph justification.""" - val = RequiredAttribute("w:val", WD_ALIGN_PARAGRAPH) + val: WD_ALIGN_PARAGRAPH = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "w:val", WD_ALIGN_PARAGRAPH + ) class CT_PPr(BaseOxmlElement): @@ -86,7 +88,7 @@ class CT_PPr(BaseOxmlElement): "w:sectPr", "w:pPrChange", ) - pStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + pStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:pStyle", successors=_tag_seq[1:] ) keepNext = ZeroOrOne("w:keepNext", successors=_tag_seq[2:]) diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index f17d33845..88efae83c 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -29,7 +29,7 @@ class CT_R(BaseOxmlElement): _add_drawing: Callable[[], CT_Drawing] _add_t: Callable[..., CT_Text] - rPr: CT_RPr | None = ZeroOrOne("w:rPr") # pyright: ignore[reportGeneralTypeIssues] + rPr: CT_RPr | None = ZeroOrOne("w:rPr") # pyright: ignore[reportAssignmentType] br = ZeroOrMore("w:br") cr = ZeroOrMore("w:cr") drawing = ZeroOrMore("w:drawing") @@ -120,12 +120,11 @@ def text(self) -> str: equivalent. """ return "".join( - str(e) - for e in self.xpath("w:br | w:cr | w:noBreakHyphen | w:ptab | w:t | w:tab") + 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): + @text.setter + def text(self, text: str): # pyright: ignore[reportIncompatibleMethodOverride] self.clear_content() _RunContentAppender.append_to_run_from_text(self, text) @@ -141,12 +140,10 @@ def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr: class CT_Br(BaseOxmlElement): """`` element, indicating a line, page, or column break in a run.""" - type: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + type: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:type", ST_BrType, default="textWrapping" ) - clear: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] - "w:clear", ST_BrClear - ) + clear: str | None = OptionalAttribute("w:clear", ST_BrClear) # pyright: ignore def __str__(self) -> str: """Text equivalent of this element. Actual value depends on break type. diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index d075f88f1..077bcd583 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -126,16 +126,12 @@ class BaseAttribute: Provides common methods. """ - def __init__( - self, attr_name: str, simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType] - ): + 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: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._prop_name = prop_name @@ -159,14 +155,12 @@ def _clark_name(self): return self._attr_name @property - def _getter(self) -> Callable[[BaseOxmlElement], Any | None]: - ... + def _getter(self) -> Callable[[BaseOxmlElement], Any | None]: ... @property def _setter( self, - ) -> Callable[[BaseOxmlElement, Any | None], None]: - ... + ) -> Callable[[BaseOxmlElement, Any | None], None]: ... class OptionalAttribute(BaseAttribute): @@ -181,7 +175,7 @@ def __init__( self, attr_name: str, simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType], - default: BaseXmlEnum | BaseSimpleType | None = None, + default: BaseXmlEnum | BaseSimpleType | str | bool | None = None, ): super(OptionalAttribute, self).__init__(attr_name, simple_type) self._default = default @@ -259,8 +253,7 @@ def get_attr_value(obj: BaseOxmlElement) -> Any | None: 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) @@ -292,9 +285,7 @@ def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()): self._nsptagname = nsptagname self._successors = successors - def populate_class_members( - self, element_cls: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """Baseline behavior for adding the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._prop_name = prop_name @@ -508,9 +499,7 @@ class OneAndOnlyOne(_BaseChildElement): def __init__(self, nsptagname: str): super(OneAndOnlyOne, self).__init__(nsptagname, ()) - def populate_class_members( - self, element_cls: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """Add the appropriate methods to `element_cls`.""" super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @@ -528,9 +517,7 @@ def get_child_element(obj: BaseOxmlElement): ) return child - get_child_element.__doc__ = ( - "Required ``<%s>`` child element." % self._nsptagname - ) + get_child_element.__doc__ = "Required ``<%s>`` child element." % self._nsptagname return get_child_element @@ -538,9 +525,7 @@ class OneOrMore(_BaseChildElement): """Defines a repeating child element for MetaOxmlElement that must appear at least once.""" - def populate_class_members( - self, element_cls: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """Add the appropriate methods to `element_cls`.""" super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() @@ -554,9 +539,7 @@ def populate_class_members( class ZeroOrMore(_BaseChildElement): """Defines an optional repeating child element for MetaOxmlElement.""" - def populate_class_members( - self, element_cls: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """Add the appropriate methods to `element_cls`.""" super(ZeroOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() @@ -570,9 +553,7 @@ def populate_class_members( class ZeroOrOne(_BaseChildElement): """Defines an optional child element for MetaOxmlElement.""" - def populate_class_members( - self, element_cls: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """Add the appropriate methods to `element_cls`.""" super(ZeroOrOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @@ -604,9 +585,7 @@ def _add_remover(self): def _remove_child(obj: BaseOxmlElement): obj.remove_all(self._nsptagname) - _remove_child.__doc__ = ( - "Remove all ``<%s>`` child elements." - ) % self._nsptagname + _remove_child.__doc__ = ("Remove all ``<%s>`` child elements.") % self._nsptagname self._add_to_class(self._remove_method_name, _remove_child) @lazyproperty @@ -622,16 +601,12 @@ def __init__(self, choices: Sequence[Choice], successors: Tuple[str, ...] = ()): self._choices = choices self._successors = successors - def populate_class_members( - self, element_cls: MetaOxmlElement, prop_name: str - ) -> None: + def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None: """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( - element_cls, self._prop_name, self._successors - ) + choice.populate_class_members(element_cls, self._prop_name, self._successors) self._add_group_remover() def _add_choice_getter(self): @@ -649,9 +624,7 @@ def _remove_choice_group(obj: BaseOxmlElement): for tagname in self._member_nsptagnames: obj.remove_all(tagname) - _remove_choice_group.__doc__ = ( - "Remove the current choice group child element if present." - ) + _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) @property @@ -680,9 +653,7 @@ def _remove_choice_group_method_name(self): # -- lxml typing isn't quite right here, just ignore this error on _Element -- -class BaseOxmlElement( # pyright: ignore[reportGeneralTypeIssues] - etree.ElementBase, metaclass=MetaOxmlElement -): +class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement): """Effective base class for all custom element classes. Adds standardized behavior to all classes in one place. @@ -726,9 +697,7 @@ def xml(self) -> str: """ return serialize_for_reading(self) - def xpath( # pyright: ignore[reportIncompatibleMethodOverride] - self, xpath_str: str - ) -> Any: + def xpath(self, xpath_str: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride] """Override of `lxml` _Element.xpath() method. Provides standard Open XML namespace mapping (`nsmap`) in centralized location. diff --git a/src/docx/table.py b/src/docx/table.py index 31372284c..55cf77f41 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -2,27 +2,37 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Tuple, overload +from typing import TYPE_CHECKING, cast, overload +from typing_extensions import TypeAlias + +from docx import types as t from docx.blkcntnr import BlockItemContainer from docx.enum.style import WD_STYLE_TYPE +from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT from docx.oxml.simpletypes import ST_Merge -from docx.shared import Inches, Parented, lazyproperty +from docx.oxml.table import CT_TblGridCol +from docx.shared import Inches, Parented, StoryChild, lazyproperty if TYPE_CHECKING: - from docx import types as t - from docx.enum.table import WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION - from docx.oxml.table import CT_Tbl, CT_TblPr + from docx.enum.table import WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION + from docx.oxml.table import CT_Row, CT_Tbl, CT_TblPr, CT_Tc from docx.shared import Length - from docx.styles.style import _TableStyle # pyright: ignore[reportPrivateUsage] + from docx.styles.style import ( + ParagraphStyle, + _TableStyle, # pyright: ignore[reportPrivateUsage] + ) + +TableParent: TypeAlias = "Table | _Columns | _Rows" -class Table(Parented): +class Table(StoryChild): """Proxy class for a WordprocessingML ```` element.""" - def __init__(self, tbl: CT_Tbl, parent: t.StoryChild): + def __init__(self, tbl: CT_Tbl, parent: t.ProvidesStoryPart): super(Table, self).__init__(parent) - self._element = self._tbl = tbl + self._element = tbl + self._tbl = tbl def add_column(self, width: Length): """Return a |_Column| object of `width`, newly added rightmost to the table.""" @@ -40,7 +50,8 @@ def add_row(self): tr = tbl.add_tr() for gridCol in tbl.tblGrid.gridCol_lst: tc = tr.add_tc() - tc.width = gridCol.w + if gridCol.w is not None: + tc.width = gridCol.w return _Row(tr, self) @property @@ -79,7 +90,7 @@ def cell(self, row_idx: int, col_idx: int) -> _Cell: cell_idx = col_idx + (row_idx * self._column_count) return self._cells[cell_idx] - def column_cells(self, column_idx: int) -> List[_Cell]: + def column_cells(self, column_idx: int) -> list[_Cell]: """Sequence of cells in the column at `column_idx` in this table.""" cells = self._cells idxs = range(column_idx, len(cells), self._column_count) @@ -90,7 +101,7 @@ def columns(self): """|_Columns| instance representing the sequence of columns in this table.""" return _Columns(self._tbl, self) - def row_cells(self, row_idx: int) -> List[_Cell]: + def row_cells(self, row_idx: int) -> list[_Cell]: """Sequence of cells in the row at `row_idx` in this table.""" column_count = self._column_count start = row_idx * column_count @@ -116,7 +127,7 @@ def style(self) -> _TableStyle | None: `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) + return cast("_TableStyle | None", self.part.get_style(style_id, WD_STYLE_TYPE.TABLE)) @style.setter def style(self, style_or_name: _TableStyle | None): @@ -140,21 +151,21 @@ def table_direction(self) -> WD_TABLE_DIRECTION | None: For example: `WD_TABLE_DIRECTION.LTR`. |None| indicates the value is inherited from the style hierarchy. """ - return self._element.bidiVisual_val + return cast("WD_TABLE_DIRECTION | None", self._tbl.bidiVisual_val) @table_direction.setter def table_direction(self, value: WD_TABLE_DIRECTION | None): self._element.bidiVisual_val = value @property - def _cells(self) -> List[_Cell]: + def _cells(self) -> list[_Cell]: """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 = [] + cells: list[_Cell] = [] for tc in self._tbl.iter_tcs(): for grid_span_idx in range(tc.grid_span): if tc.vMerge == ST_Merge.CONTINUE: @@ -178,11 +189,12 @@ def _tblPr(self) -> CT_TblPr: class _Cell(BlockItemContainer): """Table cell.""" - def __init__(self, tc, parent): - super(_Cell, self).__init__(tc, parent) + def __init__(self, tc: CT_Tc, parent: TableParent): + super(_Cell, self).__init__(tc, cast(t.ProvidesStoryPart, parent)) + self._parent = parent self._tc = self._element = tc - def add_paragraph(self, text="", style=None): + def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = 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 @@ -195,9 +207,12 @@ def add_paragraph(self, text="", style=None): """ 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. + def add_table( # pyright: ignore[reportIncompatibleMethodOverride] + self, rows: int, cols: int + ) -> Table: + """Return a table newly added to this cell after any existing cell content. + + The new table will have `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. @@ -207,7 +222,7 @@ def add_table(self, rows, cols): self.add_paragraph() return table - def merge(self, other_cell): + def merge(self, other_cell: _Cell): """Return a merged cell created by spanning the rectangular region having this cell and `other_cell` as diagonal corners. @@ -244,7 +259,7 @@ def text(self) -> str: return "\n".join(p.text for p in self.paragraphs) @text.setter - def text(self, text): + def text(self, text: str): """Write-only. Set entire contents of cell to the string `text`. Any existing content or @@ -270,7 +285,7 @@ def vertical_alignment(self): return tcPr.vAlign_val @vertical_alignment.setter - def vertical_alignment(self, value): + def vertical_alignment(self, value: WD_CELL_VERTICAL_ALIGNMENT | None): tcPr = self._element.get_or_add_tcPr() tcPr.vAlign_val = value @@ -280,34 +295,35 @@ def width(self): return self._tc.width @width.setter - def width(self, value): + def width(self, value: Length): self._tc.width = value class _Column(Parented): """Table column.""" - def __init__(self, gridCol, parent): + def __init__(self, gridCol: CT_TblGridCol, parent: TableParent): super(_Column, self).__init__(parent) + self._parent = parent self._gridCol = gridCol @property - def cells(self): + def cells(self) -> tuple[_Cell, ...]: """Sequence of |_Cell| instances corresponding to cells in this column.""" return tuple(self.table.column_cells(self._index)) @property - def table(self): + def table(self) -> Table: """Reference to the |Table| object this column belongs to.""" return self._parent.table @property - def width(self): + def width(self) -> Length | None: """The width of this column in EMU, or |None| if no explicit width is set.""" return self._gridCol.w @width.setter - def width(self, value): + def width(self, value: Length | None): self._gridCol.w = value @property @@ -322,11 +338,12 @@ class _Columns(Parented): Supports ``len()``, iteration and indexed access. """ - def __init__(self, tbl, parent): + def __init__(self, tbl: CT_Tbl, parent: TableParent): super(_Columns, self).__init__(parent) + self._parent = parent self._tbl = tbl - def __getitem__(self, idx): + def __getitem__(self, idx: int): """Provide indexed access, e.g. 'columns[0]'.""" try: gridCol = self._gridCol_lst[idx] @@ -343,7 +360,7 @@ def __len__(self): return len(self._gridCol_lst) @property - def table(self): + def table(self) -> Table: """Reference to the |Table| object this column collection belongs to.""" return self._parent.table @@ -358,42 +375,45 @@ def _gridCol_lst(self): class _Row(Parented): """Table row.""" - def __init__(self, tr, parent): + def __init__(self, tr: CT_Row, parent: TableParent): super(_Row, self).__init__(parent) + self._parent = parent self._tr = self._element = tr @property - def cells(self) -> Tuple[_Cell]: + 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): + def height(self) -> Length | None: """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 - def height(self, value): + def height(self, value: Length | None): self._tr.trHeight_val = 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.""" + def height_rule(self) -> WD_ROW_HEIGHT_RULE | None: + """Return the height rule of this cell as a member of the :ref:`WdRowHeightRule`. + + This value is |None| if no explicit height_rule is set. + """ return self._tr.trHeight_hRule @height_rule.setter - def height_rule(self, value): + def height_rule(self, value: WD_ROW_HEIGHT_RULE | None): self._tr.trHeight_hRule = value @property - def table(self): + def table(self) -> Table: """Reference to the |Table| object this row belongs to.""" return self._parent.table @property - def _index(self): + def _index(self) -> int: """Index of this row in its table, starting from zero.""" return self._tr.tr_idx @@ -404,19 +424,18 @@ class _Rows(Parented): Supports ``len()``, iteration, indexed access, and slicing. """ - def __init__(self, tbl, parent): + def __init__(self, tbl: CT_Tbl, parent: TableParent): super(_Rows, self).__init__(parent) + self._parent = parent self._tbl = tbl @overload - def __getitem__(self, idx: int) -> _Row: - ... + def __getitem__(self, idx: int) -> _Row: ... @overload - def __getitem__(self, idx: slice) -> List[_Row]: - ... + def __getitem__(self, idx: slice) -> list[_Row]: ... - def __getitem__(self, idx: int | slice) -> _Row | 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] @@ -427,6 +446,6 @@ def __len__(self): return len(self._tbl.tr_lst) @property - def table(self): + def table(self) -> Table: """Reference to the |Table| object this row collection belongs to.""" return self._parent.table diff --git a/tests/test_table.py b/tests/test_table.py index 0ef273e3f..eef4b1df1 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,7 +1,14 @@ +# pyright: reportPrivateUsage=false + """Test suite for the docx.table module.""" +from __future__ import annotations + +from typing import cast + import pytest +from docx.document import Document from docx.enum.style import WD_STYLE_TYPE from docx.enum.table import ( WD_ALIGN_VERTICAL, @@ -10,7 +17,7 @@ WD_TABLE_DIRECTION, ) from docx.oxml.parser import parse_xml -from docx.oxml.table import CT_Tc +from docx.oxml.table import CT_Tbl, CT_Tc from docx.parts.document import DocumentPart from docx.shared import Inches from docx.table import Table, _Cell, _Column, _Columns, _Row, _Rows @@ -20,7 +27,7 @@ from .oxml.unitdata.text import a_p from .unitutil.cxml import element, xml from .unitutil.file import snippet_seq -from .unitutil.mock import instance_mock, property_mock +from .unitutil.mock import FixtureRequest, Mock, instance_mock, property_mock class DescribeTable: @@ -89,14 +96,44 @@ def it_knows_it_is_the_table_its_children_belong_to(self, table_fixture): table = table_fixture assert table.table is table - def it_knows_its_direction(self, direction_get_fixture): - table, expected_value = direction_get_fixture - assert table.table_direction == expected_value - - def it_can_change_its_direction(self, direction_set_fixture): - table, new_value, expected_xml = direction_set_fixture + @pytest.mark.parametrize( + ("tbl_cxml", "expected_value"), + [ + # ("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 it_knows_its_direction( + self, tbl_cxml: str, expected_value: WD_TABLE_DIRECTION | None, document_: Mock + ): + tbl = cast(CT_Tbl, element(tbl_cxml)) + assert Table(tbl, document_).table_direction == expected_value + + @pytest.mark.parametrize( + ("tbl_cxml", "new_value", "expected_cxml"), + [ + ("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 it_can_change_its_direction( + self, tbl_cxml: str, new_value: WD_TABLE_DIRECTION, expected_cxml: str, document_: Mock + ): + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) table.table_direction = new_value - assert table._element.xml == expected_xml + assert table._element.xml == xml(expected_cxml) def it_knows_its_table_style(self, style_get_fixture): table, style_id_, style_ = style_get_fixture @@ -245,41 +282,6 @@ def column_count_fixture(self): 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), - ] - ) - 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"), - ] - ) - def direction_set_fixture(self, request): - tbl_cxml, new_value, expected_cxml = request.param - table = Table(element(tbl_cxml), None) - expected_xml = xml(expected_cxml) - return table, new_value, expected_xml - @pytest.fixture def row_cells_fixture(self, _cells_, _column_count_): table = Table(None, None) @@ -331,6 +333,10 @@ def _cells_(self, request): def _column_count_(self, request): return property_mock(request, Table, "_column_count") + @pytest.fixture + def document_(self, request: FixtureRequest): + return instance_mock(request, Document) + @pytest.fixture def document_part_(self, request): return instance_mock(request, DocumentPart) From cf5286cf8bbd0902a05b6d73d2b9c8ecf2eb697b Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 15:24:32 -0700 Subject: [PATCH 078/131] rfctr: modernize table tests --- features/steps/cell.py | 50 - features/steps/table.py | 165 ++- ...ble.feature => tbl-cell-add-table.feature} | 0 ...cel-text.feature => tbl-cell-text.feature} | 0 src/docx/oxml/table.py | 1 + src/docx/table.py | 2 +- tests/oxml/test_table.py | 479 ++++--- tests/oxml/unitdata/table.py | 88 -- tests/test_table.py | 1198 +++++++---------- 9 files changed, 857 insertions(+), 1126 deletions(-) delete mode 100644 features/steps/cell.py rename features/{cel-add-table.feature => tbl-cell-add-table.feature} (100%) rename features/{cel-text.feature => tbl-cell-text.feature} (100%) delete mode 100644 tests/oxml/unitdata/table.py diff --git a/features/steps/cell.py b/features/steps/cell.py deleted file mode 100644 index 10896872b..000000000 --- a/features/steps/cell.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Step implementations for table cell-related features.""" - -from behave import given, then, when - -from docx import Document - -from helpers import test_docx - -# given =================================================== - - -@given("a table cell") -def given_a_table_cell(context): - 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") -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") -def when_assign_string_to_cell_text_attribute(context): - cell = context.cell - text = "foobar" - cell.text = text - context.expected_text = text - - -# then ===================================================== - - -@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] - assert len(table.rows) == 2 - assert len(table.columns) == 2 - - -@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 - msg = "expected '%s', got '%s'" % (expected_text, text) - assert text == expected_text, msg diff --git a/features/steps/table.py b/features/steps/table.py index 95f2fab75..0b08f567c 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -1,6 +1,9 @@ +# pyright: reportPrivateUsage=false + """Step implementations for table-related features.""" from behave import given, then, when +from behave.runner import Context from docx import Document from docx.enum.table import ( @@ -10,7 +13,7 @@ WD_TABLE_DIRECTION, ) from docx.shared import Inches -from docx.table import _Column, _Columns, _Row, _Rows +from docx.table import Table, _Column, _Columns, _Row, _Rows from helpers import test_docx @@ -18,12 +21,12 @@ @given("a 2 x 2 table") -def given_a_2x2_table(context): +def given_a_2x2_table(context: Context): context.table_ = Document().add_table(rows=2, cols=2) @given("a 3x3 table having {span_state}") -def given_a_3x3_table_having_span_state(context, span_state): +def given_a_3x3_table_having_span_state(context: Context, span_state: str): table_idx = { "only uniform cells": 0, "a horizontal span": 1, @@ -35,7 +38,7 @@ def given_a_3x3_table_having_span_state(context, span_state): @given("a _Cell object with {state} vertical alignment as cell") -def given_a_Cell_object_with_vertical_alignment_as_cell(context, state): +def given_a_Cell_object_with_vertical_alignment_as_cell(context: Context, state: str): table_idx = { "inherited": 0, "bottom": 1, @@ -48,26 +51,32 @@ def given_a_Cell_object_with_vertical_alignment_as_cell(context, state): @given("a column collection having two columns") -def given_a_column_collection_having_two_columns(context): +def given_a_column_collection_having_two_columns(context: Context): docx_path = test_docx("blk-containing-table") document = Document(docx_path) context.columns = document.tables[0].columns @given("a row collection having two rows") -def given_a_row_collection_having_two_rows(context): +def given_a_row_collection_having_two_rows(context: Context): docx_path = test_docx("blk-containing-table") document = Document(docx_path) context.rows = document.tables[0].rows @given("a table") -def given_a_table(context): +def given_a_table(context: Context): context.table_ = Document().add_table(rows=2, cols=2) +@given("a table cell") +def given_a_table_cell(context: Context): + table = Document(test_docx("tbl-2x2-table")).tables[0] + context.cell = table.cell(0, 0) + + @given("a table cell having a width of {width}") -def given_a_table_cell_having_a_width_of_width(context, width): +def given_a_table_cell_having_a_width_of_width(context: Context, width: str): table_idx = {"no explicit setting": 0, "1 inch": 1, "2 inches": 2}[width] document = Document(test_docx("tbl-props")) table = document.tables[table_idx] @@ -76,7 +85,7 @@ def given_a_table_cell_having_a_width_of_width(context, width): @given("a table column having a width of {width_desc}") -def given_a_table_having_a_width_of_width_desc(context, width_desc): +def given_a_table_having_a_width_of_width_desc(context: Context, width_desc: str): col_idx = { "no explicit setting": 0, "1440": 1, @@ -87,7 +96,7 @@ def given_a_table_having_a_width_of_width_desc(context, width_desc): @given("a table having {alignment} alignment") -def given_a_table_having_alignment_alignment(context, alignment): +def given_a_table_having_alignment_alignment(context: Context, alignment: str): table_idx = { "inherited": 3, "left": 4, @@ -100,7 +109,7 @@ def given_a_table_having_alignment_alignment(context, alignment): @given("a table having an autofit layout of {autofit}") -def given_a_table_having_an_autofit_layout_of_autofit(context, autofit): +def given_a_table_having_an_autofit_layout_of_autofit(context: Context, autofit: str): tbl_idx = { "no explicit setting": 0, "autofit": 1, @@ -111,7 +120,7 @@ def given_a_table_having_an_autofit_layout_of_autofit(context, autofit): @given("a table having {style} style") -def given_a_table_having_style(context, style): +def given_a_table_having_style(context: Context, style: str): table_idx = { "no explicit": 0, "Table Grid": 1, @@ -123,14 +132,14 @@ def given_a_table_having_style(context, style): @given("a table having table direction set {setting}") -def given_a_table_having_table_direction_setting(context, setting): +def given_a_table_having_table_direction_setting(context: Context, setting: str): table_idx = ["to inherit", "right-to-left", "left-to-right"].index(setting) document = Document(test_docx("tbl-on-off-props")) context.table_ = document.tables[table_idx] @given("a table having two columns") -def given_a_table_having_two_columns(context): +def given_a_table_having_two_columns(context: Context): docx_path = test_docx("blk-containing-table") document = Document(docx_path) # context.table is used internally by behave, underscore added @@ -139,14 +148,14 @@ def given_a_table_having_two_columns(context): @given("a table having two rows") -def given_a_table_having_two_rows(context): +def given_a_table_having_two_rows(context: Context): docx_path = test_docx("blk-containing-table") document = Document(docx_path) context.table_ = document.tables[0] @given("a table row having height of {state}") -def given_a_table_row_having_height_of_state(context, state): +def given_a_table_row_having_height_of_state(context: Context, state: str): table_idx = {"no explicit setting": 0, "2 inches": 2, "3 inches": 3}[state] document = Document(test_docx("tbl-props")) table = document.tables[table_idx] @@ -154,10 +163,8 @@ def given_a_table_row_having_height_of_state(context, state): @given("a table row having height rule {state}") -def given_a_table_row_having_height_rule_state(context, state): - table_idx = {"no explicit setting": 0, "automatic": 1, "at least": 2, "exactly": 3}[ - state - ] +def given_a_table_row_having_height_rule_state(context: Context, state: str): + table_idx = {"no explicit setting": 0, "automatic": 1, "at least": 2, "exactly": 3}[state] document = Document(test_docx("tbl-props")) table = document.tables[table_idx] context.row = table.rows[0] @@ -167,35 +174,48 @@ def given_a_table_row_having_height_rule_state(context, state): @when("I add a 1.0 inch column to the table") -def when_I_add_a_1_inch_column_to_table(context): +def when_I_add_a_1_inch_column_to_table(context: Context): context.column = context.table_.add_column(Inches(1.0)) +@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): + context.table_ = context.cell.add_table(2, 2) + + @when("I add a row to the table") -def when_add_row_to_table(context): +def when_add_row_to_table(context: Context): table = context.table_ context.row = table.add_row() +@when("I assign a string to the cell text attribute") +def when_assign_string_to_cell_text_attribute(context: Context): + cell = context.cell + text = "foobar" + cell.text = text + context.expected_text = text + + @when("I assign {value} to cell.vertical_alignment") -def when_I_assign_value_to_cell_vertical_alignment(context, value): +def when_I_assign_value_to_cell_vertical_alignment(context: Context, value: str): context.cell.vertical_alignment = eval(value) @when("I assign {value} to row.height") -def when_I_assign_value_to_row_height(context, value): +def when_I_assign_value_to_row_height(context: Context, value: str): new_value = None if value == "None" else int(value) context.row.height = new_value @when("I assign {value} to row.height_rule") -def when_I_assign_value_to_row_height_rule(context, value): +def when_I_assign_value_to_row_height_rule(context: Context, value: str): new_value = None if value == "None" else getattr(WD_ROW_HEIGHT_RULE, value) context.row.height_rule = new_value @when("I assign {value_str} to table.alignment") -def when_I_assign_value_to_table_alignment(context, value_str): +def when_I_assign_value_to_table_alignment(context: Context, value_str: str): value = { "None": None, "WD_TABLE_ALIGNMENT.LEFT": WD_TABLE_ALIGNMENT.LEFT, @@ -207,7 +227,7 @@ def when_I_assign_value_to_table_alignment(context, value_str): @when("I assign {value} to table.style") -def when_apply_value_to_table_style(context, value): +def when_apply_value_to_table_style(context: Context, value: str): table, styles = context.table_, context.document.styles if value == "None": new_value = None @@ -219,14 +239,14 @@ def when_apply_value_to_table_style(context, value): @when("I assign {value} to table.table_direction") -def when_assign_value_to_table_table_direction(context, value): +def when_assign_value_to_table_table_direction(context: Context, value: str): new_value = None if value == "None" else getattr(WD_TABLE_DIRECTION, value) context.table_.table_direction = new_value @when("I merge from cell {origin} to cell {other}") -def when_I_merge_from_cell_origin_to_cell_other(context, origin, other): - def cell(table, idx): +def when_I_merge_from_cell_origin_to_cell_other(context: Context, origin: str, other: str): + def cell(table: Table, idx: int): row, col = idx // 3, idx % 3 return table.cell(row, col) @@ -237,19 +257,19 @@ def cell(table, idx): @when("I set the cell width to {width}") -def when_I_set_the_cell_width_to_width(context, width): +def when_I_set_the_cell_width_to_width(context: Context, width: str): new_value = {"1 inch": Inches(1)}[width] context.cell.width = new_value @when("I set the column width to {width_emu}") -def when_I_set_the_column_width_to_width_emu(context, width_emu): +def when_I_set_the_column_width_to_width_emu(context: Context, width_emu: str): new_value = None if width_emu == "None" else int(width_emu) context.column.width = new_value @when("I set the table autofit to {setting}") -def when_I_set_the_table_autofit_to_setting(context, setting): +def when_I_set_the_table_autofit_to_setting(context: Context, setting: str): new_value = {"autofit": True, "fixed": False}[setting] table = context.table_ table.autofit = new_value @@ -258,21 +278,27 @@ def when_I_set_the_table_autofit_to_setting(context, setting): # then ===================================================== +@then("cell.tables[0] is a 2 x 2 table") +def then_cell_tables_0_is_a_2x2_table(context: Context): + cell = context.cell + table = cell.tables[0] + assert len(table.rows) == 2 + assert len(table.columns) == 2 + + @then("cell.vertical_alignment is {value}") -def then_cell_vertical_alignment_is_value(context, value): +def then_cell_vertical_alignment_is_value(context: Context, value: str): expected_value = { "None": None, "WD_ALIGN_VERTICAL.BOTTOM": WD_ALIGN_VERTICAL.BOTTOM, "WD_ALIGN_VERTICAL.CENTER": WD_ALIGN_VERTICAL.CENTER, }[value] actual_value = context.cell.vertical_alignment - assert actual_value is expected_value, ( - "cell.vertical_alignment is %s" % actual_value - ) + assert actual_value is expected_value, "cell.vertical_alignment is %s" % actual_value @then("I can access a collection column by index") -def then_can_access_collection_column_by_index(context): +def then_can_access_collection_column_by_index(context: Context): columns = context.columns for idx in range(2): column = columns[idx] @@ -280,7 +306,7 @@ def then_can_access_collection_column_by_index(context): @then("I can access a collection row by index") -def then_can_access_collection_row_by_index(context): +def then_can_access_collection_row_by_index(context: Context): rows = context.rows for idx in range(2): row = rows[idx] @@ -288,21 +314,21 @@ def then_can_access_collection_row_by_index(context): @then("I can access the column collection of the table") -def then_can_access_column_collection_of_table(context): +def then_can_access_column_collection_of_table(context: Context): table = context.table_ columns = table.columns assert isinstance(columns, _Columns) @then("I can access the row collection of the table") -def then_can_access_row_collection_of_table(context): +def then_can_access_row_collection_of_table(context: Context): table = context.table_ rows = table.rows assert isinstance(rows, _Rows) @then("I can iterate over the column collection") -def then_can_iterate_over_column_collection(context): +def then_can_iterate_over_column_collection(context: Context): columns = context.columns actual_count = 0 for column in columns: @@ -312,7 +338,7 @@ def then_can_iterate_over_column_collection(context): @then("I can iterate over the row collection") -def then_can_iterate_over_row_collection(context): +def then_can_iterate_over_row_collection(context: Context): rows = context.rows actual_count = 0 for row in rows: @@ -322,7 +348,7 @@ def then_can_iterate_over_row_collection(context): @then("row.height is {value}") -def then_row_height_is_value(context, value): +def then_row_height_is_value(context: Context, value: str): expected_height = None if value == "None" else int(value) actual_height = context.row.height assert actual_height == expected_height, "expected %s, got %s" % ( @@ -332,7 +358,7 @@ def then_row_height_is_value(context, value): @then("row.height_rule is {value}") -def then_row_height_rule_is_value(context, value): +def then_row_height_rule_is_value(context: Context, value: str): expected_rule = None if value == "None" else getattr(WD_ROW_HEIGHT_RULE, value) actual_rule = context.row.height_rule assert actual_rule == expected_rule, "expected %s, got %s" % ( @@ -342,7 +368,7 @@ def then_row_height_rule_is_value(context, value): @then("table.alignment is {value_str}") -def then_table_alignment_is_value(context, value_str): +def then_table_alignment_is_value(context: Context, value_str: str): value = { "None": None, "WD_TABLE_ALIGNMENT.LEFT": WD_TABLE_ALIGNMENT.LEFT, @@ -354,7 +380,7 @@ def then_table_alignment_is_value(context, value_str): @then("table.cell({row}, {col}).text is {expected_text}") -def then_table_cell_row_col_text_is_text(context, row, col, expected_text): +def then_table_cell_row_col_text_is_text(context: Context, row: str, col: str, expected_text: str): table = context.table_ row_idx, col_idx = int(row), int(col) cell_text = table.cell(row_idx, col_idx).text @@ -362,68 +388,76 @@ def then_table_cell_row_col_text_is_text(context, row, col, expected_text): @then("table.style is styles['{style_name}']") -def then_table_style_is_styles_style_name(context, style_name): +def then_table_style_is_styles_style_name(context: Context, style_name: str): table, styles = context.table_, context.document.styles expected_style = styles[style_name] assert table.style == expected_style, "got '%s'" % table.style @then("table.table_direction is {value}") -def then_table_table_direction_is_value(context, value): +def then_table_table_direction_is_value(context: Context, value: str): expected_value = None if value == "None" else getattr(WD_TABLE_DIRECTION, value) actual_value = context.table_.table_direction assert actual_value == expected_value, "got '%s'" % actual_value +@then("the cell contains the string I assigned") +def then_cell_contains_string_assigned(context: Context): + cell, expected_text = context.cell, context.expected_text + text = cell.paragraphs[0].runs[0].text + msg = "expected '%s', got '%s'" % (expected_text, text) + assert text == expected_text, msg + + @then("the column cells text is {expected_text}") -def then_the_column_cells_text_is_expected_text(context, expected_text): +def then_the_column_cells_text_is_expected_text(context: Context, expected_text: str): table = context.table_ cells_text = " ".join(c.text for col in table.columns for c in col.cells) assert cells_text == expected_text, "got %s" % cells_text @then("the length of the column collection is 2") -def then_len_of_column_collection_is_2(context): +def then_len_of_column_collection_is_2(context: Context): columns = context.table_.columns assert len(columns) == 2 @then("the length of the row collection is 2") -def then_len_of_row_collection_is_2(context): +def then_len_of_row_collection_is_2(context: Context): rows = context.table_.rows assert len(rows) == 2 @then("the new column has 2 cells") -def then_new_column_has_2_cells(context): +def then_new_column_has_2_cells(context: Context): assert len(context.column.cells) == 2 @then("the new column is 1.0 inches wide") -def then_new_column_is_1_inches_wide(context): +def then_new_column_is_1_inches_wide(context: Context): assert context.column.width == Inches(1) @then("the new row has 2 cells") -def then_new_row_has_2_cells(context): +def then_new_row_has_2_cells(context: Context): assert len(context.row.cells) == 2 @then("the reported autofit setting is {autofit}") -def then_the_reported_autofit_setting_is_autofit(context, autofit): +def then_the_reported_autofit_setting_is_autofit(context: Context, autofit: str): expected_value = {"autofit": True, "fixed": False}[autofit] table = context.table_ assert table.autofit is expected_value @then("the reported column width is {width_emu}") -def then_the_reported_column_width_is_width_emu(context, width_emu): +def then_the_reported_column_width_is_width_emu(context: Context, width_emu: str): expected_value = None if width_emu == "None" else int(width_emu) assert context.column.width == expected_value, "got %s" % context.column.width @then("the reported width of the cell is {width}") -def then_the_reported_width_of_the_cell_is_width(context, width): +def then_the_reported_width_of_the_cell_is_width(context: Context, width: str): expected_width = {"None": None, "1 inch": Inches(1)}[width] actual_width = context.cell.width assert actual_width == expected_width, "expected %s, got %s" % ( @@ -433,7 +467,7 @@ def then_the_reported_width_of_the_cell_is_width(context, width): @then("the row cells text is {encoded_text}") -def then_the_row_cells_text_is_expected_text(context, encoded_text): +def then_the_row_cells_text_is_expected_text(context: Context, encoded_text: str): expected_text = encoded_text.replace("\\", "\n") table = context.table_ cells_text = " ".join(c.text for row in table.rows for c in row.cells) @@ -441,32 +475,33 @@ def then_the_row_cells_text_is_expected_text(context, encoded_text): @then("the table has {count} columns") -def then_table_has_count_columns(context, count): +def then_table_has_count_columns(context: Context, count: str): column_count = int(count) columns = context.table_.columns assert len(columns) == column_count @then("the table has {count} rows") -def then_table_has_count_rows(context, count): +def then_table_has_count_rows(context: Context, count: str): row_count = int(count) rows = context.table_.rows assert len(rows) == row_count @then("the width of cell {n_str} is {inches_str} inches") -def then_the_width_of_cell_n_is_x_inches(context, n_str, inches_str): - def _cell(table, idx): +def then_the_width_of_cell_n_is_x_inches(context: Context, n_str: str, inches_str: str): + def _cell(table: Table, idx: int): row, col = idx // 3, idx % 3 return table.cell(row, col) idx, inches = int(n_str) - 1, float(inches_str) cell = _cell(context.table_, idx) + assert cell.width is not None assert cell.width == Inches(inches), "got %s" % cell.width.inches @then("the width of each cell is {inches} inches") -def then_the_width_of_each_cell_is_inches(context, inches): +def then_the_width_of_each_cell_is_inches(context: Context, inches: str): table = context.table_ expected_width = Inches(float(inches)) for cell in table._cells: @@ -474,7 +509,7 @@ def then_the_width_of_each_cell_is_inches(context, inches): @then("the width of each column is {inches} inches") -def then_the_width_of_each_column_is_inches(context, inches): +def then_the_width_of_each_column_is_inches(context: Context, inches: str): table = context.table_ expected_width = Inches(float(inches)) for column in table.columns: diff --git a/features/cel-add-table.feature b/features/tbl-cell-add-table.feature similarity index 100% rename from features/cel-add-table.feature rename to features/tbl-cell-add-table.feature diff --git a/features/cel-text.feature b/features/tbl-cell-text.feature similarity index 100% rename from features/cel-text.feature rename to features/tbl-cell-text.feature diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index da3c6b51d..e0aed09a3 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -51,6 +51,7 @@ class CT_Row(BaseOxmlElement): add_tc: Callable[[], CT_Tc] get_or_add_trPr: Callable[[], CT_TrPr] + _add_trPr: Callable[[], CT_TrPr] tc_lst: list[CT_Tc] # -- custom inserter below -- diff --git a/src/docx/table.py b/src/docx/table.py index 55cf77f41..709bc8dbb 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -130,7 +130,7 @@ def style(self) -> _TableStyle | None: return cast("_TableStyle | None", self.part.get_style(style_id, WD_STYLE_TYPE.TABLE)) @style.setter - def style(self, style_or_name: _TableStyle | None): + def style(self, style_or_name: _TableStyle | str | None): style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.TABLE) self._tbl.tblStyle_val = style_id diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 395c812a6..6a177ab77 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Test suite for the docx.oxml.text module.""" from __future__ import annotations @@ -13,50 +15,48 @@ from ..unitutil.cxml import element, xml from ..unitutil.file import snippet_seq -from ..unitutil.mock import call, instance_mock, method_mock, property_mock +from ..unitutil.mock import FixtureRequest, Mock, call, instance_mock, method_mock, property_mock class DescribeCT_Row: - def it_can_add_a_trPr(self, add_trPr_fixture): - tr, expected_xml = add_trPr_fixture - tr._add_trPr() - assert tr.xml == expected_xml - def it_raises_on_tc_at_grid_col(self, tc_raise_fixture): - tr, idx = tc_raise_fixture - with pytest.raises(ValueError): # noqa: PT011 - tr.tc_at_grid_col(idx) - - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tr_cxml", "expected_cxml"), + [ ("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) - expected_xml = xml(expected_cxml) - return tr, expected_xml - - @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]) - tr = tbl.tr_lst[row_idx] - return tr, col_idx + def it_can_add_a_trPr(self, tr_cxml: str, expected_cxml: str): + tr = cast(CT_Row, element(tr_cxml)) + tr._add_trPr() + assert tr.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + ("snippet_idx", "row_idx", "col_idx", "err_msg"), + [ + (0, 0, 3, "index out of bounds"), + (1, 0, 1, "no cell on grid column 1"), + ], + ) + def it_raises_on_tc_at_grid_col( + self, snippet_idx: int, row_idx: int, col_idx: int, err_msg: str + ): + tr = cast(CT_Tbl, parse_xml(snippet_seq("tbl-cells")[snippet_idx])).tr_lst[row_idx] + with pytest.raises(ValueError, match=err_msg): + tr.tc_at_grid_col(col_idx) class DescribeCT_Tc: + """Unit-test suite for `docx.oxml.table.CT_Tc` objects.""" + def it_can_merge_to_another_tc( - self, tr_, _span_dimensions_, _tbl_, _grow_to_, top_tc_ + self, tr_: Mock, _span_dimensions_: Mock, _tbl_: Mock, _grow_to_: Mock, top_tc_: Mock ): top_tr_ = tr_ - tc, other_tc = element("w:tc"), element("w:tc") + tc, other_tc = cast(CT_Tc, element("w:tc")), cast(CT_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_] @@ -69,118 +69,9 @@ def it_can_merge_to_another_tc( top_tc_._grow_to.assert_called_once_with(width, height) assert merged_tc is top_tc_ - def it_knows_its_extents_to_help(self, extents_fixture): - tc, attr_name, expected_value = extents_fixture - extent = getattr(tc, attr_name) - assert extent == expected_value - - def it_calculates_the_dimensions_of_a_span_to_help(self, span_fixture): - tc, other_tc, expected_dimensions = span_fixture - dimensions = tc._span_dimensions(other_tc) - assert dimensions == expected_dimensions - - def it_raises_on_invalid_span(self, span_raise_fixture): - tc, other_tc = span_raise_fixture - with pytest.raises(InvalidSpanError): - tc._span_dimensions(other_tc) - - def it_can_grow_itself_to_help_merge(self, grow_to_fixture): - tc, width, height, top_tc, expected_calls = grow_to_fixture - tc._grow_to(width, height, top_tc) - assert tc._span_to_width.call_args_list == expected_calls - - 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") - - 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_), - ] - assert tc.vMerge == vMerge - - def it_knows_its_inner_content_block_item_elements(self): - tc = cast(CT_Tc, element("w:tc/(w:p,w:tbl,w:p)")) - assert [type(e) for e in tc.inner_content_elements] == [CT_P, CT_Tbl, CT_P] - - def it_can_swallow_the_next_tc_help_merge(self, swallow_fixture): - tc, grid_width, top_tc, tr, expected_xml = swallow_fixture - tc._swallow_next_tc(grid_width, top_tc) - assert tr.xml == expected_xml - - def it_adds_cell_widths_on_swallow(self, add_width_fixture): - tc, grid_width, top_tc, tr, expected_xml = add_width_fixture - tc._swallow_next_tc(grid_width, top_tc) - assert tr.xml == expected_xml - - def it_raises_on_invalid_swallow(self, swallow_raise_fixture): - tc, grid_width, top_tc, tr = swallow_raise_fixture - with pytest.raises(InvalidSpanError): - tc._swallow_next_tc(grid_width, top_tc) - - def it_can_move_its_content_to_help_merge(self, move_fixture): - tc, tc_2, expected_tc_xml, expected_tc_2_xml = move_fixture - tc._move_content_to(tc_2) - assert tc.xml == expected_tc_xml - assert tc_2.xml == expected_tc_2_xml - - def it_raises_on_tr_above(self, tr_above_raise_fixture): - tc = tr_above_raise_fixture - 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))", - ), - ] - ) - def add_width_fixture(self, request): - tr_cxml, tc_idx, grid_width, expected_tr_cxml = request.param - tr = element(tr_cxml) - tc = top_tc = tr[tc_idx] - expected_tr_xml = xml(expected_tr_cxml) - return tc, grid_width, top_tc, tr, expected_tr_xml - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("snippet_idx", "row", "col", "attr_name", "expected_value"), + [ (0, 0, 0, "top", 0), (2, 0, 1, "top", 0), (2, 1, 1, "top", 0), @@ -195,63 +86,22 @@ def add_width_fixture(self, request): (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 + def it_knows_its_extents_to_help( + self, snippet_idx: int, row: int, col: int, attr_name: str, expected_value: int + ): 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), - ] - ) - def grow_to_fixture(self, request, _span_to_width_): - snippet_idx, row, col, width, height = request.param - tbl = self._snippet_tbl(snippet_idx) - tc = tbl.tr_lst[row].tc_lst[col] - start = 0 if height == 1 else 1 - end = start + height - expected_calls = [ - call(width, tc, None), - 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)", - ), - ] - ) - def move_fixture(self, request): - 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=[ + extent = getattr(tc, attr_name) + + assert extent == expected_value + + @pytest.mark.parametrize( + ("snippet_idx", "row", "col", "row_2", "col_2", "expected_value"), + [ (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)), @@ -262,17 +112,28 @@ def move_fixture(self, request): (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 + def it_calculates_the_dimensions_of_a_span_to_help( + self, + snippet_idx: int, + row: int, + col: int, + row_2: int, + col_2: int, + expected_value: tuple[int, int, int, int], + ): tbl = self._snippet_tbl(snippet_idx) tc = tbl.tr_lst[row].tc_lst[col] - tc_2 = tbl.tr_lst[row_2].tc_lst[col_2] - return tc, tc_2, expected_value + other_tc = tbl.tr_lst[row_2].tc_lst[col_2] + + dimensions = tc._span_dimensions(other_tc) - @pytest.fixture( - params=[ + assert dimensions == expected_value + + @pytest.mark.parametrize( + ("snippet_idx", "row", "col", "row_2", "col_2"), + [ (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 @@ -280,17 +141,72 @@ def span_fixture(self, request): (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 + def it_raises_on_invalid_span( + self, snippet_idx: int, row: int, col: int, row_2: int, col_2: int + ): tbl = self._snippet_tbl(snippet_idx) tc = tbl.tr_lst[row].tc_lst[col] - tc_2 = tbl.tr_lst[row_2].tc_lst[col_2] - return tc, tc_2 + other_tc = tbl.tr_lst[row_2].tc_lst[col_2] + + with pytest.raises(InvalidSpanError): + tc._span_dimensions(other_tc) + + @pytest.mark.parametrize( + ("snippet_idx", "row", "col", "width", "height"), + [ + (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 it_can_grow_itself_to_help_merge( + self, snippet_idx: int, row: int, col: int, width: int, height: int, _span_to_width_: Mock + ): + tbl = self._snippet_tbl(snippet_idx) + tc = tbl.tr_lst[row].tc_lst[col] + start = 0 if height == 1 else 1 + end = start + height + + tc._grow_to(width, height, None) - @pytest.fixture( - params=[ + assert ( + _span_to_width_.call_args_list + == [ + call(width, tc, None), + call(width, tc, "restart"), + call(width, tc, "continue"), + call(width, tc, "continue"), + ][start:end] + ) + + def it_can_extend_its_horz_span_to_help_merge( + self, top_tc_: Mock, grid_span_: Mock, _move_content_to_: Mock, _swallow_next_tc_: Mock + ): + grid_span_.side_effect = [1, 3, 4] + grid_width, vMerge = 4, "continue" + tc = cast(CT_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_), + ] + assert tc.vMerge == vMerge + + def it_knows_its_inner_content_block_item_elements(self): + tc = cast(CT_Tc, element("w:tc/(w:p,w:tbl,w:p)")) + assert [type(e) for e in tc.inner_content_elements] == [CT_P, CT_Tbl, CT_P] + + @pytest.mark.parametrize( + ("tr_cxml", "tc_idx", "grid_width", "expected_cxml"), + [ ( "w:tr/(w:tc/w:p,w:tc/w:p)", 0, @@ -307,8 +223,7 @@ def span_raise_fixture(self, request): '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: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)", @@ -322,75 +237,145 @@ def span_raise_fixture(self, request): 3, "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))", ), - ] + ], + ) + def it_can_swallow_the_next_tc_help_merge( + self, tr_cxml: str, tc_idx: int, grid_width: int, expected_cxml: str + ): + tr = cast(CT_Row, element(tr_cxml)) + tc = top_tc = tr.tc_lst[tc_idx] + + tc._swallow_next_tc(grid_width, top_tc) + + assert tr.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + ("tr_cxml", "tc_idx", "grid_width", "expected_cxml"), + [ + # 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 swallow_fixture(self, request): - tr_cxml, tc_idx, grid_width, expected_tr_cxml = request.param - tr = element(tr_cxml) - tc = top_tc = tr[tc_idx] - expected_tr_xml = xml(expected_tr_cxml) - return tc, grid_width, top_tc, tr, expected_tr_xml - - @pytest.fixture( - params=[ + def it_adds_cell_widths_on_swallow( + self, tr_cxml: str, tc_idx: int, grid_width: int, expected_cxml: str + ): + tr = cast(CT_Row, element(tr_cxml)) + tc = top_tc = tr.tc_lst[tc_idx] + tc._swallow_next_tc(grid_width, top_tc) + assert tr.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + ("tr_cxml", "tc_idx", "grid_width"), + [ ("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) - tc = top_tc = tr[tc_idx] - return tc, grid_width, top_tc, tr - - @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]) + def it_raises_on_invalid_swallow(self, tr_cxml: str, tc_idx: int, grid_width: int): + tr = cast(CT_Row, element(tr_cxml)) + tc = top_tc = tr.tc_lst[tc_idx] + + with pytest.raises(InvalidSpanError): + tc._swallow_next_tc(grid_width, top_tc) + + @pytest.mark.parametrize( + ("tc_cxml", "tc_2_cxml", "expected_tc_cxml", "expected_tc_2_cxml"), + [ + ("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 it_can_move_its_content_to_help_merge( + self, tc_cxml: str, tc_2_cxml: str, expected_tc_cxml: str, expected_tc_2_cxml: str + ): + tc, tc_2 = cast(CT_Tc, element(tc_cxml)), cast(CT_Tc, element(tc_2_cxml)) + + tc._move_content_to(tc_2) + + assert tc.xml == xml(expected_tc_cxml) + assert tc_2.xml == xml(expected_tc_2_cxml) + + @pytest.mark.parametrize(("snippet_idx", "row_idx", "col_idx"), [(0, 0, 0), (4, 0, 0)]) + def it_raises_on_tr_above(self, snippet_idx: int, row_idx: int, col_idx: int): + tbl = cast(CT_Tbl, parse_xml(snippet_seq("tbl-cells")[snippet_idx])) tc = tbl.tr_lst[row_idx].tc_lst[col_idx] - return tc - # fixture components --------------------------------------------- + with pytest.raises(ValueError, match="no tr above topmost tr"): + tc._tr_above + + # fixtures ------------------------------------------------------- @pytest.fixture - def grid_span_(self, request): + def grid_span_(self, request: FixtureRequest): return property_mock(request, CT_Tc, "grid_span") @pytest.fixture - def _grow_to_(self, request): + def _grow_to_(self, request: FixtureRequest): return method_mock(request, CT_Tc, "_grow_to") @pytest.fixture - def _move_content_to_(self, request): + def _move_content_to_(self, request: FixtureRequest): return method_mock(request, CT_Tc, "_move_content_to") @pytest.fixture - def _span_dimensions_(self, request): + def _span_dimensions_(self, request: FixtureRequest): return method_mock(request, CT_Tc, "_span_dimensions") @pytest.fixture - def _span_to_width_(self, request): + def _span_to_width_(self, request: FixtureRequest): 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 - file. - """ - return parse_xml(snippet_seq("tbl-cells")[idx]) + def _snippet_tbl(self, idx: int) -> CT_Tbl: + """A element for snippet at `idx` in 'tbl-cells' snippet file.""" + return cast(CT_Tbl, parse_xml(snippet_seq("tbl-cells")[idx])) @pytest.fixture - def _swallow_next_tc_(self, request): + def _swallow_next_tc_(self, request: FixtureRequest): return method_mock(request, CT_Tc, "_swallow_next_tc") @pytest.fixture - def _tbl_(self, request): + def _tbl_(self, request: FixtureRequest): return property_mock(request, CT_Tc, "_tbl") @pytest.fixture - def top_tc_(self, request): + def top_tc_(self, request: FixtureRequest): return instance_mock(request, CT_Tc) @pytest.fixture - def tr_(self, request): + def tr_(self, request: FixtureRequest): return instance_mock(request, CT_Row) diff --git a/tests/oxml/unitdata/table.py b/tests/oxml/unitdata/table.py deleted file mode 100644 index 4f760c1a8..000000000 --- a/tests/oxml/unitdata/table.py +++ /dev/null @@ -1,88 +0,0 @@ -"""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",) - - -class CT_TblBuilder(BaseBuilder): - __tag__ = "w:tbl" - __nspfxs__ = ("w",) - __attrs__ = () - - -class CT_TblGridBuilder(BaseBuilder): - __tag__ = "w:tblGrid" - __nspfxs__ = ("w",) - __attrs__ = ("w:w",) - - -class CT_TblGridColBuilder(BaseBuilder): - __tag__ = "w:gridCol" - __nspfxs__ = ("w",) - __attrs__ = ("w:w",) - - -class CT_TblPrBuilder(BaseBuilder): - __tag__ = "w:tblPr" - __nspfxs__ = ("w",) - __attrs__ = () - - -class CT_TblWidthBuilder(BaseBuilder): - __tag__ = "w:tblW" - __nspfxs__ = ("w",) - __attrs__ = ("w:w", "w:type") - - -class CT_TcBuilder(BaseBuilder): - __tag__ = "w:tc" - __nspfxs__ = ("w",) - __attrs__ = ("w:id",) - - -class CT_TcPrBuilder(BaseBuilder): - __tag__ = "w:tcPr" - __nspfxs__ = ("w",) - __attrs__ = () - - -def a_gridCol(): - return CT_TblGridColBuilder() - - -def a_tbl(): - return CT_TblBuilder() - - -def a_tblGrid(): - return CT_TblGridBuilder() - - -def a_tblPr(): - return CT_TblPrBuilder() - - -def a_tblStyle(): - return CT_StringBuilder("w:tblStyle") - - -def a_tblW(): - return CT_TblWidthBuilder() - - -def a_tc(): - return CT_TcBuilder() - - -def a_tcPr(): - return CT_TcPrBuilder() - - -def a_tr(): - return CT_RowBuilder() diff --git a/tests/test_table.py b/tests/test_table.py index eef4b1df1..65f7cb423 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -17,37 +17,45 @@ WD_TABLE_DIRECTION, ) from docx.oxml.parser import parse_xml -from docx.oxml.table import CT_Tbl, CT_Tc +from docx.oxml.table import CT_Row, CT_Tbl, CT_TblGridCol, CT_Tc from docx.parts.document import DocumentPart -from docx.shared import Inches +from docx.shared import Emu, Inches, Length 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 -from .oxml.unitdata.text import a_p from .unitutil.cxml import element, xml from .unitutil.file import snippet_seq from .unitutil.mock import FixtureRequest, Mock, instance_mock, property_mock class DescribeTable: - def it_can_add_a_row(self, add_row_fixture): - table, expected_xml = add_row_fixture + """Unit-test suite for `docx.table._Rows` objects.""" + + def it_can_add_a_row(self, document_: Mock): + snippets = snippet_seq("add-row-col") + tbl = cast(CT_Tbl, parse_xml(snippets[0])) + table = Table(tbl, document_) + row = table.add_row() - assert table._tbl.xml == expected_xml + + assert table._tbl.xml == snippets[1] assert isinstance(row, _Row) assert row._tr is table._tbl.tr_lst[-1] assert row._parent is table - def it_can_add_a_column(self, add_column_fixture): - table, width, expected_xml = add_column_fixture - column = table.add_column(width) - assert table._tbl.xml == expected_xml + def it_can_add_a_column(self, document_: Mock): + snippets = snippet_seq("add-row-col") + tbl = cast(CT_Tbl, parse_xml(snippets[0])) + table = Table(tbl, document_) + + column = table.add_column(Inches(1.5)) + + assert table._tbl.xml == snippets[2] assert isinstance(column, _Column) assert column._gridCol is table._tbl.tblGrid.gridCol_lst[-1] assert column._parent is table - def it_provides_access_to_a_cell_by_row_and_col_indices(self, table): + def it_provides_access_to_a_cell_by_row_and_col_indices(self, table: Table): for row_idx in range(2): for col_idx in range(2): cell = table.cell(row_idx, col_idx) @@ -56,50 +64,121 @@ def it_provides_access_to_a_cell_by_row_and_col_indices(self, table): tc = tr.tc_lst[col_idx] assert tc is cell._tc - def it_provides_access_to_the_table_rows(self, table): + def it_provides_access_to_the_table_rows(self, table: Table): rows = table.rows assert isinstance(rows, _Rows) - def it_provides_access_to_the_table_columns(self, table): + def it_provides_access_to_the_table_columns(self, table: Table): columns = table.columns assert isinstance(columns, _Columns) - def it_provides_access_to_the_cells_in_a_column(self, col_cells_fixture): - table, column_idx, expected_cells = col_cells_fixture + def it_provides_access_to_the_cells_in_a_column( + self, _cells_: Mock, _column_count_: Mock, document_: Mock + ): + table = Table(cast(CT_Tbl, element("w:tbl")), document_) + _cells_.return_value = [0, 1, 2, 3, 4, 5, 6, 7, 8] + _column_count_.return_value = 3 + column_idx = 1 + column_cells = table.column_cells(column_idx) - assert column_cells == expected_cells - def it_provides_access_to_the_cells_in_a_row(self, row_cells_fixture): - table, row_idx, expected_cells = row_cells_fixture - row_cells = table.row_cells(row_idx) - assert row_cells == expected_cells + assert column_cells == [1, 4, 7] + + def it_provides_access_to_the_cells_in_a_row( + self, _cells_: Mock, _column_count_: Mock, document_: Mock + ): + table = Table(cast(CT_Tbl, element("w:tbl")), document_) + _cells_.return_value = [0, 1, 2, 3, 4, 5, 6, 7, 8] + _column_count_.return_value = 3 + + row_cells = table.row_cells(1) + + assert row_cells == [3, 4, 5] - def it_knows_its_alignment_setting(self, alignment_get_fixture): - table, expected_value = alignment_get_fixture + @pytest.mark.parametrize( + ("tbl_cxml", "expected_value"), + [ + ("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 it_knows_its_alignment_setting( + self, tbl_cxml: str, expected_value: WD_TABLE_ALIGNMENT | None, document_: Mock + ): + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) assert table.alignment == expected_value - def it_can_change_its_alignment_setting(self, alignment_set_fixture): - table, new_value, expected_xml = alignment_set_fixture + @pytest.mark.parametrize( + ("tbl_cxml", "new_value", "expected_cxml"), + [ + ("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 it_can_change_its_alignment_setting( + self, + tbl_cxml: str, + new_value: WD_TABLE_ALIGNMENT | None, + expected_cxml: str, + document_: Mock, + ): + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) table.alignment = new_value - assert table._tbl.xml == expected_xml + assert table._tbl.xml == xml(expected_cxml) - def it_knows_whether_it_should_autofit(self, autofit_get_fixture): - table, expected_value = autofit_get_fixture + @pytest.mark.parametrize( + ("tbl_cxml", "expected_value"), + [ + ("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 it_knows_whether_it_should_autofit( + self, tbl_cxml: str, expected_value: bool, document_: Mock + ): + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) assert table.autofit is expected_value - def it_can_change_its_autofit_setting(self, autofit_set_fixture): - table, new_value, expected_xml = autofit_set_fixture + @pytest.mark.parametrize( + ("tbl_cxml", "new_value", "expected_cxml"), + [ + ("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/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 it_can_change_its_autofit_setting( + self, tbl_cxml: str, new_value: bool, expected_cxml: str, document_: Mock + ): + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) table.autofit = new_value - assert table._tbl.xml == expected_xml + assert table._tbl.xml == xml(expected_cxml) - def it_knows_it_is_the_table_its_children_belong_to(self, table_fixture): - table = table_fixture + def it_knows_it_is_the_table_its_children_belong_to(self, table: Table): assert table.table is table @pytest.mark.parametrize( ("tbl_cxml", "expected_value"), [ - # ("w:tbl/w:tblPr", None), + ("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), @@ -135,202 +214,95 @@ def it_can_change_its_direction( table.table_direction = new_value assert table._element.xml == xml(expected_cxml) - 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) - 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) - assert table._tbl.xml == expected_xml - - def it_provides_access_to_its_cells_to_help(self, cells_fixture): - table, cell_count, unique_count, matches = cells_fixture - cells = table._cells - assert len(cells) == cell_count - assert len(set(cells)) == unique_count - for matching_idxs in matches: - comparator_idx = matching_idxs[0] - for idx in matching_idxs[1:]: - assert cells[idx] is cells[comparator_idx] + def it_knows_its_table_style(self, part_prop_: Mock, document_part_: Mock, document_: Mock): + part_prop_.return_value = document_part_ + style_ = document_part_.get_style.return_value + table = Table(cast(CT_Tbl, element("w:tbl/w:tblPr/w:tblStyle{w:val=BarBaz}")), document_) - def it_knows_its_column_count_to_help(self, column_count_fixture): - table, expected_value = column_count_fixture - column_count = table._column_count - assert column_count == expected_value - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def add_column_fixture(self): - snippets = snippet_seq("add-row-col") - tbl = parse_xml(snippets[0]) - table = Table(tbl, None) - width = Inches(1.5) - expected_xml = snippets[2] - return table, width, expected_xml - - @pytest.fixture - def add_row_fixture(self): - snippets = snippet_seq("add-row-col") - tbl = parse_xml(snippets[0]) - table = Table(tbl, None) - expected_xml = snippets[1] - return table, expected_xml + style = table.style - @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 + document_part_.get_style.assert_called_once_with("BarBaz", WD_STYLE_TYPE.TABLE) + assert style is style_ - @pytest.fixture( - params=[ - ( - "w:tbl/w:tblPr", - WD_TABLE_ALIGNMENT.LEFT, - "w:tbl/w:tblPr/w:jc{w:val=left}", - ), + @pytest.mark.parametrize( + ("tbl_cxml", "new_value", "style_id", "expected_cxml"), + [ + ("w:tbl/w:tblPr", "Tbl A", "TblA", "w:tbl/w:tblPr/w:tblStyle{w:val=TblA}"), ( - "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:tblStyle{w:val=TblA}", + "Tbl B", + "TblB", + "w:tbl/w:tblPr/w:tblStyle{w:val=TblB}", ), - ("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), - ] + ("w:tbl/w:tblPr/w:tblStyle{w:val=TblB}", None, None, "w:tbl/w:tblPr"), + ], ) - def autofit_get_fixture(self, request): - tbl_cxml, expected_autofit = request.param - table = Table(element(tbl_cxml), None) - return table, expected_autofit + def it_can_change_its_table_style( + self, + tbl_cxml: str, + new_value: str | None, + style_id: str | None, + expected_cxml: str, + document_: Mock, + part_prop_: Mock, + document_part_: Mock, + ): + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) + part_prop_.return_value = document_part_ + document_part_.get_style_id.return_value = style_id - @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=[ + table.style = new_value + + document_part_.get_style_id.assert_called_once_with(new_value, WD_STYLE_TYPE.TABLE) + assert table._tbl.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + ("snippet_idx", "cell_count", "unique_count", "matches"), + [ (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 + def it_provides_access_to_its_cells_to_help( + self, + snippet_idx: int, + cell_count: int, + unique_count: int, + matches: tuple[tuple[int, ...]], + document_: Mock, + ): tbl_xml = snippet_seq("tbl-cells")[snippet_idx] - table = Table(parse_xml(tbl_xml), None) - return table, cell_count, unique_count, matches + table = Table(cast(CT_Tbl, parse_xml(tbl_xml)), document_) - @pytest.fixture - def col_cells_fixture(self, _cells_, _column_count_): - table = Table(None, None) - _cells_.return_value = [0, 1, 2, 3, 4, 5, 6, 7, 8] - _column_count_.return_value = 3 - column_idx = 1 - expected_cells = [1, 4, 7] - return table, column_idx, expected_cells + cells = table._cells - @pytest.fixture - def column_count_fixture(self): + assert len(cells) == cell_count + assert len(set(cells)) == unique_count + for matching_idxs in matches: + comparator_idx = matching_idxs[0] + for idx in matching_idxs[1:]: + assert cells[idx] is cells[comparator_idx] + + def it_knows_its_column_count_to_help(self, document_: Mock): 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 - def row_cells_fixture(self, _cells_, _column_count_): - table = Table(None, None) - _cells_.return_value = [0, 1, 2, 3, 4, 5, 6, 7, 8] - _column_count_.return_value = 3 - row_idx = 1 - expected_cells = [3, 4, 5] - return table, row_idx, expected_cells + table = Table(cast(CT_Tbl, element(tbl_cxml)), document_) - @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 - 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"), - ] - ) - def style_set_fixture(self, request, part_prop_): - tbl_cxml, value, style_id, expected_cxml = request.param - table = Table(element(tbl_cxml), None) - part_prop_.return_value.get_style_id.return_value = style_id - expected_xml = xml(expected_cxml) - return table, value, expected_xml + column_count = table._column_count - @pytest.fixture - def table_fixture(self): - table = Table(None, None) - return table + assert column_count == expected_value - # fixture components --------------------------------------------- + # fixtures ------------------------------------------------------- @pytest.fixture - def _cells_(self, request): + def _cells_(self, request: FixtureRequest): return property_mock(request, Table, "_cells") @pytest.fixture - def _column_count_(self, request): + def _column_count_(self, request: FixtureRequest): return property_mock(request, Table, "_column_count") @pytest.fixture @@ -338,130 +310,78 @@ def document_(self, request: FixtureRequest): return instance_mock(request, Document) @pytest.fixture - def document_part_(self, request): + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def part_prop_(self, request, document_part_): - return property_mock(request, Table, "part", return_value=document_part_) + def part_prop_(self, request: FixtureRequest): + return property_mock(request, Table, "part") @pytest.fixture - def table(self): - tbl = _tbl_bldr(rows=2, cols=2).element - table = Table(tbl, None) - return table + def table(self, document_: Mock): + tbl_cxml = "w:tbl/(w:tblGrid/(w:gridCol,w:gridCol),w:tr/(w:tc,w:tc),w:tr/(w:tc,w:tc))" + return Table(cast(CT_Tbl, element(tbl_cxml)), document_) class Describe_Cell: - def it_knows_what_text_it_contains(self, text_get_fixture): - cell, expected_text = text_get_fixture + """Unit-test suite for `docx.table._Cell` objects.""" + + @pytest.mark.parametrize( + ("tc_cxml", "expected_text"), + [ + ("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 it_knows_what_text_it_contains(self, tc_cxml: str, expected_text: str, parent_: Mock): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) text = cell.text assert text == expected_text - 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 - - def it_knows_its_vertical_alignment(self, alignment_get_fixture): - cell, expected_value = alignment_get_fixture - vertical_alignment = cell.vertical_alignment - assert vertical_alignment == expected_value - - def it_can_change_its_vertical_alignment(self, alignment_set_fixture): - cell, new_value, expected_xml = alignment_set_fixture - cell.vertical_alignment = new_value - assert cell._element.xml == expected_xml - - def it_knows_its_width_in_EMU(self, width_get_fixture): - cell, expected_width = width_get_fixture - assert cell.width == expected_width - - def it_can_change_its_width(self, width_set_fixture): - cell, value, expected_xml = width_set_fixture - cell.width = value - assert cell.width == value - assert cell._tc.xml == expected_xml - - def it_provides_access_to_the_paragraphs_it_contains(self, paragraphs_fixture): - cell = paragraphs_fixture - paragraphs = cell.paragraphs - assert len(paragraphs) == 2 - count = 0 - for idx, paragraph in enumerate(paragraphs): - assert isinstance(paragraph, Paragraph) - assert paragraph is paragraphs[idx] - count += 1 - assert count == 2 - - def it_provides_access_to_the_tables_it_contains(self, tables_fixture): - # test len(), iterable, and indexed access - cell, expected_count = tables_fixture - tables = cell.tables - assert len(tables) == expected_count - count = 0 - for idx, table in enumerate(tables): - assert isinstance(table, Table) - assert tables[idx] is table - count += 1 - assert count == expected_count - - def it_can_add_a_paragraph(self, add_paragraph_fixture): - cell, expected_xml = add_paragraph_fixture - p = cell.add_paragraph() - assert cell._tc.xml == expected_xml - assert isinstance(p, Paragraph) - - def it_can_add_a_table(self, add_table_fixture): - cell, expected_xml = add_table_fixture - table = cell.add_table(rows=2, cols=2) - assert cell._element.xml == expected_xml - assert isinstance(table, Table) - - def it_can_merge_itself_with_other_cells(self, merge_fixture): - cell, other_cell, merged_tc_ = merge_fixture - merged_cell = cell.merge(other_cell) - cell._tc.merge.assert_called_once_with(other_cell._tc) - assert isinstance(merged_cell, _Cell) - assert merged_cell._tc is merged_tc_ - assert merged_cell._parent is cell._parent - - # 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.mark.parametrize( + ("tc_cxml", "new_text", "expected_cxml"), + [ + ("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 add_paragraph_fixture(self, request): - tc_cxml, after_tc_cxml = request.param - cell = _Cell(element(tc_cxml), None) - expected_xml = xml(after_tc_cxml) - return cell, expected_xml - - @pytest.fixture - def add_table_fixture(self, request): - cell = _Cell(element("w:tc/w:p"), None) - expected_xml = snippet_seq("new-tbl")[1] - return cell, expected_xml + def it_can_replace_its_content_with_a_string_of_text( + self, tc_cxml: str, new_text: str, expected_cxml: str, parent_: Mock + ): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + cell.text = new_text + assert cell._tc.xml == xml(expected_cxml) - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tc_cxml", "expected_value"), + [ ("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 + def it_knows_its_vertical_alignment( + self, tc_cxml: str, expected_value: WD_ALIGN_VERTICAL | None, parent_: Mock + ): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + assert cell.vertical_alignment == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tc_cxml", "new_value", "expected_cxml"), + [ ("w:tc", WD_ALIGN_VERTICAL.TOP, "w:tc/w:tcPr/w:vAlign{w:val=top}"), ( "w:tc/w:tcPr", @@ -476,330 +396,272 @@ def alignment_get_fixture(self, request): ("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) - expected_xml = xml(expected_cxml) - return cell, new_value, expected_xml - - @pytest.fixture - def merge_fixture(self, tc_, tc_2_, parent_, merged_tc_): - cell, other_cell = _Cell(tc_, parent_), _Cell(tc_2_, parent_) - tc_.merge.return_value = merged_tc_ - return cell, other_cell, 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), - ] - ) - 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"), - ] + ], ) - def text_get_fixture(self, request): - tc_cxml, expected_text = request.param - cell = _Cell(element(tc_cxml), None) - return cell, expected_text + def it_can_change_its_vertical_alignment( + self, tc_cxml: str, new_value: WD_ALIGN_VERTICAL | None, expected_cxml: str, parent_: Mock + ): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + cell.vertical_alignment = new_value + assert cell._element.xml == xml(expected_cxml) - @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=[ + @pytest.mark.parametrize( + ("tc_cxml", "expected_value"), + [ ("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 + def it_knows_its_width_in_EMU(self, tc_cxml: str, expected_value: int | None, parent_: Mock): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + assert cell.width == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tc_cxml", "new_value", "expected_cxml"), + [ ("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 it_can_change_its_width( + self, tc_cxml: str, new_value: Length, expected_cxml: str, parent_: Mock + ): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + cell.width = new_value + assert cell.width == new_value + assert cell._tc.xml == xml(expected_cxml) + + def it_provides_access_to_the_paragraphs_it_contains(self, parent_: Mock): + cell = _Cell(cast(CT_Tc, element("w:tc/(w:p, w:p)")), parent_) + + paragraphs = cell.paragraphs + + # -- every w:p produces a Paragraph instance -- + assert len(paragraphs) == 2 + assert all(isinstance(p, Paragraph) for p in paragraphs) + # -- the return value is iterable and indexable -- + assert all(p is paragraphs[idx] for idx, p in enumerate(paragraphs)) + + @pytest.mark.parametrize( + ("tc_cxml", "expected_table_count"), + [ + ("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 width_set_fixture(self, request): - tc_cxml, new_value, expected_cxml = request.param - cell = _Cell(element(tc_cxml), None) - expected_xml = xml(expected_cxml) - return cell, new_value, expected_xml + def it_provides_access_to_the_tables_it_contains( + self, tc_cxml: str, expected_table_count: int, parent_: Mock + ): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + + tables = cell.tables + + # --- test len(), iterable, and indexed access + assert len(tables) == expected_table_count + assert all(isinstance(t, Table) for t in tables) + assert all(t is tables[idx] for idx, t in enumerate(tables)) + + @pytest.mark.parametrize( + ("tc_cxml", "expected_cxml"), + [ + ("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 it_can_add_a_paragraph(self, tc_cxml: str, expected_cxml: str, parent_: Mock): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) - # fixture components --------------------------------------------- + p = cell.add_paragraph() + + assert isinstance(p, Paragraph) + assert cell._tc.xml == xml(expected_cxml) + + def it_can_add_a_table(self, parent_: Mock): + cell = _Cell(cast(CT_Tc, element("w:tc/w:p")), parent_) + + table = cell.add_table(rows=2, cols=2) + + assert isinstance(table, Table) + assert cell._element.xml == snippet_seq("new-tbl")[1] + + def it_can_merge_itself_with_other_cells( + self, tc_: Mock, tc_2_: Mock, parent_: Mock, merged_tc_: Mock + ): + cell, other_cell = _Cell(tc_, parent_), _Cell(tc_2_, parent_) + tc_.merge.return_value = merged_tc_ + + merged_cell = cell.merge(other_cell) + + assert isinstance(merged_cell, _Cell) + tc_.merge.assert_called_once_with(other_cell._tc) + assert merged_cell._tc is merged_tc_ + assert merged_cell._parent is cell._parent + + # fixtures ------------------------------------------------------- @pytest.fixture - def merged_tc_(self, request): + def merged_tc_(self, request: FixtureRequest): return instance_mock(request, CT_Tc) @pytest.fixture - def parent_(self, request): + def parent_(self, request: FixtureRequest): return instance_mock(request, Table) @pytest.fixture - def tc_(self, request): + def tc_(self, request: FixtureRequest): return instance_mock(request, CT_Tc) @pytest.fixture - def tc_2_(self, request): + def tc_2_(self, request: FixtureRequest): return instance_mock(request, CT_Tc) class Describe_Column: - def it_provides_access_to_its_cells(self, cells_fixture): - column, column_idx, expected_cells = cells_fixture - cells = column.cells - column.table.column_cells.assert_called_once_with(column_idx) - assert cells == expected_cells + """Unit-test suite for `docx.table._Cell` objects.""" - def it_provides_access_to_the_table_it_belongs_to(self, table_fixture): - column, table_ = table_fixture - assert column.table is table_ + def it_provides_access_to_its_cells(self, _index_prop_: Mock, table_prop_: Mock, table_: Mock): + table_prop_.return_value = table_ + _index_prop_.return_value = 4 + column = _Column(cast(CT_TblGridCol, element("w:gridCol{w:w=500}")), table_) + table_.column_cells.return_value = [3, 2, 1] - def it_knows_its_width_in_EMU(self, width_get_fixture): - column, expected_width = width_get_fixture - assert column.width == expected_width - - def it_can_change_its_width(self, width_set_fixture): - column, value, expected_xml = width_set_fixture - column.width = value - assert column.width == value - assert column._gridCol.xml == expected_xml - - def it_knows_its_index_in_table_to_help(self, index_fixture): - column, expected_idx = index_fixture - assert column._index == expected_idx - - # fixtures ------------------------------------------------------- + cells = column.cells - @pytest.fixture - def cells_fixture(self, _index_, table_prop_, table_): - column = _Column(None, None) - _index_.return_value = column_idx = 4 - expected_cells = (3, 2, 1) - table_.column_cells.return_value = list(expected_cells) - return column, column_idx, expected_cells + table_.column_cells.assert_called_once_with(4) + assert cells == (3, 2, 1) - @pytest.fixture - def index_fixture(self): - 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 + def it_provides_access_to_the_table_it_belongs_to(self, table_: Mock): + table_.table = table_ + column = _Column(cast(CT_TblGridCol, element("w:gridCol{w:w=500}")), table_) - @pytest.fixture - def table_fixture(self, parent_, table_): - column = _Column(None, parent_) - parent_.table = table_ - return column, table_ + assert column.table is table_ - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("gridCol_cxml", "expected_width"), + [ ("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}"), + def it_knows_its_width_in_EMU( + self, gridCol_cxml: str, expected_width: int | None, table_: Mock + ): + column = _Column(cast(CT_TblGridCol, element(gridCol_cxml)), table_) + assert column.width == expected_width + + @pytest.mark.parametrize( + ("gridCol_cxml", "new_value", "expected_cxml"), + [ + ("w:gridCol", Emu(914400), "w:gridCol{w:w=1440}"), + ("w:gridCol{w:w=4242}", Inches(0.5), "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) - expected_xml = xml(expected_cxml) - return column, new_value, expected_xml + def it_can_change_its_width( + self, gridCol_cxml: str, new_value: Length | None, expected_cxml: str, table_: Mock + ): + column = _Column(cast(CT_TblGridCol, element(gridCol_cxml)), table_) + + column.width = new_value + + assert column.width == new_value + assert column._gridCol.xml == xml(expected_cxml) - # fixture components --------------------------------------------- + def it_knows_its_index_in_table_to_help(self, table_: Mock): + tbl = cast(CT_Tbl, element("w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)")) + gridCol = tbl.tblGrid.gridCol_lst[1] + column = _Column(gridCol, table_) + assert column._index == 1 + + # fixtures ------------------------------------------------------- @pytest.fixture - def _index_(self, request): + def _index_prop_(self, request: FixtureRequest): return property_mock(request, _Column, "_index") @pytest.fixture - def parent_(self, request): + def parent_(self, request: FixtureRequest): return instance_mock(request, Table) @pytest.fixture - def table_(self, request): + def table_(self, request: FixtureRequest): return instance_mock(request, Table) @pytest.fixture - def table_prop_(self, request, table_): - return property_mock(request, _Column, "table", return_value=table_) + def table_prop_(self, request: FixtureRequest): + return property_mock(request, _Column, "table") class Describe_Columns: - def it_knows_how_many_columns_it_contains(self, columns_fixture): - columns, column_count = columns_fixture - assert len(columns) == column_count - - def it_can_interate_over_its__Column_instances(self, columns_fixture): - columns, column_count = columns_fixture - actual_count = 0 - for column in columns: - assert isinstance(column, _Column) - actual_count += 1 - assert actual_count == column_count - - def it_provides_indexed_access_to_columns(self, columns_fixture): - columns, column_count = columns_fixture - for idx in range(-column_count, column_count): - column = columns[idx] - assert isinstance(column, _Column) - - def it_raises_on_indexed_access_out_of_range(self, columns_fixture): - columns, column_count = columns_fixture - too_low = -1 - column_count - too_high = column_count - with pytest.raises(IndexError): - columns[too_low] - with pytest.raises(IndexError): - columns[too_high] + """Unit-test suite for `docx.table._Columns` objects.""" - def it_provides_access_to_the_table_it_belongs_to(self, table_fixture): - columns, table_ = table_fixture - assert columns.table is table_ + def it_has_sequence_behaviors(self, table_: Mock): + columns = _Columns(cast(CT_Tbl, element("w:tbl/w:tblGrid/(w:gridCol,w:gridCol)")), table_) - # fixtures ------------------------------------------------------- + # -- it supports len() -- + assert len(columns) == 2 + # -- it is iterable -- + assert len(tuple(c for c in columns)) == 2 + assert all(type(c) is _Column for c in columns) + # -- it is indexable -- + assert all(type(columns[i]) is _Column for i in range(2)) - @pytest.fixture - def columns_fixture(self): - column_count = 2 - tbl = _tbl_bldr(rows=2, cols=column_count).element - columns = _Columns(tbl, None) - return columns, column_count + def it_raises_on_indexed_access_out_of_range(self, table_: Mock): + columns = _Columns(cast(CT_Tbl, element("w:tbl/w:tblGrid/(w:gridCol,w:gridCol)")), table_) - @pytest.fixture - def table_fixture(self, table_): - columns = _Columns(None, table_) + with pytest.raises(IndexError): + columns[2] + with pytest.raises(IndexError): + columns[-3] + + def it_provides_access_to_the_table_it_belongs_to(self, table_: Mock): + columns = _Columns(cast(CT_Tbl, element("w:tbl")), table_) table_.table = table_ - return columns, table_ - # fixture components --------------------------------------------- + assert columns.table is table_ + + # fixtures ------------------------------------------------------- @pytest.fixture - def table_(self, request): + def table_(self, request: FixtureRequest): return instance_mock(request, Table) class Describe_Row: - def it_knows_its_height(self, height_get_fixture): - row, expected_height = height_get_fixture - assert row.height == expected_height - - def it_can_change_its_height(self, height_set_fixture): - row, value, expected_xml = height_set_fixture - row.height = value - assert row._tr.xml == expected_xml - - def it_knows_its_height_rule(self, height_rule_get_fixture): - row, expected_rule = height_rule_get_fixture - assert row.height_rule == expected_rule - - def it_can_change_its_height_rule(self, height_rule_set_fixture): - row, rule, expected_xml = height_rule_set_fixture - row.height_rule = rule - assert row._tr.xml == expected_xml - - def it_provides_access_to_its_cells(self, cells_fixture): - row, row_idx, expected_cells = cells_fixture - cells = row.cells - row.table.row_cells.assert_called_once_with(row_idx) - assert cells == expected_cells - - def it_provides_access_to_the_table_it_belongs_to(self, table_fixture): - row, table_ = table_fixture - assert row.table is table_ - - def it_knows_its_index_in_table_to_help(self, idx_fixture): - row, expected_idx = idx_fixture - assert row._index == expected_idx - - # fixtures ------------------------------------------------------- + """Unit-test suite for `docx.table._Row` objects.""" - @pytest.fixture - def cells_fixture(self, _index_, table_prop_, table_): - row = _Row(None, None) - _index_.return_value = row_idx = 6 - expected_cells = (1, 2, 3) - table_.row_cells.return_value = list(expected_cells) - return row, row_idx, expected_cells - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tr_cxml", "expected_value"), + [ ("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 + def it_knows_its_height(self, tr_cxml: str, expected_value: int | None, parent_: Mock): + row = _Row(cast(CT_Row, element(tr_cxml)), parent_) + assert row.height == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tr_cxml", "new_value", "expected_cxml"), + [ ("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}"), @@ -812,16 +674,18 @@ def height_get_fixture(self, request): ("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=[ + def it_can_change_its_height( + self, tr_cxml: str, new_value: Length | None, expected_cxml: str, parent_: Mock + ): + row = _Row(cast(CT_Row, element(tr_cxml)), parent_) + row.height = new_value + assert row._tr.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + ("tr_cxml", "expected_value"), + [ ("w:tr", None), ("w:tr/w:trPr", None), ("w:tr/w:trPr/w:trHeight{w:val=0, w:hRule=auto}", WD_ROW_HEIGHT.AUTO), @@ -833,15 +697,17 @@ def height_set_fixture(self, request): "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 + def it_knows_its_height_rule( + self, tr_cxml: str, expected_value: WD_ROW_HEIGHT | None, parent_: Mock + ): + row = _Row(cast(CT_Row, element(tr_cxml)), parent_) + assert row.height_rule == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("tr_cxml", "new_value", "expected_cxml"), + [ ("w:tr", WD_ROW_HEIGHT.AUTO, "w:tr/w:trPr/w:trHeight{w:hRule=auto}"), ( "w:tr/w:trPr", @@ -866,143 +732,125 @@ def height_rule_get_fixture(self, request): ("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) - expected_xml = xml(expected_cxml) - return row, new_rule, expected_xml + def it_can_change_its_height_rule( + self, tr_cxml: str, new_value: WD_ROW_HEIGHT | None, expected_cxml: str, parent_: Mock + ): + row = _Row(cast(CT_Row, element(tr_cxml)), parent_) + row.height_rule = new_value + assert row._tr.xml == xml(expected_cxml) - @pytest.fixture - def idx_fixture(self): - tbl = element("w:tbl/(w:tr,w:tr,w:tr)") - tr, expected_idx = tbl[1], 1 - row = _Row(tr, None) - return row, expected_idx + def it_provides_access_to_its_cells( + self, _index_prop_: Mock, table_prop_: Mock, table_: Mock, parent_: Mock + ): + row = _Row(cast(CT_Row, element("w:tr")), parent_) + _index_prop_.return_value = row_idx = 6 + expected_cells = (1, 2, 3) + table_.row_cells.return_value = list(expected_cells) - @pytest.fixture - def table_fixture(self, parent_, table_): - row = _Row(None, parent_) + cells = row.cells + + table_.row_cells.assert_called_once_with(row_idx) + assert cells == expected_cells + + def it_provides_access_to_the_table_it_belongs_to(self, parent_: Mock, table_: Mock): parent_.table = table_ - return row, table_ + row = _Row(cast(CT_Row, element("w:tr")), parent_) + assert row.table is table_ + + def it_knows_its_index_in_table_to_help(self, parent_: Mock): + tbl = element("w:tbl/(w:tr,w:tr,w:tr)") + row = _Row(cast(CT_Row, tbl[1]), parent_) + assert row._index == 1 - # fixture components --------------------------------------------- + # fixtures ------------------------------------------------------- @pytest.fixture - def _index_(self, request): + def _index_prop_(self, request: FixtureRequest): return property_mock(request, _Row, "_index") @pytest.fixture - def parent_(self, request): + def parent_(self, request: FixtureRequest): return instance_mock(request, Table) @pytest.fixture - def table_(self, request): + def table_(self, request: FixtureRequest): return instance_mock(request, Table) @pytest.fixture - def table_prop_(self, request, table_): + def table_prop_(self, request: FixtureRequest, table_: Mock): return property_mock(request, _Row, "table", return_value=table_) class Describe_Rows: - def it_knows_how_many_rows_it_contains(self, rows_fixture): - rows, row_count = rows_fixture - assert len(rows) == row_count - - def it_can_iterate_over_its__Row_instances(self, rows_fixture): - rows, row_count = rows_fixture - actual_count = 0 - for row in rows: - assert isinstance(row, _Row) - actual_count += 1 - assert actual_count == row_count - - def it_provides_indexed_access_to_rows(self, rows_fixture): - rows, row_count = rows_fixture - for idx in range(-row_count, row_count): - row = rows[idx] - assert isinstance(row, _Row) + """Unit-test suite for `docx.table._Rows` objects.""" - def it_provides_sliced_access_to_rows(self, slice_fixture): - rows, start, end, expected_count = slice_fixture - slice_of_rows = rows[start:end] - assert len(slice_of_rows) == expected_count - tr_lst = rows._tbl.tr_lst - for idx, row in enumerate(slice_of_rows): - assert tr_lst.index(row._tr) == start + idx - assert isinstance(row, _Row) - - def it_raises_on_indexed_access_out_of_range(self, rows_fixture): - rows, row_count = rows_fixture - 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, match="list index out of range"): - rows[too_high] - - def it_provides_access_to_the_table_it_belongs_to(self, table_fixture): - rows, table_ = table_fixture - assert rows.table is table_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def rows_fixture(self): - row_count = 2 - tbl = _tbl_bldr(rows=row_count, cols=2).element - rows = _Rows(tbl, None) - return rows, row_count - - @pytest.fixture( - params=[ - (3, 1, 3, 2), - (3, 0, -1, 2), - ] + @pytest.mark.parametrize( + ("tbl_cxml", "expected_len"), + [ + ("w:tbl", 0), + ("w:tbl/w:tr", 1), + ("w:tbl/(w:tr,w:tr)", 2), + ("w:tbl/(w:tr,w:tr,w:tr)", 3), + ], ) - def slice_fixture(self, request): - row_count, start, end, expected_count = request.param - tbl = _tbl_bldr(rows=row_count, cols=2).element - rows = _Rows(tbl, None) - return rows, start, end, expected_count - - @pytest.fixture - def table_fixture(self, table_): - rows = _Rows(None, table_) - table_.table = table_ - return rows, table_ + def it_has_sequence_behaviors(self, tbl_cxml: str, expected_len: int, parent_: Mock): + tbl = cast(CT_Tbl, element(tbl_cxml)) + table = Table(tbl, parent_) + rows = _Rows(tbl, table) - # fixture components --------------------------------------------- + # -- it supports len() -- + assert len(rows) == expected_len + # -- it is iterable -- + assert len(tuple(r for r in rows)) == expected_len + assert all(type(r) is _Row for r in rows) + # -- it is indexable -- + assert all(type(rows[i]) is _Row for i in range(expected_len)) - @pytest.fixture - def table_(self, request): - return instance_mock(request, Table) + @pytest.mark.parametrize( + ("tbl_cxml", "out_of_range_idx"), + [ + ("w:tbl", 0), + ("w:tbl", 1), + ("w:tbl", -1), + ("w:tbl/w:tr", 1), + ("w:tbl/w:tr", -2), + ("w:tbl/(w:tr,w:tr,w:tr)", 3), + ("w:tbl/(w:tr,w:tr,w:tr)", -4), + ], + ) + def it_raises_on_indexed_access_out_of_range( + self, tbl_cxml: str, out_of_range_idx: int, parent_: Mock + ): + rows = _Rows(cast(CT_Tbl, element(tbl_cxml)), parent_) + with pytest.raises(IndexError, match="list index out of range"): + rows[out_of_range_idx] -# fixtures ----------------------------------------------------------- + @pytest.mark.parametrize(("start", "end", "expected_len"), [(1, 3, 2), (0, -1, 2)]) + def it_provides_sliced_access_to_rows( + self, start: int, end: int, expected_len: int, parent_: Mock + ): + tbl = cast(CT_Tbl, element("w:tbl/(w:tr,w:tr,w:tr)")) + rows = _Rows(tbl, parent_) + slice_of_rows = rows[start:end] -def _tbl_bldr(rows, cols): - tblGrid_bldr = a_tblGrid() - for i in range(cols): - tblGrid_bldr.with_child(a_gridCol()) - tbl_bldr = a_tbl().with_nsdecls().with_child(tblGrid_bldr) - for i in range(rows): - tr_bldr = _tr_bldr(cols) - tbl_bldr.with_child(tr_bldr) - return tbl_bldr + assert len(slice_of_rows) == expected_len + for idx, row in enumerate(slice_of_rows): + assert tbl.tr_lst.index(row._tr) == start + idx + assert isinstance(row, _Row) + def it_provides_access_to_the_table_it_belongs_to(self, parent_: Mock): + tbl = cast(CT_Tbl, element("w:tbl")) + table = Table(tbl, parent_) + rows = _Rows(tbl, table) -def _tc_bldr(): - return a_tc().with_child(a_p()) + assert rows.table is table + # fixtures ------------------------------------------------------- -def _tr_bldr(cols): - tr_bldr = a_tr() - for i in range(cols): - tc_bldr = _tc_bldr() - tr_bldr.with_child(tc_bldr) - return tr_bldr + @pytest.fixture + def parent_(self, request: FixtureRequest): + return instance_mock(request, Document) From d8a328985810a14308f3c90bd5bd3f6795b3956d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 16:07:13 -0700 Subject: [PATCH 079/131] rfctr: improve expression --- src/docx/oxml/table.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index e0aed09a3..687c6e2e6 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -441,9 +441,7 @@ def grid_span(self) -> int: Determined by ./w:tcPr/w:gridSpan/@val, it defaults to 1. """ tcPr = self.tcPr - if tcPr is None: - return 1 - return tcPr.grid_span + return 1 if tcPr is None else tcPr.grid_span @grid_span.setter def grid_span(self, value: int): @@ -809,9 +807,7 @@ def grid_span(self) -> int: Determined by ./w:gridSpan/@val, it defaults to 1. """ gridSpan = self.gridSpan - if gridSpan is None: - return 1 - return gridSpan.val + return 1 if gridSpan is None else gridSpan.val @grid_span.setter def grid_span(self, value: int): @@ -898,9 +894,7 @@ class CT_TrPr(BaseOxmlElement): def trHeight_hRule(self) -> WD_ROW_HEIGHT_RULE | None: """Return the value of `w:trHeight@w:hRule`, or |None| if not present.""" trHeight = self.trHeight - if trHeight is None: - return None - return trHeight.hRule + return None if trHeight is None else trHeight.hRule @trHeight_hRule.setter def trHeight_hRule(self, value: WD_ROW_HEIGHT_RULE | None): @@ -913,9 +907,7 @@ def trHeight_hRule(self, value: WD_ROW_HEIGHT_RULE | None): def trHeight_val(self): """Return the value of `w:trHeight@w:val`, or |None| if not present.""" trHeight = self.trHeight - if trHeight is None: - return None - return trHeight.val + return None if trHeight is None else trHeight.val @trHeight_val.setter def trHeight_val(self, value: Length | None): From 6c34f128a5b5e331f1bbf88935f7c13396d33fb3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 29 Apr 2024 14:43:04 -0700 Subject: [PATCH 080/131] rfctr: modernize opc.shared.lazyproperty No need for two, use the already modernized `docx.shared.lazyproperty`. --- src/docx/opc/package.py | 44 ++++--- src/docx/opc/part.py | 14 ++- src/docx/opc/pkgwriter.py | 13 +- src/docx/opc/rel.py | 18 +-- src/docx/opc/shared.py | 28 ++--- tests/opc/test_package.py | 170 ++++++++++++--------------- tests/opc/test_part.py | 228 ++++++++++++++++-------------------- tests/opc/test_pkgwriter.py | 70 ++++++----- 8 files changed, 277 insertions(+), 308 deletions(-) diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index b5bdc0e7c..148cd39b1 100644 --- a/src/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -1,5 +1,9 @@ """Objects that implement reading and writing OPC packages.""" +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, Iterator + from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.packuri import PACKAGE_URI, PackURI from docx.opc.part import PartFactory @@ -7,7 +11,10 @@ from docx.opc.pkgreader import PackageReader from docx.opc.pkgwriter import PackageWriter from docx.opc.rel import Relationships -from docx.opc.shared import lazyproperty +from docx.shared import lazyproperty + +if TYPE_CHECKING: + from docx.opc.part import Part class OpcPackage: @@ -56,7 +63,7 @@ def walk_rels(source, visited=None): for rel in walk_rels(self): yield rel - def iter_parts(self): + def iter_parts(self) -> Iterator[Part]: """Generate exactly one reference to each of the parts in the package by performing a depth-first traversal of the rels graph.""" @@ -76,7 +83,7 @@ def walk_parts(source, visited=[]): for part in walk_parts(self): yield part - def load_rel(self, reltype, target, rId, is_external=False): + def load_rel(self, reltype: str, target: Part | str, rId: str, is_external: bool = False): """Return newly added |_Relationship| instance of `reltype` between this part and `target` with key `rId`. @@ -111,14 +118,14 @@ def next_partname(self, template): return PackURI(candidate_partname) @classmethod - def open(cls, pkg_file): + def open(cls, pkg_file: str | IO[bytes]) -> OpcPackage: """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): + def part_related_by(self, reltype: str) -> Part: """Return part to which this package has a relationship of `reltype`. Raises |KeyError| if no such relationship is found and |ValueError| if more than @@ -127,13 +134,16 @@ def part_related_by(self, reltype): return self.rels.part_with_reltype(reltype) @property - def parts(self): + def parts(self) -> list[Part]: """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.""" + def relate_to(self, part: Part, reltype: str): + """Return rId key of new or existing relationship to `part`. + + If a relationship of `reltype` to `part` already exists, its rId is returned. Otherwise a + new relationship is created and that rId is returned. + """ rel = self.rels.get_or_add(reltype, part) return rel.rId @@ -143,9 +153,11 @@ def rels(self): 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.""" + def save(self, pkg_file: str | IO[bytes]): + """Save this package to `pkg_file`. + + `pkg_file` can be either a file-path or a file-like object. + """ for part in self.parts: part.before_marshal() PackageWriter.write(pkg_file, self.rels, self.parts) @@ -190,9 +202,7 @@ def _unmarshal_parts(pkg_reader, package, part_factory): """ parts = {} for partname, content_type, reltype, blob in pkg_reader.iter_sparts(): - parts[partname] = part_factory( - partname, content_type, reltype, blob, package - ) + parts[partname] = part_factory(partname, content_type, reltype, blob, package) return parts @staticmethod @@ -202,7 +212,5 @@ def _unmarshal_relationships(pkg_reader, package, parts): 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] - ) + 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/part.py b/src/docx/opc/part.py index a4ad3e7b2..142f49dd1 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -7,8 +7,9 @@ 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.opc.shared import cls_method_fn from docx.oxml.parser import parse_xml +from docx.shared import lazyproperty if TYPE_CHECKING: from docx.package import Package @@ -81,9 +82,10 @@ def drop_rel(self, rId: str): def load(cls, partname: str, content_type: str, blob: bytes, package: 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`. + def load_rel(self, reltype: str, target: Part | str, rId: str, is_external: bool = False): + """Return newly added |_Relationship| instance of `reltype`. + + The new relationship relates the `target` part to this part 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 @@ -118,7 +120,7 @@ def part_related_by(self, reltype: str) -> Part: """ return self.rels.part_with_reltype(reltype) - def relate_to(self, target: Part, reltype: str, is_external: bool = False) -> str: + def relate_to(self, target: Part | str, 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 @@ -142,7 +144,7 @@ def rels(self): """|Relationships| instance holding the relationships for this part.""" return Relationships(self._partname.baseURI) - def target_ref(self, rId): + def target_ref(self, rId: str) -> str: """Return URL contained in target ref of relationship identified by `rId`.""" rel = self.rels[rId] return rel.target_ref diff --git a/src/docx/opc/pkgwriter.py b/src/docx/opc/pkgwriter.py index 75af6ac75..e63516979 100644 --- a/src/docx/opc/pkgwriter.py +++ b/src/docx/opc/pkgwriter.py @@ -4,6 +4,10 @@ OpcPackage.save(). """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable + 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 @@ -11,6 +15,9 @@ from docx.opc.shared import CaseInsensitiveDict from docx.opc.spec import default_content_types +if TYPE_CHECKING: + from docx.opc.part import Part + class PackageWriter: """Writes a zip-format OPC package to `pkg_file`, where `pkg_file` can be either a @@ -38,13 +45,13 @@ def _write_content_types_stream(phys_writer, parts): phys_writer.write(CONTENT_TYPES_URI, cti.blob) @staticmethod - def _write_parts(phys_writer, parts): + def _write_parts(phys_writer: PhysPkgWriter, parts: Iterable[Part]): """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) + if len(part.rels): + phys_writer.write(part.partname.rels_uri, part.rels.xml) @staticmethod def _write_pkg_rels(phys_writer, pkg_rels): diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index efac5e06b..5fae7ad9c 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -2,10 +2,13 @@ from __future__ import annotations -from typing import Any, Dict +from typing import TYPE_CHECKING, Any, Dict from docx.opc.oxml import CT_Relationships +if TYPE_CHECKING: + from docx.opc.part import Part + class Relationships(Dict[str, "_Relationship"]): """Collection object for |_Relationship| instances, having list semantics.""" @@ -16,7 +19,7 @@ def __init__(self, baseURI: str): self._target_parts_by_rId: Dict[str, Any] = {} def add_relationship( - self, reltype: str, target: str | Any, rId: str, is_external: bool = False + self, reltype: str, target: Part | str, rId: str, is_external: bool = False ) -> "_Relationship": """Return a newly added |_Relationship| instance.""" rel = _Relationship(rId, reltype, target, self._baseURI, is_external) @@ -25,7 +28,7 @@ def add_relationship( self._target_parts_by_rId[rId] = target return rel - def get_or_add(self, reltype, target_part): + def get_or_add(self, reltype: str, target_part: Part) -> _Relationship: """Return relationship of `reltype` to `target_part`, newly added if not already present in collection.""" rel = self._get_matching(reltype, target_part) @@ -64,7 +67,9 @@ def xml(self): 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): + def _get_matching( + self, reltype: str, target: Part | str, is_external: bool = False + ) -> _Relationship | None: """Return relationship of matching `reltype`, `target`, and `is_external` from collection, or None if not found.""" @@ -99,7 +104,7 @@ def _get_rel_of_type(self, reltype): return matching[0] @property - def _next_rId(self): + def _next_rId(self) -> str: """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): @@ -135,8 +140,7 @@ def rId(self): def target_part(self): if self._is_external: raise ValueError( - "target_part property on _Relationship is undef" - "ined when target mode is External" + "target_part property on _Relationship is undef" "ined when target mode is External" ) return self._target diff --git a/src/docx/opc/shared.py b/src/docx/opc/shared.py index 1862f66db..9d4c0a6d3 100644 --- a/src/docx/opc/shared.py +++ b/src/docx/opc/shared.py @@ -1,7 +1,13 @@ """Objects shared by opc modules.""" +from __future__ import annotations -class CaseInsensitiveDict(dict): +from typing import Any, Dict, TypeVar + +_T = TypeVar("_T") + + +class CaseInsensitiveDict(Dict[str, Any]): """Mapping type that behaves like dict except that it matches without respect to the case of the key. @@ -23,23 +29,3 @@ def __setitem__(self, key, 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/tests/opc/test_package.py b/tests/opc/test_package.py index 7fdeaa422..d8fcef453 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -1,5 +1,9 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for docx.opc.package module""" +from __future__ import annotations + import pytest from docx.opc.constants import RELATIONSHIP_TYPE as RT @@ -12,8 +16,8 @@ from docx.opc.rel import Relationships, _Relationship from ..unitutil.mock import ( + FixtureRequest, Mock, - PropertyMock, call, class_mock, instance_mock, @@ -25,6 +29,8 @@ class DescribeOpcPackage: + """Unit-test suite for `docx.opc.package.OpcPackage` objects.""" + def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): # mockery ---------------------- pkg_file = Mock(name="pkg_file") @@ -42,19 +48,26 @@ def it_initializes_its_rels_collection_on_first_reference(self, Relationships_): Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) assert rels == Relationships_.return_value - def it_can_add_a_relationship_to_a_part(self, pkg_with_rels_, rel_attrs_): - reltype, target, rId = rel_attrs_ - pkg = pkg_with_rels_ - # exercise --------------------- - pkg.load_rel(reltype, target, rId) - # verify ----------------------- - pkg._rels.add_relationship.assert_called_once_with(reltype, target, rId, False) + def it_can_add_a_relationship_to_a_part(self, rels_prop_: Mock, rels_: Mock, part_: Mock): + rels_prop_.return_value = rels_ + pkg = OpcPackage() + + pkg.load_rel("http://rel/type", part_, "rId99") - 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_) - assert _rId == rId + rels_.add_relationship.assert_called_once_with("http://rel/type", part_, "rId99", False) + + def it_can_establish_a_relationship_to_another_part( + self, rels_prop_: Mock, rels_: Mock, rel_: Mock, part_: Mock + ): + rel_.rId = "rId99" + rels_.get_or_add.return_value = rel_ + rels_prop_.return_value = rels_ + pkg = OpcPackage() + + rId = pkg.relate_to(part_, "http://rel/type") + + rels_.get_or_add.assert_called_once_with("http://rel/type", part_) + assert rId == "rId99" def it_can_provide_a_list_of_the_parts_it_contains(self): # mockery ---------------------- @@ -64,7 +77,7 @@ def it_can_provide_a_list_of_the_parts_it_contains(self): 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): + def it_can_iterate_over_parts_by_walking_rels_graph(self, rels_prop_: Mock): # +----------+ +--------+ # | pkg_rels |-----> | part_1 | # +----------+ +--------+ @@ -77,7 +90,7 @@ def it_can_iterate_over_parts_by_walking_rels_graph(self): 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 = { + rels_prop_.return_value = { 1: Mock(name="rel3", is_external=False, target_part=part1), 2: Mock(name="rel4", is_external=True), } @@ -106,21 +119,22 @@ 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_: Mock, PackageWriter_: Mock, parts_prop_: Mock, parts_: list[Mock] + ): + parts_prop_.return_value = 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 core_properties = opc_package.core_properties assert core_properties is core_properties_ - def it_provides_access_to_the_core_properties_part_to_help( - self, core_props_part_fixture - ): + def it_provides_access_to_the_core_properties_part_to_help(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_ @@ -135,9 +149,7 @@ def it_creates_a_default_core_props_part_if_none_present( core_properties_part = opc_package._core_properties_part CorePropertiesPart_.default.assert_called_once_with(opc_package) - relate_to_.assert_called_once_with( - opc_package, core_properties_part_, RT.CORE_PROPERTIES - ) + relate_to_.assert_called_once_with(opc_package, core_properties_part_, RT.CORE_PROPERTIES) assert core_properties_part is core_properties_part_ # fixtures --------------------------------------------- @@ -161,134 +173,106 @@ def core_props_part_fixture(self, part_related_by_, core_properties_part_): def next_partname_fixture(self, request, iter_parts_): existing_partname_ns, next_partname_n = request.param parts_ = [ - instance_mock( - request, Part, name="part[%d]" % idx, partname="/foo/bar/baz%d.xml" % n - ) + instance_mock(request, Part, name="part[%d]" % idx, partname="/foo/bar/baz%d.xml" % n) for idx, n in enumerate(existing_partname_ns) ] expected_value = "/foo/bar/baz%d.xml" % next_partname_n return parts_, expected_value @pytest.fixture - def relate_to_part_fixture_(self, request, pkg, rels_, reltype): - 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_") - return pkg, part_, reltype, rId - - @pytest.fixture - def related_part_fixture_(self, request, rels_, reltype): + def related_part_fixture_(self, request: FixtureRequest, rels_prop_: Mock, rels_: Mock): related_part_ = instance_mock(request, Part, name="related_part_") rels_.part_with_reltype.return_value = related_part_ pkg = OpcPackage() - pkg._rels = rels_ - return pkg, reltype, related_part_ + rels_prop_.return_value = rels_ + return pkg, "http://rel/type", related_part_ # fixture components ----------------------------------- @pytest.fixture - def CorePropertiesPart_(self, request): + def CorePropertiesPart_(self, request: FixtureRequest): return class_mock(request, "docx.opc.package.CorePropertiesPart") @pytest.fixture - def core_properties_(self, request): + def core_properties_(self, request: FixtureRequest): return instance_mock(request, CoreProperties) @pytest.fixture - def core_properties_part_(self, request): + def core_properties_part_(self, request: FixtureRequest): return instance_mock(request, CorePropertiesPart) @pytest.fixture - def _core_properties_part_prop_(self, request): + def _core_properties_part_prop_(self, request: FixtureRequest): return property_mock(request, OpcPackage, "_core_properties_part") @pytest.fixture - def iter_parts_(self, request): + def iter_parts_(self, request: FixtureRequest): return method_mock(request, OpcPackage, "iter_parts") @pytest.fixture - def PackageReader_(self, request): + def PackageReader_(self, request: FixtureRequest): return class_mock(request, "docx.opc.package.PackageReader") @pytest.fixture - def PackURI_(self, request): + def PackURI_(self, request: FixtureRequest): return class_mock(request, "docx.opc.package.PackURI") @pytest.fixture - def packuri_(self, request): + def packuri_(self, request: FixtureRequest): return instance_mock(request, PackURI) @pytest.fixture - def PackageWriter_(self, request): + def PackageWriter_(self, request: FixtureRequest): return class_mock(request, "docx.opc.package.PackageWriter") @pytest.fixture - def PartFactory_(self, request): + def PartFactory_(self, request: FixtureRequest): return class_mock(request, "docx.opc.package.PartFactory") @pytest.fixture - def part_related_by_(self, request): - return method_mock(request, OpcPackage, "part_related_by") + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) @pytest.fixture - def parts(self, parts_): - """ - Return a mock patching property OpcPackage.parts, reversing the - patch after each use. - """ - p = patch.object( - OpcPackage, "parts", new_callable=PropertyMock, return_value=parts_ - ) - yield p.start() - p.stop() + def part_related_by_(self, request: FixtureRequest): + return method_mock(request, OpcPackage, "part_related_by") @pytest.fixture - def parts_(self, request): + def parts_(self, request: FixtureRequest): part_ = instance_mock(request, Part, name="part_") part_2_ = instance_mock(request, Part, name="part_2_") return [part_, part_2_] @pytest.fixture - def pkg(self, request): - return OpcPackage() + def parts_prop_(self, request: FixtureRequest): + return property_mock(request, OpcPackage, "parts") @pytest.fixture - def pkg_file_(self, request): + def pkg_file_(self, request: FixtureRequest): return loose_mock(request) @pytest.fixture - def pkg_with_rels_(self, request, rels_): - pkg = OpcPackage() - pkg._rels = rels_ - return pkg - - @pytest.fixture - def Relationships_(self, request): + def Relationships_(self, request: FixtureRequest): 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" - return reltype, target_, rId + def rel_(self, request: FixtureRequest): + return instance_mock(request, _Relationship) @pytest.fixture - def relate_to_(self, request): + def relate_to_(self, request: FixtureRequest): return method_mock(request, OpcPackage, "relate_to") @pytest.fixture - def rels_(self, request): + def rels_(self, request: FixtureRequest): return instance_mock(request, Relationships) @pytest.fixture - def reltype(self, request): - return "http://rel/type" + def rels_prop_(self, request: FixtureRequest): + return property_mock(request, OpcPackage, "rels") @pytest.fixture - def Unmarshaller_(self, request): + def Unmarshaller_(self, request: FixtureRequest): return class_mock(request, "docx.opc.package.Unmarshaller") @@ -306,9 +290,7 @@ def it_can_unmarshal_from_a_pkg_reader( Unmarshaller.unmarshal(pkg_reader_, pkg_, part_factory_) _unmarshal_parts_.assert_called_once_with(pkg_reader_, pkg_, part_factory_) - _unmarshal_relationships_.assert_called_once_with( - pkg_reader_, pkg_, parts_dict_ - ) + _unmarshal_relationships_.assert_called_once_with(pkg_reader_, pkg_, parts_dict_) for part in parts_dict_.values(): part.after_unmarshal.assert_called_once_with() pkg_.after_unmarshal.assert_called_once_with() @@ -406,13 +388,13 @@ def it_can_unmarshal_relationships(self): # fixtures --------------------------------------------- @pytest.fixture - def blobs_(self, request): + def blobs_(self, request: FixtureRequest): 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): + def content_types_(self, request: FixtureRequest): 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_ @@ -424,13 +406,13 @@ def part_factory_(self, request, parts_): return part_factory_ @pytest.fixture - def partnames_(self, request): + def partnames_(self, request: FixtureRequest): 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): + def parts_(self, request: FixtureRequest): part_ = instance_mock(request, Part, name="part_") part_2_ = instance_mock(request, Part, name="part_2") return part_, part_2_ @@ -442,7 +424,7 @@ def parts_dict_(self, request, partnames_, parts_): return {partname_: part_, partname_2_: part_2_} @pytest.fixture - def pkg_(self, request): + def pkg_(self, request: FixtureRequest): return instance_mock(request, OpcPackage) @pytest.fixture @@ -460,17 +442,15 @@ def pkg_reader_(self, request, partnames_, content_types_, reltypes_, blobs_): return pkg_reader_ @pytest.fixture - def reltypes_(self, request): + def reltypes_(self, request: FixtureRequest): 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): + def _unmarshal_parts_(self, request: FixtureRequest): 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 - ) + def _unmarshal_relationships_(self, request: FixtureRequest): + return method_mock(request, Unmarshaller, "_unmarshal_relationships", autospec=False) diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index 163912154..03eacd361 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -1,5 +1,9 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for docx.opc.part module""" +from __future__ import annotations + import pytest from docx.opc.package import OpcPackage @@ -11,6 +15,7 @@ from ..unitutil.cxml import element from ..unitutil.mock import ( ANY, + FixtureRequest, Mock, class_mock, cls_attr_mock, @@ -18,6 +23,7 @@ initializer_mock, instance_mock, loose_mock, + property_mock, ) @@ -117,150 +123,126 @@ def partname_(self, request): class DescribePartRelationshipManagementInterface: - def it_provides_access_to_its_relationships(self, rels_fixture): - part, Relationships_, partname_, rels_ = rels_fixture + """Unit-test suite for `docx.opc.package.Part` relationship behaviors.""" + + def it_provides_access_to_its_relationships( + self, Relationships_: Mock, partname_: Mock, rels_: Mock + ): + Relationships_.return_value = rels_ + part = Part(partname_, "content_type") + rels = part.rels + Relationships_.assert_called_once_with(partname_.baseURI) assert rels is rels_ - 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) - - 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): - 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_) - assert rId is rId_ - - def it_can_drop_a_relationship(self, drop_rel_fixture): - part, rId, rel_should_be_gone = drop_rel_fixture - part.drop_rel(rId) - if rel_should_be_gone: - assert rId not in part.rels - else: - assert rId in part.rels - - def it_can_find_a_related_part_by_reltype(self, related_part_fixture): - part, reltype_, related_part_ = related_part_fixture - related_part = part.part_related_by(reltype_) - part.rels.part_with_reltype.assert_called_once_with(reltype_) - assert related_part is related_part_ - - 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): - part, rId_, url_ = target_ref_fixture - url = part.target_ref(rId_) - assert url == url_ + def it_can_load_a_relationship(self, rels_prop_: Mock, rels_: Mock, other_part_: Mock): + rels_prop_.return_value = rels_ + part = Part("partname", "content_type") - # fixtures --------------------------------------------- + part.load_rel("http://rel/type", other_part_, "rId42") + + rels_.add_relationship.assert_called_once_with( + "http://rel/type", other_part_, "rId42", False + ) + + def it_can_establish_a_relationship_to_another_part( + self, rels_prop_: Mock, rels_: Mock, rel_: Mock, other_part_: Mock + ): + rels_prop_.return_value = rels_ + rels_.get_or_add.return_value = rel_ + rel_.rId = "rId18" + part = Part("partname", "content_type") + + rId = part.relate_to(other_part_, "http://rel/type") - @pytest.fixture( - params=[ + rels_.get_or_add.assert_called_once_with("http://rel/type", other_part_) + assert rId == "rId18" + + def it_can_establish_an_external_relationship(self, rels_prop_: Mock, rels_: Mock): + rels_prop_.return_value = rels_ + rels_.get_or_add_ext_rel.return_value = "rId27" + part = Part("partname", "content_type") + + rId = part.relate_to("https://hyper/link", "http://rel/type", is_external=True) + + rels_.get_or_add_ext_rel.assert_called_once_with("http://rel/type", "https://hyper/link") + assert rId == "rId27" + + @pytest.mark.parametrize( + ("part_cxml", "rel_should_be_dropped"), + [ ("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" - part._element = element(part_cxml) - part._rels = {rId: None} - return part, rId, rel_should_be_dropped + def it_can_drop_a_relationship( + self, part_cxml: str, rel_should_be_dropped: bool, rels_prop_: Mock + ): + rels_prop_.return_value = {"rId42": None} + part = Part("partname", "content_type") + part._element = element(part_cxml) # pyright: ignore[reportAttributeAccessIssue] - @pytest.fixture - def load_rel_fixture(self, part, rels_, reltype_, part_, rId_): - part._rels = rels_ - return part, rels_, reltype_, part_, rId_ + part.drop_rel("rId42") - @pytest.fixture - def relate_to_part_fixture(self, request, part, reltype_, part_, rels_, rId_): - part._rels = rels_ - target_ = part_ - return part, target_, reltype_, rId_ + assert ("rId42" not in part.rels) is rel_should_be_dropped - @pytest.fixture - def relate_to_url_fixture(self, request, part, rels_, url_, reltype_, rId_): - part._rels = rels_ - return part, url_, reltype_, rId_ + def it_can_find_a_related_part_by_reltype( + self, rels_prop_: Mock, rels_: Mock, other_part_: Mock + ): + rels_prop_.return_value = rels_ + rels_.part_with_reltype.return_value = other_part_ + part = Part("partname", "content_type") - @pytest.fixture - def related_part_fixture(self, request, part, rels_, reltype_, part_): - part._rels = rels_ - return part, reltype_, part_ + related_part = part.part_related_by("http://rel/type") - @pytest.fixture - def related_parts_fixture(self, request, part, rels_, related_parts_): - part._rels = rels_ - return part, related_parts_ + rels_.part_with_reltype.assert_called_once_with("http://rel/type") + assert related_part is other_part_ - @pytest.fixture - def rels_fixture(self, Relationships_, partname_, rels_): - part = Part(partname_, None) - return part, Relationships_, partname_, rels_ + def it_can_find_a_related_part_by_rId(self, rels_prop_: Mock, rels_: Mock, other_part_: Mock): + rels_prop_.return_value = rels_ + rels_.related_parts = {"rId24": other_part_} + part = Part("partname", "content_type") - @pytest.fixture - def target_ref_fixture(self, request, part, rId_, rel_, url_): - part._rels = {rId_: rel_} - return part, rId_, url_ + assert part.related_parts["rId24"] is other_part_ - # fixture components --------------------------------------------- + def it_can_find_the_uri_of_an_external_relationship( + self, rels_prop_: Mock, rel_: Mock, other_part_: Mock + ): + rels_prop_.return_value = {"rId7": rel_} + rel_.target_ref = "https://hyper/link" + part = Part("partname", "content_type") - @pytest.fixture - def part(self): - return Part(None, None) + url = part.target_ref("rId7") - @pytest.fixture - def part_(self, request): - return instance_mock(request, Part) + assert url == "https://hyper/link" - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - - @pytest.fixture - def Relationships_(self, request, rels_): - return class_mock(request, "docx.opc.part.Relationships", return_value=rels_) + # fixtures --------------------------------------------- @pytest.fixture - def rel_(self, request, rId_, url_): - return instance_mock(request, _Relationship, rId=rId_, target_ref=url_) + def other_part_(self, request: FixtureRequest): + return instance_mock(request, Part) @pytest.fixture - def rels_(self, request, part_, rel_, rId_, related_parts_): - rels_ = instance_mock(request, Relationships) - rels_.part_with_reltype.return_value = part_ - rels_.get_or_add.return_value = rel_ - rels_.get_or_add_ext_rel.return_value = rId_ - rels_.related_parts = related_parts_ - return rels_ + def partname_(self, request: FixtureRequest): + return instance_mock(request, PackURI) @pytest.fixture - def related_parts_(self, request): - return instance_mock(request, dict) + def Relationships_(self, request: FixtureRequest): + return class_mock(request, "docx.opc.part.Relationships") @pytest.fixture - def reltype_(self, request): - return instance_mock(request, str) + def rel_(self, request: FixtureRequest): + return instance_mock(request, _Relationship) @pytest.fixture - def rId_(self, request): - return instance_mock(request, str) + def rels_(self, request: FixtureRequest): + return instance_mock(request, Relationships) @pytest.fixture - def url_(self, request): - return instance_mock(request, str) + def rels_prop_(self, request: FixtureRequest): + return property_mock(request, Part, "rels") class DescribePartFactory: @@ -278,9 +260,7 @@ def it_constructs_part_from_selector_if_defined(self, cls_selector_fixture): part = PartFactory(partname, content_type, reltype, blob, package) # verify ----------------------- cls_selector_fn_.assert_called_once_with(content_type, reltype) - CustomPartClass_.load.assert_called_once_with( - partname, content_type, blob, package - ) + CustomPartClass_.load.assert_called_once_with(partname, content_type, blob, package) assert part is part_of_custom_type_ def it_constructs_custom_part_type_for_registered_content_types( @@ -292,9 +272,7 @@ def it_constructs_custom_part_type_for_registered_content_types( PartFactory.part_type_for[content_type] = CustomPartClass_ part = PartFactory(partname, content_type, reltype, blob, package) # verify ----------------------- - CustomPartClass_.load.assert_called_once_with( - partname, content_type, blob, package - ) + CustomPartClass_.load.assert_called_once_with(partname, content_type, blob, package) assert part is part_of_custom_type_ def it_constructs_part_using_default_class_when_no_custom_registered( @@ -302,9 +280,7 @@ def it_constructs_part_using_default_class_when_no_custom_registered( ): partname, content_type, reltype, blob, package = part_args_2_ part = PartFactory(partname, content_type, reltype, blob, package) - DefaultPartClass_.load.assert_called_once_with( - partname, content_type, blob, package - ) + DefaultPartClass_.load.assert_called_once_with(partname, content_type, blob, package) assert part is part_of_default_type_ # fixtures --------------------------------------------- @@ -319,9 +295,7 @@ 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_ - ) + return function_mock(request, "docx.opc.part.cls_method_fn", return_value=cls_selector_fn_) @pytest.fixture def cls_selector_fixture( @@ -405,9 +379,7 @@ 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_ - ): + def part_args_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 @@ -426,9 +398,7 @@ def it_can_be_constructed_by_PartFactory( part = XmlPart.load(partname_, content_type_, blob_, package_) parse_xml_.assert_called_once_with(blob_) - __init_.assert_called_once_with( - ANY, partname_, content_type_, element_, package_ - ) + __init_.assert_called_once_with(ANY, partname_, content_type_, element_, package_) assert isinstance(part, XmlPart) def it_can_serialize_to_xml(self, blob_fixture): diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py index 747300f82..aff8b22d9 100644 --- a/tests/opc/test_pkgwriter.py +++ b/tests/opc/test_pkgwriter.py @@ -1,5 +1,9 @@ +# pyright: reportPrivateUsage=false + """Test suite for opc.pkgwriter module.""" +from __future__ import annotations + import pytest from docx.opc.constants import CONTENT_TYPE as CT @@ -7,9 +11,10 @@ from docx.opc.part import Part from docx.opc.phys_pkg import _ZipPkgWriter from docx.opc.pkgwriter import PackageWriter, _ContentTypesItem +from docx.opc.rel import Relationships from ..unitutil.mock import ( - MagicMock, + FixtureRequest, Mock, call, class_mock, @@ -54,41 +59,48 @@ def it_can_write_a_pkg_rels_item(self): # verify ----------------------- 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") - rels.__len__.return_value = 1 - part1 = Mock(name="part1", _rels=rels) - part2 = Mock(name="part2", _rels=[]) - # exercise --------------------- - PackageWriter._write_parts(phys_writer, [part1, part2]) - # verify ----------------------- + def it_can_write_a_list_of_parts( + self, phys_pkg_writer_: Mock, part_: Mock, part_2_: Mock, rels_: Mock + ): + rels_.__len__.return_value = 1 + part_.rels = rels_ + part_2_.rels = [] + + PackageWriter._write_parts(phys_pkg_writer_, [part_, part_2_]) + expected_calls = [ - call(part1.partname, part1.blob), - call(part1.partname.rels_uri, part1._rels.xml), - call(part2.partname, part2.blob), + call(part_.partname, part_.blob), + call(part_.partname.rels_uri, part_.rels.xml), + call(part_2_.partname, part_2_.blob), ] - assert phys_writer.write.mock_calls == expected_calls + assert phys_pkg_writer_.write.mock_calls == expected_calls # fixtures --------------------------------------------- @pytest.fixture - def blob_(self, request): + def blob_(self, request: FixtureRequest): return instance_mock(request, str) @pytest.fixture - def cti_(self, request, blob_): + def cti_(self, request: FixtureRequest, blob_): return instance_mock(request, _ContentTypesItem, blob=blob_) @pytest.fixture - def _ContentTypesItem_(self, request, cti_): + def _ContentTypesItem_(self, request: FixtureRequest, cti_): _ContentTypesItem_ = class_mock(request, "docx.opc.pkgwriter._ContentTypesItem") _ContentTypesItem_.from_parts.return_value = cti_ return _ContentTypesItem_ @pytest.fixture - def parts_(self, request): + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) + + @pytest.fixture + def part_2_(self, request: FixtureRequest): + return instance_mock(request, Part) + + @pytest.fixture + def parts_(self, request: FixtureRequest): return instance_mock(request, list) @pytest.fixture @@ -98,9 +110,13 @@ def PhysPkgWriter_(self): p.stop() @pytest.fixture - def phys_pkg_writer_(self, request): + def phys_pkg_writer_(self, request: FixtureRequest): return instance_mock(request, _ZipPkgWriter) + @pytest.fixture + def rels_(self, request: FixtureRequest): + return instance_mock(request, Relationships) + @pytest.fixture def write_cti_fixture(self, _ContentTypesItem_, parts_, phys_pkg_writer_, blob_): return _ContentTypesItem_, parts_, phys_pkg_writer_, blob_ @@ -123,7 +139,7 @@ def _write_methods(self): patch3.stop() @pytest.fixture - def xml_for_(self, request): + def xml_for_(self, request: FixtureRequest): return method_mock(request, _ContentTypesItem, "xml_for") @@ -135,11 +151,9 @@ def it_can_compose_content_types_element(self, xml_for_fixture): # fixtures --------------------------------------------- - def _mock_part(self, request, name, partname_str, content_type): + def _mock_part(self, request: FixtureRequest, name, partname_str, content_type): partname = PackURI(partname_str) - return instance_mock( - request, Part, name=name, partname=partname, content_type=content_type - ) + return instance_mock(request, Part, name=name, partname=partname, content_type=content_type) @pytest.fixture( params=[ @@ -152,7 +166,7 @@ def _mock_part(self, request, name, partname_str, content_type): ("Override", "/zebra/foo.bar", "app/vnd.foobar"), ] ) - def xml_for_fixture(self, request): + def xml_for_fixture(self, request: FixtureRequest): elm_type, partname_str, content_type = request.param part_ = self._mock_part(request, "part_", partname_str, content_type) cti = _ContentTypesItem.from_parts([part_]) @@ -168,9 +182,7 @@ def xml_for_fixture(self, request): types_bldr.with_child( a_Default().with_Extension("rels").with_ContentType(CT.OPC_RELATIONSHIPS) ) - types_bldr.with_child( - a_Default().with_Extension("xml").with_ContentType(CT.XML) - ) + types_bldr.with_child(a_Default().with_Extension("xml").with_ContentType(CT.XML)) if elm_type == "Override": override_bldr = an_Override() From 4e5dd915f054fa1374769eb3126a88045bd62aa6 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 16:07:59 -0700 Subject: [PATCH 081/131] feat(table): add _Row.grid_cols_before --- features/steps/table.py | 14 ++++++++++++++ features/steps/test_files/tbl-props.docx | Bin 20178 -> 20397 bytes features/tbl-row-props.feature | 11 +++++++++++ src/docx/oxml/__init__.py | 1 + src/docx/oxml/table.py | 17 +++++++++++++++++ src/docx/table.py | 17 +++++++++++++++++ tests/test_table.py | 15 +++++++++++++++ 7 files changed, 75 insertions(+) diff --git a/features/steps/table.py b/features/steps/table.py index 0b08f567c..0b7bab822 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -170,6 +170,13 @@ def given_a_table_row_having_height_rule_state(context: Context, state: str): context.row = table.rows[0] +@given("a table row starting with {count} empty grid columns") +def given_a_table_row_starting_with_count_empty_grid_columns(context: Context, count: str): + document = Document(test_docx("tbl-props")) + table = document.tables[7] + context.row = table.rows[int(count)] + + # when ===================================================== @@ -347,6 +354,13 @@ def then_can_iterate_over_row_collection(context: Context): assert actual_count == 2 +@then("row.grid_cols_before is {value}") +def then_row_grid_cols_before_is_value(context: Context, value: str): + expected = int(value) + actual = context.row.grid_cols_before + assert actual == expected, "expected %s, got %s" % (expected, actual) + + @then("row.height is {value}") def then_row_height_is_value(context: Context, value: str): expected_height = None if value == "None" else int(value) diff --git a/features/steps/test_files/tbl-props.docx b/features/steps/test_files/tbl-props.docx index 9d2db676e37d73bbf58a8e00b9dd080e7b41fc3f..740896ff21833af8769d77998975da21601247be 100644 GIT binary patch delta 1899 zcmZwIYar7L0|xN_<}%lplKU-pbA~t+GD38s4UIHnx!*U$^txum+#U`3ujE(X35(k8u!;HgzCh4Li01{pY0f6uU004M?=nHjL zvV;A0%(uht%a1)wGA!hU5O-YF4%-FEX|@>@XvX-S z?(D@9@5%`G3f7de##!#|q?eP5D>}5oZB|{+L5*{m?M-IaThsdduJ}>u$R!NMcJuv< z#tIo93X+^s_lkUTKyLsYUVL$qsug^)kFZXt;8^e9Ug^(^wN?Yo~wpgIxLd zDY0XP7(M9vHf)xO#@gcB&p=ITN9vuMef){qKq37I>1Oe zl{K4>00$Bn5+~DF5s!aGuxXdsFOJJgupS zgXDEHCLKf6jeUi)H3D`|??+Pi_ZOGx4}AJY!1aY;dc&TcuBw`rENJ2_-yPmb6a zs}$7I-eD}~oPq>dE9Y$+c8v_JDt7MmR0n`un#>__{i?K2j*6LYiZg9;3X8tg*do>l zD?=G4L(YrkdVJQ!E)wPs7V^&L8+Sy6?&`=6pFK zIU;z*tznSpy`W$&>kDY~i=Rc(PVZ3(G_|o;zjjNVu zbv#3drcMn`Z(P^R{>XOzkVB}F2Osnb*G?hxpN+>iT%xzqS>w0dm^%r4h`h)WpLp!C z*1KEMTjAIC8r*iDjhhk)T?$$1StT=MPg)f3!&U77KC?^f_D7Y?KiZT&SwOGrbDT=ey%?GOZXKQ}rW<{Buso?> zwn&KTEdG6DL4PW_x+yGD!_bz{INRtaEFoeeTJ-gt%ziqQ?RrN5LhvSYiE$=s`K~xKB4H4V#J!+T}0ZXHK`HQ7`&-~M$tN;Ewz0rc#&PfLOiyS)=ID334V=D*o`~1jK*vo%mHE|Qcql5n_9rM z6zgI$IFfahk~;Fr*+E7dq^zR6j$lQXkz(y*{u6Hx-{A5x&8n^6oQYY8S(}LU*ZSu9 z9FJ-d9vBb3;N$|Q);2~DMS=J*yqvk(3A6n6uNorz)gy^W3bRs29%Ge!4_|K}G;Oi+snlRXyZLO2WGSnsbxn z0dzMxNgz^y#fOqcRuhf|OgKog7TUE!bJe7=R!KxBfr^sT$aL#{GFk_eFNK;Yw}tO{ zN7v627s{5M$MK`Xu;X4*Dn0tET&13%Ql##0(dJqMo-*=}uJ?ajO@{_hdxNn?jNwb4 z@d-N31NK$W_@10l=VH`V!o#2-RhxwiV3PEUWL--BMFd-gJ2Banm2==t{}Rs+*9!NBM%iI3WNC0LUw^>L%6cC{c`Ii z7UC=q*U!3?Y`#@4Cbneb1yiE)3C$1J@ZQN|4{s zNA~o7~r8Yb6jk1NW#DDZR1Jw<|cILvJZ@qOu zD&U4jpS)c}2>m2T4ETS=lz!|0@B=BJJ|h_l z{F7-S|26G@Jo9h^fe#2#Vog<~{&Vr8NFdlD3I<^ZCYV#I5O6T|FolCS3SPCPoU&D; h96=g^MUPW3PKuNiq!DP3(ty;F2onMTvr|70{s#XahQ$B? delta 1671 zcmV;226*|cp8?XH0kDB7f47NbNh4BLufqiZ0O>UV01*HH0C#V4WG`fIV|8t1Zgehq zZEWpZTXUN@6n^io@c1Q9ttBB4c$#()hHhutnM~8&ecVwnw%Gt;K#r6B^(%3)ah%%t zlGu%t7jty^^qq5bjt-Z6{Cporox32*L!Mk-zz^V}6C^$#hROKyf8x)d-h1<+lM9v% zS;Ujz^5P-LFFt>I_i@%Aa(@~JiReg#B=65s|FSy~BJKBjxjzYFmVbytKjS$ci4Q)H zdwets{h&AF*|3KI1jSXFaX-j&xtzn2JC=7VxR|#?iWtAFf0Qm-VVR9PZ8mJJfR)CD25 zg3z3yFfut*9uvbKa&cf&4OogI#Ql_I?5iY!T#pdq=w+)2?uBY4)nB)o{xfULUpAS) z5UYl!lje<^?O*BeXu6!7E)fPgb_w;|^@Nx8Et>|xe~3*Z(dcP0_`nDYe>IPMk60eYxJ^7;I`js{*71xr)Qsog zp7{rl=IxJt!P6Z=SHan83tj5!ZEole zs&6xUw~Sm3nbgu8ZRf~+dF$#}mZ#s*s{(hdN8CWGQ2iVcaBu_ z7!+y)haUVU?u3e-;$iMv)w6figKXObe-v#9-ZH;d^EXvJ>zs5V1rEW0cour`5I&l| z!1a*Z>dn=w&=;Wrz?%9V>+}m8(;jGm*yk01vZ{j)aL;E$Y7lt_-0;->73pgc0u6h^ zu0Cds1PHQh-Dvan{CBO9R^MN~6-m3jGjkBt32611%+@*j2``;Uk~TC-si!|zf6`hZ ze-0!k5%FW62YxWt6O>@*f}6RsBC&pWgisP)h>@6aWAK2mrT<9I RfKMX|P6Ge{Zc6|F003({4j2Fc diff --git a/features/tbl-row-props.feature b/features/tbl-row-props.feature index 377f2853e..a1c23436b 100644 --- a/features/tbl-row-props.feature +++ b/features/tbl-row-props.feature @@ -4,6 +4,17 @@ Feature: Get and set table row properties I need a way to get and set the properties of a table row + Scenario Outline: Get Row.grid_cols_before + Given a table row starting with empty grid columns + Then row.grid_cols_before is + + Examples: Row.grid_cols_before value cases + | count | + | 0 | + | 1 | + | 3 | + + Scenario Outline: Get Row.height_rule Given a table row having height rule Then row.height_rule is diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index a37ee9b8e..c694eb298 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -159,6 +159,7 @@ ) register_element_cls("w:bidiVisual", CT_OnOff) +register_element_cls("w:gridBefore", CT_DecimalNumber) register_element_cls("w:gridCol", CT_TblGridCol) register_element_cls("w:gridSpan", CT_DecimalNumber) register_element_cls("w:tbl", CT_Tbl) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index 687c6e2e6..474374aa3 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -60,6 +60,14 @@ class CT_Row(BaseOxmlElement): trPr: CT_TrPr | None = ZeroOrOne("w:trPr") # pyright: ignore[reportAssignmentType] tc = ZeroOrMore("w:tc") + @property + def grid_before(self) -> int: + """The number of unpopulated layout-grid cells at the start of this row.""" + trPr = self.trPr + if trPr is None: + return 0 + return trPr.grid_before + def tc_at_grid_col(self, idx: int) -> CT_Tc: """`` element appearing at grid column `idx`. @@ -885,11 +893,20 @@ class CT_TrPr(BaseOxmlElement): "w:del", "w:trPrChange", ) + gridBefore: CT_DecimalNumber | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:gridBefore", successors=_tag_seq[3:] + ) trHeight: CT_Height | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:trHeight", successors=_tag_seq[8:] ) del _tag_seq + @property + def grid_before(self) -> int: + """The number of unpopulated layout-grid cells at the start of this row.""" + gridBefore = self.gridBefore + return 0 if gridBefore is None else gridBefore.val + @property def trHeight_hRule(self) -> WD_ROW_HEIGHT_RULE | None: """Return the value of `w:trHeight@w:hRule`, or |None| if not present.""" diff --git a/src/docx/table.py b/src/docx/table.py index 709bc8dbb..a80a6e4b9 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -385,6 +385,23 @@ 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 grid_cols_before(self) -> int: + """Count of unpopulated grid-columns before the first cell in this row. + + Word allows a row to "start late", meaning that one or more cells are not present at the + beginning of that row. + + Note these are not simply "empty" cells. The renderer reads this value and skips forward to + the table layout-grid position of the first cell in this row; the renderer "skips" this many + columns before drawing the first cell. + + Note this also implies that not all rows are guaranteed to have the same number of cells, + e.g. `_Row.cells` could have length `n` for one row and `n - m` for the next row in the same + table. + """ + return self._tr.grid_before + @property def height(self) -> Length | None: """Return a |Length| object representing the height of this cell, or |None| if diff --git a/tests/test_table.py b/tests/test_table.py index 65f7cb423..7f164181f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -645,6 +645,21 @@ def table_(self, request: FixtureRequest): class Describe_Row: """Unit-test suite for `docx.table._Row` objects.""" + @pytest.mark.parametrize( + ("tr_cxml", "expected_value"), + [ + ("w:tr", 0), + ("w:tr/w:trPr", 0), + ("w:tr/w:trPr/w:gridBefore{w:val=0}", 0), + ("w:tr/w:trPr/w:gridBefore{w:val=3}", 3), + ], + ) + def it_knows_its_grid_cols_before( + self, tr_cxml: str, expected_value: int | None, parent_: Mock + ): + row = _Row(cast(CT_Row, element(tr_cxml)), parent_) + assert row.grid_cols_before == expected_value + @pytest.mark.parametrize( ("tr_cxml", "expected_value"), [ From 1cfcee71f81c09570160f7c0d89614db506472bf Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 16:20:04 -0700 Subject: [PATCH 082/131] feat(table): add _Row.grid_cols_after --- features/steps/table.py | 14 ++++++++++++++ features/steps/test_files/tbl-props.docx | Bin 20397 -> 20419 bytes features/tbl-row-props.feature | 11 +++++++++++ src/docx/oxml/__init__.py | 1 + src/docx/oxml/table.py | 17 +++++++++++++++++ src/docx/table.py | 17 +++++++++++++++++ tests/test_table.py | 13 +++++++++++++ 7 files changed, 73 insertions(+) diff --git a/features/steps/table.py b/features/steps/table.py index 0b7bab822..7cdb50eab 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -154,6 +154,13 @@ def given_a_table_having_two_rows(context: Context): context.table_ = document.tables[0] +@given("a table row ending with {count} empty grid columns") +def given_a_table_row_ending_with_count_empty_grid_columns(context: Context, count: str): + document = Document(test_docx("tbl-props")) + table = document.tables[8] + context.row = table.rows[int(count)] + + @given("a table row having height of {state}") def given_a_table_row_having_height_of_state(context: Context, state: str): table_idx = {"no explicit setting": 0, "2 inches": 2, "3 inches": 3}[state] @@ -354,6 +361,13 @@ def then_can_iterate_over_row_collection(context: Context): assert actual_count == 2 +@then("row.grid_cols_after is {value}") +def then_row_grid_cols_after_is_value(context: Context, value: str): + expected = int(value) + actual = context.row.grid_cols_after + assert actual == expected, "expected %s, got %s" % (expected, actual) + + @then("row.grid_cols_before is {value}") def then_row_grid_cols_before_is_value(context: Context, value: str): expected = int(value) diff --git a/features/steps/test_files/tbl-props.docx b/features/steps/test_files/tbl-props.docx index 740896ff21833af8769d77998975da21601247be..e5fdd728f713d821b47cdee620b6eb1ea5460ade 100644 GIT binary patch delta 1809 zcmV+s2k!W-p8><40kDB74v~SISh{P3Zh!^=08T!Wk0~4mVumedVuq8kDHDH7bE7yA zzR#~_SCT1&VP>jks#25L-QyOR5opw9(M zcBWOctq@21|A|C57{MPY$_y+j!_ zSP>y&#?b`A;+z$dy7NtUGfmyN!~JagIi=%dE-lzk+SmAgCW|M6ZDf^eR97wA56s=j zOLzc9e&_2jrm5EpIU;{*j+b@dVYQ{YP<`6RZ~&*A;b}R&>>J#~wo>O*<1W zyl(vzGC!w5ztvtI_ctHIodmS_fA*O0nY}TitK+^=dS8hHs+Er)1Zl>8P|hw3GW1Gn z-f7ct9L2T%sv#c|(=V7O;~Di=LCN#kui8uM6<~xa3jl|_fs21Gbb%1ANT_oN87Uvi zw~77_J~^Ry&E8dS5vR=(q?WTPs=_d0?Uym4B_0{byR^Uo@FNC(DNVBF!2% z+rQLdiJdbi3xI*9ItaVYdcceN7EN6;@Wvsl_0(6tUDqY2N*twSXHK~YN7u1s8n)a3 zj;sNs;-}!0J?(#EI=)T9@VmEc`;ZJ+AwZV3xrqLQx3qLzsbDy2Ft#-3%5=Y6_{pjY z_K!cHI5N`R4gRwx{?e6rVd)oAu0>%-A!YPq96q)7?PU?$$8>Ip=UpQoJh8%H7*~jT zlrb9Z5d5-hD6*|~PbByeHf<9_d_Xuy+V#9gI%cy$gW!LGJR$8pMQpo68^!@r1XOfn zAl-h!6p;h1WeN6;a1$LmQB<=uPZU1C(;?{&i9(fK6D#Oc5{0JO&@~*;0FGt=8>>&j zS?sy3H61s1{5tWtxMlC#-z1)AYgXU#U1Ykdsy2ydQU5L8IhC7t)W2ZFjv(fzmHff|U>_S}uXhXVxOZv(VcC;opbO+V9p1m7J zt^y5gDz>t7MPCK$2C|(d=MOIyJ?m>9=wg6O{on|X zUiX0F!#Oc*}Lk2mSq48Hw15yUrYW?RnNLbI*|egsQ|eqeDx4Mn!dnsq0{Ke z)ti6N7s4)374bgS=@&SL)l~qp&MN?URVUlPIUk2qWa#ShhEw-9q_2sPtXmtr`j|Bm zK+v=_y~)$_U$sVBeSY~~B<=Rh%!XJ)pwVkGTj%H}R_R2Nw60)`UG1fk)(ZF@_e=3= z^&Rt4ab_0G+{uf$690Ur*z@mK6-iDl`ze2_g&f&EpA!INsuna}eRtdCA({&?HaES% z`Q}0yX;?F`^XmKY(azw2tr?mN&qZHV#X!f?wB#|g#kCLbwq=aTc{&|e_dr?PVGN6Ac z`9r$Ae7>ApU%+T!ZGCdU?uFu}o)VXRQ8(K zAA;X}_#d+@Fqa(;k%607x@&}PfCc~nPCk>-O)3InhLinGI{`wIIZi(Tl#_W*J^_T2 zxK2C*2|be!Jsgu&PY{z7L=X!A000000001h0UVP(PeK9}OOt_5CI&}L00000rp{>F delta 1787 zcmV6(0kDB74k>S&SevqHRB8qQ0G&FMk0~4mu4XM}u4a?5DHDHLbE7yA zexF~#`X##$8B5|2xROjE3^P+TQ|b9z`Ps#% zn-5o$PLFz{$d8%CCB#{0l6ZgDts!Gcr`^uHp&z-~$0+bpn$bS{=+UT6`~AT4+Y_4h z+6X|9UnME^{4C?sS#CUbvsNiCqRpg5u2)?m0CY|V@l?h#BAWOy@9fhwav3Mn0p~~` zM#&|YnYb*t55j;w30c}y%=o%BO5;u`<8q2a@N_tU&NvFI!F1i&0%(8vQ*oqCKo%_B z(&Rb3cIt;5FpaZekj(71tu)>_tc0vpxTfqRfy!=+-c8*J|CtGF0>4)ee+GWGfmyNgWYWVIi;gyCN0=a+SmADDvJk#m&hvDsIFSJ8<@MH zn{Wq;yw2A_OjGwhL4ls;7AfMVw3`$3wqAC$90L56Ng z%{na_j-t5MUp3@IV)}*fU_7P%DkwOc{i?m7UI9j^G6!(L8@PYyLKlS4l7u>kkdgAC zJWcd|@X3KmWMBx0Q0XLY>VD-QknLcMEwyN6{v#8uxcszA_Md5uf8J!?j4T`Ki!^QA zZ2wY+1$NGy%mD_fVq@ai>jBT}TQqh3zB>w8t*5^F?Yb^GRpKZuJKdD?a5RmWhHk+I zaG(mXLRP^kd)j};-1wFshTq*q+lPLi6#^hsXCiu!?!wY>rGmk*!PwHAOVj;w?gteG z?CyU+wjtJ>4f|(}|D{X*!qP8(xfX>Tg_PlsQMhXD+lwMz?$fy;p5Gez;ISD7gSbL; zPZ^`p7S0bHU51v@JrU=}#IOv4$R6PwDeJjUbi}5E`oVvrze3u3hHLIt52OEx}C{6s?P@&V+UhL0`jOzaEKf2!Dz4_&7 zpV4FsSC)U}F2Y@X!yU@YeWx_-v)+9Gn>5lFb&)0mydm7bC4HeyY_-V^-Cp&rNAHH7 zDVTjP&RfLSl7Caxvu=@2q`<)nz>a}l-Gz^)FR&eCH+pjQ zCiH(rs0$QXypMJI1&(fZWq{4|azI|yK?~UDvms@O91U(bb$>(p8W=;(+_0;USt9|A z3{%ybJU#zaYoz7pm+wW=cF)W#L{tnKy(aVG7`XN`YH+m8bSKeJ+O-5cHEmr@TwMCTaU?(O^{;lMFT!z zG0AV2ml6~8lr#z0P)g|}lZNg%hzG@WCNhnn@@_v$*pnpc2PvUhkpEFe`n{+qh~Iyw zssD#`dHH-{x4x87!P-h?kKJ=+PCYm-?ozP`;wu$j?8eO*Z^U1dwW`nE1c`CqT`>Wp z$t?ZkXGNUUK~5HL$+tVRw-EGOq@^=O#!&Ma{~zZhEY z?NLtzEHP6M`?mq}ICNQ?%N~Vg1TCoI&syTD`d{hIhyMXkO9KQH000080JoDdP7||W zFnJvgDQ}xto3d(DY6buRojQ}}O)3JeW|I?6I{_?{PfkApev^+*J^^l%&Q3f6^E{Ib dPeuU-lT%MZ0{=>rF-sPctxpgJF-!me007H)alilo diff --git a/features/tbl-row-props.feature b/features/tbl-row-props.feature index a1c23436b..1b006f204 100644 --- a/features/tbl-row-props.feature +++ b/features/tbl-row-props.feature @@ -4,6 +4,17 @@ Feature: Get and set table row properties I need a way to get and set the properties of a table row + Scenario Outline: Get Row.grid_cols_after + Given a table row ending with empty grid columns + Then row.grid_cols_after is + + Examples: Row.grid_cols_after value cases + | count | + | 0 | + | 1 | + | 2 | + + Scenario Outline: Get Row.grid_cols_before Given a table row starting with empty grid columns Then row.grid_cols_before is diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index c694eb298..bf32932f9 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -159,6 +159,7 @@ ) register_element_cls("w:bidiVisual", CT_OnOff) +register_element_cls("w:gridAfter", CT_DecimalNumber) register_element_cls("w:gridBefore", CT_DecimalNumber) register_element_cls("w:gridCol", CT_TblGridCol) register_element_cls("w:gridSpan", CT_DecimalNumber) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index 474374aa3..ddebee71b 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -60,6 +60,14 @@ class CT_Row(BaseOxmlElement): trPr: CT_TrPr | None = ZeroOrOne("w:trPr") # pyright: ignore[reportAssignmentType] tc = ZeroOrMore("w:tc") + @property + def grid_after(self) -> int: + """The number of unpopulated layout-grid cells at the end of this row.""" + trPr = self.trPr + if trPr is None: + return 0 + return trPr.grid_after + @property def grid_before(self) -> int: """The number of unpopulated layout-grid cells at the start of this row.""" @@ -893,6 +901,9 @@ class CT_TrPr(BaseOxmlElement): "w:del", "w:trPrChange", ) + gridAfter: CT_DecimalNumber | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:gridAfter", successors=_tag_seq[4:] + ) gridBefore: CT_DecimalNumber | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:gridBefore", successors=_tag_seq[3:] ) @@ -901,6 +912,12 @@ class CT_TrPr(BaseOxmlElement): ) del _tag_seq + @property + def grid_after(self) -> int: + """The number of unpopulated layout-grid cells at the end of this row.""" + gridAfter = self.gridAfter + return 0 if gridAfter is None else gridAfter.val + @property def grid_before(self) -> int: """The number of unpopulated layout-grid cells at the start of this row.""" diff --git a/src/docx/table.py b/src/docx/table.py index a80a6e4b9..9faf8e672 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -385,6 +385,23 @@ 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 grid_cols_after(self) -> int: + """Count of unpopulated grid-columns after the last cell in this row. + + Word allows a row to "end early", meaning that one or more cells are not present at the + end of that row. + + Note these are not simply "empty" cells. The renderer reads this value and "skips" this + many columns after drawing the last cell. + + Note this also implies that not all rows are guaranteed to have the same number of cells, + e.g. `_Row.cells` could have length `n` for one row and `n - m` for the next row in the same + table. Visually this appears as a column (at the beginning or end, not in the middle) with + one or more cells missing. + """ + return self._tr.grid_after + @property def grid_cols_before(self) -> int: """Count of unpopulated grid-columns before the first cell in this row. diff --git a/tests/test_table.py b/tests/test_table.py index 7f164181f..4eb1c8efb 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -645,6 +645,19 @@ def table_(self, request: FixtureRequest): class Describe_Row: """Unit-test suite for `docx.table._Row` objects.""" + @pytest.mark.parametrize( + ("tr_cxml", "expected_value"), + [ + ("w:tr", 0), + ("w:tr/w:trPr", 0), + ("w:tr/w:trPr/w:gridAfter{w:val=0}", 0), + ("w:tr/w:trPr/w:gridAfter{w:val=4}", 4), + ], + ) + def it_knows_its_grid_cols_after(self, tr_cxml: str, expected_value: int | None, parent_: Mock): + row = _Row(cast(CT_Row, element(tr_cxml)), parent_) + assert row.grid_cols_after == expected_value + @pytest.mark.parametrize( ("tr_cxml", "expected_value"), [ From 5a1d6143f1f30a50d09babf2934a6f0857f62c93 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 20:06:31 -0700 Subject: [PATCH 083/131] docs: update Table docs --- docs/index.rst | 1 + docs/user/tables.rst | 202 +++++++++++++++++++++++++++++++++++++++++++ src/docx/table.py | 13 ++- 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 docs/user/tables.rst diff --git a/docs/index.rst b/docs/index.rst index cdb8b5455..1b1029787 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -74,6 +74,7 @@ User Guide user/install user/quickstart user/documents + user/tables user/text user/sections user/hdrftr diff --git a/docs/user/tables.rst b/docs/user/tables.rst new file mode 100644 index 000000000..40ef20933 --- /dev/null +++ b/docs/user/tables.rst @@ -0,0 +1,202 @@ +.. _tables: + +Working with Tables +=================== + +Word provides sophisticated capabilities to create tables. As usual, this power comes with +additional conceptual complexity. + +This complexity becomes most apparent when *reading* tables, in particular from documents drawn from +the wild where there is limited or no prior knowledge as to what the tables might contain or how +they might be structured. + +These are some of the important concepts you'll need to understand. + + +Concept: Simple (uniform) tables +-------------------------------- + +:: + + +---+---+---+ + | a | b | c | + +---+---+---+ + | d | e | f | + +---+---+---+ + | g | h | i | + +---+---+---+ + +The basic concept of a table is intuitive enough. You have *rows* and *columns*, and at each (row, +column) position is a different *cell*. It can be described as a *grid* or a *matrix*. Let's call +this concept a *uniform table*. A relational database table and a Pandas dataframe are both examples +of a uniform table. + +The following invariants apply to uniform tables: + +* Each row has the same number of cells, one for each column. +* Each column has the same number of cells, one for each row. + + +Complication 1: Merged Cells +---------------------------- + +:: + + +---+---+---+ +---+---+---+ + | a | b | | | b | c | + +---+---+---+ + a +---+---+ + | c | d | e | | | d | e | + +---+---+---+ +---+---+---+ + | f | g | h | | f | g | h | + +---+---+---+ +---+---+---+ + +While very suitable for data processing, a uniform table lacks expressive power desireable for +tables intended for a human reader. + +Perhaps the most important characteristic a uniform table lacks is *merged cells*. It is very common +to want to group multiple cells into one, for example to form a column-group heading or provide the +same value for a sequence of cells rather than repeat it for each cell. These make a rendered table +more *readable* by reducing the cognitive load on the human reader and make certain relationships +explicit that might easily be missed otherwise. + +Unfortunately, accommodating merged cells breaks both the invariants of a uniform table: + +* Each row can have a different number of cells. +* Each column can have a different number of cells. + +This challenges reading table contents programatically. One might naturally want to read the table +into a uniform matrix data structure like a 3 x 3 "2D array" (list of lists perhaps), but this is +not directly possible when the table is not known to be uniform. + + +Concept: The layout grid +------------------------ + +:: + + + - + - + - + + | | | | + + - + - + - + + | | | | + + - + - + - + + | | | | + + - + - + - + + +In Word, each table has a *layout grid*. + +- The layout grid is *uniform*. There is a layout position for every (layout-row, layout-column) + pair. +- The layout grid itself is not visible. However it is represented and referenced by certain + elements and attributes within the table XML +- Each table cell is located at a layout-grid position; i.e. the top-left corner of each cell is the + top-left corner of a layout-grid cell. +- Each table cell occupies one or more whole layout-grid cells. A merged cell will occupy multiple + layout-grid cells. No table cell can occupy a partial layout-grid cell. +- Another way of saying this is that every vertical boundary (left and right) of a cell aligns with + a layout-grid vertical boundary, likewise for horizontal boundaries. But not all layout-grid + boundaries need be occupied by a cell boundary of the table. + + +Complication 2: Omitted Cells +----------------------------- + +:: + + +---+---+ +---+---+---+ + | a | b | | a | b | c | + +---+---+---+ +---+---+---+ + | c | d | | d | + +---+---+ +---+---+---+ + | e | | e | f | g | + +---+ +---+---+---+ + +Word is unusual in that it allows cells to be omitted from the beginning or end (but not the middle) +of a row. A typical practical example is a table with both a row of column headings and a column of +row headings, but no top-left cell (position 0, 0), such as this XOR truth table. + +:: + + +---+---+ + | T | F | + +---+---+---+ + | T | F | T | + +---+---+---+ + | F | T | F | + +---+---+---+ + +In `python-docx`, omitted cells in a |_Row| object are represented by the ``.grid_cols_before`` and +``.grid_cols_after`` properties. In the example above, for the first row, ``.grid_cols_before`` +would equal ``1`` and ``.grid_cols_after`` would equal ``0``. + +Note that omitted cells are not just "empty" cells. They represent layout-grid positions that are +unoccupied by a cell and they cannot be represented by a |_Cell| object. This distinction becomes +important when trying to produce a uniform representation (e.g. a 2D array) for an arbitrary Word +table. + + +Concept: `python-docx` approximates uniform tables by default +------------------------------------------------------------- + +To accurately represent an arbitrary table would require a complex graph data structure. Navigating +this data structure would be at least as complex as navigating the `python-docx` object graph for a +table. When extracting content from a collection of arbitrary Word files, such as for indexing the +document, it is common to choose a simpler data structure and *approximate* the table in that +structure. + +Reflecting on how a relational table or dataframe represents tabular information, a straightforward +approximation would simply repeat merged-cell values for each layout-grid cell occupied by the +merged cell:: + + + +---+---+---+ +---+---+---+ + | a | b | -> | a | a | b | + +---+---+---+ +---+---+---+ + | | d | e | -> | c | d | e | + + c +---+---+ +---+---+---+ + | | f | g | -> | c | f | g | + +---+---+---+ +---+---+---+ + +This is what ``_Row.cells`` does by default. Conceptually:: + + >>> [tuple(c.text for c in r.cells) for r in table.rows] + [ + (a, a, b), + (c, d, e), + (c, f, g), + ] + +Note this only produces a uniform "matrix" of cells when there are no omitted cells. Dealing with +omitted cells requires a more sophisticated approach when maintaining column integrity is required:: + + # +---+---+ + # | a | b | + # +---+---+---+ + # | c | d | + # +---+---+ + # | e | + # +---+ + + def iter_row_cell_texts(row: _Row) -> Iterator[str]: + for _ in range(row.grid_cols_before): + yield "" + for c in row.cells: + yield c.text + for _ in range(row.grid_cols_after): + yield "" + + >>> [tuple(iter_row_cell_texts(r)) for r in table.rows] + [ + ("", "a", "b"), + ("c", "d", ""), + ("", "e", ""), + ] + + +Complication 3: Tables are Recursive +------------------------------------ + +Further complicating table processing is their recursive nature. In Word, as in HTML, a table cell +can itself include one or more tables. + +These can be detected using ``_Cell.tables`` or ``_Cell.iter_inner_content()``. The latter preserves +the document order of the table with respect to paragraphs also in the cell. diff --git a/src/docx/table.py b/src/docx/table.py index 9faf8e672..a272560bc 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -382,7 +382,18 @@ def __init__(self, tr: CT_Row, parent: TableParent): @property def cells(self) -> tuple[_Cell, ...]: - """Sequence of |_Cell| instances corresponding to cells in this row.""" + """Sequence of |_Cell| instances corresponding to cells in this row. + + Note that Word allows table rows to start later than the first column and end before the + last column. + + - Only cells actually present are included in the return value. + - This implies the length of this cell sequence may differ between rows of the same table. + - If you are reading the cells from each row to form a rectangular "matrix" data structure + of the table cell values, you will need to account for empty leading and/or trailing + layout-grid positions using `.grid_cols_before` and `.grid_cols_after`. + + """ return tuple(self.table.row_cells(self._index)) @property From 6d49a690bb22fcec30614ef30156242ba0ececd5 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 28 Apr 2024 17:39:35 -0700 Subject: [PATCH 084/131] rfctr(table): reimplement CT_Tc._tr_above Use XPath rather than oxml structures. --- src/docx/oxml/table.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index ddebee71b..963f3ebf7 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -748,11 +748,10 @@ def _tr_above(self) -> CT_Row: 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] + tr_aboves = self.xpath("./ancestor::w:tr[position()=1]/preceding-sibling::w:tr[1]") + if not tr_aboves: + raise ValueError("no tr above topmost tr in w:tbl") + return tr_aboves[0] @property def _tr_below(self) -> CT_Row | None: From 382d43e41964d935e8a30d885592205d3ecf22d2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 22:46:14 -0700 Subject: [PATCH 085/131] feat(table): add _Cell.grid_span --- features/steps/table.py | 16 +++++++++++++++- features/steps/test_files/tbl-cell-props.docx | Bin 0 -> 13773 bytes features/tbl-cell-props.feature | 11 +++++++++++ src/docx/table.py | 9 +++++++++ tests/test_table.py | 13 +++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 features/steps/test_files/tbl-cell-props.docx diff --git a/features/steps/table.py b/features/steps/table.py index 7cdb50eab..38d49ee0a 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -13,7 +13,7 @@ WD_TABLE_DIRECTION, ) from docx.shared import Inches -from docx.table import Table, _Column, _Columns, _Row, _Rows +from docx.table import Table, _Cell, _Column, _Columns, _Row, _Rows from helpers import test_docx @@ -37,6 +37,13 @@ def given_a_3x3_table_having_span_state(context: Context, span_state: str): context.table_ = document.tables[table_idx] +@given("a _Cell object spanning {count} layout-grid cells") +def given_a_Cell_object_spanning_count_layout_grid_cells(context: Context, count: str): + document = Document(test_docx("tbl-cell-props")) + table = document.tables[0] + context.cell = _Cell(table._tbl.tr_lst[int(count)].tc_lst[0], table) + + @given("a _Cell object with {state} vertical alignment as cell") def given_a_Cell_object_with_vertical_alignment_as_cell(context: Context, state: str): table_idx = { @@ -292,6 +299,13 @@ def when_I_set_the_table_autofit_to_setting(context: Context, setting: str): # then ===================================================== +@then("cell.grid_span is {count}") +def then_cell_grid_span_is_count(context: Context, count: str): + expected = int(count) + actual = context.cell.grid_span + assert actual == expected, f"expected {expected}, got {actual}" + + @then("cell.tables[0] is a 2 x 2 table") def then_cell_tables_0_is_a_2x2_table(context: Context): cell = context.cell diff --git a/features/steps/test_files/tbl-cell-props.docx b/features/steps/test_files/tbl-cell-props.docx new file mode 100644 index 0000000000000000000000000000000000000000..627fb66fc388257cd6a4659ddc433d3f88c6b9d9 GIT binary patch literal 13773 zcmbum19Y8R+cvsl+iq;zZfv8m?Z!>gu(55sv27cTZJW(W_kO>3@Ag0c8Q&Nu$yym} zu5sPZ{mgsL=ejlJr9ePY0RR9bpekufgc5ZC+7$=@*arguP~Jzigl%n{jBT8BmEG)& z9Y4{zT3gk{Ny&8!pa$H(e3M}Rl;qMGz!=yqWM52TpdC4*g?Y-ub#}z%}ity%9U1_?|S|6XKl@|dJJ8|#7A|Q$9#f> z1*Fwoeq^jtBJfBa+U2U|8YF4d@u=d4Ix>%h#X(HCQ{&R@JxOJcii-2x`Z?N-oY+^p zdaWoqPIFcGWk!`ZCT+vMZ~+4t+2%nLa8IiT7Jh;#_mSJtu@*>+46&{2z^AA|Cy3%=Bb0|P~Ws8n!2UGX=7&$Lwo z`N3q{ttfvZ?wMfc78CA_0-py1Txz;+6}Pk}LPd+v=Ik30^CtGV-l9;9QgYgv%k?;C zD>U?ak|3X+hkM0)`AH}9Q0Evz{x?IxpOrelz8mNS7yy8I|F7#{Y~@H#_v^hpW=s-< z5z*(|>syqR?5b61@SuqYQZ{?^6TpzE1!wgoO1jR=BR9BNzFaTz`u>uUdCX$}<6Ri# z@P5yMij*n!LJC-27V5Eb_0tS

4i)GBIwBc$IDta(=_Wlq_?0%cqmvzaXjR4 zL#mk&nmyp4Sz<~XzjoTgmo)`6rbK?O^tV+?r}Q~jyqo##LSc(W;v4X>b5+D`q~box z`~o~(K6*kdbG~UY|AfLeTsW=!jjjlY`fH6+eBwJ>+3>U%Gj0#C-)G4#4GlNweL2H| z004;Zvt(pzDDPlv=SZ(_XZLF@rz^@>t}r0BomV0@Jxr^wDGEX91XO3&PR4a|4Y@`2 z*B7-%krDp9kGC3Ft!;Q>xn4Us9IIJMRkK`#>CbE9R(@_S#^gGTcj?J?KWNwSu23hk zgAV)bT0|y27rt0`KLrt%$5>UiF}-O(0=J5=u}ypJTO z$7QW>!d$}|&>RG$bvHh(HVb8tVDju)i6D^a zkGuh{f!Dz8VmXdTc)4XlCJPWwT4J5oOi33!zT^i@J}x7#mE7Wc5)8%`T%rnW=}x6> z)^tc`4Qtu_WXqadzj_NR(2hl0@j0?S(KQHvv3FZCcq_KKVuyGMJayn%E=6fs|J`U! z)DJeg??#({U)o6jHkzTWgYjQhi($0wW@C_GLsh)uK?2qEXHeyU_srV-aaW4c^`T*tS*)O{Ee#Pe+Mv~uK zm7|_m)xRXbAy*jcV7oHsgMURX+YgHiZ;}n-7QsU?c!Y;G5f-B$w~SIf#*eQmDgkqiqN^`RkJ{}OONHQytP1v+CLj}1ppgm&@Oh;to6is&cdYE4-&uY@ zZ$hr6QL2#d9JO|7k9!oAPD3u=Lw#Qn54SPS`|4m~e>h(s{UPI6YsBMm+Gv9!wa+vc z{b=Nx*IH*^U%-{l_aRNK&@*hG{GiPf98+~&tItx2v(^FM9btN@){vi)S z3QSN9RO()^?E1kRKNw`7A81jcW#!WVAR@vp5*iChR5Yr2BCg@u*B1f%I4(^w)H*s5 zAW^MSMQF%+p-C#`IDllXe`^!HzS3dsy3@}?_NK=*?vAwQmi-LJv!N!!GsH0lw$6cd z2ntl_hin8y+MAl6Iywr3*Gud@K5aro=t^_x9ruY(990M*MMtSTpo#D50JtTo9f8&lvM zXnH~hg`8OW3yi)Jp#!4681jJSr2(>-p9j|ejZ2tAqv zEIqS{J^PSNjM7XPghGP2i33=BXXnyyUD3T#Sc?7p`-^`qfz$ zLm-3sX1}IcG7{0pF(>(5w1GyvA(!*N-wV1&?4!^k6=7fHIv@H6Nw{nL?=^f~)Z&&9 zC>8n;{vWJDE(U}I&A!i@o&bXOaZ2hUKgpm6-8}o$UAYNL)w0=xi{*tVXpo8bnStqmhr14-(UQUK= z>2ZL@;rQTVb`t@_cLRhX)P=zlEe%eX5&}YZ5f%8=gj2jHKg?Go4<{KTXVdHwkyxju zIbJ*51uHpsHM=~MxRcgS%oE)meM9OZ;6d9HpTQoz9rE)42JtD6XV=D3l>T$9M;+Xf z1`OpO)sPl^1~;wJN#NZG>PLG+n_Vg$4O-V8({UgE9%cYhoG(zp>%~JU2}VD{kD|T_ zZ+Sy?Qs0gB5&Bc0@_5zIHMm-WAk5m9T7s!UAf6~&o^UaVgH4MUM7V1`SJ=cUVsJM1 z5hOW=u>0W%`DoWQS()6mm!Ab+uj3TBO-N2u7WkXT@3RVMNceG za9u!>7D-Q-$a*Ra2CA-m#*Z3~4KGeuFX2U`#>3();V&>V!a~A|!}?$?1K|vZqId(r zdO|h_zKb%WKulOac=s4?c6%J~M$JXvPhSyD(qK%pAO*8v?e#P-9CdtQwF%(f{8_1T zs$HD`f$6^H9(v2;Q_L=Gn~@Q#0|X>`)lBg*)t(0z&5#BLr`7?qq}8Ih}8`0O8e1 znIJ%{mOGHnWRLhPeM3(XEa)&YnCUmQEMh~TGTf6JwPR} z%~+qVWiWa~gdVTBUALioJU=`=Lih)W(L{<2;Gm3zgvxDHAcZI?&P*?queTaos@@GO zJGh+wT+V2F{PK2k4I&EhW`AIJnJ6YHn?c3vwRf_xKTJ(ksa@Xe@|t>X>0MM_+A%Xp zVcYb!u^$wvx$J#-a^!v2y05kynrNhr+6gX9z(^Md3XL|vm?qmVnq3eLYM93MLt>)W z5G%dSWd(I#=BXgV?O9G6pVe8lsGCMpop;G|$*M_)94m#anjc+#?+e-wJw0K2RzJc( z=y6PDaP*=%7kTkj4s53_da50)8B!@!_{7xjBEZs2R&km0ai#~bS-$96t*Tx9Za&bKDENN0NIN@R1aZIp~>?;~b4 z7HEof=01oz#V>R7N!A5*iI>-SYN$; zEgFd4qtmw}vUU6<64T_%4UCL2Zfm8$?`3+{^Xa}F>vk^tLVCGXw8q%MO*tfMVsXXXOj!ij3yxj4j6Q_kprQf6xY0|42QAsQLR+c==UBNWY-Org$YHkf0)W~+M zkAx1Y8#`>8jGS02J%1bSr@c6K__l~7)HdZk6j2Yi z#nzmN{9RO)&WnwkHyrTf4g|zTSi~VF(SSIal64@@oWWWQBL6}rF41{v;TCJ5m?~Wd z5nmVXjtlf0N@X+*#yX9`EWZb=QV;6-Xd;DX9t3&tO`tEe5?0l8mkHABOrXOTLQLKp z7ZuFQu#61J?h$|Z`qnNgL2O8ao^}--@%v*AHBig9&B|@`SE^%FUa?(ifnw1Xi9)7z zWK4VQ?n!30>s~RmoUgEl)#E71$5bOGU z{86@-<7lsoJJML6qVmbz%DeB|(Y{{@vgBBrN<9*}+}nqC%zEYd&z-BaRUylb4SMP3 zpS;ui4r6R%=lcpVGb%nGHrp(^d1(*H63fj(C3!99mp2%v!N(I@vK;r>pl&~m@!o-= zy4~%Lpe8E2fbzf!35w#Za^|k-Pu5`9c}OdWbebAHKO~{8-Z<0?_kcSwV#e>67!Jwi zGC!p#h#nrs}3dKWo{wx+AxjVH9d38X$KmIed*Y&azi-Yxg1vE zTMvV>aAM#%DlyYGT)VdXbX#IhPBp)YJ`%CQw1c7fC71^@)n4?DhOlHu?bh|Ue7@v( z2haLo$}%G5=g%ageFbRr@WCk zr4pZ!H>B70xpFJ9^UqboRRbxbKkcD+38I&>&1nQY6>RCL|ZJ`RUbCFduq`X(D zL_)+3mxPg<6ziFS+m9Xhxt#>O5Z*^G@_qG4nFV1}tz))Bal8GQc?4vdn(^~U#eT62 z{-&+&?uL_74>ke~!$;q+RAOE4bMqvLxlwSX6NVBhk7Lu~vKiCa`Ntx!GO}ecR-ksq zwh+ZfnBtV&Lu9V;52l-CCK?`XtsiPmPGx@--o?PB<7oQvM~lP6*YEBhH8gSOedcX` za)_iU9d!?nw>$+a1cBmj$UOSA>sd#p-PE>w7Al!oiZgx=ncbyH5dDJ)J4pyq zl-x<^|MIs*Pg)iN4tQYy-E~LPkRC;S52QxlqhH*2*Zs4tgAx7TiGue)*xC9$uk$OM z`a5Y5J0{!1farhj20en285|1?md()yP^?7k{oy<>Az~nQcNN<(fC^o_M1|{+rg1uI zYMx%s*PPJmdmYZ|mv7$|f@{G^>Gm+@w&52_|4r}~3C_aL) zh!>!owBV>qSH6dv%U3%t0ZBvrEoD0{f)xc;4jA{sR1Jzjs$6YifenlLp`q4Hv-Alb zxoCeAbv)xR@obU2$lyU9B+)Q=TVkVw@YiVDI&Em7c)!DOMqR@>`c0-OINF{(#esHy zu+&TJZBF87CHQgiL0C%$V^}PQrr{u;WWzCZ8sn#Ny|!}w`RkvOR~jLVWo}7nScpmG zHMP*_)>U9rSE|N?bR?m1q1C$y8RX{B{$OfQBdP7D-@wQ7fX!(aDP3YC$)0&{U_#jK zECDT(2%&j2v`G!b5ijs^y#ljXpD3&*G#KqM)=e7b8^^-$k5BVaOIrmEpR+$*8i#|? z`z`3jPYlw^oQmy{b#Yo{g^gAi(2?lmbwYAQjD93=m-}bdpjyFhF@P5oreV>ol)5E>7vSJXaeQ=qRlEeu&+XDnB!hex%iZZ=jE!?{>05dpA#-bE+F9LZe61>^ zt+C*3uLGYG9e%4byT-cU-Ff605x2f6EYKPkPsfe-Q&!bZCi@o*(WYu1& zt}TYl2@qEO_5&t)&uqlpdJ3@-qL<^M*3r}mZNq*nl~ZV>&B?PI+o|+@L!8)=wTAuF z)?=p{A>wEpDiKh$LZ9eg$le3B(O!-=lN7-1c7t=&p@`D=s&D7$xhfs%g6H)AY@c75 zUUKK9~glbj1!{y@xS8SzSt0zm0CD6zBKh3jgZhYD0Tv zd^dhW?_-_5qzR!#H=?}6Z1B)~^oX@Xeu{`0x(n$@YK~4<1V!HxKF&iMR}0>_J)P%g z)YZGh6iy^ermQ!kOd1bQ6NkRooR2RCeB3Z|8I=Hf)z$8bFwlPocc#bE604Xd{uw!?FyQQ6gfSz1{Ml80TRrq;i7J~RpXK{ zo_OwvtV}3;fZ%kZVKEE7Lkd^~#Q__js21c}* zQ)$t`7IGh{afww7*@uD@V^yAZ@w5Ab;9X0iC;hAo!e9$I;@{-16x3`5J7c z{mbX)&28^Gm#|lhy8F`n_|ED;Nn;3iNr=#xv#IZTYEHFcom40{!UBtd`>aj%z&Qh; zVV2Rl$abBetQ%nrR%;>GG- z$g6-OU?+xiaQnH|m-xj+8fS4-y5SP<_f^JvB21h8PIqTef9DSWqC7`qCns|oQ^&um z7pluP%N&Sa6Kas|mm^#;czqg>_TN}4KBM$5XG!w=#PqYVxBBLtH0rKrlSnf8vGk)q z8`+O1GYdPI930D4rh#!!21+s#$c%3*0CJ*8(NZ4|p06EV_9++=f@2sc_4v4au)LZ% zThdqmqxJR#6vQ=%5Uf;z&_@AET({V>CO*_I%jsS;s!EB9^i;EI)xDn6x@eM4VqFYir zW%k~xWj5&-l1vLkvMh)Y1ZrFmwXH)ION{H`8?bGTX&~w;yhg=9JS+RsT^Q|e?l8gU zqW91aIh%xl8DWBjH4(wVfQvZgAD_p;z=A0LXs<1t@USmdATR>kIOYK~jY(NP6Y(8V7TRAoB~gCn=Z1_y!a zwqF*L`5r^6%JO9C{xs&#XU@|ZnImE4AS31KVw&*-n2h5ib-p)Z=x=+0=+Ldlcd-Jh2c&Yi+VLt5R zpZ)h!V)SI<2?f{HGGn%@A4Nl6ljqra;^&`l=78!6k20&5gC@alAnlK??ty-P- zx@l_1KNQS|dNNyAnTN~Zl<7ou;{jJMN3OBm)&re)FVneeZCOj$w%pW@tg&6!C)Ze4 zjj_TXVOX)g9iCVYC9McQeva&5ycJ%(y|6aU&m1|0Wm|P!Lk0^qxp^jw3$OD+l>RQY ziKdH6Q49x?Yc@1Qt>ebic63(jtbhB!=U@iO+Q(U9SQn$tlqNbA9d+K!E-^o$B3Ok} z4Kndee$cwikqZuy(hV^pdAiBZ!z1zH_<3Ro9R1x5P>rSAYK}Q>JSC`0t;zg-Ka(IV zD64@30GfgT0K)&UhNF|4mGNJ@c{f$cahn2h;QX3Wx%naPlG!Egn<>?VObHWnvVFBj zKctp2$t3W&A>V6OBCVLqY_+@Nk5n+HH*B*@F*=UNCuHz(6r~g?MssKW&zkTk%I?}U zWaUB6rC)Osa^aBhq4M-%=c#APD|PwOc?c}8u^CR)J+dQ(x$M0RbGBWylrjTEz9b-_ z)H~3rh=+M8hq{Q*ZZmOZF@*(hGUMA_FP%Aa39~!qe$d(R9H(rFa>8QLM}Dq1@V%u7 zxO2;Mw4Fe-olta(j)^n3e#Qka?ufFha+tx3!wpEID&Af3tWO^zlGN6?wjqB+1Ft}t z-cAjO`Zfy8W?xQxKSDlG^>tsuy=%`13llGq#P~?o*w|b_Cy76QLQV0#3`>9_!`aa= zr9ME|5O%DT-DGKrIkl(ks94rII=to1Z__vzW1&RBw~haK8OP~$x!#?qtM?R6Qf1%W z)L5XGsM2U_LtitjMxLc+Fhpv$>&EGHGw%~-;EBFr7R?7uG*T1$Qg-1q5q+y}>6z9# zxWyOMX|X+-@baWaJa$^o#wI0gs?=1J&phMMg)}=0E7$K4;c9F4Mbp5~%VgKdQLV(c zR>>>sYw=yh#r3wS$xi_|X6IJ{)fBQC%OX*(lN|%9f>t>&-+KpQDupFs*0N%|zc|{k z6SRJ!$c%n~``jE~%Scq-Fz4pQ??u87Lc$*;?j0!Z{ZZUIBI+d!=ZP_Z& zk`jWM+8A@REKZ)GM)7CosN^B=`vAoI04zhz^om-lLJUtLEW)wz7%w|esrGs^=7@O# z^EjS@QMULFca>zKyoyc1NU|^3swkwpMI36Vq-teQXEQ8urddU)Ip&N#fo(%t2kJeQ zTM+k6xt|>d;;<){t2F2RN#DutPK#3kQZ_CF6y~+p2u8bofp;~T8gdXf88B$m?vgT! zixad6K<3O&>_`={oiUQW$1@M$k*81ab*6L=HpOmB!q;iPaBNL#KD))8m5e;HK`Bmo`4YamC?9OAK9bJDv zPVrd~T4J}L)E-WG#<&R_+SIE8s8^%N5`d(7f?fNXaHe|_a_rgj>C2k4{<;>H!IuG_ z*`=WuZAqmzi8K8dXM18;loU<|BLc(w=IlIHG6F$G%SpuAI&e5aBg;wb652F>k``7U z$S19@fggHV&4RzdF5c@?G64ee2LY%RL?Awm-(DEk_P$pQl?XrKUDcBBA6Z~S}lR{y9}RzOQ%;5T1HXp>D&V-qP5+0N15CU8`|a6jZxbu&*iib>QgRpijwh zk?<#y0$;@TX16ajI*lDDj_N=$RWkJ6?9a4o=8}im@5$>yk#$|Eb!?@EtGQF^o7}?7 z7K&V@N2Jbaj7&#j1KHP(ke8@hIq=C_niA*A4JUHcMWbAQd@E=-R_dAMJ~%<|J7Umq z2r{tzzJdh86A(EGpy++T= zgT<>IDZ}|WE#Gko<@~)Pp!EkQjGwL1^dsx!CPIcu7Pp%LUoQB0d< zz=&(bT7PhEyKQF6~%Dhue6L$@nt;86WkaZOA@3c)M991%xtMo`k6{ zAyiEj+!Q3XII5pSOs|YkbES8QRl>^8kz6hu~_dJ#dnXb>_DxJ!eF?-FS5pDcBDQG>9n#t-t((#sPbDEz5#ycAU)FS zprv;XY83$huz%+uHFGC3Q4Ag!%2ENkY)^q|zQ~bOTv-AWb`E+r}c~MSIg@)qkGVQU>TSf8Nf8@c! z<;w1hm%FzCrA(ugXV>A$PC!Yl9KpJmx?H^XWpQ;FuiA|B!KjtovE9-k#Xv5Q!a2yE zcy_O1$7hjAq!Cg_GgI2U<-^Y&t2KJ|uL zVt&tX8xgrZ6&uVO`%yo#jZW{Q0Y(5AiNCc8z69sv`~aMco1NNe|6M7nLgQusl3UAe zWB`2)EX!s5m_n0f$tMQKNdG-B=(NMrZ!i+*?ID9$hN3~S1TIAFH0Ut*mDF=wU1R3uqjP$)bJF)CKP#KiaA>a5rm z=I1&UKQKEcul~p>Q7>**_MrCaR&yiWEkw9>NXyR&5FEE6lH13?K_KAD z*B`{M2OkD>=TI<)YPDf+kw9A~#JQEg765(nMj0n|-d|E4J*h}X=UZ8mAj{LRFugO1 zK^r&kv`IQvND1dM=YX8 zPT?zNp)2N(SB?=orqk1r=&~ur#spuXddp%Q7-}Q~yGE&?z?e&S5Jt;l=-vlhI!CF7 z0L-PMUTw+3G5g;(HEPP5OqUAljaRx&D1A({j>Hp zzq!(D_8i?Ksq6Yp5`Qap$L@Dp8StPxE;X{DJ-3Q%pF=$J+>)1jpo^d9^_mysd?YM$ z6UPNl0u6io*{_;WKYBJBfGFniPKl5GaTVkzhp54Tl$EKB*>Xb9Lb_1DzbJ7E@O7qg zA9nd~N-Sf2r$hyuzbR4c7bV)lVa*ChXysP~d-}J9Zf~#MNo~S3g?V0-F!7zG({p?u z$Gu;|K%89q^>JO(zZ_b+ChFxH@Yy!@Q1I8KTL-)5;AD5Ns*Hk{n zc-zL*i599;4@+U(6RfC@+{wDpleeimeEqr@4cbZ-MPS143@(*gl1qA5NF;SYaDrMm zlZYbqK;`*M-4f!}%Yfjl4mdMNp%tINqCp=of&?NEdG&9w0-ID!fZ)Ah^u| zWsr9^`$ZBLY#lY&lTjpD8U8fCbKb`PA4**RUsEFapOkp#^1r1--alB}XKML>ONqC? zQQ~RiFG@UBf_bOJpnp-~x|0OyF8qco3^?n#B?wJF)XjttkLGu3ps&2iGhqYhd?-WN zMTyXQ+8%zd`}xkoq0I4>XL2WyB<&Zf?Q02f#tm6Aj<3krmHLlK38`xugX0P0aJH*k z$eKtxxRLRzDC)G$Lw-0yY%LJQlDMFSXkuxT&RrHZF{=DAeVX z+pIm-!**^iGn%9$$qeO)8NeE1={P%DnMbGllHgnfVNc1|EtK45N>@3lKZ{ZA?V+un zU6ia7=qzJ~JZOj~O4bqoV#IYW96x?$?srBcfgfApx}4WHi4_lBHD4N+i0(f9#falD z+a~Y_aPN$`^otSI{$j*$l?5|zz`rwMoFGwS|2reLPyqmF|HFt*X2#aW^nblG{>rqR zYRcFyvthR1t06Y;%swqUe`^*!Sv;LDfJ1H&j;s~YvZ7Gp{DCee<&SR^3J!aHGE1Zo14NHJo*lZ17jwr!_(0FW_R_C+n!Ac_ za39tS;O_=b>pZ^x?3>5l0DgienH4*&qujDwsQlCjT?vWd1}WY+QQ29)3yq{r8Q(}7 zMA#t$vNO_zWQ>n`6&)@G{}m!;o;cs4X@$lO@frOhJ7{TsbqaDB5*9Vz`4kx7_!B>x zpnb!F2Dc7sxvnQYR-8HoL5;&B{!!^+w6IkQb1^8-dgv9;ok1X|8yM7qkbFs%^QA_T zQA~n38dZcQM|$$Mhd-T85K&Tn?r_l+^IGNU_I4!I+`J@(1?)nna^X~Ex`ljmvOZsw z`VsZkyL4JYA_};V1uVvCB-~+DJT|@{L9S-)&#EGs_F?t=|r`@^jflB;WZO{&=S9|22d(ZSgy`Lx_jvqhUW_Z>C z24g$dzdW5Z=yquPAWHNeS7yFmZv8m6<$JphA{T9h0}ql|0eA#|_zi5I7zPgVFij`j>W{NfY{t`oK@)*<}tHXazSix)o8@9ao2l4Q7CaC_YdALvpos8(a@>;;Xi zql*iLlR7;DqL!nkNsMC8oIf3a^||Y>%lPqac`zw`SS-j<=PTL&?w$}4@N+A;IWk(k zT4=S$Z~-^W!lWD85lx{jPb>Z`}kU(>(Go|B2(9|s3X7Bn(Cf^BPqHu3J{8 zoF}xLmT~ljT>9d&mbGntv{U*IM}Fi^neps|(&zY6cUUaNg{g<&GGoF6f9xYyZ)~B1 z&67r>uIov%tnSVIkfptrN$W+WTXfTW@QRv{0FS|Xb1%k4fT}ecGCw!G7m%HE?WMGp z5ZBs7H9I>`p8xUm%Dh-11XE5tQgS=Vedpm+_Yf&OuUk6(X(w4OL7Y;i2HTvx1{ry$ zh(QMIktc1!AxzC74Iim_QgA>c@w<1vOMPw;LviBdeQi#8E5%Z`pKa0Vw|RDSr>(DF z>+9Bv*l6^6vaG{*@Wi}tftv`_%8_EK*6Xdhrl%}Uzy|K4ZJwPSa|%JqtuzxX@6z7W zD&S$EGJ~tZ9NO{qbeC`iC2U8-q@{H#=5v~}n(_B4p$9{Vn`SIR^$Z`~45;dySq!OK zoQM#Yozp=WNKooQ@|}+qxVVx=Ks20xb{hE>PcW&_U+e)Vc%R!AIc^Gx17nOs%Tjl& zi8%cT3*MFU=*qg9vK#(bTK(x1Z;7t3I&Jyv#+tnpZ%~Yt+z?VrM-~&bgrk;r*k8)H z1xDNIKDl)!NjXjxP@O~x0uXw=;1_-_?&nM--lJr#ZPN3w>6q6pXV;!pn?M?de(uz2STBp#kb=I&7AsyI=7vx z65$|Xlp9f3#ixu6L#t*Q9|1l;TvMZbcFiL6{+!4{zW*Sn<}rZ?$&&Ma-Ks3zpNKlR zE|72*_R*_VT*r8Va~R?wyZ7PDHSVT6WL z@@_Ge3H&+0p4r6_3y7=hbI0iFMKvOQ2JMeA$+pS zqK5$%FzM0Wy%VFfZ4|~R6vKpx*AgdKk6?#O%2-Eh*fjQZ&#b|Q*>Xchct}{h_62*4 zZbKs5x|)fe30!q6>txo5;5=y6u%7~w8!{X{A$e@_1T4Enj zyN`1SD=Z5aZ&4k03J1yHh}_1XKYdj;$DXK2#~iKVrGf%~I{dPMf+Iq*n|Zhf2xs2y zOi&Doc5HYmvVJ9p_5fmE0a&4R;J|hQ()dgA)lUMWxEpbfKpy-W>sm$oR+{<2gE%aW zHd*wx16CGu|7kXT8gF^C@R8-q0NY0&G1&CRP84T*I5>!WLUhZY`^(l>?;ZF+z^EYq z_ai0m)ArYo&-)zyzaB98Q^21`9sY&`06sv)?;`$n@ZnG3pWXle04LrHk$;{A_!IqS z8~Q(Jf6#xS|EDSaPxzmG)BnJc-#w@QUi|ki>OY12*}?ITkg@m9`2Vkdj$e}S|6_&! zvwH#aFG0Tykmrl@e$MAFNx%0pfc@Re@JAoRp9=n2)%{07P4xeW{jJ9PC;rcR-9Pxw zcOCyy>)+M8KjD9t2mXOay*Ir44gYVk;7{ layout-grid cells + Then cell.grid_span is + + Examples: Cell.grid_span value cases + | count | + | 1 | + | 2 | + | 4 | + + Scenario Outline: Get _Cell.vertical_alignment Given a _Cell object with vertical alignment as cell Then cell.vertical_alignment is diff --git a/src/docx/table.py b/src/docx/table.py index a272560bc..e88232840 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -222,6 +222,15 @@ def add_table( # pyright: ignore[reportIncompatibleMethodOverride] self.add_paragraph() return table + @property + def grid_span(self) -> int: + """Number of layout-grid cells this cell spans horizontally. + + A "normal" cell has a grid-span of 1. A horizontally merged cell has a grid-span of 2 or + more. + """ + return self._tc.grid_span + def merge(self, other_cell: _Cell): """Return a merged cell created by spanning the rectangular region having this cell and `other_cell` as diagonal corners. diff --git a/tests/test_table.py b/tests/test_table.py index 4eb1c8efb..993fb3f23 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -326,6 +326,19 @@ def table(self, document_: Mock): class Describe_Cell: """Unit-test suite for `docx.table._Cell` objects.""" + @pytest.mark.parametrize( + ("tc_cxml", "expected_value"), + [ + ("w:tc", 1), + ("w:tc/w:tcPr", 1), + ("w:tc/w:tcPr/w:gridSpan{w:val=1}", 1), + ("w:tc/w:tcPr/w:gridSpan{w:val=4}", 4), + ], + ) + def it_knows_its_grid_span(self, tc_cxml: str, expected_value: int, parent_: Mock): + cell = _Cell(cast(CT_Tc, element(tc_cxml)), parent_) + assert cell.grid_span == expected_value + @pytest.mark.parametrize( ("tc_cxml", "expected_text"), [ From 7508051c7664a8a5ce8828e7fa33e1075578617c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 23:06:00 -0700 Subject: [PATCH 086/131] feat(table): add CT_Tc.grid_offset This property was formerly known as `._grid_col` but that didn't account for `.grid_before` in the computation. --- src/docx/oxml/table.py | 28 ++++++++++++++++------------ tests/oxml/test_table.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index 963f3ebf7..4715900e6 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -450,6 +450,18 @@ def clear_content(self): for e in self.xpath("./*[not(self::w:tcPr)]"): self.remove(e) + @property + def grid_offset(self) -> int: + """Starting offset of `tc` in the layout-grid columns of its table. + + A cell in the leftmost grid-column has offset 0. + """ + grid_before = self._tr.grid_before + preceding_tc_grid_spans = sum( + tc.grid_span for tc in self.xpath("./preceding-sibling::w:tc") + ) + return grid_before + preceding_tc_grid_spans + @property def grid_span(self) -> int: """The integer number of columns this cell spans. @@ -484,7 +496,7 @@ def iter_block_items(self): @property def left(self) -> int: """The grid column index at which this ```` element appears.""" - return self._grid_col + return self.grid_offset def merge(self, other_tc: CT_Tc) -> CT_Tc: """Return top-left `w:tc` element of a new span. @@ -510,7 +522,7 @@ def right(self) -> int: 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 + return self.grid_offset + self.grid_span @property def top(self) -> int: @@ -553,14 +565,6 @@ def _add_width_of(self, other_tc: CT_Tc): if self.width and other_tc.width: self.width = Length(self.width + other_tc.width) - @property - def _grid_col(self) -> int: - """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: int, height: int, top_tc: CT_Tc | None = None): """Grow this cell to `width` grid columns and `height` rows. @@ -727,7 +731,7 @@ def _tbl(self) -> CT_Tbl: @property def _tc_above(self) -> CT_Tc: """The `w:tc` element immediately above this one in its grid column.""" - return self._tr_above.tc_at_grid_col(self._grid_col) + return self._tr_above.tc_at_grid_col(self.grid_offset) @property def _tc_below(self) -> CT_Tc | None: @@ -735,7 +739,7 @@ def _tc_below(self) -> CT_Tc | None: tr_below = self._tr_below if tr_below is None: return None - return tr_below.tc_at_grid_col(self._grid_col) + return tr_below.tc_at_grid_col(self.grid_offset) @property def _tr(self) -> CT_Row: diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 6a177ab77..937496346 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -52,6 +52,22 @@ def it_raises_on_tc_at_grid_col( class DescribeCT_Tc: """Unit-test suite for `docx.oxml.table.CT_Tc` objects.""" + @pytest.mark.parametrize( + ("tr_cxml", "tc_idx", "expected_value"), + [ + ("w:tr/(w:tc/w:p,w:tc/w:p)", 0, 0), + ("w:tr/(w:tc/w:p,w:tc/w:p)", 1, 1), + ("w:tr/(w:trPr/w:gridBefore{w:val=2},w:tc/w:p,w:tc/w:p)", 0, 2), + ("w:tr/(w:trPr/w:gridBefore{w:val=2},w:tc/w:p,w:tc/w:p)", 1, 3), + ("w:tr/(w:trPr/w:gridBefore{w:val=4},w:tc/w:p,w:tc/w:p,w:tc/w:p,w:tc/w:p)", 2, 6), + ], + ) + def it_knows_its_grid_offset(self, tr_cxml: str, tc_idx: int, expected_value: int): + tr = cast(CT_Row, element(tr_cxml)) + tc = tr.tc_lst[tc_idx] + + assert tc.grid_offset == expected_value + def it_can_merge_to_another_tc( self, tr_: Mock, _span_dimensions_: Mock, _tbl_: Mock, _grow_to_: Mock, top_tc_: Mock ): From 512f269b7560c02f81c16c7858ed67bfdc956dae Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 27 Apr 2024 23:33:38 -0700 Subject: [PATCH 087/131] rfctr(table): reimplement CT_Tc.tc_at_grid_offset This method was formerly named `.tc_at_grid_col()`. New implementation takes `CT_Tr.grid_before` into account. --- src/docx/oxml/table.py | 31 +++++++++++++++++++------------ tests/oxml/test_table.py | 20 ++++++-------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index 4715900e6..42e8cc95c 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -76,19 +76,26 @@ def grid_before(self) -> int: return 0 return trPr.grid_before - def tc_at_grid_col(self, idx: int) -> CT_Tc: - """`` element appearing at grid column `idx`. + def tc_at_grid_offset(self, grid_offset: int) -> CT_Tc: + """The `tc` element in this tr at exact `grid offset`. - Raises |ValueError| if no `w:tc` element begins at that grid column. + Raises ValueError when this `w:tr` contains no `w:tc` with exact starting `grid_offset`. """ - grid_col = 0 + # -- account for omitted cells at the start of the row -- + remaining_offset = grid_offset - self.grid_before + for tc in self.tc_lst: - if grid_col == idx: + # -- We've gone past grid_offset without finding a tc, no sense searching further. -- + if remaining_offset < 0: + break + # -- We've arrived at grid_offset, this is the `w:tc` we're looking for. -- + if remaining_offset == 0: 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") + # -- We're not there yet, skip forward the number of layout-grid cells this cell + # -- occupies. + remaining_offset -= tc.grid_span + + raise ValueError(f"no `tc` element at grid_offset={grid_offset}") @property def tr_idx(self) -> int: @@ -505,7 +512,7 @@ def merge(self, other_tc: CT_Tc) -> CT_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 = self._tbl.tr_lst[top].tc_at_grid_offset(left) top_tc._grow_to(width, height) return top_tc @@ -731,7 +738,7 @@ def _tbl(self) -> CT_Tbl: @property def _tc_above(self) -> CT_Tc: """The `w:tc` element immediately above this one in its grid column.""" - return self._tr_above.tc_at_grid_col(self.grid_offset) + return self._tr_above.tc_at_grid_offset(self.grid_offset) @property def _tc_below(self) -> CT_Tc | None: @@ -739,7 +746,7 @@ def _tc_below(self) -> CT_Tc | None: tr_below = self._tr_below if tr_below is None: return None - return tr_below.tc_at_grid_col(self.grid_offset) + return tr_below.tc_at_grid_offset(self.grid_offset) @property def _tr(self) -> CT_Row: diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 937496346..46b2f4ed1 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -34,19 +34,11 @@ def it_can_add_a_trPr(self, tr_cxml: str, expected_cxml: str): tr._add_trPr() assert tr.xml == xml(expected_cxml) - @pytest.mark.parametrize( - ("snippet_idx", "row_idx", "col_idx", "err_msg"), - [ - (0, 0, 3, "index out of bounds"), - (1, 0, 1, "no cell on grid column 1"), - ], - ) - def it_raises_on_tc_at_grid_col( - self, snippet_idx: int, row_idx: int, col_idx: int, err_msg: str - ): + @pytest.mark.parametrize(("snippet_idx", "row_idx", "col_idx"), [(0, 0, 3), (1, 0, 1)]) + def it_raises_on_tc_at_grid_col(self, snippet_idx: int, row_idx: int, col_idx: int): tr = cast(CT_Tbl, parse_xml(snippet_seq("tbl-cells")[snippet_idx])).tr_lst[row_idx] - with pytest.raises(ValueError, match=err_msg): - tr.tc_at_grid_col(col_idx) + with pytest.raises(ValueError, match=f"no `tc` element at grid_offset={col_idx}"): + tr.tc_at_grid_offset(col_idx) class DescribeCT_Tc: @@ -76,12 +68,12 @@ def it_can_merge_to_another_tc( top, left, height, width = 0, 1, 2, 3 _span_dimensions_.return_value = top, left, height, width _tbl_.return_value.tr_lst = [tr_] - tr_.tc_at_grid_col.return_value = top_tc_ + tr_.tc_at_grid_offset.return_value = top_tc_ merged_tc = tc.merge(other_tc) _span_dimensions_.assert_called_once_with(tc, other_tc) - top_tr_.tc_at_grid_col.assert_called_once_with(left) + top_tr_.tc_at_grid_offset.assert_called_once_with(left) top_tc_._grow_to.assert_called_once_with(width, height) assert merged_tc is top_tc_ From f4a48b5565a3a09087f541e3ac36a447693927b4 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 28 Apr 2024 12:45:09 -0700 Subject: [PATCH 088/131] fix(table): fix _Row.cells can raise IndexError The original implementation of `_Row.cells` did not take into account the fact that rows could include unoccupied grid cells at the beginning and/or end of the row. This "advanced" feature of tables is sometimes used by the Word table layout algorithm when the user does not carefully align the right boundary of cells during resizing, so while quite unusual to be used on purpose, this arises with some frequency in human-authored documents in the wild. The prior implementation of `_Row.cells` assumed that `Table.cells()` was uniform and the cells for a row could be reliably be computed from the table column-count and row and column offsets. That assumption does not always hold and can raise `IndexError` when omitted cells are present. This reimplementation remedies that situation. As a side-effect it should also perform much better when reading large tables. --- src/docx/table.py | 38 +++++++++++++++++++++++++++++++++++--- tests/test_table.py | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/docx/table.py b/src/docx/table.py index e88232840..556e66be8 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast, overload +from typing import TYPE_CHECKING, Iterator, cast, overload from typing_extensions import TypeAlias @@ -102,7 +102,10 @@ def columns(self): return _Columns(self._tbl, self) def row_cells(self, row_idx: int) -> list[_Cell]: - """Sequence of cells in the row at `row_idx` in this table.""" + """DEPRECATED: Use `table.rows[row_idx].cells` instead. + + 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 @@ -403,7 +406,36 @@ def cells(self) -> tuple[_Cell, ...]: layout-grid positions using `.grid_cols_before` and `.grid_cols_after`. """ - return tuple(self.table.row_cells(self._index)) + + def iter_tc_cells(tc: CT_Tc) -> Iterator[_Cell]: + """Generate a cell object for each layout-grid cell in `tc`. + + In particular, a `` element with a horizontal "span" with generate the same cell + multiple times, one for each grid-cell being spanned. This approximates a row in a + "uniform" table, where each row has a cell for each column in the table. + """ + # -- a cell comprising the second or later row of a vertical span is indicated by + # -- tc.vMerge="continue" (the default value of the `w:vMerge` attribute, when it is + # -- present in the XML). The `w:tc` element at the same grid-offset in the prior row + # -- is guaranteed to be the same width (gridSpan). So we can delegate content + # -- discovery to that prior-row `w:tc` element (recursively) until we arrive at the + # -- "root" cell -- for the vertical span. + if tc.vMerge == "continue": + yield from iter_tc_cells(tc._tc_above) # pyright: ignore[reportPrivateUsage] + return + + # -- Otherwise, vMerge is either "restart" or None, meaning this `tc` holds the actual + # -- content of the cell (whether it is vertically merged or not). + cell = _Cell(tc, self.table) + for _ in range(tc.grid_span): + yield cell + + def _iter_row_cells() -> Iterator[_Cell]: + """Generate `_Cell` instance for each populated layout-grid cell in this row.""" + for tc in self._tr.tc_lst: + yield from iter_tc_cells(tc) + + return tuple(_iter_row_cells()) @property def grid_cols_after(self) -> int: diff --git a/tests/test_table.py b/tests/test_table.py index 993fb3f23..479d670c6 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -782,18 +782,41 @@ def it_can_change_its_height_rule( row.height_rule = new_value assert row._tr.xml == xml(expected_cxml) + @pytest.mark.parametrize( + ("tbl_cxml", "row_idx", "expected_len"), + [ + # -- cell corresponds to single layout-grid cell -- + ("w:tbl/w:tr/w:tc/w:p", 0, 1), + # -- cell has a horizontal span -- + ("w:tbl/w:tr/w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p)", 0, 2), + # -- cell is in latter row of vertical span -- + ( + "w:tbl/(w:tr/w:tc/(w:tcPr/w:vMerge{w:val=restart},w:p)," + "w:tr/w:tc/(w:tcPr/w:vMerge,w:p))", + 1, + 1, + ), + # -- cell both has horizontal span and is latter row of vertical span -- + ( + "w:tbl/(w:tr/w:tc/(w:tcPr/(w:gridSpan{w:val=2},w:vMerge{w:val=restart}),w:p)," + "w:tr/w:tc/(w:tcPr/(w:gridSpan{w:val=2},w:vMerge),w:p))", + 1, + 2, + ), + ], + ) def it_provides_access_to_its_cells( - self, _index_prop_: Mock, table_prop_: Mock, table_: Mock, parent_: Mock + self, tbl_cxml: str, row_idx: int, expected_len: int, parent_: Mock ): - row = _Row(cast(CT_Row, element("w:tr")), parent_) - _index_prop_.return_value = row_idx = 6 - expected_cells = (1, 2, 3) - table_.row_cells.return_value = list(expected_cells) + tbl = cast(CT_Tbl, element(tbl_cxml)) + tr = tbl.tr_lst[row_idx] + table = Table(tbl, parent_) + row = _Row(tr, table) cells = row.cells - table_.row_cells.assert_called_once_with(row_idx) - assert cells == expected_cells + assert len(cells) == expected_len + assert all(type(c) is _Cell for c in cells) def it_provides_access_to_the_table_it_belongs_to(self, parent_: Mock, table_: Mock): parent_.table = table_ @@ -821,7 +844,7 @@ def table_(self, request: FixtureRequest): @pytest.fixture def table_prop_(self, request: FixtureRequest, table_: Mock): - return property_mock(request, _Row, "table", return_value=table_) + return property_mock(request, _Row, "table") class Describe_Rows: From 89b399b8c4147c0214d8348469209547d50962c8 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 28 Apr 2024 18:03:06 -0700 Subject: [PATCH 089/131] feat(typing): add py.typed, improve public types --- src/docx/api.py | 10 +++++--- src/docx/oxml/text/parfmt.py | 45 +++++++++++++++++++++++++----------- src/docx/parts/document.py | 3 ++- src/docx/py.typed | 0 4 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 src/docx/py.typed diff --git a/src/docx/api.py b/src/docx/api.py index a17c1dad4..aea876458 100644 --- a/src/docx/api.py +++ b/src/docx/api.py @@ -6,13 +6,17 @@ from __future__ import annotations import os -from typing import IO +from typing import IO, TYPE_CHECKING, cast from docx.opc.constants import CONTENT_TYPE as CT from docx.package import Package +if TYPE_CHECKING: + from docx.document import Document as DocumentObject + from docx.parts.document import DocumentPart -def Document(docx: str | IO[bytes] | None = None): + +def Document(docx: str | IO[bytes] | None = None) -> DocumentObject: """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. @@ -20,7 +24,7 @@ def Document(docx: str | IO[bytes] | None = None): loaded. """ docx = _default_docx_path() if docx is None else docx - document_part = Package.open(docx).main_document_part + document_part = cast("DocumentPart", Package.open(docx).main_document_part) if document_part.content_type != CT.WML_DOCUMENT_MAIN: tmpl = "file '%s' is not a Word file, content type is '%s'" raise ValueError(tmpl % (docx, document_part.content_type)) diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index 94e802938..de5609636 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -28,10 +28,18 @@ 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) + left: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:left", ST_SignedTwipsMeasure + ) + right: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:right", ST_SignedTwipsMeasure + ) + firstLine: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:firstLine", ST_TwipsMeasure + ) + hanging: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:hanging", ST_TwipsMeasure + ) class CT_Jc(BaseOxmlElement): @@ -45,6 +53,7 @@ class CT_Jc(BaseOxmlElement): class CT_PPr(BaseOxmlElement): """```` element, containing the properties for a paragraph.""" + get_or_add_ind: Callable[[], CT_Ind] get_or_add_pStyle: Callable[[], CT_String] _insert_sectPr: Callable[[CT_SectPr], None] _remove_pStyle: Callable[[], None] @@ -98,13 +107,15 @@ class CT_PPr(BaseOxmlElement): 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:]) + ind: CT_Ind | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "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): + def first_line_indent(self) -> Length | None: """A |Length| value calculated from the values of `w:ind/@w:firstLine` and `w:ind/@w:hanging`. @@ -122,7 +133,7 @@ def first_line_indent(self): return firstLine @first_line_indent.setter - def first_line_indent(self, value): + def first_line_indent(self, value: Length | None): if self.ind is None and value is None: return ind = self.get_or_add_ind() @@ -135,7 +146,7 @@ def first_line_indent(self, value): ind.firstLine = value @property - def ind_left(self): + def ind_left(self) -> Length | None: """The value of `w:ind/@w:left` or |None| if not present.""" ind = self.ind if ind is None: @@ -143,14 +154,14 @@ def ind_left(self): return ind.left @ind_left.setter - def ind_left(self, value): + def ind_left(self, value: Length | None): if value is None and self.ind is None: return ind = self.get_or_add_ind() ind.left = value @property - def ind_right(self): + def ind_right(self) -> Length | None: """The value of `w:ind/@w:right` or |None| if not present.""" ind = self.ind if ind is None: @@ -158,7 +169,7 @@ def ind_right(self): return ind.right @ind_right.setter - def ind_right(self, value): + def ind_right(self, value: Length | None): if value is None and self.ind is None: return ind = self.get_or_add_ind() @@ -340,9 +351,15 @@ class CT_TabStop(BaseOxmlElement): 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) + val: WD_TAB_ALIGNMENT = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "w:val", WD_TAB_ALIGNMENT + ) + leader: WD_TAB_LEADER | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:leader", WD_TAB_LEADER, default=WD_TAB_LEADER.SPACES + ) + pos: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "w:pos", ST_SignedTwipsMeasure + ) def __str__(self) -> str: """Text equivalent of a `w:tab` element appearing in a run. diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index a157764b9..81e621c1a 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -16,6 +16,7 @@ from docx.shared import lazyproperty if TYPE_CHECKING: + from docx.opc.coreprops import CoreProperties from docx.styles.style import BaseStyle @@ -41,7 +42,7 @@ def add_header_part(self): return header_part, rId @property - def core_properties(self): + def core_properties(self) -> CoreProperties: """A |CoreProperties| object providing read/write access to the core properties of this document.""" return self.package.core_properties diff --git a/src/docx/py.typed b/src/docx/py.typed new file mode 100644 index 000000000..e69de29bb From 94802e4af62cf68469bcc0176789f158b39e3404 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 29 Apr 2024 16:57:06 -0700 Subject: [PATCH 090/131] fix: fix some shortlist issues --- pyproject.toml | 2 +- src/docx/enum/base.py | 11 +++++------ src/docx/image/image.py | 32 ++++++++++++-------------------- src/docx/oxml/ns.py | 6 +++--- src/docx/oxml/simpletypes.py | 18 ++++++------------ src/docx/text/paragraph.py | 12 +++--------- 6 files changed, 30 insertions(+), 51 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c0518a96..8d483f00b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] dependencies = [ "lxml>=3.1.0", - "typing_extensions", + "typing_extensions>=4.9.0", ] description = "Create, read, and update Microsoft Word .docx files." dynamic = ["version"] diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index e37e74299..bc96ab6a2 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -4,9 +4,10 @@ import enum import textwrap -from typing import Any, Dict, Type, TypeVar +from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar -from typing_extensions import Self +if TYPE_CHECKING: + from typing_extensions import Self _T = TypeVar("_T", bound="BaseXmlEnum") @@ -69,7 +70,7 @@ def to_xml(cls: Type[_T], value: int | _T | None) -> str | None: """XML value of this enum member, generally an XML attribute value.""" # -- presence of multi-arg `__new__()` method fools type-checker, but getting a # -- member by its value using EnumCls(val) works as usual. - return cls(value).xml_value # pyright: ignore[reportGeneralTypeIssues] + return cls(value).xml_value class DocsPageFormatter: @@ -129,9 +130,7 @@ 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 - ] + member_defs = [self._member_def(member) for member in members if member.name is not None] return "\n".join(member_defs) @property diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 945432872..710546fdb 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -11,8 +11,6 @@ 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 @@ -28,14 +26,14 @@ def __init__(self, blob: bytes, filename: str, image_header: BaseImageHeader): self._image_header = image_header @classmethod - def from_blob(cls, blob: bytes) -> Self: + def from_blob(cls, blob: bytes) -> Image: """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): + def from_file(cls, image_descriptor: str | IO[bytes]): """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): @@ -57,7 +55,7 @@ def blob(self): return self._blob @property - def content_type(self): + def content_type(self) -> str: """MIME content type for this image, e.g. ``'image/jpeg'`` for a JPEG image.""" return self._image_header.content_type @@ -167,12 +165,11 @@ def _from_stream( 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`.""" +def _ImageHeaderFactory(stream: IO[bytes]): + """A |BaseImageHeader| subclass instance that can parse headers of image in `stream`.""" from docx.image import SIGNATURES - def read_32(stream): + def read_32(stream: IO[bytes]): stream.seek(0) return stream.read(32) @@ -188,32 +185,27 @@ def read_32(stream): class BaseImageHeader: """Base class for image header subclasses like |Jpeg| and |Tiff|.""" - def __init__(self, px_width, px_height, horz_dpi, vert_dpi): + def __init__(self, px_width: int, px_height: int, horz_dpi: int, vert_dpi: int): 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): + def content_type(self) -> str: """Abstract property definition, must be implemented by all subclasses.""" - msg = ( - "content_type property must be implemented by all subclasses of " - "BaseImageHeader" - ) + msg = "content_type property must be implemented by all subclasses of " "BaseImageHeader" raise NotImplementedError(msg) @property - def default_ext(self): + def default_ext(self) -> str: """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( + "default_ext property must be implemented by all subclasses of " "BaseImageHeader" ) - raise NotImplementedError(msg) @property def px_width(self): diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py index 3238864e9..5bed1e6a0 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -1,8 +1,8 @@ """Namespace-related objects.""" -from typing import Any, Dict +from __future__ import annotations -from typing_extensions import Self +from typing import Any, Dict nsmap = { "a": "http://schemas.openxmlformats.org/drawingml/2006/main", @@ -41,7 +41,7 @@ def clark_name(self) -> str: return "{%s}%s" % (self._ns_uri, self._local_part) @classmethod - def from_clark_name(cls, clark_name: str) -> Self: + def from_clark_name(cls, clark_name: str) -> NamespacePrefixedTag: nsuri, local_name = clark_name[1:].split("}") nstag = "%s:%s" % (pfxmap[nsuri], local_name) return cls(nstag) diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index debb5dc3c..dd10ab910 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -36,12 +36,10 @@ def convert_from_xml(cls, str_value: str) -> Any: return int(str_value) @classmethod - def convert_to_xml(cls, value: Any) -> str: - ... + def convert_to_xml(cls, value: Any) -> str: ... @classmethod - def validate(cls, value: Any) -> None: - ... + def validate(cls, value: Any) -> None: ... @classmethod def validate_int(cls, value: object): @@ -49,9 +47,7 @@ def validate_int(cls, value: object): raise TypeError("value must be , got %s" % type(value)) @classmethod - def validate_int_in_range( - cls, value: int, min_inclusive: int, max_inclusive: int - ) -> None: + def validate_int_in_range(cls, value: int, min_inclusive: int, max_inclusive: int) -> None: cls.validate_int(value) if value < min_inclusive or value > max_inclusive: raise ValueError( @@ -129,8 +125,7 @@ def convert_to_xml(cls, value: bool) -> str: def validate(cls, value: Any) -> None: if value not in (True, False): raise TypeError( - "only True or False (and possibly None) may be assigned, got" - " '%s'" % value + "only True or False (and possibly None) may be assigned, got" " '%s'" % value ) @@ -248,8 +243,7 @@ def validate(cls, value: Any) -> None: # must be an RGBColor object --- if not isinstance(value, RGBColor): raise ValueError( - "rgb color value must be RGBColor object, got %s %s" - % (type(value), value) + "rgb color value must be RGBColor object, got %s %s" % (type(value), value) ) @@ -316,7 +310,7 @@ class ST_SignedTwipsMeasure(XsdInt): def convert_from_xml(cls, str_value: str) -> Length: 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)) + return Twips(int(round(float(str_value)))) @classmethod def convert_to_xml(cls, value: int | Length) -> str: diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 0a5d67674..89c032586 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -4,8 +4,6 @@ from typing import TYPE_CHECKING, Iterator, List, cast -from typing_extensions import Self - from docx import types as t from docx.enum.style import WD_STYLE_TYPE from docx.oxml.text.run import CT_R @@ -29,9 +27,7 @@ def __init__(self, p: CT_P, parent: t.ProvidesStoryPart): super(Paragraph, self).__init__(parent) self._p = self._element = p - def add_run( - self, text: str | None = None, style: str | CharacterStyle | None = None - ) -> Run: + 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 @@ -82,7 +78,7 @@ def hyperlinks(self) -> List[Hyperlink]: def insert_paragraph_before( self, text: str | None = None, style: str | ParagraphStyle | None = None - ) -> Self: + ) -> Paragraph: """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 @@ -123,9 +119,7 @@ def rendered_page_breaks(self) -> List[RenderedPageBreak]: 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 - ] + return [RenderedPageBreak(lrpb, self) for lrpb in self._p.lastRenderedPageBreaks] @property def runs(self) -> List[Run]: From 5a80006553f982ef47ebc9b4eb3652452b3c07e7 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 29 Apr 2024 17:53:03 -0700 Subject: [PATCH 091/131] fix(packaging): small packaging and doc tweaks `lxml` won't install on Apple Silicon after `4.9.2`. Dropping testing for Python 3.7. --- pyproject.toml | 2 +- requirements-docs.txt | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d483f00b..ad89abd19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "lxml>=3.1.0", + "lxml>=3.1.0,<=4.9.2", "typing_extensions>=4.9.0", ] description = "Create, read, and update Microsoft Word .docx files." diff --git a/requirements-docs.txt b/requirements-docs.txt index 11f9d2cd2..90edd8e31 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,5 @@ Sphinx==1.8.6 Jinja2==2.11.3 MarkupSafe==0.23 +alabaster<0.7.14 -e . diff --git a/tox.ini b/tox.ini index 1c4e3aea7..f8595ba45 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310, py311 +envlist = py38, py39, py310, py311 [testenv] deps = -rrequirements-test.txt From 0a09474b4d1def9fef65267ac27c9f5a48346d25 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 29 Apr 2024 18:42:34 -0700 Subject: [PATCH 092/131] rfctr: resolve some import cycles --- src/docx/blkcntnr.py | 12 +++--------- src/docx/document.py | 6 ++---- src/docx/drawing/__init__.py | 6 +++++- src/docx/shared.py | 6 ++---- src/docx/table.py | 4 ++-- src/docx/text/hyperlink.py | 10 ++++++---- src/docx/text/pagebreak.py | 2 +- src/docx/text/paragraph.py | 2 +- src/docx/text/run.py | 10 +++------- 9 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index 1327e6d08..a9969f6f6 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -18,7 +18,7 @@ from docx.text.paragraph import Paragraph if TYPE_CHECKING: - from docx import types as t + import docx.types as t from docx.oxml.document import CT_Body from docx.oxml.section import CT_HdrFtr from docx.oxml.table import CT_Tc @@ -41,9 +41,7 @@ def __init__(self, element: BlockItemElement, parent: t.ProvidesStoryPart): super(BlockItemContainer, self).__init__(parent) self._element = element - def add_paragraph( - self, text: str = "", style: str | ParagraphStyle | None = None - ) -> Paragraph: + def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = None) -> Paragraph: """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 @@ -77,11 +75,7 @@ def iter_inner_content(self) -> Iterator[Paragraph | Table]: from docx.table import Table for element in self._element.inner_content_elements: - yield ( - Paragraph(element, self) - if isinstance(element, CT_P) - else Table(element, self) - ) + yield (Paragraph(element, self) if isinstance(element, CT_P) else Table(element, self)) @property def paragraphs(self): diff --git a/src/docx/document.py b/src/docx/document.py index 4deb8aa8e..8944a0e50 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -14,7 +14,7 @@ from docx.shared import ElementProxy, Emu if TYPE_CHECKING: - from docx import types as t + import docx.types as t from docx.oxml.document import CT_Body, CT_Document from docx.parts.document import DocumentPart from docx.settings import Settings @@ -56,9 +56,7 @@ def add_page_break(self): paragraph.add_run().add_break(WD_BREAK.PAGE) return paragraph - def add_paragraph( - self, text: str = "", style: str | ParagraphStyle | None = None - ) -> Paragraph: + def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = None) -> Paragraph: """Return paragraph newly added to the end of the document. The paragraph is populated with `text` and having paragraph style `style`. diff --git a/src/docx/drawing/__init__.py b/src/docx/drawing/__init__.py index 03c9c5ab8..f40205747 100644 --- a/src/docx/drawing/__init__.py +++ b/src/docx/drawing/__init__.py @@ -2,10 +2,14 @@ from __future__ import annotations -from docx import types as t +from typing import TYPE_CHECKING + from docx.oxml.drawing import CT_Drawing from docx.shared import Parented +if TYPE_CHECKING: + import docx.types as t + class Drawing(Parented): """Container for a DrawingML object.""" diff --git a/src/docx/shared.py b/src/docx/shared.py index 7b696202f..491d42741 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -16,7 +16,7 @@ ) if TYPE_CHECKING: - from docx import types as t + import docx.types as t from docx.opc.part import XmlPart from docx.oxml.xmlchemy import BaseOxmlElement from docx.parts.story import StoryPart @@ -284,9 +284,7 @@ class ElementProxy: common type of class in python-docx other than custom element (oxml) classes. """ - def __init__( - self, element: BaseOxmlElement, parent: t.ProvidesXmlPart | None = None - ): + def __init__(self, element: BaseOxmlElement, parent: t.ProvidesXmlPart | None = None): self._element = element self._parent = parent diff --git a/src/docx/table.py b/src/docx/table.py index 556e66be8..545c46884 100644 --- a/src/docx/table.py +++ b/src/docx/table.py @@ -6,7 +6,6 @@ from typing_extensions import TypeAlias -from docx import types as t from docx.blkcntnr import BlockItemContainer from docx.enum.style import WD_STYLE_TYPE from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT @@ -15,6 +14,7 @@ from docx.shared import Inches, Parented, StoryChild, lazyproperty if TYPE_CHECKING: + import docx.types as t from docx.enum.table import WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION from docx.oxml.table import CT_Row, CT_Tbl, CT_TblPr, CT_Tc from docx.shared import Length @@ -193,7 +193,7 @@ class _Cell(BlockItemContainer): """Table cell.""" def __init__(self, tc: CT_Tc, parent: TableParent): - super(_Cell, self).__init__(tc, cast(t.ProvidesStoryPart, parent)) + super(_Cell, self).__init__(tc, cast("t.ProvidesStoryPart", parent)) self._parent = parent self._tc = self._element = tc diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py index 705a97ee4..a23df1c74 100644 --- a/src/docx/text/hyperlink.py +++ b/src/docx/text/hyperlink.py @@ -7,13 +7,15 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING -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 +if TYPE_CHECKING: + import docx.types as t + from docx.oxml.text.hyperlink import CT_Hyperlink + class Hyperlink(Parented): """Proxy object wrapping a `` element. @@ -78,7 +80,7 @@ def fragment(self) -> str: return self._hyperlink.anchor or "" @property - def runs(self) -> List[Run]: + 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 diff --git a/src/docx/text/pagebreak.py b/src/docx/text/pagebreak.py index a5e68b5aa..0977ccea9 100644 --- a/src/docx/text/pagebreak.py +++ b/src/docx/text/pagebreak.py @@ -4,11 +4,11 @@ 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: + import docx.types as t from docx.text.paragraph import Paragraph diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 89c032586..234ea66cb 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Iterator, List, cast -from docx import types as t from docx.enum.style import WD_STYLE_TYPE from docx.oxml.text.run import CT_R from docx.shared import StoryChild @@ -15,6 +14,7 @@ from docx.text.run import Run if TYPE_CHECKING: + import docx.types as t from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.oxml.text.paragraph import CT_P from docx.styles.style import CharacterStyle diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 44c41c0fe..daa604e87 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -4,7 +4,6 @@ from typing import IO, TYPE_CHECKING, Iterator, cast -from docx import types as t from docx.drawing import Drawing from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_BREAK @@ -17,6 +16,7 @@ from docx.text.pagebreak import RenderedPageBreak if TYPE_CHECKING: + import docx.types as t from docx.enum.text import WD_UNDERLINE from docx.oxml.text.run import CT_R, CT_Text from docx.shared import Length @@ -170,9 +170,7 @@ def iter_inner_content(self) -> Iterator[str | Drawing | RenderedPageBreak]: yield item elif isinstance(item, CT_LastRenderedPageBreak): yield RenderedPageBreak(item, self) - elif isinstance( # pyright: ignore[reportUnnecessaryIsInstance] - item, CT_Drawing - ): + elif isinstance(item, CT_Drawing): # pyright: ignore[reportUnnecessaryIsInstance] yield Drawing(item, self) @property @@ -185,9 +183,7 @@ def style(self) -> CharacterStyle: 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) - ) + return cast(CharacterStyle, self.part.get_style(style_id, WD_STYLE_TYPE.CHARACTER)) @style.setter def style(self, style_or_name: str | CharacterStyle | None): From e531576191d7709e27b77e9f8aecae7fd68e07a0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 29 Apr 2024 19:47:22 -0700 Subject: [PATCH 093/131] release: prepare v1.1.1 release --- HISTORY.rst | 8 ++++++++ src/docx/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8e0b1a588..51262c4b3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ Release History --------------- +1.1.1 (2024-04-29) +++++++++++++++++++ + +- Fix #531, #1146 Index error on table with misaligned borders +- Fix #1335 Tolerate invalid float value in bottom-margin +- Fix #1337 Do not require typing-extensions at runtime + + 1.1.0 (2023-11-03) ++++++++++++++++++ diff --git a/src/docx/__init__.py b/src/docx/__init__.py index b214045d1..7a4d0bbe8 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from docx.opc.part import Part -__version__ = "1.1.0" +__version__ = "1.1.1" __all__ = ["Document"] From 3f56b7d4f045c92a984491ac45cbfed50f15cdc2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 1 May 2024 12:19:31 -0700 Subject: [PATCH 094/131] rfctr(dev): use more performant `fd` for clean --- Makefile | 3 ++- requirements-test.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0478b2bce..da0d7a4ac 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,8 @@ build: $(BUILD) clean: - find . -type f -name \*.pyc -exec rm {} \; + # find . -type f -name \*.pyc -exec rm {} \; + fd -e pyc -I -x rm rm -rf dist *.egg-info .coverage .DS_Store cleandocs: diff --git a/requirements-test.txt b/requirements-test.txt index 9ee78b43f..b542c1af7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,5 +2,6 @@ behave>=1.2.3 pyparsing>=2.0.1 pytest>=2.5 +pytest-coverage pytest-xdist ruff From e4934749b8c94bec743467f7c0e26384eacbd9a4 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Apr 2024 23:13:57 -0700 Subject: [PATCH 095/131] fix: XmlPart._rel_ref_count `.rel_ref_count()` as implemented was only applicable to `XmlPart` where references to a related part could be present in the XML. Longer term it probably makes sense to override `Part.drop_rel()` in `XmlPart` and not have a `_rel_ref_count()` method in `part` at all, but this works and is less potentially disruptive. --- src/docx/opc/part.py | 19 +++++++++++++------ tests/opc/test_part.py | 39 +++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index 142f49dd1..1353bb850 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Dict, Type +from typing import TYPE_CHECKING, Callable, Dict, Type, cast from docx.opc.oxml import serialize_part_xml from docx.opc.packuri import PackURI @@ -149,11 +149,12 @@ def target_ref(self, rId: str) -> str: 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]) + def _rel_ref_count(self, rId: str) -> int: + """Return the count of references in this part to the relationship identified by `rId`. + + Only an XML part can contain references, so this is 0 for `Part`. + """ + return 0 class PartFactory: @@ -231,3 +232,9 @@ def part(self): That chain of delegation ends here for child objects. """ return self + + def _rel_ref_count(self, rId: str) -> int: + """Return the count of references in this part's XML to the relationship + identified by `rId`.""" + rIds = cast("list[str]", self._element.xpath("//@r:id")) + return len([_rId for _rId in rIds if _rId == rId]) diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index 03eacd361..b156a63f8 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -169,24 +169,13 @@ def it_can_establish_an_external_relationship(self, rels_prop_: Mock, rels_: Moc rels_.get_or_add_ext_rel.assert_called_once_with("http://rel/type", "https://hyper/link") assert rId == "rId27" - @pytest.mark.parametrize( - ("part_cxml", "rel_should_be_dropped"), - [ - ("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 it_can_drop_a_relationship( - self, part_cxml: str, rel_should_be_dropped: bool, rels_prop_: Mock - ): + def it_can_drop_a_relationship(self, rels_prop_: Mock): rels_prop_.return_value = {"rId42": None} - part = Part("partname", "content_type") - part._element = element(part_cxml) # pyright: ignore[reportAttributeAccessIssue] + part = Part(PackURI("/partname"), "content_type") part.drop_rel("rId42") - assert ("rId42" not in part.rels) is rel_should_be_dropped + assert "rId42" not in part.rels def it_can_find_a_related_part_by_reltype( self, rels_prop_: Mock, rels_: Mock, other_part_: Mock @@ -411,6 +400,24 @@ def it_knows_its_the_part_for_its_child_objects(self, part_fixture): xml_part = part_fixture assert xml_part.part is xml_part + @pytest.mark.parametrize( + ("part_cxml", "rel_should_be_dropped"), + [ + ("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 it_only_drops_a_relationship_with_zero_reference_count( + self, part_cxml: str, rel_should_be_dropped: bool, rels_prop_: Mock, package_: Mock + ): + rels_prop_.return_value = {"rId42": None} + part = XmlPart(PackURI("/partname"), "content_type", element(part_cxml), package_) + + part.drop_rel("rId42") + + assert ("rId42" not in part.rels) is rel_should_be_dropped + # fixtures ------------------------------------------------------- @pytest.fixture @@ -452,6 +459,10 @@ def parse_xml_(self, request, element_): def partname_(self, request): return instance_mock(request, PackURI) + @pytest.fixture + def rels_prop_(self, request: FixtureRequest): + return property_mock(request, XmlPart, "rels") + @pytest.fixture def serialize_part_xml_(self, request): return function_mock(request, "docx.opc.part.serialize_part_xml") From f246fde2534e0e0de3c942164db7251f0693d962 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Apr 2024 11:07:39 -0700 Subject: [PATCH 096/131] rfctr: improve typing --- features/steps/coreprops.py | 17 +- src/docx/enum/__init__.py | 11 -- src/docx/image/image.py | 2 +- src/docx/opc/coreprops.py | 33 ++-- src/docx/opc/oxml.py | 13 +- src/docx/opc/package.py | 18 ++- src/docx/opc/packuri.py | 18 +-- src/docx/opc/part.py | 29 ++-- src/docx/opc/parts/coreprops.py | 21 ++- src/docx/opc/rel.py | 35 ++-- src/docx/oxml/coreprops.py | 71 ++++---- src/docx/oxml/document.py | 2 +- src/docx/oxml/parser.py | 2 +- src/docx/oxml/settings.py | 25 ++- src/docx/oxml/shape.py | 56 ++++--- src/docx/oxml/shared.py | 6 +- src/docx/oxml/styles.py | 8 +- src/docx/oxml/table.py | 2 +- src/docx/oxml/text/font.py | 32 ++-- src/docx/package.py | 21 ++- src/docx/parts/document.py | 11 +- src/docx/parts/hdrftr.py | 18 ++- src/docx/parts/image.py | 15 +- src/docx/parts/settings.py | 31 ++-- src/docx/parts/story.py | 8 +- src/docx/section.py | 12 +- src/docx/settings.py | 21 ++- src/docx/shape.py | 26 ++- src/docx/text/run.py | 4 +- tests/opc/parts/test_coreprops.py | 35 ++-- tests/opc/test_coreprops.py | 258 +++++++++++++++--------------- tests/opc/test_part.py | 101 ++++-------- 32 files changed, 501 insertions(+), 461 deletions(-) diff --git a/features/steps/coreprops.py b/features/steps/coreprops.py index 0f6b6a854..0d4e55eb7 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from behave import given, then, when +from behave.runner import Context from docx import Document from docx.opc.coreprops import CoreProperties @@ -13,12 +14,12 @@ @given("a document having known core properties") -def given_a_document_having_known_core_properties(context): +def given_a_document_having_known_core_properties(context: Context): context.document = Document(test_docx("doc-coreprops")) @given("a document having no core properties part") -def given_a_document_having_no_core_properties_part(context): +def given_a_document_having_no_core_properties_part(context: Context): context.document = Document(test_docx("doc-no-coreprops")) @@ -26,12 +27,12 @@ def given_a_document_having_no_core_properties_part(context): @when("I access the core properties object") -def when_I_access_the_core_properties_object(context): +def when_I_access_the_core_properties_object(context: Context): context.document.core_properties @when("I assign new values to the properties") -def when_I_assign_new_values_to_the_properties(context): +def when_I_assign_new_values_to_the_properties(context: Context): context.propvals = ( ("author", "Creator"), ("category", "Category"), @@ -58,7 +59,7 @@ def when_I_assign_new_values_to_the_properties(context): @then("a core properties part with default values is added") -def then_a_core_properties_part_with_default_values_is_added(context): +def then_a_core_properties_part_with_default_values_is_added(context: Context): core_properties = context.document.core_properties assert core_properties.title == "Word Document" assert core_properties.last_modified_by == "python-docx" @@ -71,14 +72,14 @@ def then_a_core_properties_part_with_default_values_is_added(context): @then("I can access the core properties object") -def then_I_can_access_the_core_properties_object(context): +def then_I_can_access_the_core_properties_object(context: Context): document = context.document core_properties = document.core_properties assert isinstance(core_properties, CoreProperties) @then("the core property values match the known values") -def then_the_core_property_values_match_the_known_values(context): +def then_the_core_property_values_match_the_known_values(context: Context): known_propvals = ( ("author", "Steve Canny"), ("category", "Category"), @@ -106,7 +107,7 @@ def then_the_core_property_values_match_the_known_values(context): @then("the core property values match the new values") -def then_the_core_property_values_match_the_new_values(context): +def then_the_core_property_values_match_the_new_values(context: Context): core_properties = context.document.core_properties for name, expected_value in context.propvals: value = getattr(core_properties, name) diff --git a/src/docx/enum/__init__.py b/src/docx/enum/__init__.py index bfab52d36..e69de29bb 100644 --- a/src/docx/enum/__init__.py +++ b/src/docx/enum/__init__.py @@ -1,11 +0,0 @@ -"""Enumerations used in python-docx.""" - - -class Enumeration: - @classmethod - def from_xml(cls, xml_val): - return cls._xml_to_idx[xml_val] - - @classmethod - def to_xml(cls, enum_val): - return cls._idx_to_xml[enum_val] diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 710546fdb..0022b5b45 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -114,7 +114,7 @@ def height(self) -> Inches: return Inches(self.px_height / self.vert_dpi) def scaled_dimensions( - self, width: int | None = None, height: int | None = None + self, width: int | Length | None = None, height: int | Length | None = None ) -> Tuple[Length, Length]: """(cx, cy) pair representing scaled dimensions of this image. diff --git a/src/docx/opc/coreprops.py b/src/docx/opc/coreprops.py index 2fd9a75c8..c564550d4 100644 --- a/src/docx/opc/coreprops.py +++ b/src/docx/opc/coreprops.py @@ -3,12 +3,21 @@ These are broadly-standardized attributes like author, last-modified, etc. """ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from docx.oxml.coreprops import CT_CoreProperties + +if TYPE_CHECKING: + from docx.oxml.coreprops import CT_CoreProperties + class CoreProperties: """Corresponds to part named ``/docProps/core.xml``, containing the core document properties for this document package.""" - def __init__(self, element): + def __init__(self, element: CT_CoreProperties): self._element = element @property @@ -16,7 +25,7 @@ def author(self): return self._element.author_text @author.setter - def author(self, value): + def author(self, value: str): self._element.author_text = value @property @@ -24,7 +33,7 @@ def category(self): return self._element.category_text @category.setter - def category(self, value): + def category(self, value: str): self._element.category_text = value @property @@ -32,7 +41,7 @@ def comments(self): return self._element.comments_text @comments.setter - def comments(self, value): + def comments(self, value: str): self._element.comments_text = value @property @@ -40,7 +49,7 @@ def content_status(self): return self._element.contentStatus_text @content_status.setter - def content_status(self, value): + def content_status(self, value: str): self._element.contentStatus_text = value @property @@ -56,7 +65,7 @@ def identifier(self): return self._element.identifier_text @identifier.setter - def identifier(self, value): + def identifier(self, value: str): self._element.identifier_text = value @property @@ -64,7 +73,7 @@ def keywords(self): return self._element.keywords_text @keywords.setter - def keywords(self, value): + def keywords(self, value: str): self._element.keywords_text = value @property @@ -72,7 +81,7 @@ def language(self): return self._element.language_text @language.setter - def language(self, value): + def language(self, value: str): self._element.language_text = value @property @@ -80,7 +89,7 @@ def last_modified_by(self): return self._element.lastModifiedBy_text @last_modified_by.setter - def last_modified_by(self, value): + def last_modified_by(self, value: str): self._element.lastModifiedBy_text = value @property @@ -112,7 +121,7 @@ def subject(self): return self._element.subject_text @subject.setter - def subject(self, value): + def subject(self, value: str): self._element.subject_text = value @property @@ -120,7 +129,7 @@ def title(self): return self._element.title_text @title.setter - def title(self, value): + def title(self, value: str): self._element.title_text = value @property @@ -128,5 +137,5 @@ def version(self): return self._element.version_text @version.setter - def version(self, value): + def version(self, value: str): self._element.version_text = value diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index 0249de918..7da72f50d 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -7,6 +7,10 @@ deleted or only hold the package related custom element classes. """ +from __future__ import annotations + +from typing import cast + from lxml import etree from docx.opc.constants import NAMESPACE as NS @@ -138,7 +142,7 @@ class CT_Relationship(BaseOxmlElement): target part.""" @staticmethod - def new(rId, reltype, target, target_mode=RTM.INTERNAL): + def new(rId: str, reltype: str, target: str, target_mode: str = RTM.INTERNAL): """Return a new ```` element.""" xml = '' % nsmap["pr"] relationship = parse_xml(xml) @@ -178,7 +182,7 @@ def target_mode(self): class CT_Relationships(BaseOxmlElement): """```` element, the root element in a .rels file.""" - def add_rel(self, rId, reltype, target, is_external=False): + def add_rel(self, rId: str, reltype: str, target: str, is_external: bool = False): """Add a child ```` element with attributes set according to parameter values.""" target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL @@ -186,11 +190,10 @@ def add_rel(self, rId, reltype, target, is_external=False): self.append(relationship) @staticmethod - def new(): + def new() -> CT_Relationships: """Return a new ```` element.""" xml = '' % nsmap["pr"] - relationships = parse_xml(xml) - return relationships + return cast(CT_Relationships, parse_xml(xml)) @property def Relationship_lst(self): diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index 148cd39b1..3b1eef256 100644 --- a/src/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import IO, TYPE_CHECKING, Iterator +from typing import IO, TYPE_CHECKING, Iterator, cast from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.packuri import PACKAGE_URI, PackURI @@ -14,7 +14,9 @@ from docx.shared import lazyproperty if TYPE_CHECKING: + from docx.opc.coreprops import CoreProperties from docx.opc.part import Part + from docx.opc.rel import _Relationship # pyright: ignore[reportPrivateUsage] class OpcPackage: @@ -37,16 +39,18 @@ def after_unmarshal(self): pass @property - def core_properties(self): + def core_properties(self) -> CoreProperties: """|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): + def iter_rels(self) -> Iterator[_Relationship]: """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): + def walk_rels( + source: OpcPackage | Part, visited: list[Part] | None = None + ) -> Iterator[_Relationship]: visited = [] if visited is None else visited for rel in source.rels.values(): yield rel @@ -103,7 +107,7 @@ def main_document_part(self): """ return self.part_related_by(RT.OFFICE_DOCUMENT) - def next_partname(self, template): + def next_partname(self, template: str) -> PackURI: """Return a |PackURI| instance representing partname matching `template`. The returned part-name has the next available numeric suffix to distinguish it @@ -163,13 +167,13 @@ def save(self, pkg_file: str | IO[bytes]): PackageWriter.write(pkg_file, self.rels, self.parts) @property - def _core_properties_part(self): + def _core_properties_part(self) -> CorePropertiesPart: """|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) + return cast(CorePropertiesPart, self.part_related_by(RT.CORE_PROPERTIES)) except KeyError: core_properties_part = CorePropertiesPart.default(self) self.relate_to(core_properties_part, RT.CORE_PROPERTIES) diff --git a/src/docx/opc/packuri.py b/src/docx/opc/packuri.py index fe330d89b..fdbb67ed8 100644 --- a/src/docx/opc/packuri.py +++ b/src/docx/opc/packuri.py @@ -3,6 +3,8 @@ Also some useful known pack URI strings such as PACKAGE_URI. """ +from __future__ import annotations + import posixpath import re @@ -16,22 +18,21 @@ class PackURI(str): _filename_re = re.compile("([a-zA-Z]+)([1-9][0-9]*)?") - def __new__(cls, pack_uri_str): + def __new__(cls, pack_uri_str: 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`.""" + def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI: + """The absolute PackURI 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): + def baseURI(self) -> str: """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- @@ -40,9 +41,8 @@ def baseURI(self): return posixpath.split(self)[0] @property - def ext(self): - """The extension portion of this pack URI, e.g. ``'xml'`` for - ``'/word/document.xml'``. + def ext(self) -> str: + """The extension portion of this pack URI, e.g. ``'xml'`` for ``'/word/document.xml'``. Note the period is not included. """ @@ -84,7 +84,7 @@ def membername(self): """ return self[1:] - def relative_ref(self, baseURI): + def relative_ref(self, baseURI: str): """Return string containing relative reference to package item from `baseURI`. E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would return diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index 1353bb850..e3887ef41 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -1,8 +1,10 @@ +# pyright: reportImportCycles=false + """Open Packaging Convention (OPC) objects related to package parts.""" from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Dict, Type, cast +from typing import TYPE_CHECKING, Callable, Type, cast from docx.opc.oxml import serialize_part_xml from docx.opc.packuri import PackURI @@ -12,6 +14,7 @@ from docx.shared import lazyproperty if TYPE_CHECKING: + from docx.oxml.xmlchemy import BaseOxmlElement from docx.package import Package @@ -24,7 +27,7 @@ class Part: def __init__( self, - partname: str, + partname: PackURI, content_type: str, blob: bytes | None = None, package: Package | None = None, @@ -56,13 +59,13 @@ def before_marshal(self): pass @property - def blob(self): + def blob(self) -> bytes: """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 + return self._blob or b"" @property def content_type(self): @@ -79,7 +82,7 @@ def drop_rel(self, rId: str): del self.rels[rId] @classmethod - def load(cls, partname: str, content_type: str, blob: bytes, package: Package): + def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package): return cls(partname, content_type, blob, package) def load_rel(self, reltype: str, target: Part | str, rId: str, is_external: bool = False): @@ -105,7 +108,7 @@ def partname(self): return self._partname @partname.setter - def partname(self, partname): + def partname(self, partname: str): if not isinstance(partname, PackURI): tmpl = "partname must be instance of PackURI, got '%s'" raise TypeError(tmpl % type(partname).__name__) @@ -127,9 +130,9 @@ def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) new relationship is created. """ if is_external: - return self.rels.get_or_add_ext_rel(reltype, target) + return self.rels.get_or_add_ext_rel(reltype, cast(str, target)) else: - rel = self.rels.get_or_add(reltype, target) + rel = self.rels.get_or_add(reltype, cast(Part, target)) return rel.rId @property @@ -171,12 +174,12 @@ class PartFactory: """ part_class_selector: Callable[[str, str], Type[Part] | None] | None - part_type_for: Dict[str, Type[Part]] = {} + part_type_for: dict[str, Type[Part]] = {} default_part_type = Part def __new__( cls, - partname: str, + partname: PackURI, content_type: str, reltype: str, blob: bytes, @@ -206,7 +209,9 @@ class XmlPart(Part): reserializing the XML payload and managing relationships to other parts. """ - def __init__(self, partname, content_type, element, package): + def __init__( + self, partname: PackURI, content_type: str, element: BaseOxmlElement, package: Package + ): super(XmlPart, self).__init__(partname, content_type, package=package) self._element = element @@ -220,7 +225,7 @@ def element(self): return self._element @classmethod - def load(cls, partname, content_type, blob, package): + def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package): element = parse_xml(blob) return cls(partname, content_type, element, package) diff --git a/src/docx/opc/parts/coreprops.py b/src/docx/opc/parts/coreprops.py index 6e26e1d05..0d818f18d 100644 --- a/src/docx/opc/parts/coreprops.py +++ b/src/docx/opc/parts/coreprops.py @@ -1,6 +1,9 @@ """Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" -from datetime import datetime +from __future__ import annotations + +import datetime as dt +from typing import TYPE_CHECKING from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.coreprops import CoreProperties @@ -8,13 +11,19 @@ from docx.opc.part import XmlPart from docx.oxml.coreprops import CT_CoreProperties +if TYPE_CHECKING: + from docx.opc.package import OpcPackage + class CorePropertiesPart(XmlPart): - """Corresponds to part named ``/docProps/core.xml``, containing the core document - properties for this document package.""" + """Corresponds to part named ``/docProps/core.xml``. + + The "core" is short for "Dublin Core" and contains document metadata relatively common across + documents of all types, not just DOCX. + """ @classmethod - def default(cls, package): + def default(cls, package: OpcPackage): """Return a new |CorePropertiesPart| object initialized with default values for its base properties.""" core_properties_part = cls._new(package) @@ -22,7 +31,7 @@ def default(cls, package): core_properties.title = "Word Document" core_properties.last_modified_by = "python-docx" core_properties.revision = 1 - core_properties.modified = datetime.utcnow() + core_properties.modified = dt.datetime.utcnow() return core_properties_part @property @@ -32,7 +41,7 @@ def core_properties(self): return CoreProperties(self.element) @classmethod - def _new(cls, package): + def _new(cls, package: OpcPackage) -> CorePropertiesPart: partname = PackURI("/docProps/core.xml") content_type = CT.OPC_CORE_PROPERTIES coreProperties = CT_CoreProperties.new() diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index 5fae7ad9c..47e8860d8 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any, Dict, cast from docx.opc.oxml import CT_Relationships @@ -16,7 +16,7 @@ class Relationships(Dict[str, "_Relationship"]): def __init__(self, baseURI: str): super(Relationships, self).__init__() self._baseURI = baseURI - self._target_parts_by_rId: Dict[str, Any] = {} + self._target_parts_by_rId: dict[str, Any] = {} def add_relationship( self, reltype: str, target: Part | str, rId: str, is_external: bool = False @@ -37,7 +37,7 @@ def get_or_add(self, reltype: str, target_part: Part) -> _Relationship: rel = self.add_relationship(reltype, target_part, rId) return rel - def get_or_add_ext_rel(self, reltype, target_ref): + def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str: """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) @@ -46,7 +46,7 @@ def get_or_add_ext_rel(self, reltype, target_ref): rel = self.add_relationship(reltype, target_ref, rId, is_external=True) return rel.rId - def part_with_reltype(self, reltype): + def part_with_reltype(self, reltype: str) -> Part: """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) @@ -59,7 +59,7 @@ def related_parts(self): return self._target_parts_by_rId @property - def xml(self): + def xml(self) -> str: """Serialize this relationship collection into XML suitable for storage as a .rels file in an OPC package.""" rels_elm = CT_Relationships.new() @@ -73,7 +73,7 @@ def _get_matching( """Return relationship of matching `reltype`, `target`, and `is_external` from collection, or None if not found.""" - def matches(rel, reltype, target, is_external): + def matches(rel: _Relationship, reltype: str, target: Part | str, is_external: bool): if rel.reltype != reltype: return False if rel.is_external != is_external: @@ -88,7 +88,7 @@ def matches(rel, reltype, target, is_external): return rel return None - def _get_rel_of_type(self, reltype): + def _get_rel_of_type(self, reltype: str): """Return single relationship of type `reltype` from the collection. Raises |KeyError| if no matching relationship is found. Raises |ValueError| if @@ -104,7 +104,7 @@ def _get_rel_of_type(self, reltype): return matching[0] @property - def _next_rId(self) -> str: + def _next_rId(self) -> str: # pyright: ignore[reportReturnType] """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): @@ -116,7 +116,9 @@ def _next_rId(self) -> str: class _Relationship: """Value object for relationship to part.""" - def __init__(self, rId: str, reltype, target, baseURI, external=False): + def __init__( + self, rId: str, reltype: str, target: Part | str, baseURI: str, external: bool = False + ): super(_Relationship, self).__init__() self._rId = rId self._reltype = reltype @@ -125,28 +127,29 @@ def __init__(self, rId: str, reltype, target, baseURI, external=False): self._is_external = bool(external) @property - def is_external(self): + def is_external(self) -> bool: return self._is_external @property - def reltype(self): + def reltype(self) -> str: return self._reltype @property - def rId(self): + def rId(self) -> str: return self._rId @property - def target_part(self): + def target_part(self) -> Part: if self._is_external: raise ValueError( "target_part property on _Relationship is undef" "ined when target mode is External" ) - return self._target + return cast("Part", self._target) @property def target_ref(self) -> str: if self._is_external: - return self._target + return cast(str, self._target) else: - return self._target.partname.relative_ref(self._baseURI) + target = cast("Part", self._target) + return target.partname.relative_ref(self._baseURI) diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 2cafcd960..93f8890c7 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -1,13 +1,18 @@ """Custom element classes for core properties-related XML elements.""" +from __future__ import annotations + +import datetime as dt import re -from datetime import datetime, timedelta -from typing import Any +from typing import TYPE_CHECKING, Any, Callable from docx.oxml.ns import nsdecls, qn from docx.oxml.parser import parse_xml from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne +if TYPE_CHECKING: + from lxml.etree import _Element as etree_Element # pyright: ignore[reportPrivateUsage] + class CT_CoreProperties(BaseOxmlElement): """`` element, the root element of the Core Properties part. @@ -17,6 +22,8 @@ class CT_CoreProperties(BaseOxmlElement): present in the XML. String elements are limited in length to 255 unicode characters. """ + get_or_add_revision: Callable[[], etree_Element] + category = ZeroOrOne("cp:category", successors=()) contentStatus = ZeroOrOne("cp:contentStatus", successors=()) created = ZeroOrOne("dcterms:created", successors=()) @@ -28,7 +35,9 @@ class CT_CoreProperties(BaseOxmlElement): lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=()) lastPrinted = ZeroOrOne("cp:lastPrinted", successors=()) modified = ZeroOrOne("dcterms:modified", successors=()) - revision = ZeroOrOne("cp:revision", successors=()) + revision: etree_Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "cp:revision", successors=() + ) subject = ZeroOrOne("dc:subject", successors=()) title = ZeroOrOne("dc:title", successors=()) version = ZeroOrOne("cp:version", successors=()) @@ -80,7 +89,7 @@ def created_datetime(self): return self._datetime_of_element("created") @created_datetime.setter - def created_datetime(self, value): + def created_datetime(self, value: dt.datetime): self._set_element_datetime("created", value) @property @@ -88,7 +97,7 @@ def identifier_text(self): return self._text_of_element("identifier") @identifier_text.setter - def identifier_text(self, value): + def identifier_text(self, value: str): self._set_element_text("identifier", value) @property @@ -96,7 +105,7 @@ def keywords_text(self): return self._text_of_element("keywords") @keywords_text.setter - def keywords_text(self, value): + def keywords_text(self, value: str): self._set_element_text("keywords", value) @property @@ -104,7 +113,7 @@ def language_text(self): return self._text_of_element("language") @language_text.setter - def language_text(self, value): + def language_text(self, value: str): self._set_element_text("language", value) @property @@ -112,7 +121,7 @@ def lastModifiedBy_text(self): return self._text_of_element("lastModifiedBy") @lastModifiedBy_text.setter - def lastModifiedBy_text(self, value): + def lastModifiedBy_text(self, value: str): self._set_element_text("lastModifiedBy", value) @property @@ -120,15 +129,15 @@ def lastPrinted_datetime(self): return self._datetime_of_element("lastPrinted") @lastPrinted_datetime.setter - def lastPrinted_datetime(self, value): + def lastPrinted_datetime(self, value: dt.datetime): self._set_element_datetime("lastPrinted", value) @property - def modified_datetime(self): + def modified_datetime(self) -> dt.datetime | None: return self._datetime_of_element("modified") @modified_datetime.setter - def modified_datetime(self, value): + def modified_datetime(self, value: dt.datetime): self._set_element_datetime("modified", value) @property @@ -137,7 +146,7 @@ def revision_number(self): revision = self.revision if revision is None: return 0 - revision_str = revision.text + revision_str = str(revision.text) try: revision = int(revision_str) except ValueError: @@ -149,9 +158,9 @@ def revision_number(self): return revision @revision_number.setter - def revision_number(self, value): + def revision_number(self, value: int): """Set revision property to string value of integer `value`.""" - if not isinstance(value, int) or value < 1: + if not isinstance(value, int) or value < 1: # pyright: ignore[reportUnnecessaryIsInstance] tmpl = "revision property requires positive int, got '%s'" raise ValueError(tmpl % value) revision = self.get_or_add_revision() @@ -162,7 +171,7 @@ def subject_text(self): return self._text_of_element("subject") @subject_text.setter - def subject_text(self, value): + def subject_text(self, value: str): self._set_element_text("subject", value) @property @@ -170,7 +179,7 @@ def title_text(self): return self._text_of_element("title") @title_text.setter - def title_text(self, value): + def title_text(self, value: str): self._set_element_text("title", value) @property @@ -178,10 +187,10 @@ def version_text(self): return self._text_of_element("version") @version_text.setter - def version_text(self, value): + def version_text(self, value: str): self._set_element_text("version", value) - def _datetime_of_element(self, property_name): + def _datetime_of_element(self, property_name: str) -> dt.datetime | None: element = getattr(self, property_name) if element is None: return None @@ -192,7 +201,7 @@ def _datetime_of_element(self, property_name): # invalid datetime strings are ignored return None - def _get_or_add(self, prop_name): + def _get_or_add(self, prop_name: str) -> BaseOxmlElement: """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) @@ -200,8 +209,8 @@ def _get_or_add(self, prop_name): return element @classmethod - def _offset_dt(cls, dt, offset_str): - """A |datetime| instance offset from `dt` by timezone offset in `offset_str`. + def _offset_dt(cls, dt_: dt.datetime, offset_str: str) -> dt.datetime: + """A |datetime| instance offset from `dt_` by timezone offset in `offset_str`. `offset_str` is like `"-07:00"`. """ @@ -212,13 +221,13 @@ def _offset_dt(cls, dt, offset_str): 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 + td = dt.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): + def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime: # valid W3CDTF date cases: # yyyy e.g. "2003" # yyyy-mm e.g. "2003-12" @@ -235,22 +244,22 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): # "-07:30", so we have to do it ourselves parseable_part = w3cdtf_str[:19] offset_str = w3cdtf_str[19:] - dt = None + dt_ = None for tmpl in templates: try: - dt = datetime.strptime(parseable_part, tmpl) + dt_ = dt.datetime.strptime(parseable_part, tmpl) except ValueError: continue - if dt is None: + 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 + return cls._offset_dt(dt_, offset_str) + return dt_ - def _set_element_datetime(self, prop_name, value): + def _set_element_datetime(self, prop_name: str, value: dt.datetime): """Set date/time value of child element having `prop_name` to `value`.""" - if not isinstance(value, datetime): + if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance] tmpl = "property requires object, got %s" raise ValueError(tmpl % type(value)) element = self._get_or_add(prop_name) diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py index ff3736f65..36819ef75 100644 --- a/src/docx/oxml/document.py +++ b/src/docx/oxml/document.py @@ -15,7 +15,7 @@ class CT_Document(BaseOxmlElement): """```` element, the root element of a document.xml file.""" - body = ZeroOrOne("w:body") + body: CT_Body = ZeroOrOne("w:body") # pyright: ignore[reportAssignmentType] @property def sectPr_lst(self) -> List[CT_SectPr]: diff --git a/src/docx/oxml/parser.py b/src/docx/oxml/parser.py index a38362676..e16ba30ba 100644 --- a/src/docx/oxml/parser.py +++ b/src/docx/oxml/parser.py @@ -20,7 +20,7 @@ oxml_parser.set_element_class_lookup(element_class_lookup) -def parse_xml(xml: str) -> "BaseOxmlElement": +def parse_xml(xml: str | bytes) -> "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 diff --git a/src/docx/oxml/settings.py b/src/docx/oxml/settings.py index fd39fbd99..d5bb41a6d 100644 --- a/src/docx/oxml/settings.py +++ b/src/docx/oxml/settings.py @@ -1,11 +1,21 @@ """Custom element classes related to document settings.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable + from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne +if TYPE_CHECKING: + from docx.oxml.shared import CT_OnOff + class CT_Settings(BaseOxmlElement): """`w:settings` element, root element for the settings part.""" + get_or_add_evenAndOddHeaders: Callable[[], CT_OnOff] + _remove_evenAndOddHeaders: Callable[[], None] + _tag_seq = ( "w:writeProtection", "w:view", @@ -106,11 +116,13 @@ class CT_Settings(BaseOxmlElement): "w:decimalSymbol", "w:listSeparator", ) - evenAndOddHeaders = ZeroOrOne("w:evenAndOddHeaders", successors=_tag_seq[48:]) + evenAndOddHeaders: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:evenAndOddHeaders", successors=_tag_seq[48:] + ) del _tag_seq @property - def evenAndOddHeaders_val(self): + def evenAndOddHeaders_val(self) -> bool: """Value of `w:evenAndOddHeaders/@w:val` or |None| if not present.""" evenAndOddHeaders = self.evenAndOddHeaders if evenAndOddHeaders is None: @@ -118,8 +130,9 @@ def evenAndOddHeaders_val(self): return evenAndOddHeaders.val @evenAndOddHeaders_val.setter - def evenAndOddHeaders_val(self, value): - if value in [None, False]: + def evenAndOddHeaders_val(self, value: bool | None): + if value is None or value is False: self._remove_evenAndOddHeaders() - else: - self.get_or_add_evenAndOddHeaders().val = value + return + + self.get_or_add_evenAndOddHeaders().val = value diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index 05c96697a..289d35579 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from docx.oxml.ns import nsdecls from docx.oxml.parser import parse_xml @@ -34,48 +34,58 @@ 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) + embed: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "r:embed", ST_RelationshipId + ) + link: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "r:link", ST_RelationshipId + ) class CT_BlipFillProperties(BaseOxmlElement): """```` element, specifies picture properties.""" - blip = ZeroOrOne("a:blip", successors=("a:srcRect", "a:tile", "a:stretch")) + blip: CT_Blip = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:blip", successors=("a:srcRect", "a:tile", "a:stretch") + ) class CT_GraphicalObject(BaseOxmlElement): """```` element, container for a DrawingML object.""" - graphicData = OneAndOnlyOne("a:graphicData") + graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:graphicData" + ) class CT_GraphicalObjectData(BaseOxmlElement): """```` element, container for the XML of a DrawingML object.""" - pic = ZeroOrOne("pic:pic") - uri = RequiredAttribute("uri", XsdToken) + pic: CT_Picture = ZeroOrOne("pic:pic") # pyright: ignore[reportAssignmentType] + uri: str = RequiredAttribute("uri", XsdToken) # pyright: ignore[reportAssignmentType] class CT_Inline(BaseOxmlElement): """`` element, container for an inline shape.""" - extent = OneAndOnlyOne("wp:extent") - docPr = OneAndOnlyOne("wp:docPr") - graphic = OneAndOnlyOne("a:graphic") + extent: CT_PositiveSize2D = OneAndOnlyOne("wp:extent") # pyright: ignore[reportAssignmentType] + docPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "wp:docPr" + ) + graphic: CT_GraphicalObject = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "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 = cast(CT_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.uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" inline.graphic.graphicData._insert_pic(pic) return inline @@ -126,9 +136,13 @@ class CT_NonVisualPictureProperties(BaseOxmlElement): class CT_Picture(BaseOxmlElement): """```` element, a DrawingML picture.""" - nvPicPr = OneAndOnlyOne("pic:nvPicPr") - blipFill = OneAndOnlyOne("pic:blipFill") - spPr = OneAndOnlyOne("pic:spPr") + nvPicPr: CT_PictureNonVisual = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "pic:nvPicPr" + ) + blipFill: CT_BlipFillProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "pic:blipFill" + ) + spPr: CT_ShapeProperties = OneAndOnlyOne("pic:spPr") # pyright: ignore[reportAssignmentType] @classmethod def new(cls, pic_id, filename, rId, cx, cy): @@ -190,8 +204,12 @@ class CT_PositiveSize2D(BaseOxmlElement): Specifies the size of a DrawingML drawing. """ - cx = RequiredAttribute("cx", ST_PositiveCoordinate) - cy = RequiredAttribute("cy", ST_PositiveCoordinate) + cx: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "cx", ST_PositiveCoordinate + ) + cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "cy", ST_PositiveCoordinate + ) class CT_PresetGeometry2D(BaseOxmlElement): diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index a74abc4ac..8c2ebc9a9 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -18,7 +18,7 @@ class CT_DecimalNumber(BaseOxmlElement): val: int = RequiredAttribute("w:val", ST_DecimalNumber) # pyright: ignore[reportAssignmentType] @classmethod - def new(cls, nsptagname, val): + def new(cls, nsptagname: str, val: int): """Return a new ``CT_DecimalNumber`` element having tagname `nsptagname` and ``val`` attribute set to `val`.""" return OxmlElement(nsptagname, attrs={qn("w:val"): str(val)}) @@ -31,7 +31,7 @@ class CT_OnOff(BaseOxmlElement): "off". Defaults to `True`, so `` for example means "bold is turned on". """ - val: bool = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + val: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:val", ST_OnOff, default=True ) @@ -42,7 +42,7 @@ class CT_String(BaseOxmlElement): In those cases, it containing a style name in its `val` attribute. """ - val: str = RequiredAttribute("w:val", ST_String) # pyright: ignore[reportGeneralTypeIssues] + val: str = RequiredAttribute("w:val", ST_String) # pyright: ignore[reportAssignmentType] @classmethod def new(cls, nsptagname: str, val: str): diff --git a/src/docx/oxml/styles.py b/src/docx/oxml/styles.py index e0a3eaeaf..fb0e5d0dd 100644 --- a/src/docx/oxml/styles.py +++ b/src/docx/oxml/styles.py @@ -128,12 +128,10 @@ class CT_Style(BaseOxmlElement): 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 - ) + type: WD_STYLE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:type", WD_STYLE_TYPE ) - styleId: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + styleId: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:styleId", ST_String ) default = OptionalAttribute("w:default", ST_OnOff) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index 42e8cc95c..e38d58562 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -973,5 +973,5 @@ class CT_VMerge(BaseOxmlElement): """```` element, specifying vertical merging behavior of a cell.""" val: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] - "w:val", ST_Merge, default=ST_Merge.CONTINUE # pyright: ignore[reportArgumentType] + "w:val", ST_Merge, default=ST_Merge.CONTINUE ) diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 0e183cf65..140086aab 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -39,10 +39,10 @@ class CT_Fonts(BaseOxmlElement): Specifies typeface name for the various language types. """ - ascii: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + ascii: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:ascii", ST_String ) - hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:hAnsi", ST_String ) @@ -148,18 +148,14 @@ class CT_RPr(BaseOxmlElement): 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:] - ) + 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:] - ) + 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:]) @@ -268,10 +264,7 @@ def subscript(self, value: bool | None) -> None: 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 - ): + elif self.vertAlign is not None and self.vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT: self._remove_vertAlign() @property @@ -295,10 +288,7 @@ def superscript(self, value: bool | None): 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 - ): + elif self.vertAlign is not None and self.vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT: self._remove_vertAlign() @property @@ -353,10 +343,8 @@ def _set_bool_val(self, name: str, value: bool | None): class CT_Underline(BaseOxmlElement): """`` element, specifying the underlining style for a run.""" - val: WD_UNDERLINE | None = ( - OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] - "w:val", WD_UNDERLINE - ) + val: WD_UNDERLINE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:val", WD_UNDERLINE ) diff --git a/src/docx/package.py b/src/docx/package.py index 12a166bf3..7ea47e6e1 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import IO +from typing import IO, cast from docx.image.image import Image from docx.opc.constants import RELATIONSHIP_TYPE as RT @@ -44,16 +44,16 @@ def _gather_image_parts(self): continue if rel.target_part in self.image_parts: continue - self.image_parts.append(rel.target_part) + self.image_parts.append(cast("ImagePart", rel.target_part)) class ImageParts: """Collection of |ImagePart| objects corresponding to images in the package.""" def __init__(self): - self._image_parts = [] + self._image_parts: list[ImagePart] = [] - def __contains__(self, item): + def __contains__(self, item: object): return self._image_parts.__contains__(item) def __iter__(self): @@ -62,7 +62,7 @@ def __iter__(self): def __len__(self): return self._image_parts.__len__() - def append(self, item): + def append(self, item: ImagePart): self._image_parts.append(item) def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart: @@ -77,15 +77,14 @@ def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart: return matching_image_part 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.""" + def _add_image_part(self, image: Image): + """Return |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): + def _get_by_sha1(self, sha1: str) -> ImagePart | None: """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: @@ -93,7 +92,7 @@ def _get_by_sha1(self, sha1): return image_part return None - def _next_image_partname(self, ext): + def _next_image_partname(self, ext: str) -> PackURI: """The next available image partname, starting from ``/word/media/image1.{ext}`` where unused numbers are reused. @@ -101,7 +100,7 @@ def _next_image_partname(self, ext): not include the leading period. """ - def image_partname(n): + def image_partname(n: int) -> PackURI: return PackURI("/word/media/image%d.%s" % (n, ext)) used_numbers = [image_part.partname.idx for image_part in self] diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 81e621c1a..416bb1a27 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +from typing import IO, TYPE_CHECKING, cast from docx.document import Document from docx.enum.style import WD_STYLE_TYPE @@ -17,6 +17,7 @@ if TYPE_CHECKING: from docx.opc.coreprops import CoreProperties + from docx.settings import Settings from docx.styles.style import BaseStyle @@ -101,13 +102,13 @@ def numbering_part(self): self.relate_to(numbering_part, RT.NUMBERING) return numbering_part - def save(self, path_or_stream): + def save(self, path_or_stream: str | IO[bytes]): """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): + def settings(self) -> Settings: """A |Settings| object providing access to the settings in the settings part of this document.""" return self._settings_part.settings @@ -119,14 +120,14 @@ def styles(self): return self._styles_part.styles @property - def _settings_part(self): + def _settings_part(self) -> SettingsPart: """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) + return cast(SettingsPart, self.part_related_by(RT.SETTINGS)) except KeyError: settings_part = SettingsPart.default(self.package) self.relate_to(settings_part, RT.SETTINGS) diff --git a/src/docx/parts/hdrftr.py b/src/docx/parts/hdrftr.py index 46821d780..35113801c 100644 --- a/src/docx/parts/hdrftr.py +++ b/src/docx/parts/hdrftr.py @@ -1,17 +1,23 @@ """Header and footer part objects.""" +from __future__ import annotations + import os +from typing import TYPE_CHECKING from docx.opc.constants import CONTENT_TYPE as CT from docx.oxml.parser import parse_xml from docx.parts.story import StoryPart +if TYPE_CHECKING: + from docx.package import Package + class FooterPart(StoryPart): """Definition of a section footer.""" @classmethod - def new(cls, package): + def new(cls, package: Package): """Return newly created footer part.""" partname = package.next_partname("/word/footer%d.xml") content_type = CT.WML_FOOTER @@ -21,9 +27,7 @@ def new(cls, package): @classmethod 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" - ) + path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-footer.xml") with open(path, "rb") as f: xml_bytes = f.read() return xml_bytes @@ -33,7 +37,7 @@ class HeaderPart(StoryPart): """Definition of a section header.""" @classmethod - def new(cls, package): + def new(cls, package: Package): """Return newly created header part.""" partname = package.next_partname("/word/header%d.xml") content_type = CT.WML_HEADER @@ -43,9 +47,7 @@ def new(cls, package): @classmethod 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" - ) + path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-header.xml") 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 index e4580df74..5aec07077 100644 --- a/src/docx/parts/image.py +++ b/src/docx/parts/image.py @@ -3,11 +3,16 @@ from __future__ import annotations import hashlib +from typing import TYPE_CHECKING from docx.image.image import Image from docx.opc.part import Part from docx.shared import Emu, Inches +if TYPE_CHECKING: + from docx.opc.package import OpcPackage + from docx.opc.packuri import PackURI + class ImagePart(Part): """An image part. @@ -16,7 +21,7 @@ class ImagePart(Part): """ def __init__( - self, partname: str, content_type: str, blob: bytes, image: Image | None = None + self, partname: PackURI, content_type: str, blob: bytes, image: Image | None = None ): super(ImagePart, self).__init__(partname, content_type, blob) self._image = image @@ -36,7 +41,7 @@ def default_cy(self): 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 + height_in_emu = int(round(914400 * px_height / horz_dpi)) return Emu(height_in_emu) @property @@ -52,7 +57,7 @@ def filename(self): return "image.%s" % self.partname.ext @classmethod - def from_image(cls, image, partname): + def from_image(cls, image: Image, partname: PackURI): """Return an |ImagePart| instance newly created from `image` and assigned `partname`.""" return ImagePart(partname, image.content_type, image.blob, image) @@ -64,7 +69,7 @@ def image(self) -> Image: return self._image @classmethod - def load(cls, partname, content_type, blob, package): + def load(cls, partname: PackURI, content_type: str, blob: bytes, package: OpcPackage): """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) @@ -72,4 +77,4 @@ def load(cls, partname, content_type, blob, package): @property def sha1(self): """SHA1 hash digest of the blob of this image part.""" - return hashlib.sha1(self._blob).hexdigest() + return hashlib.sha1(self.blob).hexdigest() diff --git a/src/docx/parts/settings.py b/src/docx/parts/settings.py index d83c9d5ca..116facca2 100644 --- a/src/docx/parts/settings.py +++ b/src/docx/parts/settings.py @@ -1,6 +1,9 @@ """|SettingsPart| and closely related objects.""" +from __future__ import annotations + import os +from typing import TYPE_CHECKING, cast from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.packuri import PackURI @@ -8,31 +11,41 @@ from docx.oxml.parser import parse_xml from docx.settings import Settings +if TYPE_CHECKING: + from docx.oxml.settings import CT_Settings + from docx.package import Package + class SettingsPart(XmlPart): """Document-level settings part of a WordprocessingML (WML) package.""" + def __init__( + self, partname: PackURI, content_type: str, element: CT_Settings, package: Package + ): + super().__init__(partname, content_type, element, package) + self._settings = element + @classmethod - def default(cls, package): + def default(cls, package: 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()) + element = cast("CT_Settings", 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) + def settings(self) -> Settings: + """A |Settings| proxy object for the `w:settings` element in this part. + + Contains the document-level settings for this document. + """ + return Settings(self._settings) @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" - ) + 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 index b5c8ac882..7482c91a8 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import IO, TYPE_CHECKING, Tuple +from typing import IO, TYPE_CHECKING, Tuple, cast from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.part import XmlPart @@ -60,8 +60,8 @@ def get_style_id( def new_pic_inline( self, image_descriptor: str | IO[bytes], - width: Length | None = None, - height: Length | None = None, + width: int | Length | None = None, + height: int | Length | None = None, ) -> CT_Inline: """Return a newly-created `w:inline` element. @@ -92,4 +92,4 @@ def _document_part(self) -> DocumentPart: """|DocumentPart| object for this package.""" package = self.package assert package is not None - return package.main_document_part + return cast("DocumentPart", package.main_document_part) diff --git a/src/docx/section.py b/src/docx/section.py index f72b60867..982a14370 100644 --- a/src/docx/section.py +++ b/src/docx/section.py @@ -160,11 +160,7 @@ def iter_inner_content(self) -> Iterator[Paragraph | Table]: 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) - ) + yield (Paragraph(element, self) if isinstance(element, CT_P) else Table(element, self)) @property def left_margin(self) -> Length | None: @@ -269,12 +265,10 @@ def __init__(self, document_elm: CT_Document, document_part: DocumentPart): self._document_part = document_part @overload - def __getitem__(self, key: int) -> Section: - ... + def __getitem__(self, key: int) -> Section: ... @overload - def __getitem__(self, key: slice) -> List[Section]: - ... + def __getitem__(self, key: slice) -> List[Section]: ... def __getitem__(self, key: int | slice) -> Section | List[Section]: if isinstance(key, slice): diff --git a/src/docx/settings.py b/src/docx/settings.py index 78f816e87..0a5aa2f36 100644 --- a/src/docx/settings.py +++ b/src/docx/settings.py @@ -1,7 +1,16 @@ """Settings object, providing access to document-level settings.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + from docx.shared import ElementProxy +if TYPE_CHECKING: + import docx.types as t + from docx.oxml.settings import CT_Settings + from docx.oxml.xmlchemy import BaseOxmlElement + class Settings(ElementProxy): """Provides access to document-level settings for a document. @@ -9,14 +18,18 @@ class Settings(ElementProxy): Accessed using the :attr:`.Document.settings` property. """ + def __init__(self, element: BaseOxmlElement, parent: t.ProvidesXmlPart | None = None): + super().__init__(element, parent) + self._settings = cast("CT_Settings", element) + @property - def odd_and_even_pages_header_footer(self): + def odd_and_even_pages_header_footer(self) -> bool: """True if this document has distinct odd and even page headers and footers. Read/write. """ - return self._element.evenAndOddHeaders_val + return self._settings.evenAndOddHeaders_val @odd_and_even_pages_header_footer.setter - def odd_and_even_pages_header_footer(self, value): - self._element.evenAndOddHeaders_val = value + def odd_and_even_pages_header_footer(self, value: bool): + self._settings.evenAndOddHeaders_val = value diff --git a/src/docx/shape.py b/src/docx/shape.py index b91ecbf64..cd35deb35 100644 --- a/src/docx/shape.py +++ b/src/docx/shape.py @@ -3,26 +3,36 @@ A shape is a visual object that appears on the drawing layer of a document. """ +from __future__ import annotations + +from typing import TYPE_CHECKING + from docx.enum.shape import WD_INLINE_SHAPE from docx.oxml.ns import nsmap from docx.shared import Parented +if TYPE_CHECKING: + from docx.oxml.document import CT_Body + from docx.oxml.shape import CT_Inline + from docx.parts.story import StoryPart + from docx.shared import Length + 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): + def __init__(self, body_elm: CT_Body, parent: StoryPart): super(InlineShapes, self).__init__(parent) self._body = body_elm - def __getitem__(self, idx): + def __getitem__(self, idx: int): """Provide indexed access, e.g. 'inline_shapes[idx]'.""" try: inline = self._inline_lst[idx] except IndexError: msg = "inline shape index [%d] out of range" % idx raise IndexError(msg) + return InlineShape(inline) def __iter__(self): @@ -42,12 +52,12 @@ class InlineShape: """Proxy for an ```` element, representing the container for an inline graphical object.""" - def __init__(self, inline): + def __init__(self, inline: CT_Inline): super(InlineShape, self).__init__() self._inline = inline @property - def height(self): + def height(self) -> Length: """Read/write. The display height of this inline shape as an |Emu| instance. @@ -55,7 +65,7 @@ def height(self): return self._inline.extent.cy @height.setter - def height(self, cy): + def height(self, cy: Length): self._inline.extent.cy = cy self._inline.graphic.graphicData.pic.spPr.cy = cy @@ -88,6 +98,6 @@ def width(self): return self._inline.extent.cx @width.setter - def width(self, cx): + def width(self, cx: Length): self._inline.extent.cx = cx self._inline.graphic.graphicData.pic.spPr.cx = cx diff --git a/src/docx/text/run.py b/src/docx/text/run.py index daa604e87..0e2f5bc17 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -59,8 +59,8 @@ def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): def add_picture( self, image_path_or_stream: str | IO[bytes], - width: Length | None = None, - height: Length | None = None, + width: int | Length | None = None, + height: int | Length | None = None, ) -> InlineShape: """Return |InlineShape| containing image identified by `image_path_or_stream`. diff --git a/tests/opc/parts/test_coreprops.py b/tests/opc/parts/test_coreprops.py index 1db650353..5bcf49651 100644 --- a/tests/opc/parts/test_coreprops.py +++ b/tests/opc/parts/test_coreprops.py @@ -5,22 +5,32 @@ import pytest from docx.opc.coreprops import CoreProperties +from docx.opc.package import OpcPackage +from docx.opc.packuri import PackURI from docx.opc.parts.coreprops import CorePropertiesPart -from docx.oxml.coreprops import CT_CoreProperties -from ...unitutil.mock import class_mock, instance_mock +from ...unitutil.cxml import element +from ...unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock class DescribeCorePropertiesPart: - def it_provides_access_to_its_core_props_object(self, coreprops_fixture): - core_properties_part, CoreProperties_ = coreprops_fixture + """Unit-test suite for `docx.opc.parts.coreprops.CorePropertiesPart` objects.""" + + def it_provides_access_to_its_core_props_object(self, CoreProperties_: Mock, package_: Mock): + core_properties_part = CorePropertiesPart( + PackURI("/part/name"), "content/type", element("cp:coreProperties"), package_ + ) + core_properties = core_properties_part.core_properties + CoreProperties_.assert_called_once_with(core_properties_part.element) assert isinstance(core_properties, CoreProperties) - def it_can_create_a_default_core_properties_part(self): - core_properties_part = CorePropertiesPart.default(None) + def it_can_create_a_default_core_properties_part(self, package_: Mock): + core_properties_part = CorePropertiesPart.default(package_) + 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" @@ -32,16 +42,9 @@ def it_can_create_a_default_core_properties_part(self): # fixtures --------------------------------------------- @pytest.fixture - def coreprops_fixture(self, element_, CoreProperties_): - core_properties_part = CorePropertiesPart(None, None, element_, None) - return core_properties_part, CoreProperties_ - - # fixture components ----------------------------------- - - @pytest.fixture - def CoreProperties_(self, request): + def CoreProperties_(self, request: FixtureRequest): return class_mock(request, "docx.opc.parts.coreprops.CoreProperties") @pytest.fixture - def element_(self, request): - return instance_mock(request, CT_CoreProperties) + def package_(self, request: FixtureRequest): + return instance_mock(request, OpcPackage) diff --git a/tests/opc/test_coreprops.py b/tests/opc/test_coreprops.py index 2978ad5ae..0214cdbdf 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -1,160 +1,153 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for the docx.opc.coreprops module.""" -from datetime import datetime +from __future__ import annotations + +import datetime as dt +from typing import TYPE_CHECKING, cast import pytest from docx.opc.coreprops import CoreProperties from docx.oxml.parser import parse_xml +if TYPE_CHECKING: + from docx.oxml.coreprops import CT_CoreProperties + class DescribeCoreProperties: - def it_knows_the_string_property_values(self, text_prop_get_fixture): - core_properties, prop_name, expected_value = text_prop_get_fixture + """Unit-test suite for `docx.opc.coreprops.CoreProperties` objects.""" + + @pytest.mark.parametrize( + ("prop_name", "expected_value"), + [ + ("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 it_knows_the_string_property_values( + self, prop_name: str, expected_value: str, core_properties: CoreProperties + ): actual_value = getattr(core_properties, prop_name) assert actual_value == expected_value - def it_can_change_the_string_property_values(self, text_prop_set_fixture): - core_properties, prop_name, value, expected_xml = text_prop_set_fixture - setattr(core_properties, prop_name, value) - assert core_properties._element.xml == expected_xml - - def it_knows_the_date_property_values(self, date_prop_get_fixture): - core_properties, prop_name, expected_datetime = date_prop_get_fixture - actual_datetime = getattr(core_properties, prop_name) - assert actual_datetime == expected_datetime + @pytest.mark.parametrize( + ("prop_name", "tagname", "value"), + [ + ("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 it_can_change_the_string_property_values(self, prop_name: str, tagname: str, value: str): + coreProperties = self.coreProperties(tagname="", str_val="") + core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties))) - def it_can_change_the_date_property_values(self, 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 - - def it_knows_the_revision_number(self, revision_get_fixture): - core_properties, expected_revision = revision_get_fixture - assert core_properties.revision == expected_revision - - def it_can_change_the_revision_number(self, revision_set_fixture): - core_properties, revision, expected_xml = revision_set_fixture - core_properties.revision = revision - assert core_properties._element.xml == expected_xml - # fixtures ------------------------------------------------------- + assert core_properties._element.xml == self.coreProperties(tagname, value) - @pytest.fixture( - params=[ - ("created", datetime(2012, 11, 17, 16, 37, 40)), - ("last_printed", datetime(2014, 6, 4, 4, 28)), + @pytest.mark.parametrize( + ("prop_name", "expected_datetime"), + [ + ("created", dt.datetime(2012, 11, 17, 16, 37, 40)), + ("last_printed", dt.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 + def it_knows_the_date_property_values( + self, prop_name: str, expected_datetime: dt.datetime, core_properties: CoreProperties + ): + actual_datetime = getattr(core_properties, prop_name) + assert actual_datetime == expected_datetime - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("prop_name", "tagname", "value", "str_val", "attrs"), + [ ( "created", "dcterms:created", - datetime(2001, 2, 3, 4, 5), + dt.datetime(2001, 2, 3, 4, 5), "2001-02-03T04:05:00Z", ' xsi:type="dcterms:W3CDTF"', ), ( "last_printed", "cp:lastPrinted", - datetime(2014, 6, 4, 4), + dt.datetime(2014, 6, 4, 4), "2014-06-04T04:00:00Z", "", ), ( "modified", "dcterms:modified", - datetime(2005, 4, 3, 2, 1), + dt.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) - core_properties = CoreProperties(parse_xml(coreProperties)) + def it_can_change_the_date_property_values( + self, prop_name: str, tagname: str, value: dt.datetime, str_val: str, attrs: str + ): + coreProperties = self.coreProperties(tagname="", str_val="") + core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties))) 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)] - ) - def revision_get_fixture(self, request): - str_val, expected_revision = request.param - 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"), - ] + setattr(core_properties, prop_name, value) + + assert core_properties._element.xml == expected_xml + + @pytest.mark.parametrize( + ("str_val", "expected_value"), + [("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)], ) - def revision_set_fixture(self, request): - value, str_val = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CoreProperties(parse_xml(coreProperties)) + def it_knows_the_revision_number(self, str_val: str | None, expected_value: int): + tagname, str_val = ("cp:revision", str_val) if str_val else ("", "") + coreProperties = self.coreProperties(tagname, str_val or "") + core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties))) + + assert core_properties.revision == expected_value + + @pytest.mark.parametrize(("value", "str_val"), [(42, "42")]) + def it_can_change_the_revision_number(self, value: int, str_val: str): + coreProperties = self.coreProperties(tagname="", str_val="") + core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties))) 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"), - ] - ) - def text_prop_get_fixture(self, request, core_properties): - prop_name, expected_value = request.param - return core_properties, prop_name, expected_value + core_properties.revision = 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"), - ] - ) - def text_prop_set_fixture(self, request): - prop_name, tagname, value = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CoreProperties(parse_xml(coreProperties)) - expected_xml = self.coreProperties(tagname, value) - return core_properties, prop_name, value, expected_xml + assert core_properties._element.xml == expected_xml - # fixture components --------------------------------------------- + # fixtures ------------------------------------------------------- - def coreProperties(self, tagname, str_val, attrs=""): + def coreProperties(self, tagname: str, str_val: str, attrs: str = "") -> str: tmpl = ( - '%s\n' + "%s\n" ) if not tagname: child_element = "" @@ -166,27 +159,30 @@ def coreProperties(self, tagname, str_val, attrs=""): @pytest.fixture def core_properties(self): - element = parse_xml( - b"" - b'\n\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" + element = cast( + "CT_CoreProperties", + parse_xml( + b"" + b'\n\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" + ), ) return CoreProperties(element) diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index b156a63f8..dbbcaf262 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -28,99 +28,54 @@ class DescribePart: - def it_can_be_constructed_by_PartFactory( - self, partname_, content_type_, blob_, package_, __init_ - ): - part = Part.load(partname_, content_type_, blob_, package_) + """Unit-test suite for `docx.opc.part.Part` objects.""" - __init_.assert_called_once_with(ANY, partname_, content_type_, blob_, package_) + def it_can_be_constructed_by_PartFactory(self, package_: Mock, init__: Mock): + part = Part.load(PackURI("/part/name"), "content/type", b"1be2", package_) + + init__.assert_called_once_with(ANY, "/part/name", "content/type", b"1be2", package_) assert isinstance(part, Part) - def it_knows_its_partname(self, partname_get_fixture): - part, expected_partname = partname_get_fixture - assert part.partname == expected_partname + def it_knows_its_partname(self): + part = Part(PackURI("/part/name"), "content/type") + assert part.partname == "/part/name" - def it_can_change_its_partname(self, partname_set_fixture): - part, new_partname = partname_set_fixture - part.partname = new_partname - assert part.partname == new_partname + def it_can_change_its_partname(self): + part = Part(PackURI("/old/part/name"), "content/type") + part.partname = PackURI("/new/part/name") + assert part.partname == "/new/part/name" - def it_knows_its_content_type(self, content_type_fixture): - part, expected_content_type = content_type_fixture - assert part.content_type == expected_content_type + def it_knows_its_content_type(self): + part = Part(PackURI("/part/name"), "content/type") + assert part.content_type == "content/type" - def it_knows_the_package_it_belongs_to(self, package_get_fixture): - part, expected_package = package_get_fixture - assert part.package == expected_package + def it_knows_the_package_it_belongs_to(self, package_: Mock): + part = Part(PackURI("/part/name"), "content/type", package=package_) + assert part.package is package_ - def it_can_be_notified_after_unmarshalling_is_complete(self, part): + def it_can_be_notified_after_unmarshalling_is_complete(self): + part = Part(PackURI("/part/name"), "content/type") part.after_unmarshal() - def it_can_be_notified_before_marshalling_is_started(self, part): + def it_can_be_notified_before_marshalling_is_started(self): + part = Part(PackURI("/part/name"), "content/type") part.before_marshal() - def it_uses_the_load_blob_as_its_blob(self, blob_fixture): - part, load_blob = blob_fixture - assert part.blob is load_blob + def it_uses_the_load_blob_as_its_blob(self): + blob = b"abcde" + part = Part(PackURI("/part/name"), "content/type", blob) + assert part.blob is blob # fixtures --------------------------------------------- @pytest.fixture - def blob_fixture(self, blob_): - part = Part(None, None, blob_, None) - return part, blob_ - - @pytest.fixture - def content_type_fixture(self): - content_type = "content/type" - part = Part(None, content_type, None, None) - return part, content_type - - @pytest.fixture - def package_get_fixture(self, package_): - part = Part(None, None, None, package_) - return part, package_ - - @pytest.fixture - def part(self): - part = Part(None, None) - return part - - @pytest.fixture - def partname_get_fixture(self): - 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") - part = Part(old_partname, None, None, None) - return part, new_partname - - # fixture components --------------------------------------------- - - @pytest.fixture - def blob_(self, request): - return instance_mock(request, bytes) - - @pytest.fixture - def content_type_(self, request): - return instance_mock(request, str) - - @pytest.fixture - def __init_(self, request): + def init__(self, request: FixtureRequest): return initializer_mock(request, Part) @pytest.fixture - def package_(self, request): + def package_(self, request: FixtureRequest): return instance_mock(request, OpcPackage) - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - class DescribePartRelationshipManagementInterface: """Unit-test suite for `docx.opc.package.Part` relationship behaviors.""" From 0ec5dcd1eb1c9483947621040d72573d4d35398a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Apr 2024 11:08:45 -0700 Subject: [PATCH 097/131] fix(pkg): pull lxml pin Looks like this cure is worse than the disease. While it may ease installation on Apple Silicon in some instances, it breaks installation on Python 3.12. Pull this pin and we'll just have to live with troublesome `lxml` install on certain Mac/version combinations. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad89abd19..8d483f00b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "lxml>=3.1.0,<=4.9.2", + "lxml>=3.1.0", "typing_extensions>=4.9.0", ] description = "Create, read, and update Microsoft Word .docx files." From 4cbbdab6cf627309a77ca1b8e201e89c70950340 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Apr 2024 11:28:34 -0700 Subject: [PATCH 098/131] fix: accommodate docxtpl use of Part._rels --- src/docx/opc/part.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index e3887ef41..cbb4ab556 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -145,7 +145,9 @@ def related_parts(self): @lazyproperty def rels(self): """|Relationships| instance holding the relationships for this part.""" - return Relationships(self._partname.baseURI) + # -- prevent breakage in `python-docx-template` by retaining legacy `._rels` attribute -- + self._rels = Relationships(self._partname.baseURI) + return self._rels def target_ref(self, rId: str) -> str: """Return URL contained in target ref of relationship identified by `rId`.""" From 0a8e9c40729bc734fa65f354d72d1159c345becc Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 1 May 2024 11:10:48 -0700 Subject: [PATCH 099/131] fix: Python 3.12 fixes --- features/steps/coreprops.py | 18 +++++++++--------- pyproject.toml | 11 +++++++++++ pyrightconfig.json | 21 --------------------- src/docx/opc/parts/coreprops.py | 2 +- src/docx/oxml/coreprops.py | 4 ++-- tests/opc/parts/test_coreprops.py | 9 ++++++--- tests/opc/test_coreprops.py | 4 ++-- tox.ini | 2 +- 8 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 pyrightconfig.json diff --git a/features/steps/coreprops.py b/features/steps/coreprops.py index 0d4e55eb7..90467fb67 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -1,6 +1,6 @@ """Gherkin step implementations for core properties-related features.""" -from datetime import datetime, timedelta +import datetime as dt from behave import given, then, when from behave.runner import Context @@ -38,13 +38,13 @@ def when_I_assign_new_values_to_the_properties(context: Context): ("category", "Category"), ("comments", "Description"), ("content_status", "Content Status"), - ("created", datetime(2013, 6, 15, 12, 34, 56)), + ("created", dt.datetime(2013, 6, 15, 12, 34, 56, tzinfo=dt.timezone.utc)), ("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)), + ("last_printed", dt.datetime(2013, 6, 15, 12, 34, 56, tzinfo=dt.timezone.utc)), + ("modified", dt.datetime(2013, 6, 15, 12, 34, 56, tzinfo=dt.timezone.utc)), ("revision", 9), ("subject", "Subject"), ("title", "Title"), @@ -66,8 +66,8 @@ def then_a_core_properties_part_with_default_values_is_added(context: Context): 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) - modified_timedelta = datetime.utcnow() - core_properties.modified - max_expected_timedelta = timedelta(seconds=2) + modified_timedelta = dt.datetime.now(dt.timezone.utc) - core_properties.modified + max_expected_timedelta = dt.timedelta(seconds=2) assert modified_timedelta < max_expected_timedelta @@ -85,13 +85,13 @@ def then_the_core_property_values_match_the_known_values(context: Context): ("category", "Category"), ("comments", "Description"), ("content_status", "Content Status"), - ("created", datetime(2014, 12, 13, 22, 2, 0)), + ("created", dt.datetime(2014, 12, 13, 22, 2, 0, tzinfo=dt.timezone.utc)), ("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)), + ("last_printed", dt.datetime(2014, 12, 13, 22, 2, 42, tzinfo=dt.timezone.utc)), + ("modified", dt.datetime(2014, 12, 13, 22, 6, 0, tzinfo=dt.timezone.utc)), ("revision", 2), ("subject", "Subject"), ("title", "Title"), diff --git a/pyproject.toml b/pyproject.toml index 8d483f00b..91bac83d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,17 @@ Repository = "https://github.com/python-openxml/python-docx" line-length = 100 target-version = ["py37", "py38", "py39", "py310", "py311"] +[tool.pyright] +include = ["src/docx", "tests"] +pythonPlatform = "All" +pythonVersion = "3.8" +reportImportCycles = true +reportUnnecessaryCast = true +reportUnnecessaryTypeIgnoreComment = true +stubPath = "./typings" +typeCheckingMode = "strict" +verboseOutput = true + [tool.pytest.ini_options] filterwarnings = [ # -- exit on any warning not explicitly ignored here -- diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 21afeb97b..000000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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/src/docx/opc/parts/coreprops.py b/src/docx/opc/parts/coreprops.py index 0d818f18d..fda011218 100644 --- a/src/docx/opc/parts/coreprops.py +++ b/src/docx/opc/parts/coreprops.py @@ -31,7 +31,7 @@ def default(cls, package: OpcPackage): core_properties.title = "Word Document" core_properties.last_modified_by = "python-docx" core_properties.revision = 1 - core_properties.modified = dt.datetime.utcnow() + core_properties.modified = dt.datetime.now(dt.timezone.utc) return core_properties_part @property diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 93f8890c7..8ba9ff42e 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -254,8 +254,8 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime: 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_ + dt_ = cls._offset_dt(dt_, offset_str) + return dt_.replace(tzinfo=dt.timezone.utc) def _set_element_datetime(self, prop_name: str, value: dt.datetime): """Set date/time value of child element having `prop_name` to `value`.""" diff --git a/tests/opc/parts/test_coreprops.py b/tests/opc/parts/test_coreprops.py index 5bcf49651..b754d2d7e 100644 --- a/tests/opc/parts/test_coreprops.py +++ b/tests/opc/parts/test_coreprops.py @@ -1,6 +1,8 @@ """Unit test suite for the docx.opc.parts.coreprops module.""" -from datetime import datetime, timedelta +from __future__ import annotations + +import datetime as dt import pytest @@ -35,8 +37,9 @@ def it_can_create_a_default_core_properties_part(self, package_: Mock): 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) + assert core_properties.modified is not None + delta = dt.datetime.now(dt.timezone.utc) - core_properties.modified + max_expected_delta = dt.timedelta(seconds=2) assert delta < max_expected_delta # fixtures --------------------------------------------- diff --git a/tests/opc/test_coreprops.py b/tests/opc/test_coreprops.py index 0214cdbdf..5d9743397 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -68,8 +68,8 @@ def it_can_change_the_string_property_values(self, prop_name: str, tagname: str, @pytest.mark.parametrize( ("prop_name", "expected_datetime"), [ - ("created", dt.datetime(2012, 11, 17, 16, 37, 40)), - ("last_printed", dt.datetime(2014, 6, 4, 4, 28)), + ("created", dt.datetime(2012, 11, 17, 16, 37, 40, tzinfo=dt.timezone.utc)), + ("last_printed", dt.datetime(2014, 6, 4, 4, 28, tzinfo=dt.timezone.utc)), ("modified", None), ], ) diff --git a/tox.ini b/tox.ini index f8595ba45..37acaa5fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38, py39, py310, py311 +envlist = py38, py39, py310, py311, py312 [testenv] deps = -rrequirements-test.txt From 0cf6d71fb47ede07ecd5de2a8655f9f46c5f083d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 1 May 2024 12:31:09 -0700 Subject: [PATCH 100/131] release: prepare v1.1.2 release --- HISTORY.rst | 7 +++++++ src/docx/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 51262c4b3..0dab17d87 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ Release History --------------- +1.1.2 (2024-05-01) +++++++++++++++++++ + +- Fix #1383 Revert lxml<=4.9.2 pin that breaks Python 3.12 install +- Fix #1385 Support use of Part._rels by python-docx-template +- Add support and testing for Python 3.12 + 1.1.1 (2024-04-29) ++++++++++++++++++ diff --git a/src/docx/__init__.py b/src/docx/__init__.py index 7a4d0bbe8..205221027 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from docx.opc.part import Part -__version__ = "1.1.1" +__version__ = "1.1.2" __all__ = ["Document"] From 3228bc581dfc5891dfeb2db338e7c43be015df88 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 6 Jun 2025 11:26:43 -0700 Subject: [PATCH 101/131] proj: modernize project environment Primarily introducing `uv`. Also bump minimum Python to 3.9. - remove reportImportCycles from pyright config because that doesn't respect the `TYPE_CHECKING` flag so doesn't actually report useful cycles in a project like this where some types are recursive. --- .fdignore | 7 + .projections.json | 14 ++ .rgignore | 9 + pyproject.toml | 23 ++- uv.lock | 415 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 460 insertions(+), 8 deletions(-) create mode 100644 .fdignore create mode 100644 .projections.json create mode 100644 .rgignore create mode 100644 uv.lock diff --git a/.fdignore b/.fdignore new file mode 100644 index 000000000..41bdd3828 --- /dev/null +++ b/.fdignore @@ -0,0 +1,7 @@ +.tox +Session.vim +build/ +docs/.build +features/_scratch +__pycache__/ +src/*.egg-info diff --git a/.projections.json b/.projections.json new file mode 100644 index 000000000..7d68dd4c5 --- /dev/null +++ b/.projections.json @@ -0,0 +1,14 @@ +{ + "src/docx/*.py" : { + "alternate" : [ + "tests/{dirname}/test_{basename}.py" + ], + "type" : "source" + }, + "tests/**/test_*.py" : { + "alternate" : [ + "src/docx/{dirname}/{basename}.py" + ], + "type" : "test" + } +} diff --git a/.rgignore b/.rgignore new file mode 100644 index 000000000..12d71b5b4 --- /dev/null +++ b/.rgignore @@ -0,0 +1,9 @@ +.tox +Session.vim +build/ +docs/.build +features/_scratch +__pycache__/ +ref/ +src/*.egg-info +tests/test_files diff --git a/pyproject.toml b/pyproject.toml index 91bac83d5..7c343f2e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dynamic = ["version"] keywords = ["docx", "office", "openxml", "word"] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.9" [project.urls] Changelog = "https://github.com/python-openxml/python-docx/blob/master/HISTORY.rst" @@ -38,20 +38,18 @@ 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] -line-length = 100 -target-version = ["py37", "py38", "py39", "py310", "py311"] - [tool.pyright] include = ["src/docx", "tests"] pythonPlatform = "All" -pythonVersion = "3.8" -reportImportCycles = true +pythonVersion = "3.9" +reportImportCycles = false reportUnnecessaryCast = true reportUnnecessaryTypeIgnoreComment = true stubPath = "./typings" typeCheckingMode = "strict" verboseOutput = true +venvPath = "." +venv = ".venv" [tool.pytest.ini_options] filterwarnings = [ @@ -88,7 +86,6 @@ target-version = "py38" 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 -- @@ -111,3 +108,13 @@ known-local-folder = ["helpers"] [tool.setuptools.dynamic] version = {attr = "docx.__version__"} + +[dependency-groups] +dev = [ + "behave>=1.2.6", + "pyparsing>=3.2.3", + "pyright>=1.1.401", + "pytest>=8.4.0", + "ruff>=0.11.13", + "types-lxml-multi-subclass>=2025.3.30", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..bbef867c8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,415 @@ +version = 1 +revision = 1 +requires-python = ">=3.9" + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "behave" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parse" }, + { name = "parse-type" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/4b/d0a8c23b6c8985e5544ea96d27105a273ea22051317f850c2cdbf2029fe4/behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", size = 701696 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/6c/ec9169548b6c4cb877aaa6773408ca08ae2a282805b958dbc163cb19822d/behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c", size = 136779 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cssselect" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "lxml" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838 }, + { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827 }, + { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098 }, + { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261 }, + { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621 }, + { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231 }, + { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279 }, + { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405 }, + { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169 }, + { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691 }, + { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503 }, + { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346 }, + { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139 }, + { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609 }, + { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285 }, + { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507 }, + { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104 }, + { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240 }, + { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685 }, + { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164 }, + { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206 }, + { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144 }, + { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124 }, + { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520 }, + { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016 }, + { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884 }, + { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690 }, + { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418 }, + { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092 }, + { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231 }, + { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798 }, + { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195 }, + { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243 }, + { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197 }, + { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, + { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, + { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, + { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, + { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, + { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, + { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, + { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, + { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, + { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, + { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, + { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, + { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, + { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, + { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, + { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, + { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086 }, + { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613 }, + { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008 }, + { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915 }, + { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890 }, + { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817 }, + { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916 }, + { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274 }, + { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757 }, + { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028 }, + { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487 }, + { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688 }, + { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043 }, + { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569 }, + { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270 }, + { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606 }, + { url = "https://files.pythonhosted.org/packages/1e/04/acd238222ea25683e43ac7113facc380b3aaf77c53e7d88c4f544cef02ca/lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e", size = 8082189 }, + { url = "https://files.pythonhosted.org/packages/d6/4e/cc7fe9ccb9999cc648492ce970b63c657606aefc7d0fba46b17aa2ba93fb/lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40", size = 4384950 }, + { url = "https://files.pythonhosted.org/packages/56/bf/acd219c489346d0243a30769b9d446b71e5608581db49a18c8d91a669e19/lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729", size = 5209823 }, + { url = "https://files.pythonhosted.org/packages/57/51/ec31cd33175c09aa7b93d101f56eed43d89e15504455d884d021df7166a7/lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87", size = 4931808 }, + { url = "https://files.pythonhosted.org/packages/e5/68/865d229f191514da1777125598d028dc88a5ea300d68c30e1f120bfd01bd/lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd", size = 5086067 }, + { url = "https://files.pythonhosted.org/packages/82/01/4c958c5848b4e263cd9e83dff6b49f975a5a0854feb1070dfe0bdcdf70a0/lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433", size = 4929026 }, + { url = "https://files.pythonhosted.org/packages/55/31/5327d8af74d7f35e645b40ae6658761e1fee59ebecaa6a8d295e495c2ca9/lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140", size = 5134245 }, + { url = "https://files.pythonhosted.org/packages/6f/c9/204eba2400beb0016dacc2c5335ecb1e37f397796683ffdb7f471e86bddb/lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5", size = 5001020 }, + { url = "https://files.pythonhosted.org/packages/07/53/979165f50a853dab1cf3b9e53105032d55f85c5993f94afc4d9a61a22877/lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142", size = 5192346 }, + { url = "https://files.pythonhosted.org/packages/17/2b/f37b5ae28949143f863ba3066b30eede6107fc9a503bd0d01677d4e2a1e0/lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6", size = 3478275 }, + { url = "https://files.pythonhosted.org/packages/9a/d5/b795a183680126147665a8eeda8e802c180f2f7661aa9a550bba5bcdae63/lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1", size = 3806275 }, + { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319 }, + { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273 }, + { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552 }, + { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091 }, + { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862 }, + { url = "https://files.pythonhosted.org/packages/ad/fb/d19b67e4bb63adc20574ba3476cf763b3514df1a37551084b890254e4b15/lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530", size = 3891034 }, + { url = "https://files.pythonhosted.org/packages/c9/5d/6e1033ee0cdb2f9bc93164f9df14e42cb5bbf1bbed3bf67f687de2763104/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6", size = 4207420 }, + { url = "https://files.pythonhosted.org/packages/f3/4b/23ac79efc32d913259d66672c5f93daac7750a3d97cdc1c1a9a5d1c1b46c/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877", size = 4305106 }, + { url = "https://files.pythonhosted.org/packages/a4/7a/fe558bee63a62f7a75a52111c0a94556c1c1bdcf558cd7d52861de558759/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8", size = 4205587 }, + { url = "https://files.pythonhosted.org/packages/ed/5b/3207e6bd8d67c952acfec6bac9d1fa0ee353202e7c40b335ebe00879ab7d/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d", size = 4329077 }, + { url = "https://files.pythonhosted.org/packages/a1/25/d381abcfd00102d3304aa191caab62f6e3bcbac93ee248771db6be153dfd/lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987", size = 3486416 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 }, +] + +[[package]] +name = "parse-type" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parse" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/e9/a3b2ae5f8a852542788ac1f1865dcea0c549cc40af243f42cabfa0acf24d/parse_type-0.6.4.tar.gz", hash = "sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6", size = 96480 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/b3/f6cc950042bfdbe98672e7c834d930f85920fb7d3359f59096e8d2799617/parse_type-0.6.4-py2.py3-none-any.whl", hash = "sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c", size = 27442 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +] + +[[package]] +name = "pyright" +version = "1.1.401" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/9a/7ab2b333b921b2d6bfcffe05a0e0a0bbeff884bd6fb5ed50cd68e2898e53/pyright-1.1.401.tar.gz", hash = "sha256:788a82b6611fa5e34a326a921d86d898768cddf59edde8e93e56087d277cc6f1", size = 3894193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e6/1f908fce68b0401d41580e0f9acc4c3d1b248adcff00dfaad75cd21a1370/pyright-1.1.401-py3-none-any.whl", hash = "sha256:6fde30492ba5b0d7667c16ecaf6c699fab8d7a1263f6a18549e0b00bf7724c06", size = 5629193 }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, +] + +[[package]] +name = "python-docx" +source = { editable = "." } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] + +[package.dev-dependencies] +dev = [ + { name = "behave" }, + { name = "pyparsing" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "types-lxml-multi-subclass" }, +] + +[package.metadata] +requires-dist = [ + { name = "lxml", specifier = ">=3.1.0" }, + { name = "typing-extensions", specifier = ">=4.9.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "behave", specifier = ">=1.2.6" }, + { name = "pyparsing", specifier = ">=3.2.3" }, + { name = "pyright", specifier = ">=1.1.401" }, + { name = "pytest", specifier = ">=8.4.0" }, + { name = "ruff", specifier = ">=0.11.13" }, + { name = "types-lxml-multi-subclass", specifier = ">=2025.3.30" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516 }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083 }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024 }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324 }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416 }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197 }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615 }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080 }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315 }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462 }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028 }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992 }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944 }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20250516" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ed/9f092ff479e2b5598941855f314a22953bb04b5fb38bcba3f880feb833ba/types_html5lib-1.1.11.20250516.tar.gz", hash = "sha256:65043a6718c97f7d52567cc0cdf41efbfc33b1f92c6c0c5e19f60a7ec69ae720", size = 16136 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/3b/cb5b23c7b51bf48b8c9f175abb9dce2f1ecd2d2c25f92ea9f4e3720e9398/types_html5lib-1.1.11.20250516-py3-none-any.whl", hash = "sha256:5e407b14b1bd2b9b1107cbd1e2e19d4a0c46d60febd231c7ab7313d7405663c1", size = 21770 }, +] + +[[package]] +name = "types-lxml-multi-subclass" +version = "2025.3.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "cssselect" }, + { name = "types-html5lib" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/3a/7f6d1d3b921404efef20ed1ddc2b6f1333e3f0bc5b91da37874e786ff835/types_lxml_multi_subclass-2025.3.30.tar.gz", hash = "sha256:7ac7a78e592fdba16951668968b21511adda49bbefbc0f130e55501b70e068b4", size = 153188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/8e/106b4c5a67e6d52475ef51008e6c27d4ad472690d619dc32e079d28a540b/types_lxml_multi_subclass-2025.3.30-py3-none-any.whl", hash = "sha256:b0563e4e49e66eb8093c44e74b262c59e3be6d3bb3437511e3a4843fd74044d1", size = 93475 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, +] From 4262f4de7985ab5647b364c397ed038e00560098 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 6 Jun 2025 15:26:58 -0700 Subject: [PATCH 102/131] modn: improve type annotation --- src/docx/opc/coreprops.py | 9 +++++---- src/docx/opc/oxml.py | 25 ++++++++++++------------- src/docx/oxml/__init__.py | 1 + src/docx/oxml/coreprops.py | 32 ++++++++++++++++---------------- src/docx/oxml/ns.py | 4 ++-- src/docx/oxml/shape.py | 6 ++---- src/docx/oxml/text/parfmt.py | 5 +++++ src/docx/oxml/xmlchemy.py | 24 +++++++----------------- src/docx/parts/document.py | 2 +- 9 files changed, 51 insertions(+), 57 deletions(-) diff --git a/src/docx/opc/coreprops.py b/src/docx/opc/coreprops.py index c564550d4..62f0c5ab1 100644 --- a/src/docx/opc/coreprops.py +++ b/src/docx/opc/coreprops.py @@ -5,6 +5,7 @@ from __future__ import annotations +import datetime as dt from typing import TYPE_CHECKING from docx.oxml.coreprops import CT_CoreProperties @@ -57,7 +58,7 @@ def created(self): return self._element.created_datetime @created.setter - def created(self, value): + def created(self, value: dt.datetime): self._element.created_datetime = value @property @@ -97,7 +98,7 @@ def last_printed(self): return self._element.lastPrinted_datetime @last_printed.setter - def last_printed(self, value): + def last_printed(self, value: dt.datetime): self._element.lastPrinted_datetime = value @property @@ -105,7 +106,7 @@ def modified(self): return self._element.modified_datetime @modified.setter - def modified(self, value): + def modified(self, value: dt.datetime): self._element.modified_datetime = value @property @@ -113,7 +114,7 @@ def revision(self): return self._element.revision_number @revision.setter - def revision(self, value): + def revision(self, value: int): self._element.revision_number = value @property diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index 7da72f50d..7d3c489d6 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -38,7 +38,7 @@ def parse_xml(text: str) -> etree._Element: return etree.fromstring(text, oxml_parser) -def qn(tag): +def qn(tag: str) -> str: """Stands for "qualified name", a utility function to turn a namespace prefixed tag name into a Clark-notation qualified tag name for lxml. @@ -50,7 +50,7 @@ def qn(tag): return "{%s}%s" % (uri, tagroot) -def serialize_part_xml(part_elm: etree._Element): +def serialize_part_xml(part_elm: etree._Element) -> bytes: """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 @@ -59,7 +59,7 @@ def serialize_part_xml(part_elm: etree._Element): return etree.tostring(part_elm, encoding="UTF-8", standalone=True) -def serialize_for_reading(element): +def serialize_for_reading(element: etree._Element) -> str: """Serialize `element` to human-readable XML suitable for tests. No XML declaration. @@ -77,7 +77,7 @@ class BaseOxmlElement(etree.ElementBase): classes in one place.""" @property - def xml(self): + def xml(self) -> str: """Return XML string for this element, suitable for testing purposes. Pretty printed for readability and without an XML declaration at the top. @@ -86,8 +86,10 @@ def xml(self): class CT_Default(BaseOxmlElement): - """```` element, specifying the default content type to be applied to a - part with the specified extension.""" + """`` element that appears in `[Content_Types].xml` part. + + Used to specify a default content type to be applied to any part with the specified extension. + """ @property def content_type(self): @@ -101,9 +103,8 @@ def extension(self): return self.get("Extension") @staticmethod - def new(ext, content_type): - """Return a new ```` element with attributes set to parameter - values.""" + def new(ext: str, content_type: str): + """Return a new ```` element with attributes set to parameter values.""" xml = '' % nsmap["ct"] default = parse_xml(xml) default.set("Extension", ext) @@ -123,8 +124,7 @@ def content_type(self): @staticmethod def new(partname, content_type): - """Return a new ```` element with attributes set to parameter - values.""" + """Return a new ```` element with attributes set to parameter values.""" xml = '' % nsmap["ct"] override = parse_xml(xml) override.set("PartName", partname) @@ -138,8 +138,7 @@ def partname(self): class CT_Relationship(BaseOxmlElement): - """```` element, representing a single relationship from a source to a - target part.""" + """`` element, representing a single relationship from source to target part.""" @staticmethod def new(rId: str, reltype: str, target: str, target_mode: str = RTM.INTERNAL): diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index bf32932f9..3fbc114ae 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -234,6 +234,7 @@ 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:outlineLvl", CT_DecimalNumber) register_element_cls("w:pageBreakBefore", CT_OnOff) register_element_cls("w:pPr", CT_PPr) register_element_cls("w:pStyle", CT_String) diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py index 8ba9ff42e..fcff0c7ba 100644 --- a/src/docx/oxml/coreprops.py +++ b/src/docx/oxml/coreprops.py @@ -4,7 +4,7 @@ import datetime as dt import re -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, cast from docx.oxml.ns import nsdecls, qn from docx.oxml.parser import parse_xml @@ -45,14 +45,14 @@ class CT_CoreProperties(BaseOxmlElement): _coreProperties_tmpl = "\n" % nsdecls("cp", "dc", "dcterms") @classmethod - def new(cls): + def new(cls) -> CT_CoreProperties: """Return a new `` element.""" xml = cls._coreProperties_tmpl - coreProperties = parse_xml(xml) + coreProperties = cast(CT_CoreProperties, parse_xml(xml)) return coreProperties @property - def author_text(self): + def author_text(self) -> str: """The text in the `dc:creator` child element.""" return self._text_of_element("creator") @@ -77,7 +77,7 @@ def comments_text(self, value: str): self._set_element_text("description", value) @property - def contentStatus_text(self): + def contentStatus_text(self) -> str: return self._text_of_element("contentStatus") @contentStatus_text.setter @@ -85,7 +85,7 @@ def contentStatus_text(self, value: str): self._set_element_text("contentStatus", value) @property - def created_datetime(self): + def created_datetime(self) -> dt.datetime | None: return self._datetime_of_element("created") @created_datetime.setter @@ -93,7 +93,7 @@ def created_datetime(self, value: dt.datetime): self._set_element_datetime("created", value) @property - def identifier_text(self): + def identifier_text(self) -> str: return self._text_of_element("identifier") @identifier_text.setter @@ -101,7 +101,7 @@ def identifier_text(self, value: str): self._set_element_text("identifier", value) @property - def keywords_text(self): + def keywords_text(self) -> str: return self._text_of_element("keywords") @keywords_text.setter @@ -109,7 +109,7 @@ def keywords_text(self, value: str): self._set_element_text("keywords", value) @property - def language_text(self): + def language_text(self) -> str: return self._text_of_element("language") @language_text.setter @@ -117,7 +117,7 @@ def language_text(self, value: str): self._set_element_text("language", value) @property - def lastModifiedBy_text(self): + def lastModifiedBy_text(self) -> str: return self._text_of_element("lastModifiedBy") @lastModifiedBy_text.setter @@ -125,7 +125,7 @@ def lastModifiedBy_text(self, value: str): self._set_element_text("lastModifiedBy", value) @property - def lastPrinted_datetime(self): + def lastPrinted_datetime(self) -> dt.datetime | None: return self._datetime_of_element("lastPrinted") @lastPrinted_datetime.setter @@ -141,7 +141,7 @@ def modified_datetime(self, value: dt.datetime): self._set_element_datetime("modified", value) @property - def revision_number(self): + def revision_number(self) -> int: """Integer value of revision property.""" revision = self.revision if revision is None: @@ -167,7 +167,7 @@ def revision_number(self, value: int): revision.text = str(value) @property - def subject_text(self): + def subject_text(self) -> str: return self._text_of_element("subject") @subject_text.setter @@ -175,7 +175,7 @@ def subject_text(self, value: str): self._set_element_text("subject", value) @property - def title_text(self): + def title_text(self) -> str: return self._text_of_element("title") @title_text.setter @@ -183,7 +183,7 @@ def title_text(self, value: str): self._set_element_text("title", value) @property - def version_text(self): + def version_text(self) -> str: return self._text_of_element("version") @version_text.setter @@ -257,7 +257,7 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime: dt_ = cls._offset_dt(dt_, offset_str) return dt_.replace(tzinfo=dt.timezone.utc) - def _set_element_datetime(self, prop_name: str, value: dt.datetime): + def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None: """Set date/time value of child element having `prop_name` to `value`.""" if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance] tmpl = "property requires object, got %s" diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py index 5bed1e6a0..ce03940f7 100644 --- a/src/docx/oxml/ns.py +++ b/src/docx/oxml/ns.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Dict nsmap = { "a": "http://schemas.openxmlformats.org/drawingml/2006/main", @@ -29,7 +29,7 @@ class NamespacePrefixedTag(str): """Value object that knows the semantics of an XML tag having a namespace prefix.""" - def __new__(cls, nstag: str, *args: Any): + def __new__(cls, nstag: str): return super(NamespacePrefixedTag, cls).__new__(cls, nstag) def __init__(self, nstag: str): diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index 289d35579..00e7593a9 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -145,10 +145,8 @@ class CT_Picture(BaseOxmlElement): spPr: CT_ShapeProperties = OneAndOnlyOne("pic:spPr") # pyright: ignore[reportAssignmentType] @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.""" + def new(cls, pic_id: int, filename: str, rId: str, cx: Length, cy: Length) -> CT_Picture: + """A new minimum viable `` (picture) element.""" pic = parse_xml(cls._pic_xml()) pic.nvPicPr.cNvPr.id = pic_id pic.nvPicPr.cNvPr.name = filename diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py index de5609636..2133686b2 100644 --- a/src/docx/oxml/text/parfmt.py +++ b/src/docx/oxml/text/parfmt.py @@ -10,6 +10,7 @@ WD_TAB_ALIGNMENT, WD_TAB_LEADER, ) +from docx.oxml.shared import CT_DecimalNumber from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure from docx.oxml.xmlchemy import ( BaseOxmlElement, @@ -55,6 +56,7 @@ class CT_PPr(BaseOxmlElement): get_or_add_ind: Callable[[], CT_Ind] get_or_add_pStyle: Callable[[], CT_String] + get_or_add_sectPr: Callable[[], CT_SectPr] _insert_sectPr: Callable[[CT_SectPr], None] _remove_pStyle: Callable[[], None] _remove_sectPr: Callable[[], None] @@ -111,6 +113,9 @@ class CT_PPr(BaseOxmlElement): "w:ind", successors=_tag_seq[23:] ) jc = ZeroOrOne("w:jc", successors=_tag_seq[27:]) + outlineLvl: CT_DecimalNumber = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "w:outlineLvl", successors=_tag_seq[31:] + ) sectPr = ZeroOrOne("w:sectPr", successors=_tag_seq[35:]) del _tag_seq diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index 077bcd583..bc33e1f58 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -5,17 +5,7 @@ from __future__ import annotations import re -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - List, - Sequence, - Tuple, - Type, - TypeVar, -) +from typing import TYPE_CHECKING, Any, Callable, Sequence, Type, TypeVar from lxml import etree from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage] @@ -65,7 +55,7 @@ def __eq__(self, other: object) -> bool: def __ne__(self, other: object) -> bool: return not self.__eq__(other) - def _attr_seq(self, attrs: str) -> List[str]: + def _attr_seq(self, attrs: str) -> list[str]: """Return a sequence of attribute strings parsed from `attrs`. Each attribute string is stripped of whitespace on both ends. @@ -90,7 +80,7 @@ def _eq_elm_strs(self, line: str, line_2: str): return True @classmethod - def _parse_line(cls, line: str) -> Tuple[str, str, str, str]: + def _parse_line(cls, line: str) -> tuple[str, str, str, str]: """(front, attrs, close, text) 4-tuple result of parsing XML element `line`.""" match = cls._xml_elm_line_patt.match(line) if match is None: @@ -105,7 +95,7 @@ def _parse_line(cls, line: str) -> Tuple[str, str, str, str]: class MetaOxmlElement(type): """Metaclass for BaseOxmlElement.""" - def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any]): + def __init__(cls, clsname: str, bases: tuple[type, ...], namespace: dict[str, Any]): dispatchable = ( OneAndOnlyOne, OneOrMore, @@ -280,7 +270,7 @@ class _BaseChildElement: and ZeroOrMore. """ - def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()): + def __init__(self, nsptagname: str, successors: tuple[str, ...] = ()): super(_BaseChildElement, self).__init__() self._nsptagname = nsptagname self._successors = successors @@ -446,7 +436,7 @@ def populate_class_members( # pyright: ignore[reportIncompatibleMethodOverride] self, element_cls: MetaOxmlElement, group_prop_name: str, - successors: Tuple[str, ...], + successors: tuple[str, ...], ) -> None: """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls @@ -597,7 +587,7 @@ class ZeroOrOneChoice(_BaseChildElement): """Correspondes to an ``EG_*`` element group where at most one of its members may appear as a child.""" - def __init__(self, choices: Sequence[Choice], successors: Tuple[str, ...] = ()): + def __init__(self, choices: Sequence[Choice], successors: tuple[str, ...] = ()): self._choices = choices self._successors = successors diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 416bb1a27..648b58aa2 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -5,7 +5,6 @@ from typing import IO, 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 @@ -16,6 +15,7 @@ from docx.shared import lazyproperty if TYPE_CHECKING: + from docx.enum.style import WD_STYLE_TYPE from docx.opc.coreprops import CoreProperties from docx.settings import Settings from docx.styles.style import BaseStyle From afa670a9716b1ac64e6e312ac7ab5efef01ec84a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 6 Jun 2025 11:33:58 -0700 Subject: [PATCH 103/131] modn: modernize tests - inline single-test fixtures - remove obsolete pre-cxml XML builders --- src/docx/dml/color.py | 88 ++++--- src/docx/document.py | 10 +- src/docx/enum/base.py | 10 +- src/docx/opc/package.py | 7 +- src/docx/oxml/text/font.py | 63 ++--- src/docx/parts/document.py | 7 +- src/docx/parts/numbering.py | 5 +- src/docx/shared.py | 8 +- src/docx/text/run.py | 2 +- tests/dml/test_color.py | 149 ++++++------ tests/oxml/unitdata/dml.py | 63 ----- tests/parts/test_document.py | 266 ++++++++++++-------- tests/test_api.py | 67 +++-- tests/test_blkcntnr.py | 172 +++++++------ tests/test_document.py | 410 +++++++++++++++---------------- tests/test_package.py | 120 +++++---- tests/test_section.py | 156 +++++------- tests/test_settings.py | 67 ++--- tests/test_shape.py | 269 ++++++++------------ tests/test_shared.py | 128 +++++----- tests/text/test_run.py | 460 +++++++++++++++++------------------ 21 files changed, 1180 insertions(+), 1347 deletions(-) delete mode 100644 tests/oxml/unitdata/dml.py diff --git a/src/docx/dml/color.py b/src/docx/dml/color.py index d7ee0a21c..a8322d21a 100644 --- a/src/docx/dml/color.py +++ b/src/docx/dml/color.py @@ -1,83 +1,95 @@ """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 +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from typing_extensions import TypeAlias + +from docx.enum.dml import MSO_COLOR_TYPE +from docx.oxml.simpletypes import ST_HexColorAuto +from docx.shared import ElementProxy, RGBColor + +if TYPE_CHECKING: + from docx.enum.dml import MSO_THEME_COLOR + from docx.oxml.text.font import CT_Color + from docx.oxml.text.run import CT_R + +# -- other element types can be a parent of an `w:rPr` element, but for now only `w:r` is -- +RPrParent: TypeAlias = "CT_R" class ColorFormat(ElementProxy): - """Provides access to color settings such as RGB color, theme color, and luminance - adjustments.""" + """Provides access to color settings like RGB color, theme color, and luminance adjustments.""" - def __init__(self, rPr_parent): + def __init__(self, rPr_parent: RPrParent): super(ColorFormat, self).__init__(rPr_parent) + self._element = rPr_parent @property - def rgb(self): + def rgb(self) -> RGBColor | None: """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. + 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 + return cast(RGBColor, color.val) @rgb.setter - def rgb(self, value): + def rgb(self, value: RGBColor | None): if value is None and self._color is None: return rPr = self._element.get_or_add_rPr() - rPr._remove_color() + rPr._remove_color() # pyright: ignore[reportPrivateUsage] if value is not None: rPr.get_or_add_color().val = value @property - def theme_color(self): + def theme_color(self) -> MSO_THEME_COLOR | None: """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|. + 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. + `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: + if color is None: return None return color.themeColor @theme_color.setter - def theme_color(self, value): + def theme_color(self, value: MSO_THEME_COLOR | None): if value is None: - if self._color is not None: - self._element.rPr._remove_color() + if self._color is not None and self._element.rPr is not None: + self._element.rPr._remove_color() # pyright: ignore[reportPrivateUsage] return self._element.get_or_add_rPr().get_or_add_color().themeColor = value @property - def type(self) -> MSO_COLOR_TYPE: + def type(self) -> MSO_COLOR_TYPE | None: """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. + 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: @@ -89,7 +101,7 @@ def type(self) -> MSO_COLOR_TYPE: return MSO_COLOR_TYPE.RGB @property - def _color(self): + def _color(self) -> CT_Color | None: """Return `w:rPr/w:color` or |None| if not present. Helper to factor out repetitive element access. diff --git a/src/docx/document.py b/src/docx/document.py index 8944a0e50..2cf0a1c38 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -11,14 +11,13 @@ 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.shared import ElementProxy, Emu, Inches, Length if TYPE_CHECKING: import docx.types as t from docx.oxml.document import CT_Body, CT_Document from docx.parts.document import DocumentPart from docx.settings import Settings - from docx.shared import Length from docx.styles.style import ParagraphStyle, _TableStyle from docx.table import Table from docx.text.paragraph import Paragraph @@ -178,7 +177,10 @@ def tables(self) -> List[Table]: def _block_width(self) -> Length: """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) + page_width = section.page_width or Inches(8.5) + left_margin = section.left_margin or Inches(1) + right_margin = section.right_margin or Inches(1) + return Emu(page_width - left_margin - right_margin) @property def _body(self) -> _Body: @@ -198,7 +200,7 @@ def __init__(self, body_elm: CT_Body, parent: t.ProvidesStoryPart): super(_Body, self).__init__(body_elm, parent) self._body = body_elm - def clear_content(self): + def clear_content(self) -> _Body: """Return this |_Body| instance after clearing it of all content. Section properties for the main document story, if present, are preserved. diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py index bc96ab6a2..66e989757 100644 --- a/src/docx/enum/base.py +++ b/src/docx/enum/base.py @@ -37,9 +37,9 @@ class BaseXmlEnum(int, enum.Enum): corresponding member in the MS API enum of the same name. """ - xml_value: str + xml_value: str | None - def __new__(cls, ms_api_value: int, xml_value: str, docstr: str): + def __new__(cls, ms_api_value: int, xml_value: str | None, docstr: str): self = int.__new__(cls, ms_api_value) self._value_ = ms_api_value self.xml_value = xml_value @@ -70,7 +70,11 @@ def to_xml(cls: Type[_T], value: int | _T | None) -> str | None: """XML value of this enum member, generally an XML attribute value.""" # -- presence of multi-arg `__new__()` method fools type-checker, but getting a # -- member by its value using EnumCls(val) works as usual. - return cls(value).xml_value + member = cls(value) + xml_value = member.xml_value + if not xml_value: + raise ValueError(f"{cls.__name__}.{member.name} has no XML representation") + return xml_value class DocsPageFormatter: diff --git a/src/docx/opc/package.py b/src/docx/opc/package.py index 3b1eef256..3c1cdca22 100644 --- a/src/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -14,6 +14,8 @@ from docx.shared import lazyproperty if TYPE_CHECKING: + from typing_extensions import Self + from docx.opc.coreprops import CoreProperties from docx.opc.part import Part from docx.opc.rel import _Relationship # pyright: ignore[reportPrivateUsage] @@ -26,9 +28,6 @@ class OpcPackage: to a package file or file-like object containing one. """ - def __init__(self): - super(OpcPackage, self).__init__() - def after_unmarshal(self): """Entry point for any post-unmarshaling processing. @@ -122,7 +121,7 @@ def next_partname(self, template: str) -> PackURI: return PackURI(candidate_partname) @classmethod - def open(cls, pkg_file: str | IO[bytes]) -> OpcPackage: + def open(cls, pkg_file: str | IO[bytes]) -> Self: """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" pkg_reader = PackageReader.from_file(pkg_file) package = cls() diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index 140086aab..c5dc9bd2e 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -1,3 +1,5 @@ +# pyright: reportAssignmentType=false + """Custom element classes related to run properties (font).""" from __future__ import annotations @@ -20,6 +22,7 @@ RequiredAttribute, ZeroOrOne, ) +from docx.shared import RGBColor if TYPE_CHECKING: from docx.oxml.shared import CT_OnOff, CT_String @@ -29,8 +32,8 @@ 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) + val: RGBColor | str = RequiredAttribute("w:val", ST_HexColor) + themeColor: MSO_THEME_COLOR | None = OptionalAttribute("w:themeColor", MSO_THEME_COLOR) class CT_Fonts(BaseOxmlElement): @@ -39,39 +42,33 @@ class CT_Fonts(BaseOxmlElement): Specifies typeface name for the various language types. """ - ascii: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] - "w:ascii", ST_String - ) - hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] - "w:hAnsi", ST_String - ) + ascii: str | None = OptionalAttribute("w:ascii", ST_String) + hAnsi: str | None = OptionalAttribute("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 - ) + val: WD_COLOR_INDEX = RequiredAttribute("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 - ) + val: Length = RequiredAttribute("w:val", ST_HpsMeasure) class CT_RPr(BaseOxmlElement): """`` element, containing the properties for a run.""" + get_or_add_color: Callable[[], CT_Color] 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_color: Callable[[], None] _remove_highlight: Callable[[], None] _remove_rFonts: Callable[[], None] _remove_rStyle: Callable[[], None] @@ -120,15 +117,9 @@ class CT_RPr(BaseOxmlElement): "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:] - ) + rStyle: CT_String | None = ZeroOrOne("w:rStyle", successors=_tag_seq[1:]) + rFonts: CT_Fonts | None = ZeroOrOne("w:rFonts", successors=_tag_seq[2:]) + b: CT_OnOff | None = 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:]) @@ -144,19 +135,11 @@ class CT_RPr(BaseOxmlElement): 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:] - ) + color: CT_Color | None = ZeroOrOne("w:color", successors=_tag_seq[19:]) + sz: CT_HpsMeasure | None = ZeroOrOne("w:sz", successors=_tag_seq[24:]) + highlight: CT_Highlight | None = ZeroOrOne("w:highlight", successors=_tag_seq[26:]) + u: CT_Underline | None = ZeroOrOne("w:u", successors=_tag_seq[27:]) + vertAlign: CT_VerticalAlignRun | None = 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:]) @@ -343,14 +326,10 @@ def _set_bool_val(self, name: str, value: bool | None): class CT_Underline(BaseOxmlElement): """`` element, specifying the underlining style for a run.""" - val: WD_UNDERLINE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] - "w:val", WD_UNDERLINE - ) + val: WD_UNDERLINE | None = OptionalAttribute("w:val", WD_UNDERLINE) class CT_VerticalAlignRun(BaseOxmlElement): """`` element, specifying subscript or superscript.""" - val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] - "w:val", ST_VerticalAlignRun - ) + val: str = RequiredAttribute("w:val", ST_VerticalAlignRun) diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 648b58aa2..dea0845f7 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -89,14 +89,13 @@ def inline_shapes(self): return InlineShapes(self._element.body, self) @lazyproperty - def numbering_part(self): - """A |NumberingPart| object providing access to the numbering definitions for - this document. + def numbering_part(self) -> NumberingPart: + """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) + return cast(NumberingPart, self.part_related_by(RT.NUMBERING)) except KeyError: numbering_part = NumberingPart.new() self.relate_to(numbering_part, RT.NUMBERING) diff --git a/src/docx/parts/numbering.py b/src/docx/parts/numbering.py index 54a430c1b..745c8458a 100644 --- a/src/docx/parts/numbering.py +++ b/src/docx/parts/numbering.py @@ -9,9 +9,8 @@ class NumberingPart(XmlPart): or glossary.""" @classmethod - def new(cls): - """Return newly created empty numbering part, containing only the root - ```` element.""" + def new(cls) -> "NumberingPart": + """Newly created numbering part, containing only the root ```` element.""" raise NotImplementedError @lazyproperty diff --git a/src/docx/shared.py b/src/docx/shared.py index 491d42741..1d561227b 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -127,11 +127,9 @@ class RGBColor(Tuple[int, int, int]): def __new__(cls, r: int, g: int, b: int): msg = "RGBColor() takes three integer values 0-255" for val in (r, g, b): - if ( - not isinstance(val, int) # pyright: ignore[reportUnnecessaryIsInstance] - or val < 0 - or val > 255 - ): + if not isinstance(val, int): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError(msg) + if val < 0 or val > 255: raise ValueError(msg) return super(RGBColor, cls).__new__(cls, (r, g, b)) diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 0e2f5bc17..d35988370 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -233,7 +233,7 @@ def underline(self) -> bool | WD_UNDERLINE | None: return self.font.underline @underline.setter - def underline(self, value: bool): + def underline(self, value: bool | WD_UNDERLINE | None): self.font.underline = value diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index ea848e7d6..f9fcae0c6 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -1,57 +1,59 @@ -"""Test suite for docx.dml.color module.""" +# pyright: reportPrivateUsage=false + +"""Unit-test suite for the `docx.dml.color` module.""" + +from __future__ import annotations + +from typing import cast import pytest from docx.dml.color import ColorFormat from docx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR +from docx.oxml.text.run import CT_R from docx.shared import RGBColor from ..unitutil.cxml import element, xml class DescribeColorFormat: - def it_knows_its_color_type(self, type_fixture): - color_format, expected_value = type_fixture - assert color_format.type == expected_value - - def it_knows_its_RGB_value(self, rgb_get_fixture): - color_format, expected_value = rgb_get_fixture - assert color_format.rgb == expected_value - - def it_can_change_its_RGB_value(self, rgb_set_fixture): - color_format, new_value, expected_xml = rgb_set_fixture - color_format.rgb = new_value - assert color_format._element.xml == expected_xml - - def it_knows_its_theme_color(self, theme_color_get_fixture): - color_format, expected_value = theme_color_get_fixture - assert color_format.theme_color == expected_value - - def it_can_change_its_theme_color(self, theme_color_set_fixture): - color_format, new_value, expected_xml = theme_color_set_fixture - color_format.theme_color = new_value - assert color_format._element.xml == expected_xml + """Unit-test suite for `docx.dml.color.ColorFormat` objects.""" - # fixtures --------------------------------------------- + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("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 it_knows_its_color_type(self, r_cxml: str, expected_value: MSO_COLOR_TYPE | None): + assert ColorFormat(cast(CT_R, element(r_cxml))).type == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("r_cxml", "rgb"), + [ ("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 + def it_knows_its_RGB_value(self, r_cxml: str, rgb: str | None): + expected_value = RGBColor.from_string(rgb) if rgb else None + assert ColorFormat(cast(CT_R, element(r_cxml))).rgb == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("r_cxml", "new_value", "expected_cxml"), + [ ("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}"), ( @@ -71,73 +73,60 @@ def rgb_get_fixture(self, request): ), ("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 + def it_can_change_its_RGB_value( + self, r_cxml: str, new_value: RGBColor | None, expected_cxml: str + ): + color_format = ColorFormat(cast(CT_R, element(r_cxml))) + color_format.rgb = new_value + assert color_format._element.xml == xml(expected_cxml) - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ ("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"), - ] + ("w:r/w:rPr/w:color{w:themeColor=accent1}", MSO_THEME_COLOR.ACCENT_1), + ("w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=dark1}", MSO_THEME_COLOR.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) - return color_format, expected_value + def it_knows_its_theme_color(self, r_cxml: str, expected_value: MSO_THEME_COLOR | None): + color_format = ColorFormat(cast(CT_R, element(r_cxml))) + assert color_format.theme_color == expected_value - @pytest.fixture( - params=[ - ("w:r", "ACCENT_1", "w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent1}"), + @pytest.mark.parametrize( + ("r_cxml", "new_value", "expected_cxml"), + [ + ( + "w:r", + MSO_THEME_COLOR.ACCENT_1, + "w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent1}", + ), ( "w:r/w:rPr", - "ACCENT_2", + MSO_THEME_COLOR.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", + MSO_THEME_COLOR.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", + MSO_THEME_COLOR.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) - 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, - ), - ] + ], ) - def type_fixture(self, request): - r_cxml, expected_value = request.param - color_format = ColorFormat(element(r_cxml)) - return color_format, expected_value + def it_can_change_its_theme_color( + self, r_cxml: str, new_value: MSO_THEME_COLOR | None, expected_cxml: str + ): + color_format = ColorFormat(cast(CT_R, element(r_cxml))) + color_format.theme_color = new_value + assert color_format._element.xml == xml(expected_cxml) diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py deleted file mode 100644 index 325a3f690..000000000 --- a/tests/oxml/unitdata/dml.py +++ /dev/null @@ -1,63 +0,0 @@ -"""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") - - -class CT_BlipFillPropertiesBuilder(BaseBuilder): - __tag__ = "pic:blipFill" - __nspfxs__ = ("pic",) - __attrs__ = () - - -class CT_GraphicalObjectBuilder(BaseBuilder): - __tag__ = "a:graphic" - __nspfxs__ = ("a",) - __attrs__ = () - - -class CT_GraphicalObjectDataBuilder(BaseBuilder): - __tag__ = "a:graphicData" - __nspfxs__ = ("a",) - __attrs__ = ("uri",) - - -class CT_InlineBuilder(BaseBuilder): - __tag__ = "wp:inline" - __nspfxs__ = ("wp",) - __attrs__ = ("distT", "distB", "distL", "distR") - - -class CT_PictureBuilder(BaseBuilder): - __tag__ = "pic:pic" - __nspfxs__ = ("pic",) - __attrs__ = () - - -def a_blip(): - return CT_BlipBuilder() - - -def a_blipFill(): - return CT_BlipFillPropertiesBuilder() - - -def a_graphic(): - return CT_GraphicalObjectBuilder() - - -def a_graphicData(): - return CT_GraphicalObjectDataBuilder() - - -def a_pic(): - return CT_PictureBuilder() - - -def an_inline(): - return CT_InlineBuilder() diff --git a/tests/parts/test_document.py b/tests/parts/test_document.py index 3a86b5168..cfe9e870c 100644 --- a/tests/parts/test_document.py +++ b/tests/parts/test_document.py @@ -1,10 +1,14 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for the docx.parts.document module.""" import pytest from docx.enum.style import WD_STYLE_TYPE +from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.coreprops import CoreProperties +from docx.opc.packuri import PackURI from docx.package import Package from docx.parts.document import DocumentPart from docx.parts.hdrftr import FooterPart, HeaderPart @@ -15,15 +19,26 @@ from docx.styles.style import BaseStyle from docx.styles.styles import Styles -from ..oxml.parts.unitdata.document import a_body, a_document -from ..unitutil.mock import class_mock, instance_mock, method_mock, property_mock +from ..unitutil.cxml import element +from ..unitutil.mock import ( + FixtureRequest, + Mock, + class_mock, + instance_mock, + method_mock, + property_mock, +) class DescribeDocumentPart: - def it_can_add_a_footer_part(self, package_, FooterPart_, footer_part_, relate_to_): + def it_can_add_a_footer_part( + self, package_: Mock, FooterPart_: Mock, footer_part_: Mock, relate_to_: Mock + ): FooterPart_.new.return_value = footer_part_ relate_to_.return_value = "rId12" - document_part = DocumentPart(None, None, None, package_) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) footer_part, rId = document_part.add_footer_part() @@ -32,10 +47,14 @@ def it_can_add_a_footer_part(self, package_, FooterPart_, footer_part_, relate_t assert footer_part is footer_part_ assert rId == "rId12" - def it_can_add_a_header_part(self, package_, HeaderPart_, header_part_, relate_to_): + def it_can_add_a_header_part( + self, package_: Mock, HeaderPart_: Mock, header_part_: Mock, relate_to_: Mock + ): HeaderPart_.new.return_value = header_part_ relate_to_.return_value = "rId7" - document_part = DocumentPart(None, None, None, package_) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) header_part, rId = document_part.add_header_part() @@ -44,19 +63,23 @@ def it_can_add_a_header_part(self, package_, HeaderPart_, header_part_, relate_t assert header_part is header_part_ assert rId == "rId7" - def it_can_drop_a_specified_header_part(self, drop_rel_): - document_part = DocumentPart(None, None, None, None) + def it_can_drop_a_specified_header_part(self, drop_rel_: Mock, package_: Mock): + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) document_part.drop_header_part("rId42") drop_rel_.assert_called_once_with(document_part, "rId42") def it_provides_access_to_a_footer_part_by_rId( - self, related_parts_prop_, related_parts_, footer_part_ + self, related_parts_prop_: Mock, related_parts_: Mock, footer_part_: Mock, package_: Mock ): related_parts_prop_.return_value = related_parts_ related_parts_.__getitem__.return_value = footer_part_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) footer_part = document_part.footer_part("rId9") @@ -64,50 +87,79 @@ def it_provides_access_to_a_footer_part_by_rId( assert footer_part is footer_part_ def it_provides_access_to_a_header_part_by_rId( - self, related_parts_prop_, related_parts_, header_part_ + self, related_parts_prop_: Mock, related_parts_: Mock, header_part_: Mock, package_: Mock ): related_parts_prop_.return_value = related_parts_ related_parts_.__getitem__.return_value = header_part_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) header_part = document_part.header_part("rId11") related_parts_.__getitem__.assert_called_once_with("rId11") assert header_part is header_part_ - def it_can_save_the_package_to_a_file(self, save_fixture): - document, file_ = save_fixture - document.save(file_) - document._package.save.assert_called_once_with(file_) + def it_can_save_the_package_to_a_file(self, package_: Mock): + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) - def it_provides_access_to_the_document_settings(self, settings_fixture): - document_part, settings_ = settings_fixture - settings = document_part.settings - assert settings is settings_ + document_part.save("foobar.docx") - def it_provides_access_to_the_document_styles(self, styles_fixture): - document_part, styles_ = styles_fixture - styles = document_part.styles - assert styles is styles_ + package_.save.assert_called_once_with("foobar.docx") + + def it_provides_access_to_the_document_settings( + self, _settings_part_prop_: Mock, settings_part_: Mock, settings_: Mock, package_: Mock + ): + settings_part_.settings = settings_ + _settings_part_prop_.return_value = settings_part_ + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) - def it_provides_access_to_its_core_properties(self, core_props_fixture): - document_part, core_properties_ = core_props_fixture - core_properties = document_part.core_properties - assert core_properties is core_properties_ + assert document_part.settings is settings_ + + def it_provides_access_to_the_document_styles( + self, _styles_part_prop_: Mock, styles_part_: Mock, styles_: Mock, package_: Mock + ): + styles_part_.styles = styles_ + _styles_part_prop_.return_value = styles_part_ + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) + + assert document_part.styles is styles_ + + def it_provides_access_to_its_core_properties(self, package_: Mock, core_properties_: Mock): + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) + package_.core_properties = core_properties_ + + assert document_part.core_properties is core_properties_ def it_provides_access_to_the_inline_shapes_in_the_document( - self, inline_shapes_fixture + self, InlineShapes_: Mock, package_: Mock ): - document, InlineShapes_, body_elm = inline_shapes_fixture - inline_shapes = document.inline_shapes - InlineShapes_.assert_called_once_with(body_elm, document) + document_elm = element("w:document/w:body") + body_elm = document_elm[0] + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, document_elm, package_ + ) + + inline_shapes = document_part.inline_shapes + + InlineShapes_.assert_called_once_with(body_elm, document_part) assert inline_shapes is InlineShapes_.return_value def it_provides_access_to_the_numbering_part( - self, part_related_by_, numbering_part_ + self, part_related_by_: Mock, numbering_part_: Mock, package_: Mock ): part_related_by_.return_value = numbering_part_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) numbering_part = document_part.numbering_part @@ -115,11 +167,18 @@ def it_provides_access_to_the_numbering_part( assert numbering_part is numbering_part_ def and_it_creates_a_numbering_part_if_not_present( - self, part_related_by_, relate_to_, NumberingPart_, numbering_part_ + self, + part_related_by_: Mock, + relate_to_: Mock, + NumberingPart_: Mock, + numbering_part_: Mock, + package_: Mock, ): part_related_by_.side_effect = KeyError NumberingPart_.new.return_value = numbering_part_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) numbering_part = document_part.numbering_part @@ -127,20 +186,28 @@ def and_it_creates_a_numbering_part_if_not_present( relate_to_.assert_called_once_with(document_part, numbering_part_, RT.NUMBERING) assert numbering_part is numbering_part_ - def it_can_get_a_style_by_id(self, styles_prop_, styles_, style_): + def it_can_get_a_style_by_id( + self, styles_prop_: Mock, styles_: Mock, style_: Mock, package_: Mock + ): styles_prop_.return_value = styles_ styles_.get_by_id.return_value = style_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) style = document_part.get_style("BodyText", WD_STYLE_TYPE.PARAGRAPH) styles_.get_by_id.assert_called_once_with("BodyText", WD_STYLE_TYPE.PARAGRAPH) assert style is style_ - def it_can_get_the_id_of_a_style(self, style_, styles_prop_, styles_): + def it_can_get_the_id_of_a_style( + self, style_: Mock, styles_prop_: Mock, styles_: Mock, package_: Mock + ): styles_prop_.return_value = styles_ styles_.get_style_id.return_value = "BodyCharacter" - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) style_id = document_part.get_style_id(style_, WD_STYLE_TYPE.CHARACTER) @@ -148,10 +215,12 @@ def it_can_get_the_id_of_a_style(self, style_, styles_prop_, styles_): assert style_id == "BodyCharacter" def it_provides_access_to_its_settings_part_to_help( - self, part_related_by_, settings_part_ + self, part_related_by_: Mock, settings_part_: Mock, package_: Mock ): part_related_by_.return_value = settings_part_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) settings_part = document_part._settings_part @@ -159,11 +228,18 @@ def it_provides_access_to_its_settings_part_to_help( assert settings_part is settings_part_ def and_it_creates_a_default_settings_part_if_not_present( - self, package_, part_related_by_, SettingsPart_, settings_part_, relate_to_ + self, + package_: Mock, + part_related_by_: Mock, + SettingsPart_: Mock, + settings_part_: Mock, + relate_to_: Mock, ): part_related_by_.side_effect = KeyError SettingsPart_.default.return_value = settings_part_ - document_part = DocumentPart(None, None, None, package_) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) settings_part = document_part._settings_part @@ -172,10 +248,12 @@ def and_it_creates_a_default_settings_part_if_not_present( assert settings_part is settings_part_ def it_provides_access_to_its_styles_part_to_help( - self, part_related_by_, styles_part_ + self, part_related_by_: Mock, styles_part_: Mock, package_: Mock ): part_related_by_.return_value = styles_part_ - document_part = DocumentPart(None, None, None, None) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) styles_part = document_part._styles_part @@ -183,11 +261,18 @@ def it_provides_access_to_its_styles_part_to_help( assert styles_part is styles_part_ def and_it_creates_a_default_styles_part_if_not_present( - self, package_, part_related_by_, StylesPart_, styles_part_, relate_to_ + self, + package_: Mock, + part_related_by_: Mock, + StylesPart_: Mock, + styles_part_: Mock, + relate_to_: Mock, ): part_related_by_.side_effect = KeyError StylesPart_.default.return_value = styles_part_ - document_part = DocumentPart(None, None, None, package_) + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) styles_part = document_part._styles_part @@ -195,135 +280,100 @@ def and_it_creates_a_default_styles_part_if_not_present( relate_to_.assert_called_once_with(document_part, styles_part_, RT.STYLES) assert styles_part is styles_part_ - # fixtures ------------------------------------------------------- - - @pytest.fixture - def core_props_fixture(self, package_, core_properties_): - document_part = DocumentPart(None, None, None, package_) - package_.core_properties = core_properties_ - return document_part, core_properties_ - - @pytest.fixture - def inline_shapes_fixture(self, request, InlineShapes_): - 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 - - @pytest.fixture - def save_fixture(self, package_): - document_part = DocumentPart(None, None, None, package_) - file_ = "foobar.docx" - return document_part, file_ - - @pytest.fixture - 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_ - return document_part, settings_ - - @pytest.fixture - def styles_fixture(self, _styles_part_prop_, styles_part_, styles_): - document_part = DocumentPart(None, None, None, None) - _styles_part_prop_.return_value = styles_part_ - styles_part_.styles = styles_ - return document_part, styles_ - - # fixture components --------------------------------------------- + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture - def core_properties_(self, request): + def core_properties_(self, request: FixtureRequest): return instance_mock(request, CoreProperties) @pytest.fixture - def drop_rel_(self, request): + def drop_rel_(self, request: FixtureRequest): return method_mock(request, DocumentPart, "drop_rel", autospec=True) @pytest.fixture - def FooterPart_(self, request): + def FooterPart_(self, request: FixtureRequest): return class_mock(request, "docx.parts.document.FooterPart") @pytest.fixture - def footer_part_(self, request): + def footer_part_(self, request: FixtureRequest): return instance_mock(request, FooterPart) @pytest.fixture - def HeaderPart_(self, request): + def HeaderPart_(self, request: FixtureRequest): return class_mock(request, "docx.parts.document.HeaderPart") @pytest.fixture - def header_part_(self, request): + def header_part_(self, request: FixtureRequest): return instance_mock(request, HeaderPart) @pytest.fixture - def InlineShapes_(self, request): + def InlineShapes_(self, request: FixtureRequest): return class_mock(request, "docx.parts.document.InlineShapes") @pytest.fixture - def NumberingPart_(self, request): + def NumberingPart_(self, request: FixtureRequest): return class_mock(request, "docx.parts.document.NumberingPart") @pytest.fixture - def numbering_part_(self, request): + def numbering_part_(self, request: FixtureRequest): return instance_mock(request, NumberingPart) @pytest.fixture - def package_(self, request): + def package_(self, request: FixtureRequest): return instance_mock(request, Package) @pytest.fixture - def part_related_by_(self, request): + def part_related_by_(self, request: FixtureRequest): return method_mock(request, DocumentPart, "part_related_by") @pytest.fixture - def relate_to_(self, request): + def relate_to_(self, request: FixtureRequest): return method_mock(request, DocumentPart, "relate_to") @pytest.fixture - def related_parts_(self, request): + def related_parts_(self, request: FixtureRequest): return instance_mock(request, dict) @pytest.fixture - def related_parts_prop_(self, request): + def related_parts_prop_(self, request: FixtureRequest): return property_mock(request, DocumentPart, "related_parts") @pytest.fixture - def SettingsPart_(self, request): + def SettingsPart_(self, request: FixtureRequest): return class_mock(request, "docx.parts.document.SettingsPart") @pytest.fixture - def settings_(self, request): + def settings_(self, request: FixtureRequest): return instance_mock(request, Settings) @pytest.fixture - def settings_part_(self, request): + def settings_part_(self, request: FixtureRequest): return instance_mock(request, SettingsPart) @pytest.fixture - def _settings_part_prop_(self, request): + def _settings_part_prop_(self, request: FixtureRequest): return property_mock(request, DocumentPart, "_settings_part") @pytest.fixture - def style_(self, request): + def style_(self, request: FixtureRequest): return instance_mock(request, BaseStyle) @pytest.fixture - def styles_(self, request): + def styles_(self, request: FixtureRequest): return instance_mock(request, Styles) @pytest.fixture - def StylesPart_(self, request): + def StylesPart_(self, request: FixtureRequest): return class_mock(request, "docx.parts.document.StylesPart") @pytest.fixture - def styles_part_(self, request): + def styles_part_(self, request: FixtureRequest): return instance_mock(request, StylesPart) @pytest.fixture - def styles_prop_(self, request): + def styles_prop_(self, request: FixtureRequest): return property_mock(request, DocumentPart, "styles") @pytest.fixture - def _styles_part_prop_(self, request): + def _styles_part_prop_(self, request: FixtureRequest): return property_mock(request, DocumentPart, "_styles_part") diff --git a/tests/test_api.py b/tests/test_api.py index b6e6818b5..6b5d3ae07 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,66 +2,55 @@ import pytest -import docx -from docx.api import Document +from docx.api import Document as DocumentFactoryFn +from docx.document import Document as DocumentCls from docx.opc.constants import CONTENT_TYPE as CT -from .unitutil.mock import class_mock, function_mock, instance_mock +from .unitutil.mock import FixtureRequest, Mock, class_mock, function_mock, instance_mock class DescribeDocument: - def it_opens_a_docx_file(self, open_fixture): - docx, Package_, document_ = open_fixture - document = Document(docx) - Package_.open.assert_called_once_with(docx) - assert document is document_ - - def it_opens_the_default_docx_if_none_specified(self, default_fixture): - docx, Package_, document_ = default_fixture - document = Document() - Package_.open.assert_called_once_with(docx) - assert document is document_ - - def it_raises_on_not_a_Word_file(self, raise_fixture): - not_a_docx = raise_fixture - with pytest.raises(ValueError, match="file 'foobar.xlsx' is not a Word file,"): - Document(not_a_docx) + """Unit-test suite for `docx.api.Document` factory function.""" - # fixtures ------------------------------------------------------- - - @pytest.fixture - def default_fixture(self, _default_docx_path_, Package_, document_): - docx = "barfoo.docx" - _default_docx_path_.return_value = docx + def it_opens_a_docx_file(self, Package_: Mock, document_: Mock): document_part = Package_.open.return_value.main_document_part document_part.document = document_ document_part.content_type = CT.WML_DOCUMENT_MAIN - return docx, Package_, document_ - @pytest.fixture - def open_fixture(self, Package_, document_): - docx = "foobar.docx" + document = DocumentFactoryFn("foobar.docx") + + Package_.open.assert_called_once_with("foobar.docx") + assert document is document_ + + def it_opens_the_default_docx_if_none_specified( + self, _default_docx_path_: Mock, Package_: Mock, document_: Mock + ): + _default_docx_path_.return_value = "default-document.docx" document_part = Package_.open.return_value.main_document_part document_part.document = document_ document_part.content_type = CT.WML_DOCUMENT_MAIN - return docx, Package_, document_ - @pytest.fixture - def raise_fixture(self, Package_): - not_a_docx = "foobar.xlsx" + document = DocumentFactoryFn() + + Package_.open.assert_called_once_with("default-document.docx") + assert document is document_ + + def it_raises_on_not_a_Word_file(self, Package_: Mock): Package_.open.return_value.main_document_part.content_type = "BOGUS" - return not_a_docx - # fixture components --------------------------------------------- + with pytest.raises(ValueError, match="file 'foobar.xlsx' is not a Word file,"): + DocumentFactoryFn("foobar.xlsx") + + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture - def _default_docx_path_(self, request): + def _default_docx_path_(self, request: FixtureRequest): return function_mock(request, "docx.api._default_docx_path") @pytest.fixture - def document_(self, request): - return instance_mock(request, docx.document.Document) + def document_(self, request: FixtureRequest): + return instance_mock(request, DocumentCls) @pytest.fixture - def Package_(self, request): + def Package_(self, request: FixtureRequest): return class_mock(request, "docx.api.Package") diff --git a/tests/test_blkcntnr.py b/tests/test_blkcntnr.py index 1549bd8ea..ab463663f 100644 --- a/tests/test_blkcntnr.py +++ b/tests/test_blkcntnr.py @@ -1,42 +1,61 @@ +# pyright: reportPrivateUsage=false + """Test suite for the docx.blkcntnr (block item container) module.""" +from __future__ import annotations + +from typing import cast + import pytest -from docx import Document +import docx from docx.blkcntnr import BlockItemContainer +from docx.document import Document +from docx.oxml.document import CT_Body from docx.shared import Inches from docx.table import Table from docx.text.paragraph import Paragraph from .unitutil.cxml import element, xml from .unitutil.file import snippet_seq, test_file -from .unitutil.mock import call, instance_mock, method_mock +from .unitutil.mock import FixtureRequest, Mock, call, instance_mock, method_mock class DescribeBlockItemContainer: """Unit-test suite for `docx.blkcntnr.BlockItemContainer`.""" - def it_can_add_a_paragraph(self, add_paragraph_fixture, _add_paragraph_): - text, style, paragraph_, add_run_calls = add_paragraph_fixture + @pytest.mark.parametrize( + ("text", "style"), [("", None), ("Foo", None), ("", "Bar"), ("Foo", "Bar")] + ) + def it_can_add_a_paragraph( + self, + text: str, + style: str | None, + blkcntnr: BlockItemContainer, + _add_paragraph_: Mock, + paragraph_: Mock, + ): + paragraph_.style = None _add_paragraph_.return_value = paragraph_ - blkcntnr = BlockItemContainer(None, None) paragraph = blkcntnr.add_paragraph(text, style) _add_paragraph_.assert_called_once_with(blkcntnr) - assert paragraph.add_run.call_args_list == add_run_calls + assert paragraph_.add_run.call_args_list == ([call(text)] if text else []) assert paragraph.style == style assert paragraph is paragraph_ - def it_can_add_a_table(self, add_table_fixture): - blkcntnr, rows, cols, width, expected_xml = add_table_fixture + def it_can_add_a_table(self, blkcntnr: BlockItemContainer): + rows, cols, width = 2, 2, Inches(2) + table = blkcntnr.add_table(rows, cols, width) + assert isinstance(table, Table) - assert table._element.xml == expected_xml + assert table._element.xml == snippet_seq("new-tbl")[0] assert table._parent is blkcntnr def it_can_iterate_its_inner_content(self): - document = Document(test_file("blk-inner-content.docx")) + document = docx.Document(test_file("blk-inner-content.docx")) inner_content = document.iter_inner_content() @@ -55,101 +74,78 @@ def it_can_iterate_its_inner_content(self): with pytest.raises(StopIteration): next(inner_content) - 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 - assert len(paragraphs) == expected_count - count = 0 - for idx, paragraph in enumerate(paragraphs): - assert isinstance(paragraph, Paragraph) - assert paragraphs[idx] is paragraph - count += 1 - assert count == expected_count - - def it_provides_access_to_the_tables_it_contains(self, tables_fixture): - # test len(), iterable, and indexed access - blkcntnr, expected_count = tables_fixture - tables = blkcntnr.tables - assert len(tables) == expected_count - count = 0 - for idx, table in enumerate(tables): - assert isinstance(table, Table) - assert tables[idx] is table - count += 1 - assert count == expected_count - - def it_adds_a_paragraph_to_help(self, _add_paragraph_fixture): - blkcntnr, expected_xml = _add_paragraph_fixture - new_paragraph = blkcntnr._add_paragraph() - assert isinstance(new_paragraph, Paragraph) - assert new_paragraph._parent == blkcntnr - assert blkcntnr._element.xml == expected_xml - - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ - ("", None), - ("Foo", None), - ("", "Bar"), - ("Foo", "Bar"), - ] - ) - def add_paragraph_fixture(self, request, paragraph_): - text, style = request.param - paragraph_.style = None - add_run_calls = [call(text)] if text else [] - return text, style, paragraph_, add_run_calls - - @pytest.fixture - def _add_paragraph_fixture(self, request): - 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) - rows, cols, width = 2, 2, Inches(2) - expected_xml = snippet_seq("new-tbl")[0] - return blkcntnr, rows, cols, width, expected_xml - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("blkcntnr_cxml", "expected_count"), + [ ("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 + def it_provides_access_to_the_paragraphs_it_contains( + self, blkcntnr_cxml: str, expected_count: int, document_: Mock + ): + blkcntnr = BlockItemContainer(cast(CT_Body, element(blkcntnr_cxml)), document_) + + paragraphs = blkcntnr.paragraphs - @pytest.fixture( - params=[ + # -- supports len() -- + assert len(paragraphs) == expected_count + # -- is iterable -- + assert all(isinstance(p, Paragraph) for p in paragraphs) + # -- is indexable -- + assert all(p is paragraphs[idx] for idx, p in enumerate(paragraphs)) + + @pytest.mark.parametrize( + ("blkcntnr_cxml", "expected_count"), + [ ("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) - return blkcntnr, expected_count + def it_provides_access_to_the_tables_it_contains( + self, blkcntnr_cxml: str, expected_count: int, document_: Mock + ): + blkcntnr = BlockItemContainer(cast(CT_Body, element(blkcntnr_cxml)), document_) + + tables = blkcntnr.tables + + # -- supports len() -- + assert len(tables) == expected_count + # -- is iterable -- + assert all(isinstance(t, Table) for t in tables) + # -- is indexable -- + assert all(t is tables[idx] for idx, t in enumerate(tables)) - # fixture components --------------------------------------------- + def it_adds_a_paragraph_to_help(self, document_: Mock): + blkcntnr = BlockItemContainer(cast(CT_Body, element("w:body")), document_) + + new_paragraph = blkcntnr._add_paragraph() + + assert isinstance(new_paragraph, Paragraph) + assert new_paragraph._parent == blkcntnr + assert blkcntnr._element.xml == xml("w:body/w:p") + + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture - def _add_paragraph_(self, request): + def _add_paragraph_(self, request: FixtureRequest): return method_mock(request, BlockItemContainer, "_add_paragraph") @pytest.fixture - def paragraph_(self, request): + def blkcntnr(self, document_: Mock): + blkcntnr_elm = cast(CT_Body, element("w:body")) + return BlockItemContainer(blkcntnr_elm, document_) + + @pytest.fixture + def document_(self, request: FixtureRequest): + return instance_mock(request, Document) + + @pytest.fixture + def paragraph_(self, request: FixtureRequest): return instance_mock(request, Paragraph) diff --git a/tests/test_document.py b/tests/test_document.py index 6a2c5af88..739813321 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -13,7 +13,7 @@ from docx.enum.section import WD_SECTION from docx.enum.text import WD_BREAK from docx.opc.coreprops import CoreProperties -from docx.oxml.document import CT_Document +from docx.oxml.document import CT_Body, CT_Document from docx.parts.document import DocumentPart from docx.section import Section, Sections from docx.settings import Settings @@ -25,33 +25,43 @@ from docx.text.run import Run from .unitutil.cxml import element, xml -from .unitutil.mock import Mock, class_mock, instance_mock, method_mock, property_mock +from .unitutil.mock import ( + FixtureRequest, + Mock, + class_mock, + instance_mock, + method_mock, + property_mock, +) class DescribeDocument: - """Unit-test suite for `docx.Document`.""" + """Unit-test suite for `docx.document.Document`.""" - def it_can_add_a_heading(self, add_heading_fixture, add_paragraph_, paragraph_): - level, style = add_heading_fixture + @pytest.mark.parametrize( + ("level", "style"), [(0, "Title"), (1, "Heading 1"), (2, "Heading 2"), (9, "Heading 9")] + ) + def it_can_add_a_heading( + self, level: int, style: str, document: Document, add_paragraph_: Mock, paragraph_: Mock + ): add_paragraph_.return_value = paragraph_ - document = Document(None, None) paragraph = document.add_heading("Spam vs. Bacon", level) add_paragraph_.assert_called_once_with(document, "Spam vs. Bacon", style) assert paragraph is paragraph_ - def it_raises_on_heading_level_out_of_range(self): - document = Document(None, None) + def it_raises_on_heading_level_out_of_range(self, document: Document): with pytest.raises(ValueError, match="level must be in range 0-9, got -1"): document.add_heading(level=-1) 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_): + def it_can_add_a_page_break( + self, document: Document, add_paragraph_: Mock, paragraph_: Mock, run_: Mock + ): add_paragraph_.return_value = paragraph_ paragraph_.add_run.return_value = run_ - document = Document(None, None) paragraph = document.add_page_break() @@ -60,70 +70,137 @@ def it_can_add_a_page_break(self, add_paragraph_, paragraph_, run_): run_.add_break.assert_called_once_with(WD_BREAK.PAGE) assert paragraph is paragraph_ - def it_can_add_a_paragraph(self, add_paragraph_fixture): - document, text, style, paragraph_ = add_paragraph_fixture + @pytest.mark.parametrize( + ("text", "style"), [("", None), ("", "Heading 1"), ("foo\rbar", "Body Text")] + ) + def it_can_add_a_paragraph( + self, + text: str, + style: str | None, + document: Document, + body_: Mock, + body_prop_: Mock, + paragraph_: Mock, + ): + body_prop_.return_value = body_ + body_.add_paragraph.return_value = paragraph_ + paragraph = document.add_paragraph(text, style) - document._body.add_paragraph.assert_called_once_with(text, style) + + body_.add_paragraph.assert_called_once_with(text, style) assert paragraph is paragraph_ - def it_can_add_a_picture(self, add_picture_fixture): - document, path, width, height, run_, picture_ = add_picture_fixture + def it_can_add_a_picture( + self, document: Document, add_paragraph_: Mock, run_: Mock, picture_: Mock + ): + path, width, height = "foobar.png", 100, 200 + add_paragraph_.return_value.add_run.return_value = run_ + run_.add_picture.return_value = picture_ + picture = document.add_picture(path, width, height) + run_.add_picture.assert_called_once_with(path, width, height) assert picture is picture_ + @pytest.mark.parametrize( + ("sentinel_cxml", "start_type", "new_sentinel_cxml"), + [ + ("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 it_can_add_a_section( - self, add_section_fixture, Section_, section_, document_part_ + self, + sentinel_cxml: str, + start_type: WD_SECTION, + new_sentinel_cxml: str, + Section_: Mock, + section_: Mock, + document_part_: Mock, ): - document_elm, start_type, expected_xml = add_section_fixture Section_.return_value = section_ - document = Document(document_elm, document_part_) + document = Document( + cast(CT_Document, element("w:document/w:body/(w:p,%s)" % sentinel_cxml)), + document_part_, + ) section = document.add_section(start_type) - assert document.element.xml == expected_xml + assert document.element.xml == xml( + "w:document/w:body/(w:p,w:p/w:pPr/%s,%s)" % (sentinel_cxml, new_sentinel_cxml) + ) sectPr = document.element.xpath("w:body/w:sectPr")[0] Section_.assert_called_once_with(sectPr, document_part_) assert section is section_ - def it_can_add_a_table(self, add_table_fixture): - document, rows, cols, style, width, table_ = add_table_fixture + def it_can_add_a_table( + self, + document: Document, + _block_width_prop_: Mock, + body_prop_: Mock, + body_: Mock, + table_: Mock, + ): + rows, cols, style = 4, 2, "Light Shading Accent 1" + body_prop_.return_value = body_ + body_.add_table.return_value = table_ + _block_width_prop_.return_value = width = 42 + table = document.add_table(rows, cols, style) - document._body.add_table.assert_called_once_with(rows, cols, width) + + body_.add_table.assert_called_once_with(rows, cols, width) assert table == table_ assert table.style == style - def it_can_save_the_document_to_a_file(self, save_fixture): - document, file_ = save_fixture - document.save(file_) - document._part.save.assert_called_once_with(file_) + def it_can_save_the_document_to_a_file(self, document_part_: Mock): + document = Document(cast(CT_Document, element("w:document")), document_part_) + + document.save("foobar.docx") + + document_part_.save.assert_called_once_with("foobar.docx") + + def it_provides_access_to_its_core_properties( + self, document_part_: Mock, core_properties_: Mock + ): + document_part_.core_properties = core_properties_ + document = Document(cast(CT_Document, element("w:document")), document_part_) - def it_provides_access_to_its_core_properties(self, core_props_fixture): - document, core_properties_ = core_props_fixture core_properties = document.core_properties + assert core_properties is core_properties_ - def it_provides_access_to_its_inline_shapes(self, inline_shapes_fixture): - document, inline_shapes_ = inline_shapes_fixture + def it_provides_access_to_its_inline_shapes(self, document_part_: Mock, inline_shapes_: Mock): + document_part_.inline_shapes = inline_shapes_ + document = Document(cast(CT_Document, element("w:document")), document_part_) + assert document.inline_shapes is inline_shapes_ def it_can_iterate_the_inner_content_of_the_document( self, body_prop_: Mock, body_: Mock, document_part_: Mock ): - document_elm = cast(CT_Document, element("w:document")) body_prop_.return_value = body_ body_.iter_inner_content.return_value = iter((1, 2, 3)) - document = Document(document_elm, document_part_) + document = Document(cast(CT_Document, element("w:document")), document_part_) assert list(document.iter_inner_content()) == [1, 2, 3] - def it_provides_access_to_its_paragraphs(self, paragraphs_fixture): - document, paragraphs_ = paragraphs_fixture + def it_provides_access_to_its_paragraphs( + self, document: Document, body_prop_: Mock, body_: Mock, paragraphs_: Mock + ): + body_prop_.return_value = body_ + body_.paragraphs = paragraphs_ paragraphs = document.paragraphs assert paragraphs is paragraphs_ - def it_provides_access_to_its_sections(self, document_part_, Sections_, sections_): - document_elm = element("w:document") + def it_provides_access_to_its_sections( + self, document_part_: Mock, Sections_: Mock, sections_: Mock + ): + document_elm = cast(CT_Document, element("w:document")) Sections_.return_value = sections_ document = Document(document_elm, document_part_) @@ -132,267 +209,172 @@ def it_provides_access_to_its_sections(self, document_part_, Sections_, sections Sections_.assert_called_once_with(document_elm, document_part_) assert sections is sections_ - def it_provides_access_to_its_settings(self, settings_fixture): - document, settings_ = settings_fixture - assert document.settings is settings_ - - def it_provides_access_to_its_styles(self, styles_fixture): - document, styles_ = styles_fixture - assert document.styles is styles_ + def it_provides_access_to_its_settings(self, document_part_: Mock, settings_: Mock): + document_part_.settings = settings_ + document = Document(cast(CT_Document, element("w:document")), document_part_) - def it_provides_access_to_its_tables(self, tables_fixture): - document, tables_ = tables_fixture - tables = document.tables - assert tables is tables_ + assert document.settings is settings_ - def it_provides_access_to_the_document_part(self, part_fixture): - document, part_ = part_fixture - assert document.part is part_ + def it_provides_access_to_its_styles(self, document_part_: Mock, styles_: Mock): + document_part_.styles = styles_ + document = Document(cast(CT_Document, element("w:document")), document_part_) - def it_provides_access_to_the_document_body(self, body_fixture): - document, body_elm, _Body_, body_ = body_fixture - body = document._body - _Body_.assert_called_once_with(body_elm, document) - assert body is body_ + assert document.styles is styles_ - def it_determines_block_width_to_help(self, block_width_fixture): - document, expected_value = block_width_fixture - width = document._block_width - assert isinstance(width, Length) - assert width == expected_value + def it_provides_access_to_its_tables( + self, document: Document, body_prop_: Mock, body_: Mock, tables_: Mock + ): + body_prop_.return_value = body_ + body_.tables = tables_ - # fixtures ------------------------------------------------------- + assert document.tables is tables_ - @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"), - ] - ) - def add_paragraph_fixture(self, request, body_prop_, paragraph_): - text, style = request.param - document = Document(None, None) - body_prop_.return_value.add_paragraph.return_value = paragraph_ - return document, text, style, paragraph_ + def it_provides_access_to_the_document_part(self, document_part_: Mock): + document = Document(cast(CT_Document, element("w:document")), document_part_) + assert document.part is document_part_ - @pytest.fixture - def add_picture_fixture(self, request, add_paragraph_, run_, picture_): - document = Document(None, None) - 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_ + def it_provides_access_to_the_document_body( + self, _Body_: Mock, body_: Mock, document_part_: Mock + ): + _Body_.return_value = body_ + document_elm = cast(CT_Document, element("w:document/w:body")) + body_elm = document_elm[0] + document = Document(document_elm, document_part_) - @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) - expected_xml = xml( - "w:document/w:body/(w:p,w:p/w:pPr/%s,%s)" % (sentinel, new_sentinel) - ) - return document_elm, start_type, expected_xml + body = document._body - @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" - body_prop_.return_value.add_table.return_value = table_ - _block_width_prop_.return_value = width = 42 - return document, rows, cols, style, width, table_ + _Body_.assert_called_once_with(body_elm, document) + assert body is body_ - @pytest.fixture - def block_width_fixture(self, sections_prop_, section_): - document = Document(None, None) + def it_determines_block_width_to_help( + self, document: Document, sections_prop_: Mock, section_: Mock + ): sections_prop_.return_value = [None, section_] section_.page_width = 6000 section_.left_margin = 1500 section_.right_margin = 1000 - expected_value = 3500 - return document, expected_value - @pytest.fixture - def body_fixture(self, _Body_, body_): - document_elm = element("w:document/w:body") - body_elm = document_elm[0] - document = Document(document_elm, None) - return document, body_elm, _Body_, body_ - - @pytest.fixture - def core_props_fixture(self, document_part_, core_properties_): - document = Document(None, document_part_) - document_part_.core_properties = core_properties_ - return document, core_properties_ - - @pytest.fixture - def inline_shapes_fixture(self, document_part_, inline_shapes_): - document = Document(None, document_part_) - document_part_.inline_shapes = inline_shapes_ - return document, inline_shapes_ - - @pytest.fixture - def paragraphs_fixture(self, body_prop_, paragraphs_): - document = Document(None, None) - body_prop_.return_value.paragraphs = paragraphs_ - return document, paragraphs_ - - @pytest.fixture - def part_fixture(self, document_part_): - document = Document(None, document_part_) - return document, document_part_ - - @pytest.fixture - def save_fixture(self, document_part_): - document = Document(None, document_part_) - file_ = "foobar.docx" - return document, file_ - - @pytest.fixture - def settings_fixture(self, document_part_, settings_): - document = Document(None, document_part_) - document_part_.settings = settings_ - return document, settings_ - - @pytest.fixture - def styles_fixture(self, document_part_, styles_): - document = Document(None, document_part_) - document_part_.styles = styles_ - return document, styles_ + width = document._block_width - @pytest.fixture - def tables_fixture(self, body_prop_, tables_): - document = Document(None, None) - body_prop_.return_value.tables = tables_ - return document, tables_ + assert isinstance(width, Length) + assert width == 3500 - # fixture components --------------------------------------------- + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture - def add_paragraph_(self, request): + def add_paragraph_(self, request: FixtureRequest): return method_mock(request, Document, "add_paragraph") @pytest.fixture - def _Body_(self, request, body_): - return class_mock(request, "docx.document._Body", return_value=body_) + def _Body_(self, request: FixtureRequest): + return class_mock(request, "docx.document._Body") @pytest.fixture - def body_(self, request): + def body_(self, request: FixtureRequest): return instance_mock(request, _Body) @pytest.fixture - def _block_width_prop_(self, request): + def _block_width_prop_(self, request: FixtureRequest): return property_mock(request, Document, "_block_width") @pytest.fixture - def body_prop_(self, request, body_): - return property_mock(request, Document, "_body", return_value=body_) + def body_prop_(self, request: FixtureRequest): + return property_mock(request, Document, "_body") @pytest.fixture - def core_properties_(self, request): + def core_properties_(self, request: FixtureRequest): return instance_mock(request, CoreProperties) @pytest.fixture - def document_part_(self, request): + def document(self, document_part_: Mock) -> Document: + document_elm = cast(CT_Document, element("w:document")) + return Document(document_elm, document_part_) + + @pytest.fixture + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def inline_shapes_(self, request): + def inline_shapes_(self, request: FixtureRequest): return instance_mock(request, InlineShapes) @pytest.fixture - def paragraph_(self, request): + def paragraph_(self, request: FixtureRequest): return instance_mock(request, Paragraph) @pytest.fixture - def paragraphs_(self, request): + def paragraphs_(self, request: FixtureRequest): return instance_mock(request, list) @pytest.fixture - def picture_(self, request): + def picture_(self, request: FixtureRequest): return instance_mock(request, InlineShape) @pytest.fixture - def run_(self, request): + def run_(self, request: FixtureRequest): return instance_mock(request, Run) @pytest.fixture - def Section_(self, request): + def Section_(self, request: FixtureRequest): return class_mock(request, "docx.document.Section") @pytest.fixture - def section_(self, request): + def section_(self, request: FixtureRequest): return instance_mock(request, Section) @pytest.fixture - def Sections_(self, request): + def Sections_(self, request: FixtureRequest): return class_mock(request, "docx.document.Sections") @pytest.fixture - def sections_(self, request): + def sections_(self, request: FixtureRequest): return instance_mock(request, Sections) @pytest.fixture - def sections_prop_(self, request): + def sections_prop_(self, request: FixtureRequest): return property_mock(request, Document, "sections") @pytest.fixture - def settings_(self, request): + def settings_(self, request: FixtureRequest): return instance_mock(request, Settings) @pytest.fixture - def styles_(self, request): + def styles_(self, request: FixtureRequest): return instance_mock(request, Styles) @pytest.fixture - def table_(self, request): - return instance_mock(request, Table, style="UNASSIGNED") + def table_(self, request: FixtureRequest): + return instance_mock(request, Table) @pytest.fixture - def tables_(self, request): + def tables_(self, request: FixtureRequest): return instance_mock(request, list) 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() - assert body._body.xml == expected_xml - assert _body is body + """Unit-test suite for `docx.document._Body`.""" - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("cxml", "expected_cxml"), + [ ("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) - expected_xml = xml(after_cxml) - return body, expected_xml + def it_can_clear_itself_of_all_content_it_holds( + self, cxml: str, expected_cxml: str, document_: Mock + ): + body = _Body(cast(CT_Body, element(cxml)), document_) + + _body = body.clear_content() + + assert body._body.xml == xml(expected_cxml) + assert _body is body + + # -- fixtures -------------------------------------------------------------------------------- + + @pytest.fixture + def document_(self, request: FixtureRequest): + return instance_mock(request, Document) diff --git a/tests/test_package.py b/tests/test_package.py index eda5f0132..ac9839828 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,5 +1,9 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for docx.package module.""" +from __future__ import annotations + import pytest from docx.image.image import Image @@ -8,12 +12,21 @@ from docx.parts.image import ImagePart from .unitutil.file import docx_path -from .unitutil.mock import class_mock, instance_mock, method_mock, property_mock +from .unitutil.mock import ( + FixtureRequest, + Mock, + class_mock, + instance_mock, + method_mock, + property_mock, +) class DescribePackage: + """Unit-test suite for `docx.package.Package`.""" + def it_can_get_or_add_an_image_part_containing_a_specified_image( - self, image_parts_prop_, image_parts_, image_part_ + self, image_parts_prop_: Mock, image_parts_: Mock, image_part_: Mock ): image_parts_prop_.return_value = image_parts_ image_parts_.get_or_add_image_part.return_value = image_part_ @@ -26,29 +39,36 @@ def it_can_get_or_add_an_image_part_containing_a_specified_image( def it_gathers_package_image_parts_after_unmarshalling(self): package = Package.open(docx_path("having-images")) + image_parts = package.image_parts + assert len(image_parts) == 3 - for image_part in image_parts: - assert isinstance(image_part, ImagePart) + assert all(isinstance(p, ImagePart) for p in image_parts) # fixture components --------------------------------------------- @pytest.fixture - def image_part_(self, request): + def image_part_(self, request: FixtureRequest): return instance_mock(request, ImagePart) @pytest.fixture - def image_parts_(self, request): + def image_parts_(self, request: FixtureRequest): return instance_mock(request, ImageParts) @pytest.fixture - def image_parts_prop_(self, request): + def image_parts_prop_(self, request: FixtureRequest): return property_mock(request, Package, "image_parts") class DescribeImageParts: + """Unit-test suite for `docx.package.Package`.""" + def it_can_get_a_matching_image_part( - self, Image_, image_, _get_by_sha1_, image_part_ + self, + Image_: Mock, + image_: Mock, + _get_by_sha1_: Mock, + image_part_: Mock, ): Image_.from_file.return_value = image_ image_.sha1 = "f005ba11" @@ -62,7 +82,12 @@ def it_can_get_a_matching_image_part( assert image_part is image_part_ def but_it_adds_a_new_image_part_when_match_fails( - self, Image_, image_, _get_by_sha1_, _add_image_part_, image_part_ + self, + Image_: Mock, + image_: Mock, + _get_by_sha1_: Mock, + _add_image_part_: Mock, + image_part_: Mock, ): Image_.from_file.return_value = image_ image_.sha1 = "fa1afe1" @@ -77,73 +102,74 @@ def but_it_adds_a_new_image_part_when_match_fails( _add_image_part_.assert_called_once_with(image_parts, image_) assert image_part is image_part_ - def it_knows_the_next_available_image_partname(self, next_partname_fixture): - image_parts, ext, expected_partname = next_partname_fixture - assert image_parts._next_image_partname(ext) == expected_partname + @pytest.mark.parametrize( + ("existing_partname_numbers", "expected_partname_number"), + [ + ((2, 3), 1), + ((1, 3), 2), + ((1, 2), 3), + ], + ) + def it_knows_the_next_available_image_partname( + self, + request: FixtureRequest, + existing_partname_numbers: tuple[int, int], + expected_partname_number: int, + ): + image_parts = ImageParts() + for n in existing_partname_numbers: + image_parts.append( + instance_mock(request, ImagePart, partname=PackURI(f"/word/media/image{n}.png")) + ) + + next_partname = image_parts._next_image_partname("png") - def it_can_really_add_a_new_image_part( - self, _next_image_partname_, partname_, image_, ImagePart_, image_part_ + assert next_partname == PackURI("/word/media/image%d.png" % expected_partname_number) + + def it_can_add_a_new_image_part( + self, + _next_image_partname_: Mock, + image_: Mock, + ImagePart_: Mock, + image_part_: Mock, ): - _next_image_partname_.return_value = partname_ + partname = PackURI("/word/media/image7.png") + _next_image_partname_.return_value = partname ImagePart_.from_image.return_value = image_part_ image_parts = ImageParts() image_part = image_parts._add_image_part(image_) - ImagePart_.from_image.assert_called_once_with(image_, partname_) + ImagePart_.from_image.assert_called_once_with(image_, partname) assert image_part in image_parts assert image_part is image_part_ # fixtures ------------------------------------------------------- - @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) - - 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" - expected_image_partname = image_partname(expected_partname_number) - return image_parts, ext, expected_image_partname - - # fixture components --------------------------------------------- - @pytest.fixture - def _add_image_part_(self, request): + def _add_image_part_(self, request: FixtureRequest): return method_mock(request, ImageParts, "_add_image_part") @pytest.fixture - def _get_by_sha1_(self, request): + def _get_by_sha1_(self, request: FixtureRequest): return method_mock(request, ImageParts, "_get_by_sha1") @pytest.fixture - def Image_(self, request): + def Image_(self, request: FixtureRequest): return class_mock(request, "docx.package.Image") @pytest.fixture - def image_(self, request): + def image_(self, request: FixtureRequest): return instance_mock(request, Image) @pytest.fixture - def ImagePart_(self, request): + def ImagePart_(self, request: FixtureRequest): return class_mock(request, "docx.package.ImagePart") @pytest.fixture - def image_part_(self, request): + def image_part_(self, request: FixtureRequest): return instance_mock(request, ImagePart) @pytest.fixture - def _next_image_partname_(self, request): + def _next_image_partname_(self, request: FixtureRequest): return method_mock(request, ImageParts, "_next_image_partname") - - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) diff --git a/tests/test_section.py b/tests/test_section.py index 333e755b7..54d665768 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -65,9 +65,7 @@ def it_can_access_its_Section_instances_by_index( ): document_elm = cast( CT_Document, - element( - "w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)" - ), + 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_ @@ -87,9 +85,7 @@ def it_can_access_its_Section_instances_by_slice( ): document_elm = cast( CT_Document, - element( - "w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)" - ), + 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_ @@ -103,7 +99,7 @@ def it_can_access_its_Section_instances_by_slice( ] assert section_lst == [section_, section_] - # fixture components --------------------------------------------- + # -- fixtures--------------------------------------------------------------------------------- @pytest.fixture def document_part_(self, request: FixtureRequest): @@ -170,9 +166,7 @@ def it_provides_access_to_its_even_page_footer( footer = section.even_page_footer - _Footer_.assert_called_once_with( - sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE - ) + _Footer_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE) assert footer is footer_ def it_provides_access_to_its_even_page_header( @@ -184,9 +178,7 @@ def it_provides_access_to_its_even_page_header( header = section.even_page_header - _Header_.assert_called_once_with( - sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE - ) + _Header_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE) assert header is header_ def it_provides_access_to_its_first_page_footer( @@ -198,9 +190,7 @@ def it_provides_access_to_its_first_page_footer( footer = section.first_page_footer - _Footer_.assert_called_once_with( - sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE - ) + _Footer_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE) assert footer is footer_ def it_provides_access_to_its_first_page_header( @@ -212,9 +202,7 @@ def it_provides_access_to_its_first_page_header( header = section.first_page_header - _Header_.assert_called_once_with( - sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE - ) + _Header_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE) assert header is header_ def it_provides_access_to_its_default_footer( @@ -226,9 +214,7 @@ def it_provides_access_to_its_default_footer( footer = section.footer - _Footer_.assert_called_once_with( - sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY - ) + _Footer_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) assert footer is footer_ def it_provides_access_to_its_default_header( @@ -240,9 +226,7 @@ def it_provides_access_to_its_default_header( header = section.header - _Header_.assert_called_once_with( - sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY - ) + _Header_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) assert header is header_ def it_can_iterate_its_inner_content(self): @@ -562,20 +546,16 @@ def header_(self, request: FixtureRequest): class Describe_BaseHeaderFooter: """Unit-test suite for `docx.section._BaseHeaderFooter`.""" - @pytest.mark.parametrize( - ("has_definition", "expected_value"), [(False, True), (True, False)] - ) + @pytest.mark.parametrize(("has_definition", "expected_value"), [(False, True), (True, False)]) def it_knows_when_its_linked_to_the_previous_header_or_footer( - self, has_definition: bool, expected_value: bool, _has_definition_prop_: Mock + self, + has_definition: bool, + expected_value: bool, + header: _BaseHeaderFooter, + _has_definition_prop_: Mock, ): _has_definition_prop_.return_value = has_definition - header = _BaseHeaderFooter( - None, None, None # pyright: ignore[reportGeneralTypeIssues] - ) - - is_linked = header.is_linked_to_previous - - assert is_linked is expected_value + assert header.is_linked_to_previous is expected_value @pytest.mark.parametrize( ("has_definition", "value", "drop_calls", "add_calls"), @@ -592,14 +572,12 @@ def it_can_change_whether_it_is_linked_to_previous_header_or_footer( value: bool, drop_calls: int, add_calls: int, + header: _BaseHeaderFooter, _has_definition_prop_: Mock, _drop_definition_: Mock, _add_definition_: Mock, ): _has_definition_prop_.return_value = has_definition - header = _BaseHeaderFooter( - None, None, None # pyright: ignore[reportGeneralTypeIssues] - ) header.is_linked_to_previous = value @@ -607,13 +585,10 @@ def it_can_change_whether_it_is_linked_to_previous_header_or_footer( 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_: Mock, header_part_: Mock + self, header: _BaseHeaderFooter, _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 # pyright: ignore[reportGeneralTypeIssues] - ) header_part = header.part @@ -621,14 +596,11 @@ 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_: Mock, header_part_: Mock + self, header: _BaseHeaderFooter, _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 # pyright: ignore[reportGeneralTypeIssues] - ) hdr_elm = header._element @@ -636,13 +608,14 @@ 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_: Mock, _definition_prop_: Mock, header_part_: Mock + self, + header: _BaseHeaderFooter, + _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 # pyright: ignore[reportGeneralTypeIssues] - ) header_part = header._get_or_add_definition() @@ -650,6 +623,7 @@ def it_gets_the_definition_when_it_has_one( def but_it_gets_the_prior_definition_when_it_is_linked( self, + header: _BaseHeaderFooter, _has_definition_prop_: Mock, _prior_headerfooter_prop_: Mock, prior_headerfooter_: Mock, @@ -658,9 +632,6 @@ def but_it_gets_the_prior_definition_when_it_is_linked( _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 # pyright: ignore[reportGeneralTypeIssues] - ) header_part = header._get_or_add_definition() @@ -669,6 +640,7 @@ 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, + header: _BaseHeaderFooter, _has_definition_prop_: Mock, _prior_headerfooter_prop_: Mock, _add_definition_: Mock, @@ -677,9 +649,6 @@ def and_it_adds_a_definition_when_it_is_linked_and_the_first_section( _has_definition_prop_.return_value = False _prior_headerfooter_prop_.return_value = None _add_definition_.return_value = header_part_ - header = _BaseHeaderFooter( - None, None, None # pyright: ignore[reportGeneralTypeIssues] - ) header_part = header._get_or_add_definition() @@ -696,6 +665,10 @@ def _add_definition_(self, request: FixtureRequest): def _definition_prop_(self, request: FixtureRequest): return property_mock(request, _BaseHeaderFooter, "_definition") + @pytest.fixture + def document_part_(self, request: FixtureRequest): + return instance_mock(request, DocumentPart) + @pytest.fixture def _drop_definition_(self, request: FixtureRequest): return method_mock(request, _BaseHeaderFooter, "_drop_definition") @@ -708,6 +681,11 @@ def _get_or_add_definition_(self, request: FixtureRequest): def _has_definition_prop_(self, request: FixtureRequest): return property_mock(request, _BaseHeaderFooter, "_has_definition") + @pytest.fixture + def header(self, document_part_: Mock) -> _BaseHeaderFooter: + sectPr = cast(CT_SectPr, element("w:sectPr")) + return _BaseHeaderFooter(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) + @pytest.fixture def header_part_(self, request: FixtureRequest): return instance_mock(request, HeaderPart) @@ -724,25 +702,21 @@ def _prior_headerfooter_prop_(self, request: FixtureRequest): class Describe_Footer: """Unit-test suite for `docx.section._Footer`.""" - def it_can_add_a_footer_part_to_help( - self, document_part_: Mock, footer_part_: Mock - ): - sectPr = element("w:sectPr{r:a=b}") + def it_can_add_a_footer_part_to_help(self, document_part_: Mock, footer_part_: Mock): + sectPr = cast(CT_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) footer_part = footer._add_definition() document_part_.add_footer_part.assert_called_once_with() - assert sectPr.xml == xml( - "w:sectPr{r:a=b}/w:footerReference{w:type=default,r:id=rId3}" - ) + assert sectPr.xml == xml("w:sectPr{r:a=b}/w:footerReference{w:type=default,r:id=rId3}") assert footer_part is footer_part_ def it_provides_access_to_its_footer_part_to_help( self, document_part_: Mock, footer_part_: Mock ): - sectPr = element("w:sectPr/w:footerReference{w:type=even,r:id=rId3}") + sectPr = cast(CT_SectPr, element("w:sectPr/w:footerReference{w:type=even,r:id=rId3}")) document_part_.footer_part.return_value = footer_part_ footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE) @@ -752,7 +726,9 @@ def it_provides_access_to_its_footer_part_to_help( assert footer_part is footer_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}") + sectPr = cast( + CT_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) footer._drop_definition() @@ -778,28 +754,26 @@ def it_provides_access_to_the_prior_Footer_to_help( 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] + prior_sectPr, sectPr = cast(CT_SectPr, doc_elm[0]), cast(CT_SectPr, doc_elm[1]) footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE) # ---mock must occur after construction of "real" footer--- _Footer_ = class_mock(request, "docx.section._Footer", return_value=footer_) prior_footer = footer._prior_headerfooter - _Footer_.assert_called_once_with( - prior_sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE - ) + _Footer_.assert_called_once_with(prior_sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE) assert prior_footer is footer_ - def but_it_returns_None_when_its_the_first_footer(self): + def but_it_returns_None_when_its_the_first_footer(self, document_part_: Mock): doc_elm = cast(CT_Document, element("w:document/w:sectPr")) - sectPr = doc_elm[0] - footer = _Footer(sectPr, None, None) + sectPr = cast(CT_SectPr, doc_elm[0]) + footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) prior_footer = footer._prior_headerfooter assert prior_footer is None - # -- fixtures ---------------------------------------------------- + # -- fixtures--------------------------------------------------------------------------------- @pytest.fixture def document_part_(self, request: FixtureRequest): @@ -815,25 +789,23 @@ def footer_part_(self, request: FixtureRequest): 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}") + """Unit-test suite for `docx.section._Header`.""" + + def it_can_add_a_header_part_to_help(self, document_part_: Mock, header_part_: Mock): + sectPr = cast(CT_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) header_part = header._add_definition() document_part_.add_header_part.assert_called_once_with() - assert sectPr.xml == xml( - "w:sectPr{r:a=b}/w:headerReference{w:type=first,r:id=rId3}" - ) + assert sectPr.xml == xml("w:sectPr{r:a=b}/w:headerReference{w:type=first,r:id=rId3}") assert header_part is header_part_ def it_provides_access_to_its_header_part_to_help( self, document_part_: Mock, header_part_: Mock ): - sectPr = element("w:sectPr/w:headerReference{w:type=default,r:id=rId8}") + sectPr = cast(CT_SectPr, element("w:sectPr/w:headerReference{w:type=default,r:id=rId8}")) document_part_.header_part.return_value = header_part_ header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) @@ -843,7 +815,9 @@ def it_provides_access_to_its_header_part_to_help( assert header_part is header_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}") + sectPr = cast( + CT_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) header._drop_definition() @@ -866,31 +840,29 @@ def it_knows_when_it_has_a_header_part_to_help( assert has_definition is expected_value def it_provides_access_to_the_prior_Header_to_help( - self, request, document_part_: Mock, header_: Mock + self, request: FixtureRequest, document_part_: Mock, header_: Mock ): doc_elm = element("w:document/(w:sectPr,w:sectPr)") - prior_sectPr, sectPr = doc_elm[0], doc_elm[1] + prior_sectPr, sectPr = cast(CT_SectPr, doc_elm[0]), cast(CT_SectPr, doc_elm[1]) header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) # ---mock must occur after construction of "real" header--- _Header_ = class_mock(request, "docx.section._Header", return_value=header_) prior_header = header._prior_headerfooter - _Header_.assert_called_once_with( - prior_sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY - ) + _Header_.assert_called_once_with(prior_sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) assert prior_header is header_ - def but_it_returns_None_when_its_the_first_header(self): + def but_it_returns_None_when_its_the_first_header(self, document_part_: Mock): doc_elm = element("w:document/w:sectPr") - sectPr = doc_elm[0] - header = _Header(sectPr, None, None) + sectPr = cast(CT_SectPr, doc_elm[0]) + header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) prior_header = header._prior_headerfooter assert prior_header is None - # -- fixtures----------------------------------------------------- + # -- fixtures--------------------------------------------------------------------------------- @pytest.fixture def document_part_(self, request: FixtureRequest): diff --git a/tests/test_settings.py b/tests/test_settings.py index 9f430822d..ff07eda26 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,9 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for the docx.settings module.""" +from __future__ import annotations + import pytest from docx.settings import Settings @@ -8,56 +12,37 @@ class DescribeSettings: - def it_knows_when_the_document_has_distinct_odd_and_even_headers( - self, odd_and_even_get_fixture - ): - settings_elm, expected_value = odd_and_even_get_fixture - settings = Settings(settings_elm) - - odd_and_even_pages_header_footer = settings.odd_and_even_pages_header_footer - - assert odd_and_even_pages_header_footer is expected_value - - def it_can_change_whether_the_document_has_distinct_odd_and_even_headers( - self, odd_and_even_set_fixture - ): - settings_elm, value, expected_xml = odd_and_even_set_fixture - settings = Settings(settings_elm) + """Unit-test suite for the `docx.settings.Settings` objects.""" - settings.odd_and_even_pages_header_footer = value - - assert settings_elm.xml == expected_xml - - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("cxml", "expected_value"), + [ ("w:settings", False), ("w:settings/w:evenAndOddHeaders", True), ("w:settings/w:evenAndOddHeaders{w:val=0}", False), ("w:settings/w:evenAndOddHeaders{w:val=1}", True), ("w:settings/w:evenAndOddHeaders{w:val=true}", True), - ] + ], ) - def odd_and_even_get_fixture(self, request): - settings_cxml, expected_value = request.param - settings_elm = element(settings_cxml) - return settings_elm, expected_value + def it_knows_when_the_document_has_distinct_odd_and_even_headers( + self, cxml: str, expected_value: bool + ): + assert Settings(element(cxml)).odd_and_even_pages_header_footer is expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("cxml", "new_value", "expected_cxml"), + [ ("w:settings", True, "w:settings/w:evenAndOddHeaders"), ("w:settings/w:evenAndOddHeaders", False, "w:settings"), - ( - "w:settings/w:evenAndOddHeaders{w:val=1}", - True, - "w:settings/w:evenAndOddHeaders", - ), + ("w:settings/w:evenAndOddHeaders{w:val=1}", True, "w:settings/w:evenAndOddHeaders"), ("w:settings/w:evenAndOddHeaders{w:val=off}", False, "w:settings"), - ] + ], ) - def odd_and_even_set_fixture(self, request): - settings_cxml, value, expected_cxml = request.param - settings_elm = element(settings_cxml) - expected_xml = xml(expected_cxml) - return settings_elm, value, expected_xml + def it_can_change_whether_the_document_has_distinct_odd_and_even_headers( + self, cxml: str, new_value: bool, expected_cxml: str + ): + settings = Settings(element(cxml)) + + settings.odd_and_even_pages_header_footer = new_value + + assert settings._settings.xml == xml(expected_cxml) diff --git a/tests/test_shape.py b/tests/test_shape.py index da307e48f..68998b90e 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -1,194 +1,129 @@ +# pyright: reportPrivateUsage=false + """Test suite for the docx.shape module.""" +from __future__ import annotations + +from typing import cast + import pytest +from docx.document import Document from docx.enum.shape import WD_INLINE_SHAPE +from docx.oxml.document import CT_Body from docx.oxml.ns import nsmap +from docx.oxml.shape import CT_Inline from docx.shape import InlineShape, InlineShapes -from docx.shared import Length - -from .oxml.unitdata.dml import ( - a_blip, - a_blipFill, - a_graphic, - a_graphicData, - a_pic, - an_inline, -) +from docx.shared import Emu, Length + from .unitutil.cxml import element, xml -from .unitutil.mock import loose_mock +from .unitutil.mock import FixtureRequest, Mock, instance_mock 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): - inline_shapes, inline_shape_count = inline_shapes_fixture - actual_count = 0 - for inline_shape in inline_shapes: - assert isinstance(inline_shape, InlineShape) - actual_count += 1 - assert actual_count == inline_shape_count - - 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): - inline_shapes, inline_shape_count = inline_shapes_fixture - too_low = -1 - inline_shape_count - with pytest.raises(IndexError, match=r"inline shape index \[-3\] out of rang"): - inline_shapes[too_low] - too_high = inline_shape_count + """Unit-test suite for `docx.shape.InlineShapes` objects.""" + + def it_knows_how_many_inline_shapes_it_contains(self, body: CT_Body, document_: Mock): + inline_shapes = InlineShapes(body, document_) + assert len(inline_shapes) == 2 + + def it_can_iterate_over_its_InlineShape_instances(self, body: CT_Body, document_: Mock): + inline_shapes = InlineShapes(body, document_) + assert all(isinstance(s, InlineShape) for s in inline_shapes) + assert len(list(inline_shapes)) == 2 + + def it_provides_indexed_access_to_inline_shapes(self, body: CT_Body, document_: Mock): + inline_shapes = InlineShapes(body, document_) + for idx in range(-2, 2): + assert isinstance(inline_shapes[idx], InlineShape) + + def it_raises_on_indexed_access_out_of_range(self, body: CT_Body, document_: Mock): + inline_shapes = InlineShapes(body, document_) + + with pytest.raises(IndexError, match=r"inline shape index \[-3\] out of range"): + inline_shapes[-3] with pytest.raises(IndexError, match=r"inline shape index \[2\] out of range"): - inline_shapes[too_high] + inline_shapes[2] - def it_knows_the_part_it_belongs_to(self, inline_shapes_with_parent_): - inline_shapes, parent_ = inline_shapes_with_parent_ - part = inline_shapes.part - assert part is parent_.part + def it_knows_the_part_it_belongs_to(self, body: CT_Body, document_: Mock): + inline_shapes = InlineShapes(body, document_) + assert inline_shapes.part is document_.part - # fixtures ------------------------------------------------------- + # -- fixtures -------------------------------------------------------------------------------- @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)") - inline_shapes = InlineShapes(body, None) - expected_count = 2 - return inline_shapes, expected_count - - # fixture components --------------------------------------------- + def body(self) -> CT_Body: + return cast( + CT_Body, element("w:body/w:p/(w:r/w:drawing/wp:inline, w:r/w:drawing/wp:inline)") + ) @pytest.fixture - def inline_shapes_with_parent_(self, request): - parent_ = loose_mock(request, name="parent_") - inline_shapes = InlineShapes(None, parent_) - return inline_shapes, parent_ + def document_(self, request: FixtureRequest): + return instance_mock(request, Document) 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 - - def it_knows_its_display_dimensions(self, dimensions_get_fixture): - inline_shape, cx, cy = dimensions_get_fixture - width = inline_shape.width - height = inline_shape.height - assert isinstance(width, Length) - assert width == cx - assert isinstance(height, Length) - assert height == cy + """Unit-test suite for `docx.shape.InlineShape` objects.""" + + @pytest.mark.parametrize( + ("uri", "content_cxml", "expected_value"), + [ + # -- embedded picture -- + (nsmap["pic"], "/pic:pic/pic:blipFill/a:blip{r:embed=rId1}", WD_INLINE_SHAPE.PICTURE), + # -- linked picture -- + ( + nsmap["pic"], + "/pic:pic/pic:blipFill/a:blip{r:link=rId2}", + WD_INLINE_SHAPE.LINKED_PICTURE, + ), + # -- linked and embedded picture (not expected) -- + ( + nsmap["pic"], + "/pic:pic/pic:blipFill/a:blip{r:embed=rId1,r:link=rId2}", + WD_INLINE_SHAPE.LINKED_PICTURE, + ), + # -- chart -- + (nsmap["c"], "", WD_INLINE_SHAPE.CHART), + # -- SmartArt -- + (nsmap["dgm"], "", WD_INLINE_SHAPE.SMART_ART), + # -- something else we don't know about -- + ("foobar", "", WD_INLINE_SHAPE.NOT_IMPLEMENTED), + ], + ) + def it_knows_what_type_of_shape_it_is( + self, uri: str, content_cxml: str, expected_value: WD_INLINE_SHAPE + ): + cxml = "wp:inline/a:graphic/a:graphicData{uri=%s}%s" % (uri, content_cxml) + inline = cast(CT_Inline, element(cxml)) + inline_shape = InlineShape(inline) + assert inline_shape.type == expected_value - def it_can_change_its_display_dimensions(self, dimensions_set_fixture): - inline_shape, cx, cy, expected_xml = dimensions_set_fixture - inline_shape.width = cx - inline_shape.height = cy - assert inline_shape._inline.xml == expected_xml + def it_knows_its_display_dimensions(self): + inline = cast(CT_Inline, element("wp:inline/wp:extent{cx=333, cy=666}")) + inline_shape = InlineShape(inline) - # fixtures ------------------------------------------------------- + width, height = inline_shape.width, inline_shape.height - @pytest.fixture - def dimensions_get_fixture(self): - inline_cxml, expected_cx, expected_cy = ( - "wp:inline/wp:extent{cx=333, cy=666}", - 333, - 666, + assert isinstance(width, Length) + assert width == 333 + assert isinstance(height, Length) + assert height == 666 + + def it_can_change_its_display_dimensions(self): + inline_shape = InlineShape( + cast( + CT_Inline, + element( + "wp:inline/(wp:extent{cx=333,cy=666},a:graphic/a:graphicData/pic:pic/" + "pic:spPr/a:xfrm/a:ext{cx=333,cy=666})" + ), + ) ) - inline_shape = InlineShape(element(inline_cxml)) - return inline_shape, expected_cx, expected_cy - @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})", + inline_shape.width = Emu(444) + inline_shape.height = Emu(888) + + assert inline_shape._inline.xml == xml( + "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", - ] - ) - def shape_type_fixture(self, request): - if request.param == "embed pic": - inline = self._inline_with_picture(embed=True) - shape_type = WD_INLINE_SHAPE.PICTURE - - 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": - 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"]) - shape_type = WD_INLINE_SHAPE.CHART - - 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") - shape_type = WD_INLINE_SHAPE.NOT_IMPLEMENTED - - return InlineShape(inline), shape_type - - # fixture components --------------------------------------------- - - def _inline_with_picture(self, embed=False, link=False): - picture_ns = nsmap["pic"] - - blip_bldr = a_blip() - if embed: - blip_bldr.with_embed("rId1") - if link: - 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)) - ) - ) - ) - ).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)) - ) - ).element - return inline diff --git a/tests/test_shared.py b/tests/test_shared.py index 3fbe54b07..fb6c273cb 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,17 +1,25 @@ """Test suite for the docx.shared module.""" +from __future__ import annotations + import pytest from docx.opc.part import XmlPart from docx.shared import Cm, ElementProxy, Emu, Inches, Length, Mm, Pt, RGBColor, Twips from .unitutil.cxml import element -from .unitutil.mock import instance_mock +from .unitutil.mock import FixtureRequest, Mock, instance_mock 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 + """Unit-test suite for `docx.shared.ElementProxy` objects.""" + + def it_knows_when_its_equal_to_another_proxy_object(self): + p, q = element("w:p"), element("w:p") + proxy = ElementProxy(p) + proxy_2 = ElementProxy(p) + proxy_3 = ElementProxy(q) + not_a_proxy = "Foobar" assert (proxy == proxy_2) is True assert (proxy == proxy_3) is False @@ -21,66 +29,33 @@ def it_knows_when_its_equal_to_another_proxy_object(self, eq_fixture): assert (proxy != proxy_3) is True assert (proxy != not_a_proxy) is True - def it_knows_its_element(self, element_fixture): - proxy, element = element_fixture - assert proxy.element is element - - def it_knows_its_part(self, part_fixture): - proxy, part_ = part_fixture - assert proxy.part is part_ - - # fixture -------------------------------------------------------- - - @pytest.fixture - def element_fixture(self): + def it_knows_its_element(self): p = element("w:p") proxy = ElementProxy(p) - return proxy, p - - @pytest.fixture - def eq_fixture(self): - p, q = element("w:p"), element("w:p") - proxy = ElementProxy(p) - proxy_2 = ElementProxy(p) - proxy_3 = ElementProxy(q) - not_a_proxy = "Foobar" - return proxy, proxy_2, proxy_3, not_a_proxy + assert proxy.element is p - @pytest.fixture - def part_fixture(self, other_proxy_, part_): + def it_knows_its_part(self, other_proxy_: Mock, part_: Mock): other_proxy_.part = part_ - proxy = ElementProxy(None, other_proxy_) - return proxy, part_ + proxy = ElementProxy(element("w:p"), other_proxy_) + assert proxy.part is part_ - # fixture components --------------------------------------------- + # -- fixture --------------------------------------------------------------------------------- @pytest.fixture - def other_proxy_(self, request): + def other_proxy_(self, request: FixtureRequest): return instance_mock(request, ElementProxy) @pytest.fixture - def part_(self, request): + def part_(self, request: FixtureRequest): return instance_mock(request, XmlPart) class DescribeLength: - def it_can_construct_from_convenient_units(self, construct_fixture): - UnitCls, units_val, emu = construct_fixture - length = UnitCls(units_val) - assert isinstance(length, Length) - assert length == emu - - def it_can_self_convert_to_convenient_units(self, units_fixture): - emu, units_prop_name, expected_length_in_units, type_ = units_fixture - length = Length(emu) - length_in_units = getattr(length, units_prop_name) - assert length_in_units == expected_length_in_units - assert isinstance(length_in_units, type_) - - # fixtures ------------------------------------------------------- + """Unit-test suite for `docx.shared.Length` objects.""" - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("UnitCls", "units_val", "emu"), + [ (Length, 914400, 914400), (Inches, 1.1, 1005840), (Cm, 2.53, 910799), @@ -88,32 +63,47 @@ def it_can_self_convert_to_convenient_units(self, units_fixture): (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), - ] + def it_can_construct_from_convenient_units(self, UnitCls: type, units_val: float, emu: int): + length = UnitCls(units_val) + assert isinstance(length, Length) + assert length == emu + + @pytest.mark.parametrize( + ("prop_name", "expected_value", "expected_type"), + [ + ("inches", 1.0, float), + ("cm", 2.54, float), + ("emu", 914400, int), + ("mm", 25.4, float), + ("pt", 72.0, float), + ("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_ + def it_can_self_convert_to_convenient_units( + self, prop_name: str, expected_value: float, expected_type: type + ): + # -- use an inch for the initial value -- + length = Length(914400) + length_in_units = getattr(length, prop_name) + assert length_in_units == expected_value + assert isinstance(length_in_units, expected_type) class DescribeRGBColor: + """Unit-test suite for `docx.shared.RGBColor` objects.""" + def it_is_natively_constructed_using_three_ints_0_to_255(self): - RGBColor(0x12, 0x34, 0x56) - with pytest.raises(ValueError, match=r"RGBColor\(\) takes three integer valu"): - RGBColor("12", "34", "56") + rgb_color = RGBColor(0x12, 0x34, 0x56) + + assert isinstance(rgb_color, RGBColor) + # -- it is comparable to a tuple[int, int, int] -- + assert rgb_color == (18, 52, 86) + + def it_raises_with_helpful_error_message_on_wrong_types(self): + with pytest.raises(TypeError, match=r"RGBColor\(\) takes three integer valu"): + RGBColor("12", "34", "56") # pyright: ignore with pytest.raises(ValueError, match=r"\(\) takes three integer values 0-255"): RGBColor(-1, 34, 56) with pytest.raises(ValueError, match=r"RGBColor\(\) takes three integer valu"): @@ -124,7 +114,7 @@ def it_can_construct_from_a_hex_string_rgb_value(self): 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(0xF3, 0x8A, 0x56)) == "F38A56" def it_has_a_custom_repr(self): rgb_color = RGBColor(0x42, 0xF0, 0xBA) diff --git a/tests/text/test_run.py b/tests/text/test_run.py index 772c5ad82..a54120fdd 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -15,23 +15,67 @@ from docx.parts.document import DocumentPart from docx.shape import InlineShape from docx.text.font import Font +from docx.text.paragraph import Paragraph from docx.text.run import Run from ..unitutil.cxml import element, xml -from ..unitutil.mock import class_mock, instance_mock, property_mock +from ..unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock, property_mock 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 - assert getattr(run, prop_name) == expected_state + @pytest.mark.parametrize( + ("r_cxml", "bool_prop_name", "expected_value"), + [ + ("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 it_knows_its_bool_prop_states( + self, r_cxml: str, bool_prop_name: str, expected_value: bool | None, paragraph_: Mock + ): + run = Run(cast(CT_R, element(r_cxml)), paragraph_) + assert getattr(run, bool_prop_name) == expected_value - def it_can_change_its_bool_prop_settings(self, bool_prop_set_fixture): - run, prop_name, value, expected_xml = bool_prop_set_fixture - setattr(run, prop_name, value) - assert run._r.xml == expected_xml + @pytest.mark.parametrize( + ("initial_r_cxml", "bool_prop_name", "value", "expected_cxml"), + [ + # -- 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 it_can_change_its_bool_prop_settings( + self, + initial_r_cxml: str, + bool_prop_name: str, + value: bool | None, + expected_cxml: str, + paragraph_: Mock, + ): + run = Run(cast(CT_R, element(initial_r_cxml)), paragraph_) + + setattr(run, bool_prop_name, value) + + assert run._r.xml == xml(expected_cxml) @pytest.mark.parametrize( ("r_cxml", "expected_value"), @@ -43,11 +87,9 @@ def it_can_change_its_bool_prop_settings(self, bool_prop_set_fixture): ], ) def it_knows_whether_it_contains_a_page_break( - self, r_cxml: str, expected_value: bool + self, r_cxml: str, expected_value: bool, paragraph_: Mock ): - r = cast(CT_R, element(r_cxml)) - run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues] - + run = Run(cast(CT_R, element(r_cxml)), paragraph_) assert run.contains_page_break == expected_value @pytest.mark.parametrize( @@ -80,48 +122,138 @@ def it_can_iterate_its_inner_content_items( 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 + def it_knows_its_character_style( + self, part_prop_: Mock, document_part_: Mock, paragraph_: Mock + ): + style_ = document_part_.get_style.return_value + part_prop_.return_value = document_part_ + style_id = "Barfoo" + run = Run(cast(CT_R, element(f"w:r/w:rPr/w:rStyle{{w:val={style_id}}}")), paragraph_) + style = run.style - run.part.get_style.assert_called_once_with(style_id_, WD_STYLE_TYPE.CHARACTER) + + document_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 + @pytest.mark.parametrize( + ("r_cxml", "value", "style_id", "expected_cxml"), + [ + ("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 it_can_change_its_character_style( + self, + r_cxml: str, + value: str | None, + style_id: str | None, + expected_cxml: str, + part_prop_: Mock, + paragraph_: Mock, + ): + part_ = part_prop_.return_value + part_.get_style_id.return_value = style_id + run = Run(cast(CT_R, element(r_cxml)), paragraph_) + run.style = value - 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): - run, expected_value = underline_get_fixture + part_.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.CHARACTER) + assert run._r.xml == xml(expected_cxml) + + @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: bool | WD_UNDERLINE | None, paragraph_: Mock + ): + run = Run(cast(CT_R, element(r_cxml)), paragraph_) assert run.underline is expected_value - def it_can_change_its_underline_type(self, underline_set_fixture): - run, underline, expected_xml = underline_set_fixture - run.underline = underline - assert run._r.xml == expected_xml + @pytest.mark.parametrize( + ("initial_r_cxml", "new_underline", "expected_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, + initial_r_cxml: str, + new_underline: bool | WD_UNDERLINE | None, + expected_cxml: str, + paragraph_: Mock, + ): + run = Run(cast(CT_R, element(initial_r_cxml)), paragraph_) + + run.underline = new_underline + + assert run._r.xml == xml(expected_cxml) @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) + def it_raises_on_assign_invalid_underline_value(self, invalid_value: Any, paragraph_: Mock): + run = Run(cast(CT_R, element("w:r/w:rPr")), paragraph_) 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 + def it_provides_access_to_its_font(self, Font_: Mock, font_: Mock, paragraph_: Mock): + Font_.return_value = font_ + run = Run(cast(CT_R, element("w:r")), paragraph_) + font = run.font + Font_.assert_called_once_with(run._element) assert font is font_ - def it_can_add_text(self, add_text_fixture, Text_): - r, text_str, expected_xml = add_text_fixture - run = Run(r, None) + @pytest.mark.parametrize( + ("r_cxml", "new_text", "expected_cxml"), + [ + ("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 it_can_add_text( + self, r_cxml: str, new_text: str, expected_cxml: str, Text_: Mock, paragraph_: Mock + ): + run = Run(cast(CT_R, element(r_cxml)), paragraph_) - _text = run.add_text(text_str) + text = run.add_text(new_text) - assert run._r.xml == expected_xml - assert _text is Text_.return_value + assert run._r.xml == xml(expected_cxml) + assert text is Text_.return_value @pytest.mark.parametrize( ("break_type", "expected_cxml"), @@ -134,28 +266,42 @@ def it_can_add_text(self, add_text_fixture, Text_): (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) + def it_can_add_a_break(self, break_type: WD_BREAK, expected_cxml: str, paragraph_: Mock): + run = Run(cast(CT_R, element("w:r")), paragraph_) run.add_break(break_type) - assert run._r.xml == expected_xml + assert run._r.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + ("r_cxml", "expected_cxml"), [('w:r/w:t"foo"', 'w:r/(w:t"foo", w:tab)')] + ) + def it_can_add_a_tab(self, r_cxml: str, expected_cxml: str, paragraph_: Mock): + run = Run(cast(CT_R, element(r_cxml)), paragraph_) - def it_can_add_a_tab(self, add_tab_fixture): - run, expected_xml = add_tab_fixture run.add_tab() - assert run._r.xml == expected_xml - def it_can_add_a_picture(self, add_picture_fixture): - run, image, width, height, inline = add_picture_fixture[:5] - expected_xml, InlineShape_, picture_ = add_picture_fixture[5:] + assert run._r.xml == xml(expected_cxml) + + def it_can_add_a_picture( + self, + part_prop_: Mock, + document_part_: Mock, + InlineShape_: Mock, + picture_: Mock, + paragraph_: Mock, + ): + part_prop_.return_value = document_part_ + run = Run(cast(CT_R, element("w:r/wp:x")), paragraph_) + image = "foobar.png" + width, height, inline = 1111, 2222, element("wp:inline{id=42}") + document_part_.new_pic_inline.return_value = inline + InlineShape_.return_value = picture_ picture = run.add_picture(image, width, height) - run.part.new_pic_inline.assert_called_once_with(image, width, height) - assert run._r.xml == expected_xml + document_part_.new_pic_inline.assert_called_once_with(image, width, height) + assert run._r.xml == xml("w:r/(wp:x,w:drawing/wp:inline{id=42})") InlineShape_.assert_called_once_with(inline) assert picture is picture_ @@ -174,15 +320,13 @@ def it_can_add_a_picture(self, add_picture_fixture): ], ) def it_can_remove_its_content_but_keep_formatting( - self, initial_r_cxml: str, expected_cxml: str + self, initial_r_cxml: str, expected_cxml: str, paragraph_: Mock ): - r = cast(CT_R, element(initial_r_cxml)) - run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues] - expected_xml = xml(expected_cxml) + run = Run(cast(CT_R, element(initial_r_cxml)), paragraph_) cleared_run = run.clear() - assert run._r.xml == expected_xml + assert run._r.xml == xml(expected_cxml) assert cleared_run is run @pytest.mark.parametrize( @@ -194,212 +338,58 @@ def it_can_remove_its_content_but_keep_formatting( ('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] + def it_knows_the_text_it_contains(self, r_cxml: str, expected_text: str, paragraph_: Mock): + run = Run(cast(CT_R, element(r_cxml)), paragraph_) assert run.text == expected_text - def it_can_replace_the_text_it_contains(self, text_set_fixture): - run, text, expected_xml = text_set_fixture - run.text = text - assert run._r.xml == expected_xml - - # fixtures ------------------------------------------------------- - - @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})") - 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)'), - ] - ) - 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"'), - ] - ) - 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), - ] - ) - 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"), - ] - ) - 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 - def font_fixture(self, Font_, font_): - 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 - 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"), - ] - ) - def style_set_fixture(self, request, part_prop_): - r_cxml, value, style_id, expected_cxml = request.param - run = Run(element(r_cxml), None) - part_prop_.return_value.get_style_id.return_value = style_id - expected_xml = xml(expected_cxml) - return run, value, expected_xml - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("new_text", "expected_cxml"), + [ ("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"' - run = Run(element(initial_r_cxml), None) - 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), - ] + ], ) - def underline_get_fixture(self, request): - r_cxml, expected_underline = request.param - run = Run(element(r_cxml), None) - return run, expected_underline + def it_can_replace_the_text_it_contains( + self, new_text: str, expected_cxml: str, paragraph_: Mock + ): + run = Run(cast(CT_R, element('w:r/w:t"should get deleted"')), paragraph_) - @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 + run.text = new_text - # fixture components --------------------------------------------- + assert run._r.xml == xml(expected_cxml) + + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture - def document_part_(self, request): + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def Font_(self, request, font_): - return class_mock(request, "docx.text.run.Font", return_value=font_) + def Font_(self, request: FixtureRequest): + return class_mock(request, "docx.text.run.Font") @pytest.fixture - def font_(self, request): + def font_(self, request: FixtureRequest): return instance_mock(request, Font) @pytest.fixture - def InlineShape_(self, request): + def InlineShape_(self, request: FixtureRequest): 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_) + def paragraph_(self, request: FixtureRequest): + return instance_mock(request, Paragraph) + + @pytest.fixture + def part_prop_(self, request: FixtureRequest): + return property_mock(request, Run, "part") @pytest.fixture - def picture_(self, request): + def picture_(self, request: FixtureRequest): return instance_mock(request, InlineShape) @pytest.fixture - def Text_(self, request): + def Text_(self, request: FixtureRequest): return class_mock(request, "docx.text.run._Text") From 592fa8f332819a7a8175dd023d3e04597ebba524 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 6 Jun 2025 10:58:26 -0700 Subject: [PATCH 104/131] modn: improve ruff compliance Run `ruff format` on code base. --- docs/conf.py | 4 +- features/steps/block.py | 4 +- features/steps/document.py | 8 +- features/steps/hyperlink.py | 32 +- features/steps/pagebreak.py | 72 ++--- features/steps/paragraph.py | 16 +- features/steps/parfmt.py | 4 +- features/steps/section.py | 16 +- features/steps/settings.py | 8 +- features/steps/shape.py | 7 +- features/steps/text.py | 18 +- src/docx/blkcntnr.py | 2 +- src/docx/image/__init__.py | 2 +- src/docx/image/constants.py | 128 ++++---- src/docx/image/image.py | 4 +- src/docx/image/jpeg.py | 20 +- src/docx/opc/constants.py | 472 ++++++++---------------------- src/docx/opc/packuri.py | 3 +- src/docx/opc/pkgreader.py | 8 +- src/docx/opc/rel.py | 6 +- src/docx/oxml/simpletypes.py | 2 +- src/docx/oxml/table.py | 10 +- src/docx/oxml/text/font.py | 8 +- src/docx/oxml/text/pagebreak.py | 12 +- src/docx/oxml/xmlchemy.py | 6 +- src/docx/parts/settings.py | 3 +- src/docx/parts/styles.py | 4 +- src/docx/styles/styles.py | 17 +- src/docx/text/font.py | 6 +- src/docx/text/tabstops.py | 4 +- src/docx/types.py | 6 +- tests/image/test_bmp.py | 4 +- tests/image/test_gif.py | 2 +- tests/image/test_helpers.py | 4 +- tests/image/test_image.py | 8 +- tests/image/test_jpeg.py | 20 +- tests/image/test_png.py | 24 +- tests/image/test_tiff.py | 30 +- tests/opc/test_pkgreader.py | 32 +- tests/opc/test_rel.py | 20 +- tests/oxml/parts/test_document.py | 5 +- tests/oxml/test__init__.py | 9 +- tests/oxml/test_styles.py | 3 +- tests/oxml/test_table.py | 11 +- tests/oxml/test_xmlchemy.py | 66 ++--- tests/oxml/text/test_hyperlink.py | 4 +- tests/parts/test_hdrftr.py | 8 +- tests/parts/test_image.py | 8 +- tests/parts/test_numbering.py | 4 +- tests/parts/test_settings.py | 8 +- tests/parts/test_story.py | 4 +- tests/styles/test_style.py | 14 +- tests/styles/test_styles.py | 23 +- tests/test_enum.py | 4 +- tests/text/test_font.py | 24 +- tests/text/test_pagebreak.py | 8 +- tests/text/test_paragraph.py | 18 +- tests/unitutil/file.py | 4 +- tests/unitutil/mock.py | 8 +- 59 files changed, 383 insertions(+), 906 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 06b428064..e37e9be7e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -270,9 +270,7 @@ # Custom sidebar templates, maps document names to template names. # html_sidebars = {} -html_sidebars = { - "**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"] -} +html_sidebars = {"**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"]} # Additional templates that should be rendered to pages, maps page names to # template names. diff --git a/features/steps/block.py b/features/steps/block.py index c365c9510..e3d5c6154 100644 --- a/features/steps/block.py +++ b/features/steps/block.py @@ -13,9 +13,7 @@ @given("a _Cell object with paragraphs and tables") def given_a_cell_with_paragraphs_and_tables(context: Context): - context.cell = ( - Document(test_docx("blk-paras-and-tables")).tables[1].rows[0].cells[0] - ) + context.cell = Document(test_docx("blk-paras-and-tables")).tables[1].rows[0].cells[0] @given("a Document object with paragraphs and tables") diff --git a/features/steps/document.py b/features/steps/document.py index 49165efc3..1c12ac106 100644 --- a/features/steps/document.py +++ b/features/steps/document.py @@ -126,17 +126,13 @@ def when_add_picture_specifying_width_and_height(context): @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) - ) + context.picture = document.add_picture(test_file("monty-truth.png"), height=Inches(1.5)) @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) - ) + context.picture = document.add_picture(test_file("monty-truth.png"), width=Inches(1.5)) @when("I add a picture specifying only the image file") diff --git a/features/steps/hyperlink.py b/features/steps/hyperlink.py index 2bba31ed8..14fa9f7be 100644 --- a/features/steps/hyperlink.py +++ b/features/steps/hyperlink.py @@ -27,9 +27,7 @@ def given_a_hyperlink_having_a_uri_fragment(context: Context): @given("a hyperlink having address {address} and fragment {fragment}") -def given_a_hyperlink_having_address_and_fragment( - context: Context, address: str, fragment: str -): +def given_a_hyperlink_having_address_and_fragment(context: Context, address: str, fragment: str): paragraph_idxs: Dict[Tuple[str, str], int] = { ("''", "linkedBookmark"): 1, ("https://foo.com", "''"): 2, @@ -73,60 +71,46 @@ def given_a_hyperlink_having_one_or_more_runs(context: Context, one_or_more: str 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}" + 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}" + assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}" @then("hyperlink.fragment is the URI fragment of the hyperlink") def then_hyperlink_fragment_is_the_URI_fragment_of_the_hyperlink(context: Context): actual_value = context.hyperlink.fragment expected_value = "linkedBookmark" - assert ( - actual_value == expected_value - ), f"expected: {expected_value}, got: {actual_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}" + 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}" + 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}" + assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}" @then("hyperlink.url is {value}") def then_hyperlink_url_is_value(context: Context, value: str): actual_value = context.hyperlink.url expected_value = "" if value == "''" else value - assert ( - actual_value == expected_value - ), f"expected: {expected_value}, got: {actual_value}" + assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}" diff --git a/features/steps/pagebreak.py b/features/steps/pagebreak.py index 7d443da46..870428127 100644 --- a/features/steps/pagebreak.py +++ b/features/steps/pagebreak.py @@ -38,33 +38,23 @@ def then_rendered_page_break_preceding_paragraph_fragment_includes_the_hyperlink actual_value = type(para_frag).__name__ expected_value = "Paragraph" - assert ( - actual_value == expected_value - ), f"expected: '{expected_value}', got: '{actual_value}'" + assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'" actual_value = para_frag.text expected_value = "Page break in>><pqr stu - """ % nsdecls( - "w" - ) + """ % nsdecls("w") r = parse_xml(r_xml) context.run = Run(r, None) @@ -235,9 +233,7 @@ def then_run_font_is_the_Font_object_for_the_run(context): 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}" + assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}" @then("run.style is styles['{style_name}']") @@ -267,15 +263,15 @@ 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) + 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") diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index a9969f6f6..951e03427 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -67,7 +67,7 @@ def add_table(self, rows: int, cols: int, width: Length) -> Table: from docx.table import Table tbl = CT_Tbl.new_tbl(rows, cols, width) - self._element._insert_tbl(tbl) # # pyright: ignore[reportPrivateUsage] + self._element._insert_tbl(tbl) # pyright: ignore[reportPrivateUsage] return Table(tbl, self) def iter_inner_content(self) -> Iterator[Paragraph | Table]: diff --git a/src/docx/image/__init__.py b/src/docx/image/__init__.py index d28033ef1..9d5e4b05b 100644 --- a/src/docx/image/__init__.py +++ b/src/docx/image/__init__.py @@ -12,7 +12,7 @@ SIGNATURES = ( # class, offset, signature_bytes - (Png, 0, b"\x89PNG\x0D\x0A\x1A\x0A"), + (Png, 0, b"\x89PNG\x0d\x0a\x1a\x0a"), (Jfif, 6, b"JFIF"), (Exif, 6, b"Exif"), (Gif, 0, b"GIF87a"), diff --git a/src/docx/image/constants.py b/src/docx/image/constants.py index 729a828b2..03fae5855 100644 --- a/src/docx/image/constants.py +++ b/src/docx/image/constants.py @@ -5,58 +5,58 @@ 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" + 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) @@ -78,18 +78,18 @@ class JPEG_MARKER_CODE: 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", + 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 diff --git a/src/docx/image/image.py b/src/docx/image/image.py index 0022b5b45..e5e7f8a13 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -194,7 +194,7 @@ def __init__(self, px_width: int, px_height: int, horz_dpi: int, vert_dpi: int): @property def content_type(self) -> str: """Abstract property definition, must be implemented by all subclasses.""" - msg = "content_type property must be implemented by all subclasses of " "BaseImageHeader" + msg = "content_type property must be implemented by all subclasses of BaseImageHeader" raise NotImplementedError(msg) @property @@ -204,7 +204,7 @@ def default_ext(self) -> str: An abstract property definition, must be implemented by all subclasses. """ raise NotImplementedError( - "default_ext property must be implemented by all subclasses of " "BaseImageHeader" + "default_ext property must be implemented by all subclasses of BaseImageHeader" ) @property diff --git a/src/docx/image/jpeg.py b/src/docx/image/jpeg.py index b0114a998..74da51871 100644 --- a/src/docx/image/jpeg.py +++ b/src/docx/image/jpeg.py @@ -188,20 +188,20 @@ def next(self, start): 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`. + '\xff', starting with the byte at offset `start`. - If the byte at offset `start` is not '\xFF', `start` and the returned `offset` + 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 + """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 @@ -209,7 +209,7 @@ def _offset_of_next_ff_byte(self, start): """ 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 @@ -263,7 +263,7 @@ 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'`` + """The single-byte code that identifies the type of this marker, e.g. ``'\xe0'`` for start of image (SOI).""" return self._marker_code @@ -284,9 +284,7 @@ def segment_length(self): class _App0Marker(_Marker): """Represents a JFIF APP0 marker segment.""" - def __init__( - self, marker_code, offset, length, density_units, x_density, y_density - ): + def __init__(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,9 +330,7 @@ def from_stream(cls, stream, marker_code, offset): density_units = stream.read_byte(offset, 9) 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 - ) + return cls(marker_code, offset, segment_length, density_units, x_density, y_density) class _App1Marker(_Marker): diff --git a/src/docx/opc/constants.py b/src/docx/opc/constants.py index 89d3c16cc..a3d0e0812 100644 --- a/src/docx/opc/constants.py +++ b/src/docx/opc/constants.py @@ -9,27 +9,15 @@ class CONTENT_TYPE: 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" - ) + 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_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml" OFC_CUSTOM_XML_PROPERTIES = ( "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" ) @@ -40,209 +28,126 @@ class CONTENT_TYPE: 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_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_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_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" PML_COMMENT_AUTHORS = ( - "application/vnd.openxmlformats-officedocument.presentationml.commen" - "tAuthors+xml" + "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml" ) PML_HANDOUT_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.handou" - "tMaster+xml" + "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml" ) PML_NOTES_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesM" - "aster+xml" - ) - PML_NOTES_SLIDE = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" + "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+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" + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" ) + PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" PML_PRINTER_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.presentationml.printe" - "rSettings" + "application/vnd.openxmlformats-officedocument.presentationml.printerSettings" ) PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" PML_SLIDESHOW_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.slides" - "how.main+xml" + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" ) PML_SLIDE_LAYOUT = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideL" - "ayout+xml" + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" ) PML_SLIDE_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideM" - "aster+xml" + "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" ) PML_SLIDE_UPDATE_INFO = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideU" - "pdateInfo+xml" + "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml" ) PML_TABLE_STYLES = ( - "application/vnd.openxmlformats-officedocument.presentationml.tableS" - "tyles+xml" + "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+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" + "application/vnd.openxmlformats-officedocument.presentationml.template.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_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_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" SML_EXTERNAL_LINK = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.externa" - "lLink+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" ) SML_PIVOT_CACHE_DEFINITION = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa" - "cheDefinition+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" ) SML_PIVOT_CACHE_RECORDS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa" - "cheRecords+xml" - ) - SML_PIVOT_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+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_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" + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml" ) + SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" SML_SHARED_STRINGS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedS" - "trings+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" ) SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - SML_SHEET_MAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - ) + 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" + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+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" + "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml" ) SML_TEMPLATE_MAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.templat" - "e.main+xml" - ) - SML_USER_NAMES = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.template.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" + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+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_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" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.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" - ) + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" + ) + WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" + WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml" + WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" + WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" + WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" + WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+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" + "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings" ) + 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" + "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml" ) XML = "application/xml" X_EMF = "image/x-emf" @@ -257,9 +162,7 @@ class NAMESPACE: DML_WORDPROCESSING_DRAWING = ( "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ) - OFC_RELATIONSHIPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - ) + 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" @@ -274,259 +177,130 @@ class RELATIONSHIP_TARGET_MODE: 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" - ) + 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" + "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/certificate" ) CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - CHARTSHEET = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/chartsheet" - ) + 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" + "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" + "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" + "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" ) CUSTOM_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/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" + "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" + "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" + "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" + "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" + "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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" ) EXTERNAL_LINK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/externalLink" + "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" - ) + 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" + "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" + "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" - ) + 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" + "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/digital-signature/origin" + PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" PIVOT_CACHE_DEFINITION = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/pivotCacheDefinition" + "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" - ) + 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" + "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" + "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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" ) SHEET_METADATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/sheetMetadata" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata" ) SIGNATURE = ( - "http://schemas.openxmlformats.org/package/2006/relationships/digita" - "l-signature/signature" + "http://schemas.openxmlformats.org/package/2006/relationships/digital-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_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" + "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" + "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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride" ) + THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/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" - ) + 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" + "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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource" ) + XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" diff --git a/src/docx/opc/packuri.py b/src/docx/opc/packuri.py index fdbb67ed8..89437b164 100644 --- a/src/docx/opc/packuri.py +++ b/src/docx/opc/packuri.py @@ -10,8 +10,7 @@ class PackURI(str): - """Provides access to pack URI components such as the baseURI and the filename - slice. + """Provides access to pack URI components such as the baseURI and the filename slice. Behaves as |str| otherwise. """ diff --git a/src/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py index f00e7b5f0..15207e517 100644 --- a/src/docx/opc/pkgreader.py +++ b/src/docx/opc/pkgreader.py @@ -22,9 +22,7 @@ def from_file(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) - sparts = PackageReader._load_serialized_parts( - phys_reader, pkg_srels, content_types - ) + sparts = PackageReader._load_serialized_parts(phys_reader, pkg_srels, content_types) phys_reader.close() return PackageReader(content_types, pkg_srels, sparts) @@ -80,9 +78,7 @@ def _walk_phys_parts(phys_reader, srels, visited_partnames=None): part_srels = PackageReader._srels_for(phys_reader, partname) blob = phys_reader.blob_for(partname) yield (partname, blob, reltype, part_srels) - next_walker = PackageReader._walk_phys_parts( - phys_reader, part_srels, visited_partnames - ) + next_walker = PackageReader._walk_phys_parts(phys_reader, part_srels, visited_partnames) for partname, blob, reltype, srels in next_walker: yield (partname, blob, reltype, srels) diff --git a/src/docx/opc/rel.py b/src/docx/opc/rel.py index 47e8860d8..153b308d0 100644 --- a/src/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -79,9 +79,7 @@ def matches(rel: _Relationship, reltype: str, target: Part | str, is_external: b if rel.is_external != is_external: return False rel_target = rel.target_ref if rel.is_external else rel.target_part - if rel_target != target: - return False - return True + return rel_target == target for rel in self.values(): if matches(rel, reltype, target, is_external): @@ -142,7 +140,7 @@ def rId(self) -> str: def target_part(self) -> Part: if self._is_external: raise ValueError( - "target_part property on _Relationship is undef" "ined when target mode is External" + "target_part property on _Relationship is undefined when target mode is External" ) return cast("Part", self._target) diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index dd10ab910..69d4b65d4 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -125,7 +125,7 @@ def convert_to_xml(cls, value: bool) -> str: def validate(cls, value: Any) -> None: if value not in (True, False): raise TypeError( - "only True or False (and possibly None) may be assigned, got" " '%s'" % value + "only True or False (and possibly None) may be assigned, got '%s'" % value ) diff --git a/src/docx/oxml/table.py b/src/docx/oxml/table.py index e38d58562..9457da207 100644 --- a/src/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -519,7 +519,7 @@ def merge(self, other_tc: CT_Tc) -> CT_Tc: @classmethod def new(cls) -> CT_Tc: """A new `w:tc` element, containing an empty paragraph as the required EG_BlockLevelElt.""" - return cast(CT_Tc, parse_xml("\n" " \n" "" % nsdecls("w"))) + return cast(CT_Tc, parse_xml("" % nsdecls("w"))) @property def right(self) -> int: @@ -583,7 +583,9 @@ def vMerge_val(top_tc: CT_Tc): return ( ST_Merge.CONTINUE if top_tc is not self - else None if height == 1 else ST_Merge.RESTART + else None + if height == 1 + else ST_Merge.RESTART ) top_tc = self if top_tc is None else top_tc @@ -609,9 +611,7 @@ def _is_empty(self) -> bool: # -- cell must include at least one block item but can be a `w:tbl`, `w:sdt`, # -- `w:customXml` or a `w:p` only_item = block_items[0] - if isinstance(only_item, CT_P) and len(only_item.r_lst) == 0: - return True - return False + return isinstance(only_item, CT_P) and len(only_item.r_lst) == 0 def _move_content_to(self, other_tc: CT_Tc): """Append the content of this cell to `other_tc`. diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py index c5dc9bd2e..32eb567ba 100644 --- a/src/docx/oxml/text/font.py +++ b/src/docx/oxml/text/font.py @@ -236,9 +236,7 @@ def subscript(self) -> bool | None: vertAlign = self.vertAlign if vertAlign is None: return None - if vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT: - return True - return False + return vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT @subscript.setter def subscript(self, value: bool | None) -> None: @@ -260,9 +258,7 @@ def superscript(self) -> bool | None: vertAlign = self.vertAlign if vertAlign is None: return None - if vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT: - return True - return False + return vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT @superscript.setter def superscript(self, value: bool | None): diff --git a/src/docx/oxml/text/pagebreak.py b/src/docx/oxml/text/pagebreak.py index 943f9b6c2..45a6f51a7 100644 --- a/src/docx/oxml/text/pagebreak.py +++ b/src/docx/oxml/text/pagebreak.py @@ -46,9 +46,7 @@ def following_fragment_p(self) -> CT_P: # -- 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 + self._following_frag_in_hlink if self._is_in_hyperlink else self._following_frag_in_run ) @property @@ -116,9 +114,7 @@ def preceding_fragment_p(self) -> CT_P: # -- 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 + 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: @@ -139,9 +135,7 @@ def _first_lrpb_in_p(self, p: CT_P) -> CT_LastRenderedPageBreak: Raises `ValueError` if there are no rendered page-breaks in `p`. """ - lrpbs = p.xpath( - "./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak" - ) + 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] diff --git a/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index bc33e1f58..df75ee18c 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -75,9 +75,7 @@ def _eq_elm_strs(self, line: str, line_2: str): return False if close != close_2: return False - if text != text_2: - return False - return True + return text == text_2 @classmethod def _parse_line(cls, line: str) -> tuple[str, str, str, str]: @@ -464,7 +462,7 @@ def get_or_change_to_child(obj: BaseOxmlElement): 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) diff --git a/src/docx/parts/settings.py b/src/docx/parts/settings.py index 116facca2..7fe371f09 100644 --- a/src/docx/parts/settings.py +++ b/src/docx/parts/settings.py @@ -27,8 +27,7 @@ def __init__( @classmethod def default(cls, package: Package): - """Return a newly created settings part, containing a default `w:settings` - element tree.""" + """Return a newly created settings part, containing a default `w:settings` element tree.""" partname = PackURI("/word/settings.xml") content_type = CT.WML_SETTINGS element = cast("CT_Settings", parse_xml(cls._default_settings_xml())) diff --git a/src/docx/parts/styles.py b/src/docx/parts/styles.py index dffa762ef..6e065beee 100644 --- a/src/docx/parts/styles.py +++ b/src/docx/parts/styles.py @@ -36,9 +36,7 @@ def styles(self): @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" - ) + 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/src/docx/styles/styles.py b/src/docx/styles/styles.py index 98a56e520..b05b3ebb1 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -40,10 +40,7 @@ def __getitem__(self, key: str): 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." - ) + msg = "style lookup by style_id is deprecated. Use style name as key instead." warn(msg, UserWarning, stacklevel=2) return StyleFactory(style_elm) @@ -118,9 +115,7 @@ def _get_by_id(self, style_id: str | None, style_type: WD_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: + 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 @@ -129,17 +124,13 @@ def _get_style_id_from_name( """ 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: + 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) - ) + 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/src/docx/text/font.py b/src/docx/text/font.py index acd60795b..0439f4547 100644 --- a/src/docx/text/font.py +++ b/src/docx/text/font.py @@ -398,11 +398,7 @@ def underline(self, value: bool | WD_UNDERLINE | None) -> None: # -- 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 + WD_UNDERLINE.SINGLE if value is True else WD_UNDERLINE.NONE if value is False else value ) rPr.u_val = val diff --git a/src/docx/text/tabstops.py b/src/docx/text/tabstops.py index 824085d2b..0f8c22c9c 100644 --- a/src/docx/text/tabstops.py +++ b/src/docx/text/tabstops.py @@ -50,9 +50,7 @@ def __len__(self): return 0 return len(tabs.tab_lst) - def add_tab_stop( - self, position, alignment=WD_TAB_ALIGNMENT.LEFT, leader=WD_TAB_LEADER.SPACES - ): + 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. diff --git a/src/docx/types.py b/src/docx/types.py index 00bc100a1..06d1a571a 100644 --- a/src/docx/types.py +++ b/src/docx/types.py @@ -19,8 +19,7 @@ class ProvidesStoryPart(Protocol): """ @property - def part(self) -> StoryPart: - ... + def part(self) -> StoryPart: ... class ProvidesXmlPart(Protocol): @@ -32,5 +31,4 @@ class ProvidesXmlPart(Protocol): """ @property - def part(self) -> XmlPart: - ... + def part(self) -> XmlPart: ... diff --git a/tests/image/test_bmp.py b/tests/image/test_bmp.py index 15b322b66..27c0e8f5c 100644 --- a/tests/image/test_bmp.py +++ b/tests/image/test_bmp.py @@ -14,8 +14,8 @@ 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 = io.BytesIO(bytes_) diff --git a/tests/image/test_gif.py b/tests/image/test_gif.py index a533da04d..4aa6581ba 100644 --- a/tests/image/test_gif.py +++ b/tests/image/test_gif.py @@ -13,7 +13,7 @@ class DescribeGif: def it_can_construct_from_a_gif_stream(self, Gif__init__): cx, cy = 42, 24 - bytes_ = b"filler\x2A\x00\x18\x00" + bytes_ = b"filler\x2a\x00\x18\x00" stream = io.BytesIO(bytes_) gif = Gif.from_stream(stream) diff --git a/tests/image/test_helpers.py b/tests/image/test_helpers.py index 9192564dc..03421ff5f 100644 --- a/tests/image/test_helpers.py +++ b/tests/image/test_helpers.py @@ -28,8 +28,8 @@ def it_can_read_a_long(self, read_long_fixture): @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), + (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): diff --git a/tests/image/test_image.py b/tests/image/test_image.py index bd5ed0903..c13e87305 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -27,9 +27,7 @@ class DescribeImage: - def it_can_construct_from_an_image_blob( - self, blob_, BytesIO_, _from_stream_, stream_, image_ - ): + def it_can_construct_from_an_image_blob(self, blob_, BytesIO_, _from_stream_, stream_, image_): image = Image.from_blob(blob_) BytesIO_.assert_called_once_with(blob_) @@ -231,9 +229,7 @@ def filename_(self, request): @pytest.fixture def _from_stream_(self, request, image_): - return method_mock( - request, Image, "_from_stream", autospec=False, return_value=image_ - ) + return method_mock(request, Image, "_from_stream", autospec=False, return_value=image_) @pytest.fixture def height_prop_(self, request): diff --git a/tests/image/test_jpeg.py b/tests/image/test_jpeg.py index a558e1d4e..129a07d80 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -247,7 +247,7 @@ def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture): ) def from_stream_fixture(self, request, _Marker__init_): marker_code, offset, length = request.param - bytes_ = b"\xFF\xD8\xFF\xE0\x00\x10" + 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 @@ -258,7 +258,7 @@ def _Marker__init_(self, request): 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(io.BytesIO(bytes_), BIG_ENDIAN) @@ -318,9 +318,7 @@ def it_can_construct_from_non_Exif_APP1_segment(self, _App1Marker__init_): app1_marker = _App1Marker.from_stream(stream, marker_code, offset) - _App1Marker__init_.assert_called_once_with( - ANY, marker_code, offset, length, 72, 72 - ) + _App1Marker__init_.assert_called_once_with(ANY, marker_code, offset, length, 72, 72) assert isinstance(app1_marker, _App1Marker) def it_gets_a_tiff_from_its_Exif_segment_to_help_construct(self, get_tiff_fixture): @@ -348,9 +346,7 @@ def _App1Marker__init_(self, request): 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_ - ) + BytesIO_ = class_mock(request, "docx.image.jpeg.io.BytesIO", return_value=substream_) offset, segment_length, segment_bytes = 0, 16, bytes_[8:] return ( stream_reader, @@ -390,7 +386,7 @@ def _tiff_from_exif_segment_(self, request, tiff_): 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(io.BytesIO(bytes_), BIG_ENDIAN) @@ -509,7 +505,7 @@ def _MarkerFinder__init_(self, request): ) 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" + 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) @@ -626,9 +622,7 @@ def stream_(self, request): @pytest.fixture def StreamReader_(self, request, stream_reader_): - return class_mock( - request, "docx.image.jpeg.StreamReader", return_value=stream_reader_ - ) + return class_mock(request, "docx.image.jpeg.StreamReader", return_value=stream_reader_) @pytest.fixture def stream_reader_(self, request): diff --git a/tests/image/test_png.py b/tests/image/test_png.py index 61e7fdbed..5379b403b 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -30,9 +30,7 @@ class DescribePng: - def it_can_construct_from_a_png_stream( - self, stream_, _PngParser_, png_parser_, Png__init__ - ): + def it_can_construct_from_a_png_stream(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 png_parser_.px_height = px_height @@ -42,9 +40,7 @@ def it_can_construct_from_a_png_stream( png = Png.from_stream(stream_) _PngParser_.parse.assert_called_once_with(stream_) - Png__init__.assert_called_once_with( - ANY, px_width, px_height, horz_dpi, vert_dpi - ) + Png__init__.assert_called_once_with(ANY, px_width, px_height, horz_dpi, vert_dpi) assert isinstance(png, Png) def it_knows_its_content_type(self): @@ -157,9 +153,7 @@ def stream_(self, request): class Describe_Chunks: - def it_can_construct_from_a_stream( - self, stream_, _ChunkParser_, chunk_parser_, _Chunks__init_ - ): + def it_can_construct_from_a_stream(self, stream_, _ChunkParser_, chunk_parser_, _Chunks__init_): chunk_lst = [1, 2] chunk_parser_.iter_chunks.return_value = iter(chunk_lst) @@ -277,9 +271,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_ - ) + return function_mock(request, "docx.image.png._ChunkFactory", side_effect=chunk_lst_) @pytest.fixture def chunk_lst_(self, chunk_, chunk_2_): @@ -315,9 +307,7 @@ 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_ - ) + return class_mock(request, "docx.image.png.StreamReader", return_value=stream_rdr_) @pytest.fixture def stream_(self, request): @@ -409,7 +399,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" + 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 @@ -430,7 +420,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" + 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 b7f37afe5..35344eede 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -32,9 +32,7 @@ class DescribeTiff: - def it_can_construct_from_a_tiff_stream( - self, stream_, _TiffParser_, tiff_parser_, Tiff__init_ - ): + def it_can_construct_from_a_tiff_stream(self, stream_, _TiffParser_, tiff_parser_, Tiff__init_): px_width, px_height = 111, 222 horz_dpi, vert_dpi = 333, 444 tiff_parser_.px_width = px_width @@ -45,9 +43,7 @@ def it_can_construct_from_a_tiff_stream( tiff = Tiff.from_stream(stream_) _TiffParser_.parse.assert_called_once_with(stream_) - Tiff__init_.assert_called_once_with( - ANY, px_width, px_height, horz_dpi, vert_dpi - ) + Tiff__init_.assert_called_once_with(ANY, px_width, px_height, horz_dpi, vert_dpi) assert isinstance(tiff, Tiff) def it_knows_its_content_type(self): @@ -186,9 +182,7 @@ def stream_(self, request): @pytest.fixture def StreamReader_(self, request, stream_rdr_): - return class_mock( - request, "docx.image.tiff.StreamReader", return_value=stream_rdr_ - ) + return class_mock(request, "docx.image.tiff.StreamReader", return_value=stream_rdr_) @pytest.fixture def stream_rdr_(self, request, ifd0_offset_): @@ -244,9 +238,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_ - ) + return class_mock(request, "docx.image.tiff._IfdParser", return_value=ifd_parser_) @pytest.fixture def ifd_parser_(self, request): @@ -386,9 +378,7 @@ def offset_(self, request): class Describe_IfdEntry: - def it_can_construct_from_a_stream_and_offset( - self, _parse_value_, _IfdEntry__init_, value_ - ): + 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(io.BytesIO(bytes_), BIG_ENDIAN) offset, tag_code, value_count, value_offset = 0, 1, 2, 3 @@ -396,9 +386,7 @@ def it_can_construct_from_a_stream_and_offset( ifd_entry = _IfdEntry.from_stream(stream_rdr, offset) - _parse_value_.assert_called_once_with( - stream_rdr, offset, value_count, value_offset - ) + _parse_value_.assert_called_once_with(stream_rdr, offset, value_count, value_offset) _IfdEntry__init_.assert_called_once_with(ANY, tag_code, value_) assert isinstance(ifd_entry, _IfdEntry) @@ -432,7 +420,7 @@ def it_can_parse_an_ascii_string_IFD_entry(self): class Describe_ShortIfdEntry: def it_can_parse_a_short_int_IFD_entry(self): - bytes_ = b"foobaroo\x00\x2A" + 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 @@ -440,7 +428,7 @@ def it_can_parse_a_short_int_IFD_entry(self): class Describe_LongIfdEntry: def it_can_parse_a_long_int_IFD_entry(self): - bytes_ = b"foobaroo\x00\x00\x00\x2A" + 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 @@ -448,7 +436,7 @@ def it_can_parse_a_long_int_IFD_entry(self): class Describe_RationalIfdEntry: def it_can_parse_a_rational_IFD_entry(self): - bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x54" + 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/test_pkgreader.py b/tests/opc/test_pkgreader.py index 8e14f0e01..0aed52c8d 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -44,9 +44,7 @@ def it_can_construct_from_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, "/") - _load_serialized_parts.assert_called_once_with( - phys_reader, pkg_srels, content_types - ) + _load_serialized_parts.assert_called_once_with(phys_reader, pkg_srels, content_types) phys_reader.close.assert_called_once_with() _init_.assert_called_once_with(ANY, content_types, pkg_srels, sparts) assert isinstance(pkg_reader, PackageReader) @@ -94,17 +92,11 @@ def it_can_load_serialized_parts(self, _SerializedPart_, _walk_phys_parts): Mock(name="spart_2"), ) # exercise --------------------- - retval = PackageReader._load_serialized_parts( - phys_reader, pkg_srels, content_types - ) + retval = PackageReader._load_serialized_parts(phys_reader, pkg_srels, content_types) # 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 @@ -208,9 +200,7 @@ def _init_(self, request): return initializer_mock(request, PackageReader) @pytest.fixture - def iter_sparts_fixture( - self, sparts_, partnames_, content_types_, reltypes_, blobs_ - ): + def iter_sparts_fixture(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]), @@ -220,9 +210,7 @@ def iter_sparts_fixture( @pytest.fixture def _load_serialized_parts(self, request): - return method_mock( - request, PackageReader, "_load_serialized_parts", autospec=False - ) + return method_mock(request, PackageReader, "_load_serialized_parts", autospec=False) @pytest.fixture def partnames_(self, request): @@ -283,15 +271,11 @@ def it_can_construct_from_ct_item_xml(self, from_xml_fixture): 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 - ): + def it_matches_an_override_on_case_insensitive_partname(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 - ): + def it_falls_back_to_case_insensitive_extension_default_match(self, match_default_fixture): ct_map, partname, content_type = match_default_fixture assert ct_map[partname] == content_type diff --git a/tests/opc/test_rel.py b/tests/opc/test_rel.py index 7b7a98dfe..f56fecd22 100644 --- a/tests/opc/test_rel.py +++ b/tests/opc/test_rel.py @@ -77,18 +77,14 @@ def it_can_find_a_relationship_by_rId(self): rels["foobar"] = rel assert rels["foobar"] == rel - def it_can_find_or_add_a_relationship( - self, rels_with_matching_rel_, rels_with_missing_rel_ - ): + def it_can_find_or_add_a_relationship(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 rels, reltype, part, new_rel = rels_with_missing_rel_ assert rels.get_or_add(reltype, part) == new_rel - def it_can_find_or_add_an_external_relationship( - self, add_matching_ext_rel_fixture_ - ): + def it_can_find_or_add_an_external_relationship(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 @@ -235,20 +231,14 @@ def rels_with_missing_rel_(self, request, rels, _Relationship_): @pytest.fixture def rels_with_rId_gap(self, request): rels = Relationships(None) - rel_with_rId1 = instance_mock( - request, _Relationship, name="rel_with_rId1", rId="rId1" - ) - rel_with_rId3 = instance_mock( - request, _Relationship, name="rel_with_rId3", rId="rId3" - ) + rel_with_rId1 = instance_mock(request, _Relationship, name="rel_with_rId1", rId="rId1") + rel_with_rId3 = instance_mock(request, _Relationship, name="rel_with_rId3", rId="rId3") 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 - ): + def rels_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 diff --git a/tests/oxml/parts/test_document.py b/tests/oxml/parts/test_document.py index 90b587674..149a65790 100644 --- a/tests/oxml/parts/test_document.py +++ b/tests/oxml/parts/test_document.py @@ -38,9 +38,6 @@ def clear_fixture(self, request): def section_break_fixture(self): 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/test__init__.py b/tests/oxml/test__init__.py index 5f392df38..9f19094b4 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -12,15 +12,12 @@ class DescribeOxmlElement: def it_returns_an_lxml_element_with_matching_tag_name(self): element = OxmlElement("a:foo") assert isinstance(element, etree._Element) - assert element.tag == ( - "{http://schemas.openxmlformats.org/drawingml/2006/main}foo" - ) + assert element.tag == ("{http://schemas.openxmlformats.org/drawingml/2006/main}foo") def it_adds_supplied_attributes(self): element = OxmlElement("a:foo", {"a": "b", "c": "d"}) assert etree.tostring(element) == ( - '' + '' ).encode("utf-8") def it_adds_additional_namespace_declarations_when_supplied(self): @@ -43,7 +40,7 @@ def it_strips_whitespace_between_elements(self, whitespace_fixture): @pytest.fixture def whitespace_fixture(self): - pretty_xml_text = "\n" " text\n" "\n" + pretty_xml_text = "\n text\n\n" stripped_xml_text = "text" return pretty_xml_text, stripped_xml_text diff --git a/tests/oxml/test_styles.py b/tests/oxml/test_styles.py index 7677a8a9e..8814dd6aa 100644 --- a/tests/oxml/test_styles.py +++ b/tests/oxml/test_styles.py @@ -31,8 +31,7 @@ def it_can_add_a_style_of_type(self, add_fixture): "heading 1", WD_STYLE_TYPE.PARAGRAPH, True, - "w:styles/w:style{w:type=paragraph,w:styleId=Heading1}/w:name{w:val" - "=heading 1}", + "w:styles/w:style{w:type=paragraph,w:styleId=Heading1}/w:name{w:val=heading 1}", ), ] ) diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 46b2f4ed1..2c9e05344 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -19,7 +19,6 @@ class DescribeCT_Row: - @pytest.mark.parametrize( ("tr_cxml", "expected_cxml"), [ @@ -231,7 +230,7 @@ def it_knows_its_inner_content_block_item_elements(self): '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: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)", @@ -266,7 +265,7 @@ def it_can_swallow_the_next_tc_help_merge( "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))", + "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 ( @@ -277,17 +276,17 @@ def it_can_swallow_the_next_tc_help_merge( ), # 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))", + "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)", + "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))", + "w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=1440,w:type=dxa},w:gridSpan{w:val=2}),w:p))", ), ], ) diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index fca309851..76b53c957 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -131,7 +131,7 @@ def it_returns_unicode_text(self, type_fixture): @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 @@ -176,8 +176,7 @@ def it_knows_if_two_xml_lines_are_equivalent(self, xml_line_case): ('', "", None), ("t", "", "t"), ( - '2013-12-23T23:15:00Z", + '2013-12-23T23:15:00Z', "", @@ -250,22 +249,16 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, choice, expected_xml = insert_fixture parent._insert_choice(choice) assert parent.xml == expected_xml - assert parent._insert_choice.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_choice.__doc__.startswith("Return the passed ```` ") def it_adds_an_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture choice = parent._add_choice() assert parent.xml == expected_xml assert isinstance(choice, CT_Choice) - assert parent._add_choice.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_choice.__doc__.startswith("Add a new ```` child element ") - def it_adds_a_get_or_change_to_method_for_the_child_element( - self, get_or_change_to_fixture - ): + def it_adds_a_get_or_change_to_method_for_the_child_element(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) @@ -302,10 +295,7 @@ def getter_fixture(self, request): @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 = ( @@ -362,27 +352,21 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, oomChild, expected_xml = insert_fixture parent._insert_oomChild(oomChild) assert parent.xml == expected_xml - assert parent._insert_oomChild.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_oomChild.__doc__.startswith("Return the passed ```` ") def it_adds_a_private_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture oomChild = parent._add_oomChild() assert parent.xml == expected_xml assert isinstance(oomChild, CT_OomChild) - assert parent._add_oomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_oomChild.__doc__.startswith("Add a new ```` child element ") def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture oomChild = parent.add_oomChild() assert parent.xml == expected_xml assert isinstance(oomChild, CT_OomChild) - assert parent._add_oomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_oomChild.__doc__.startswith("Add a new ```` child element ") # fixtures ------------------------------------------------------- @@ -444,9 +428,7 @@ def it_adds_a_setter_property_for_the_attr(self, setter_fixture): assert parent.xml == expected_xml def it_adds_a_docstring_for_the_property(self): - assert CT_Parent.optAttr.__doc__.startswith( - "ST_IntegerType type-converted value of " - ) + assert CT_Parent.optAttr.__doc__.startswith("ST_IntegerType type-converted value of ") # fixtures ------------------------------------------------------- @@ -477,9 +459,7 @@ def it_adds_a_setter_property_for_the_attr(self, setter_fixture): assert parent.xml == expected_xml def it_adds_a_docstring_for_the_property(self): - assert CT_Parent.reqAttr.__doc__.startswith( - "ST_IntegerType type-converted value of " - ) + assert CT_Parent.reqAttr.__doc__.startswith("ST_IntegerType type-converted value of ") def it_raises_on_get_when_attribute_not_present(self): parent = a_parent().with_nsdecls().element @@ -532,27 +512,21 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, zomChild, expected_xml = insert_fixture parent._insert_zomChild(zomChild) assert parent.xml == expected_xml - assert parent._insert_zomChild.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_zomChild.__doc__.startswith("Return the passed ```` ") def it_adds_an_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture zomChild = parent._add_zomChild() assert parent.xml == expected_xml assert isinstance(zomChild, CT_ZomChild) - assert parent._add_zomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_zomChild.__doc__.startswith("Add a new ```` child element ") def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture zomChild = parent.add_zomChild() assert parent.xml == expected_xml assert isinstance(zomChild, CT_ZomChild) - assert parent._add_zomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_zomChild.__doc__.startswith("Add a new ```` child element ") def it_removes_the_property_root_name_used_for_declaration(self): assert not hasattr(CT_Parent, "zomChild") @@ -614,17 +588,13 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture): zooChild = parent._add_zooChild() assert parent.xml == expected_xml assert isinstance(zooChild, CT_ZooChild) - assert parent._add_zooChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_zooChild.__doc__.startswith("Add a new ```` child element ") def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, zooChild, expected_xml = insert_fixture parent._insert_zooChild(zooChild) assert parent.xml == expected_xml - assert parent._insert_zooChild.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_zooChild.__doc__.startswith("Return the passed ```` ") def it_adds_a_get_or_add_method_for_the_child_element(self, get_or_add_fixture): parent, expected_xml = get_or_add_fixture @@ -743,9 +713,7 @@ class CT_Parent(BaseOxmlElement): (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=()) diff --git a/tests/oxml/text/test_hyperlink.py b/tests/oxml/text/test_hyperlink.py index f55ab9c22..f5cec4761 100644 --- a/tests/oxml/text/test_hyperlink.py +++ b/tests/oxml/text/test_hyperlink.py @@ -30,9 +30,7 @@ def it_has_a_relationship_that_contains_the_hyperlink_address(self): ("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 - ): + 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 diff --git a/tests/parts/test_hdrftr.py b/tests/parts/test_hdrftr.py index ee0cc7134..bb98acead 100644 --- a/tests/parts/test_hdrftr.py +++ b/tests/parts/test_hdrftr.py @@ -27,9 +27,7 @@ def it_is_used_by_loader_to_construct_footer_part( FooterPart_load_.assert_called_once_with(partname, content_type, blob, package_) assert part is footer_part_ - def it_can_create_a_new_footer_part( - self, package_, _default_footer_xml_, parse_xml_, _init_ - ): + def it_can_create_a_new_footer_part(self, package_, _default_footer_xml_, parse_xml_, _init_): ftr = element("w:ftr") package_.next_partname.return_value = "/word/footer24.xml" _default_footer_xml_.return_value = "" @@ -95,9 +93,7 @@ def it_is_used_by_loader_to_construct_header_part( HeaderPart_load_.assert_called_once_with(partname, content_type, blob, package_) assert part is header_part_ - def it_can_create_a_new_header_part( - self, package_, _default_header_xml_, parse_xml_, _init_ - ): + def it_can_create_a_new_header_part(self, package_, _default_header_xml_, parse_xml_, _init_): hdr = element("w:hdr") package_.next_partname.return_value = "/word/header42.xml" _default_header_xml_.return_value = "" diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index acf0b0727..395f57726 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -24,17 +24,13 @@ def it_is_used_by_PartFactory_to_construct_image_part( part = PartFactory(partname_, content_type, reltype, blob_, package_) - image_part_load_.assert_called_once_with( - partname_, content_type, blob_, package_ - ) + image_part_load_.assert_called_once_with(partname_, content_type, blob_, package_) assert part is image_part_ def it_can_construct_from_an_Image_instance(self, image_, partname_, _init_): image_part = ImagePart.from_image(image_, partname_) - _init_.assert_called_once_with( - ANY, partname_, image_.content_type, image_.blob, image_ - ) + _init_.assert_called_once_with(ANY, partname_, image_.content_type, image_.blob, image_) assert isinstance(image_part, ImagePart) def it_knows_its_default_dimensions_in_EMU(self, dimensions_fixture): diff --git a/tests/parts/test_numbering.py b/tests/parts/test_numbering.py index 7655206ec..1ed0f2a05 100644 --- a/tests/parts/test_numbering.py +++ b/tests/parts/test_numbering.py @@ -24,9 +24,7 @@ def it_provides_access_to_the_numbering_definitions(self, num_defs_fixture): # fixtures ------------------------------------------------------- @pytest.fixture - def num_defs_fixture( - self, _NumberingDefinitions_, numbering_elm_, numbering_definitions_ - ): + def num_defs_fixture(self, _NumberingDefinitions_, numbering_elm_, numbering_definitions_): numbering_part = NumberingPart(None, None, numbering_elm_, None) return ( numbering_part, diff --git a/tests/parts/test_settings.py b/tests/parts/test_settings.py index 581cc6173..73b8a5e9a 100644 --- a/tests/parts/test_settings.py +++ b/tests/parts/test_settings.py @@ -14,9 +14,7 @@ class DescribeSettingsPart: - def it_is_used_by_loader_to_construct_settings_part( - self, load_, package_, settings_part_ - ): + def it_is_used_by_loader_to_construct_settings_part(self, load_, package_, settings_part_): partname, blob = "partname", "blob" content_type = CT.WML_SETTINGS load_.return_value = settings_part_ @@ -61,9 +59,7 @@ def package_(self, request): @pytest.fixture def Settings_(self, request, settings_): - return class_mock( - request, "docx.parts.settings.Settings", return_value=settings_ - ) + return class_mock(request, "docx.parts.settings.Settings", return_value=settings_) @pytest.fixture def settings_(self, request): diff --git a/tests/parts/test_story.py b/tests/parts/test_story.py index b65abe8b7..9a1dc7fab 100644 --- a/tests/parts/test_story.py +++ b/tests/parts/test_story.py @@ -30,9 +30,7 @@ def it_can_get_or_add_an_image(self, package_, image_part_, image_, relate_to_): assert rId == "rId42" assert image is image_ - def it_can_get_a_style_by_id_and_type( - self, _document_part_prop_, document_part_, style_ - ): + def it_can_get_a_style_by_id_and_type(self, _document_part_prop_, document_part_, style_): style_id = "BodyText" style_type = WD_STYLE_TYPE.PARAGRAPH _document_part_prop_.return_value = document_part_ diff --git a/tests/styles/test_style.py b/tests/styles/test_style.py index b24e02733..6201f9927 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -75,9 +75,7 @@ def character_style_(self, request): @pytest.fixture def _TableStyle_(self, request, table_style_): - return class_mock( - request, "docx.styles.style._TableStyle", return_value=table_style_ - ) + return class_mock(request, "docx.styles.style._TableStyle", return_value=table_style_) @pytest.fixture def table_style_(self, request): @@ -529,17 +527,11 @@ def next_get_fixture(self, request): 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]) - next_style = ( - None - if next_style_name is None - else ParagraphStyle(style_elms[next_style_name]) - ) + next_style = ParagraphStyle(style_elms[next_style_name]) if next_style_name else None expected_xml = xml(style_cxml) return style, next_style, expected_xml diff --git a/tests/styles/test_styles.py b/tests/styles/test_styles.py index ea9346bdc..7493388d0 100644 --- a/tests/styles/test_styles.py +++ b/tests/styles/test_styles.py @@ -52,9 +52,7 @@ def it_can_add_a_new_style(self, add_fixture): style = styles.add_style(name, style_type, builtin) - styles._element.add_style_of_type.assert_called_once_with( - name_, style_type, builtin - ) + styles._element.add_style_of_type.assert_called_once_with(name_, style_type, builtin) StyleFactory_.assert_called_once_with(style_elm_) assert style is style_ @@ -110,9 +108,7 @@ def and_it_can_get_a_style_id_from_a_style_name(self, _get_style_id_from_name_): style_id = styles.get_style_id("Style Name", style_type) - _get_style_id_from_name_.assert_called_once_with( - styles, "Style Name", style_type - ) + _get_style_id_from_name_.assert_called_once_with(styles, "Style Name", style_type) assert style_id == "StyleId" def but_it_returns_None_for_a_style_or_name_of_None(self): @@ -132,9 +128,7 @@ def it_gets_a_style_by_id_to_help(self, _get_by_id_fixture): assert StyleFactory_.call_args_list == StyleFactory_calls assert style is style_ - def it_gets_a_style_id_from_a_name_to_help( - self, _getitem_, _get_style_id_from_style_, style_ - ): + 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" _getitem_.return_value = style_ _get_style_id_from_style_.return_value = style_id_ @@ -173,9 +167,7 @@ def it_provides_access_to_the_latent_styles(self, latent_styles_fixture): ("Heading 1", "heading 1", WD_STYLE_TYPE.PARAGRAPH, True), ] ) - def add_fixture( - self, request, styles_elm_, _getitem_, style_elm_, StyleFactory_, style_ - ): + 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 @@ -207,8 +199,7 @@ def add_raises_fixture(self, _getitem_): WD_STYLE_TYPE.PARAGRAPH, ), ( - "w:styles/(w:style{w:type=table,w:default=1},w:style{w:type=table,w" - ":default=1})", + "w:styles/(w:style{w:type=table,w:default=1},w:style{w:type=table,w:default=1})", True, WD_STYLE_TYPE.TABLE, ), @@ -387,9 +378,7 @@ def _get_style_id_from_style_(self, request): @pytest.fixture def LatentStyles_(self, request, latent_styles_): - return class_mock( - request, "docx.styles.styles.LatentStyles", return_value=latent_styles_ - ) + return class_mock(request, "docx.styles.styles.LatentStyles", return_value=latent_styles_) @pytest.fixture def latent_styles_(self, request): diff --git a/tests/test_enum.py b/tests/test_enum.py index 1b8a14f5b..79607a7e0 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -60,9 +60,7 @@ def and_it_can_find_the_member_from_None_when_a_member_maps_that(self): assert SomeXmlAttr.from_xml(None) == SomeXmlAttr.BAZ 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'" - ): + with pytest.raises(ValueError, match="SomeXmlAttr has no XML mapping for 'baz'"): SomeXmlAttr.from_xml("baz") diff --git a/tests/text/test_font.py b/tests/text/test_font.py index 6a9da0223..471c5451b 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -62,9 +62,7 @@ def it_knows_its_typeface_name(self, r_cxml: str, expected_value: str | None): ), ], ) - def it_can_change_its_typeface_name( - self, r_cxml: str, value: str, expected_r_cxml: str - ): + 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) @@ -95,9 +93,7 @@ def it_knows_its_size(self, r_cxml: str, expected_value: Length | None): ("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 - ): + 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) @@ -224,9 +220,7 @@ def it_can_change_its_bool_prop_settings( ("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 - ): + 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 @@ -283,9 +277,7 @@ def it_can_change_whether_it_is_subscript( ("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 - ): + 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 @@ -343,9 +335,7 @@ def it_can_change_whether_it_is_superscript( ("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 - ): + 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 @@ -393,9 +383,7 @@ def it_can_change_its_underline_type( ("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 - ): + 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 diff --git a/tests/text/test_pagebreak.py b/tests/text/test_pagebreak.py index c7494dca2..bc7848797 100644 --- a/tests/text/test_pagebreak.py +++ b/tests/text/test_pagebreak.py @@ -107,13 +107,7 @@ def it_produces_None_for_following_fragment_when_page_break_is_trailing( def it_can_split_off_the_following_paragraph_content_when_in_a_run( self, fake_parent: t.ProvidesStoryPart ): - p_cxml = ( - "w:p/(" - " w:pPr/w:ind" - ' ,w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")' - ' ,w:r/w:t"foo"' - ")" - ) + 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) diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index c1451c3c1..0329b1dd3 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -85,9 +85,7 @@ def it_can_iterate_its_inner_content_items( def it_knows_its_paragraph_style(self, style_get_fixture): paragraph, style_id_, style_ = style_get_fixture style = paragraph.style - paragraph.part.get_style.assert_called_once_with( - style_id_, WD_STYLE_TYPE.PARAGRAPH - ) + paragraph.part.get_style.assert_called_once_with(style_id_, WD_STYLE_TYPE.PARAGRAPH) assert style is style_ def it_can_change_its_paragraph_style(self, style_set_fixture): @@ -95,9 +93,7 @@ def it_can_change_its_paragraph_style(self, style_set_fixture): paragraph.style = value - paragraph.part.get_style_id.assert_called_once_with( - value, WD_STYLE_TYPE.PARAGRAPH - ) + paragraph.part.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.PARAGRAPH) assert paragraph._p.xml == expected_xml @pytest.mark.parametrize( @@ -108,8 +104,7 @@ def it_can_change_its_paragraph_style(self, style_set_fixture): ("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)", + "w:p/(w:r/w:lastRenderedPageBreak,w:hyperlink/w:r/w:lastRenderedPageBreak)", 2, ), ( @@ -144,8 +139,7 @@ def it_provides_access_to_the_rendered_page_breaks_it_contains( ('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")', + '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", ), ], @@ -385,9 +379,7 @@ def part_prop_(self, request, 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_] - ) + return class_mock(request, "docx.text.paragraph.Run", side_effect=[run_, run_2_]) @pytest.fixture def r_(self, request): diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 795052c8e..226585bc7 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -43,9 +43,7 @@ def snippet_text(snippet_file_name: str): Return the unicode text read from the test snippet file having `snippet_file_name`. """ - snippet_file_path = os.path.join( - test_file_dir, "snippets", "%s.txt" % snippet_file_name - ) + snippet_file_path = os.path.join(test_file_dir, "snippets", "%s.txt" % snippet_file_name) with open(snippet_file_path, "rb") as f: snippet_bytes = f.read() return snippet_bytes.decode("utf-8") diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index d0e41ce93..de05cc206 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -75,16 +75,12 @@ def function_mock( return _patch.start() -def initializer_mock( - request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any -): +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. """ - _patch = patch.object( - cls, "__init__", autospec=autospec, return_value=None, **kwargs - ) + _patch = patch.object(cls, "__init__", autospec=autospec, return_value=None, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() From d9da49bdfd87908e86dbae3516050419181cf63c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 6 Jun 2025 11:21:05 -0700 Subject: [PATCH 105/131] fix: remove redundant w:pic "insertion" The removed `._insert_pic(pic)` call is redundant because the `pic` element is already inserted by the prior `CT_Inline.new()` call. The reason the second `._insert_pic()` call did not add a second `pic` element is that the `pic` element is the same instance in both cases. So that `pic` element is "moved" from where it was, back to the same place. --- src/docx/oxml/shape.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index 00e7593a9..c6df8e7b8 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -100,7 +100,6 @@ def new_pic_inline( 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 From 5cb32d7787a79ad255ad6f11ad735d2513dd08c1 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:26:57 -0700 Subject: [PATCH 106/131] xfail: acceptance test for Document.comments --- features/doc-comments.feature | 40 ++++++++++ features/steps/comments.py | 69 ++++++++++++++++++ .../steps/test_files/comments-rich-para.docx | Bin 0 -> 19974 bytes src/docx/comments.py | 24 ++++++ 4 files changed, 133 insertions(+) create mode 100644 features/doc-comments.feature create mode 100644 features/steps/comments.py create mode 100644 features/steps/test_files/comments-rich-para.docx create mode 100644 src/docx/comments.py diff --git a/features/doc-comments.feature b/features/doc-comments.feature new file mode 100644 index 000000000..c49edaa77 --- /dev/null +++ b/features/doc-comments.feature @@ -0,0 +1,40 @@ +Feature: Document.comments + In order to operate on comments added to a document + As a developer using python-docx + I need access to the comments collection for the document + And I need methods allowing access to the comments in the collection + + + @wip + Scenario Outline: Access document comments + Given a document having comments part + Then document.comments is a Comments object + + Examples: having a comments part or not + | a-or-no | + | a | + | no | + + + @wip + Scenario Outline: Comments.__len__() + Given a Comments object with comments + Then len(comments) == + + Examples: len(comments) values + | count | + | 0 | + | 4 | + + + @wip + Scenario: Comments.__iter__() + Given a Comments object with 4 comments + Then iterating comments yields 4 Comment objects + + + @wip + Scenario: Comments.get() + Given a Comments object with 4 comments + When I call comments.get(2) + Then the result is a Comment object with id 2 diff --git a/features/steps/comments.py b/features/steps/comments.py new file mode 100644 index 000000000..81993aeda --- /dev/null +++ b/features/steps/comments.py @@ -0,0 +1,69 @@ +"""Step implementations for document comments-related features.""" + +from behave import given, then, when +from behave.runner import Context + +from docx import Document +from docx.comments import Comment, Comments + +from helpers import test_docx + +# given ==================================================== + + +@given("a Comments object with {count} comments") +def given_a_comments_object_with_count_comments(context: Context, count: str): + testfile_name = {"0": "doc-default", "4": "comments-rich-para"}[count] + context.comments = Document(test_docx(testfile_name)).comments + + +@given("a document having a comments part") +def given_a_document_having_a_comments_part(context: Context): + context.document = Document(test_docx("comments-rich-para")) + + +@given("a document having no comments part") +def given_a_document_having_no_comments_part(context: Context): + context.document = Document(test_docx("doc-default")) + + +# when ===================================================== + + +@when("I call comments.get(2)") +def when_I_call_comments_get_2(context: Context): + context.comment = context.comments.get(2) + + +# then ===================================================== + + +@then("document.comments is a Comments object") +def then_document_comments_is_a_Comments_object(context: Context): + document = context.document + assert type(document.comments) is Comments + + +@then("iterating comments yields {count} Comment objects") +def then_iterating_comments_yields_count_comments(context: Context, count: str): + comment_iter = iter(context.comments) + + comment = next(comment_iter) + assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}" + + remaining = list(comment_iter) + assert len(remaining) == int(count) - 1, "iterating comments did not yield the expected count" + + +@then("len(comments) == {count}") +def then_len_comments_eq_count(context: Context, count: str): + actual = len(context.comments) + expected = int(count) + assert actual == expected, f"expected len(comments) of {expected}, got {actual}" + + +@then("the result is a Comment object with id 2") +def then_the_result_is_a_comment_object_with_id_2(context: Context): + comment = context.comment + assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}" + assert comment.comment_id == 2, f"expected comment_id `2`, got '{comment.comment_id}'" diff --git a/features/steps/test_files/comments-rich-para.docx b/features/steps/test_files/comments-rich-para.docx new file mode 100644 index 0000000000000000000000000000000000000000..e63db413e871466da97e906aef754bc06b2f9601 GIT binary patch literal 19974 zcmeIa1$!LH&M-P=W_AoQGjmL_V`gTEIc8=%jyW+iGsn!#%*@Qp81tQEchBzTocjyD zx94f~Ojk)&s*wsLRmn+$fujQ;0nh*dfC%vVhN{OG1OT{#1OU(g(4d+htgRf3tQ>Te zTy2c(wdq|fE#78-$~bNY+IRVnjOQEJu)iM{S0TxP9!>%Itzo7Ayj675hntgl|%55hb+A+ z{zsY+A0wTb9&3&vT$)txoND&MtWUUHiKhf6(L9s{ukIIpJ|;|773M8;xF3ghW;n0L zT;od}>tS};VEf2{lfEIMvJn+VC|P?5pG(DCX}3w;ir)+s$A~H8DnT@=Ze^T%u?{Z^ z+F_QDpZb&&YVCE@nDp5~V}9^sQEd?ccb45Tl;4QP4@SK+Ia zpA(&ZY_cVy2`Dx8qEQMB=-*!lPLijDTvNMgVY8lr4=_6S?m}o07?`ZUpqwlCKXCCR zXIFAzeoIv}DejZDdBtvOk(YD~0n@Y*AZSB(`u555)?+-Sf_I_%!6?)VD0Z)}U;w#) z=`KMeZrvF$MFyyva6sMFwKKA?XQ2NH|4(QCA6C?VuwES1Vb;Y6|NhMP*>|Evez5~P zTZX}4dJV+u=HRv0xjuAA53mN(2Yr;r7+77kQ?M^VcABm7++D(FmW)KEnOQS2O?Rk5Ug6dN{Kiq7&#=b)74a-|5#gPymD=4(htc)S zp=MM!w73?G3}h*h`o*xg=gkv`ZrN@5J>UFkVKQO9F+6%0YoThq>;>)O@Ux#0saNZ6 zSiecp7PtbAVE?TjEVisA%)pceZ~y=g01e`7ZD+{vhmsgt8#r15hq<5Q+g}O;0vzOk zx&Pn3ierXk{20*#?#Nrg4s>9%H^~%@v&k(m2s5S#=utE#2^?MAk0=*$jK7l8j;-Ck zw&J126-h6FTYt1D!6324js(PvSE`VajNNxFu))yUk-27LFi!fC6uVld;q^JmTjD4?MHEpq_i ze9GW!zlKz%kYgiI-dHdJ02H9q{gSzVN}IA03;?WwROHjo+tCFM00Mpklse#)3;<|R z)wC|+MEtsPIn=b_E-Bl=6%Tj1Q{1A;65>}0c|0Ztj$INreOqU3 zyu~XCqD3J)3Lsyb9p;2tRpJ#eOY>qUqKvAW$D+i}qsDeTyIDfV_QboZ*{~`^kVS3^ zka_ftU~WB<+NKmN*R#Suf2znZmn#Jz&}k?TErTN9S_rxL9=Wz+plYG}V)3EvxzoY=vKG3+O7IL)(dYv&xx4#?YlpC_7NI!Uchzbkc-#+|;LGl=s=dQSY;y%ZPXrK5!}> zDc9Q>)f65nl3X)ZlbU1U#{Ae*6YigmJe_~c{W5Yu9SarJD`F{pOmU;ccS?_%TX~V8 zEctk%(bR^E6`#U3V#8c1bv3WvbicA&>@@MU6zVo+T}H^GD{=qRJ09Y8YsZ&#R^9SM zzdeNM!od&js-sX=y(}tF(%yyi?s^1wmE<~zShbQY4dO&)%g#w* z%55M<_T(9wMo^zZ;TZ7y3$krT!&{#WHlbEoSwB;~&a<3+^X3*eCeAV_Gb0PQL^2xQ z%S0~Mw1TBv_RmmHg-$1JPq!QBrZ7y0fSN7)pn4ajiOCA;FIXP+%{h;c`a3TDUcK7Mp$s;ON#$9PL%7d=RVvKxs z+BOVE^rc%g`T@lp#u@omWS6V+%|K6arNuE9-(mAN(&?`8avu^BcBbcd&oUvbQGDvc zd;^78=WvG&YSgo~A+p%lB^UP?-~!ayI*k-L=gm%n{y^1Surhky?f+YH9u= zGJAU7OK>D>h}JBVfwtKkTeEL_%eK4-w|X;FSIb5Ep}FH5L%*?Ao($ODL-bPeXl{jW z6K}EG2^va^rpEjJ$n+(7%8x;eM;5q+Y#39o3^yJEclbHn!+G zxC=E&md%7_qop9bhnIHh)TlP#w#fA6q8i+EN>tfByn9@l_uFCguyk}v3n??`nIjxN zuglW81^;~YGnOXXGMfS8GtwUu)1RH-CtxR-e;N%p2LS*`;Qpf%G_bb(IpW&?9CCkk zf?dE4@aO$oC%B?69iGjJ+)`Hk3SujJh4L0$`i|0j)S^tJsG#Ytl7oZO+p;~*HY!Zz zajnkct#<@cR9crAVl>Fojn>Umq1Ld+{X=B#AZ`^g(S3{A@o!BM?^+Qn+kv_CP z@M&lJ^&|b%mXI+`^Q`tSzcHh3W_@|~h$)lLRI%9*<*)vliGU6FUJ}lroAYJf09(~} zeo@W7>HRLkd*n=0hs*%g4_ry-g3S;&>?{418}nz|bjDq>c!oN$k{844=zL z+Z!#&K0ymPjtH^L;=9q;ma=`o7plzx%_wpYxP(T>FE?GOXOoS9AVV^+ex1ND&V z>2Ug~xre3iaCoP7lw4sYsPDdbE|>ino1DD5`WmphB?R6*niURr!fP(eEVuD_E4jcl z!u*={E!&p`5Sz}N{2U<7&hhq2Z^iYakH@%q^Tv-#YvVh6)0d%<7%SOi=^xw|_g=`` z=-;n!Z6{%-+wYQzpi2m1y2%cuFkZ*`h-MH$7-saN$qim92~B<hl?&EU5!Z1O` z=dVo{P8^U1SWA@755tjC;(`@U8Iqq%(1_cC#{1-&Wnv>#7x*xae-Z%N{Jtp0*}vNt*L7zdRrzL#yj$4=8OoT8H_o+&+DsB)kqC4ijLB~Z6jtPM#_j}1 zww}|rEQ1pK9aR^Ev3W6HGbqj=`9=k*HI*@y7AdYU<9FGC0xX9RD@BGf7()oW7ZVzhPG^; zPZbBAFlQsTbf!z55$>lZ9?mpO5f(a`ozC(7-Od3aM$m|-?z+WqA7zXUygngmenTF{ z>%%3K^t&ByX1cZ~6^!aSFd!QkN7WtlACliS9EDcb3du7=L>&IE>WU^{*D@$Um6q3( zf7X=0R~aT43t=oc9#U)@{$b_j*_O>aTPUrb7k8-x99ExVs{o1z8YW1Vev06z6tQ5# z=`{<#N}blGpHOm0>0RdzLv)+<=3RoeP$!onb;%y3FIpl6kuFm)wkSd;|LhuemM!Wi zHtksqyqbTrkwK3$YU>Qrnmr+%!XwjS4p6=`x@WTL}EEucVwmh z3i%c@D_z4`78u{KtHT~A&$HT-S)d2Pp4Yb08_6%*A4bOMru#i2{7Bm#lQ)g_9Nq!+ zqql-vKgB;qytOtJG!aiqTZA6Ku=y}ZBcKztdr@Ds6XCQ5_t{RkC68ypOUgOx$iV*H zx#bF>+QYsrBaUWvhnN?hdgBgm>&C0QTN^48Y-b@dj?d&5@*2{|+t<)C@fHa*_5P;B z)jZVz+a#D1Pz-(Vj9bg)Icg>I~6bmG6>!hqis)GHK$R@ zw;J~m5DxBXvl6&1l1?ncEHVIEBZ$`k@w~54a!fW(Dm|YND4eaDaL?gOD8y(eHe~B= zO-nZCz7IDobmdjFY9pQQb`|~T4Fko6@$XX`h@$k_y$Y=MGfs~i$JL%dT|`^KgR~mA zf(gtDQd6v(K@F2yN>A$3==?(>s{P^{+i#&p_4c2$Qc*8olFq9|6P^T~3`k+Qj3tN| zOOF)pyTVbj9o0kf@|RS<+HOOYvap*#5gwAv*;u;IqGns+JTP*S_uht}XY*Gqw_ART zB#4p#7J>ck^_Fp_HYiEul|h)JfeXIw-3?bcEsY-4A+=DC(!P~o;Q^#0>ng0vUn9g{ zX2*!&bH^>GUspy6Y^5R>A3pvDxIp6op7Q9>#){k?KqR6>s zmG2lceb6w~!h@S;5PeHe3WoVGCMcEAhimdEsDcxj4kv8|%?7%;4i|fm4JUUq@CS|7 z`1_xdLAEdyEsA}bD$E z1fuleWG08_lLZPAw=-GBmpkom;D<~@2Ie15BVqlW6 z>PJ;nP6}pKb+Q=vk^-r`0TKrVmhqnwfMq}c$v{E@6IoCIpr8OS2nZ-J;N0Zb^{WWf zUuB>PDCl`Jv#{D$Ol)9a5<{X3W_G+T0^os_0Yw2r0q_IvkpDsU|Mx3`&VgN@ou+b0 z;viFJl1Ei^CqGBHpkdC^0P>zn5hT-yJFKcF+wc=-qJrlGT=b18EWmu(y zQc)f=+3yUAJXkf^k$DRD-1+1Yap+Jd%qrv-+W@vFt24>g8Cv zChR`GdU0bg)u_-${N>a7#iE@bTjTnVY&OGqNO$r*j*hw}vy_TV<$b>-+vq2joqZRU z@BAszyt?kGvl6?gwIvkP)mbT|*VFmg+&kv%)3(|ov4s?t9EWa(PVHbJNB?e7TlYid z&dC&yqJ*bxa;HF~*0z;>chLtHM}iAn>pP39=P|{Q(h~zkM6mjt+_1onPfOls&@V2s z@JB4NEIXo}QO?sgetNbd%4F#AI8f+2W0b8rzI7N%`O)9{0%{pY#188mWl+l-H#XL% zWEhmDzhxzq6b<-ORmIn&vE|Ry$|fkg)t@=P9-;b$(MXO0M^{LPk9c66_rU zIFXpZ+vQW{gQ?bP^M@izeihT6m%M}pE0eRh&oiQTBwEjLCP=0Pw*ac(FaO~T2x{E^g){g97_HFvP!~!Pw799!n*#<UWjZVj?_F#i%ei%&0!7k{olKrPFW^wUs61I+aV`@9pQQ zV%7C*B)r(4@p$);M`jw>o@Pao(G_19n2~c>m>`Gj`8}#`3;7#xr;HlXTh$+x37132 zIub<}MO%DvAMS$0*H;-3IPLWHJo%cJp8BuYw+iw!C}`8ad*Pa}rO4Mp<{yDx*)4`N zY30KvMt2;#*1}%=a3@F@F_}6f6OL4`Hu{3tCHAw~Q5D457sbh!+)%ahqfh&ssv2Xy z%SVSo*H-DL>Nx*!Bq>~@pnbum* z6+une$t258Z51AMC)nZOf=?AVPtg|Npq|nyi$dAe<&n{no+w3PcaT?1iHwdf-1BoJ zU)ygSk>s0GUf$lm*XijG59ZSiX{u{f)FK?ex$Y7|7l8c`u`J@~tG@hXyG1NLpRxP*)Cwj=ueg@NJ)-_^!3?Y?ep-xkNTrtq&#@M%m(?&_R< zB_~GCr#LRUx*c;57CepED@VmQ%O1k>+_MWu^*@5k2et_6YNA8jR%bK@O~up~o(aZW-)Lqda?AsFE!HgdYR z=FPABET3h3_g1t$dX6tg<2XD>m)vgid5ZlD*h0)Y0Ay=vebLr=CJhNTGM{ZM#Kw!W zDw!9_(<1%O-LJXO-fIQvZqaqaS*S)OQQX5}ew;|miVeKNg^q6bfOm*&zISvsl{J20 zO6`TJ`O+(~s+alQD`3B_@7-wK-B|hwqxgPWjM) zO2H*U^N=K}j=dhGfhtbmt2dLfV?q|M==+9ukPJYOQ49beeAXFYYOE^~RIj%^*NtXN zV_tOeU&%R;aX1w2wTV`cjm%yfb#~+q&2_66zi^D|P?E$T>(6UY9X{&PT{w*gi$)Xu zR27ca2kn6M=--lazKJg!W~yLj`dD4&#@Jf4>P9x!8EIVXyFQ_D6m@FMC-T_SRxJ+{ zS6Pc80#Z+_>;XHK-P0`{zg%;h81qTXM!OC&)+Lh?qi|HdI(gyRMF&ZG;nERlN!J1* zF^}tOzn`N@$a80MbO-rrZqD-^zAUjjPE@Jg8u|im)aIBk(cLL^nw2~DFNnh%nuawI zUCJ}&OTzcuqc}2VKw+zNW&c#%S5ejZ!1ODDnL8jqIeo|Q6gSPvD>ia|VZ#$AuKtP- zoSEAvKRK<}XmMMdBUBc`>?MERq72HjatgZ&M-#eui7ydxio4*RMYJS2`U;vdC#r)i zkuZBvk8ub=p~UTJ(A~mGQVxJGlJUpr2I3vZBlfT&;`+x^6qA2r?~;A$3EhhBh#EbKBIf6Vjh}n|I#xTA+e_1gB_nxZMKZ3 zfeCwsT(mZnkW(DJdv@D+$Ht+Sbox}qC0Pw&p}H}1j6p}d-{fS2G-(`8nw7zk$t!@2 zUx)uY+liE9Qn2S!L>z8&a{oa)R4MhBKWh0W#KzHcftx*K8H=4k#M5%zKj6AhZla38 z!VHZal8QFr`EbL@y6BoT`B@fmRx{B`5IunW{m;JNXuFYG8S@>gHDU<2^!X~P4qY=uK;@5HN2zz`!^XNM-mzTsy6@tZ{CDG zy&Km$rKrtNlX{cT}J=f=k4W%uDMhQk@J0Wqv3hb)ikYQuNYV2otSjZ<=M zQE{Q-RA)%`8Q-?&L-Qm`a$qFM&X7KJf5c8qTKA`C8ngjN5wGRB;mOLWyqzSfR{w9~ ziUToo*IScI%m(09r1667hh_RBj{{_vtLAY`UO{Y_4LHp=EDU6(p0rDyFr-l(bkG^i zU3%1A#ThN`qug|{;VyrR$8T)>`zP4z!oMBcwbv#IlfZzKfwuqv1~Bih)uF>DBTFNO zUy$ji4e?lAGWd-sax2s&VL-!>+sM9NG25irrTHBDYK?Dri-LJxNt)@z%3=*;z{hku z81R&c5AY2;$b#@p-P=)w2Ib-@gpr2?;w*GG-Qn84Fik$n{H=vAjfIVkto9q5&k65D z6W}_d!ZdkRZShk=<`YQu`dU00X=}7Z#bQEGj9^ad;b@nGxzoLzKvLsT!YaTnzj21Y zvm)I&_Gq9LVGQZ!V()?(mc|Z`^rJ@_G6)M3u$CICk;s(~LlM`a5MU~3VM>fZaYBrd za5l;Hc)y`b?2n5r-&&?*@Ila^Z>sPC6%Ix26pv`pRxg!_o3H&Y_ehSERLWsF=*51B z8*9*m z)R^u(ZXS=)Qp4dc`PprzeeZg%?p#~f6TTTTSK1nm9}hAS!9Jt+r2|o7g?M?`6C59&*5+~jIMf%e%lF{ea9g`<6yxXGz`^f%H8#bN+UE6m zF%oTk;u6`{_Oeqn=k;{2dzdS5)MQeY&hLKnJm>ZN)D3!O=a3Lc7=aD9_m0oWssfw+ zlRAL#@-CY5EAoJ(L$q85I28_@+glPaPfu^k%5}H->MeZ4gHFh1);1I5)$ku$sBq^R z=x^G2dq0KS9;KCjuenr7pvd&JBLx zexc5V_tSZ(id`Y`<62qsgV;T+;mg9zm66N3=aG6gL>*H^dBZp%70B#~Z#?wITOdt) zPj7h}ze1BA@~wDm*z z#%W#p$x=kXF-Vs2?y?gpBg@#9j|_4x)!F5|ScSGs@x(-Opvb2)eW?j1T!Z7~R{gH3 zM7^t!>lx_EK_iQ*5oN>P!b1wR_a!+&Z-NcJF$y{n1{QxATh%AIpp`=<(aE`gn1)XT z(Q>R{(>eHLmZQ))lUWm8Po!_)<*#84bal|oVBNZC+jM!I+4a8PU4`mjzl-zkei^Oh zdOxl97A^q{TseSl#pmnBi<`Ri^{S96GJ`NtoGWw+k2TQf+q zbn{c+osZk2mEH*t7*@4Xby}*hfovIE9$>L~vdUWN*Nf7QS0(Kh=iRT(u?p|q7e{dw z8IDrpH1&%X7*=iy^Mh*e?oViJcINY;2_Cd+$u-kBERd`phX*niaBF;ejxfa(uy085 zhU1#F3}m^7h@ANBXIw6&T!tK!dnm`rLY`qW*`=~#<}$8{yk z+IP0!-iR&VDTp9jAx|3YwtcdQ^}0V*q@_1@ggfQ0Z%ccpv^i zZYDoz)o^`;CutUXUjF2BpOF5k9O{SSy_NUTA(}O_O72fW12r`1X*<`ds{<~~9VF_E zQI2}_%KE9iL~4%ncw%RwTLya(qxW>+0hh_jM(s=}c9tVd8x8BD#r2V;aiSA0Yf<>} zLMA90tCaVKX`PPl(iC+_BM$be#$FAHU2FhLSX>M*~35IFZCJ0C~)yfTtNSo!WA#cZN1{zP`+b-617*7|%=_{)q zuFeKqI=LVbw;|0OrbuLd?<~U%^O>O;U`5-XP3|crra8)*qZPe@qdSdUr}6#RhtP}#!dtMm83I0sR1$>S#+!C? zc#T{UHfRZ*?5zE|#oRpABF7fvI=1shr1;u^)UsC9-I->m7oa)n??{5bOr)=8IVkUd zkp#PNKb>fQ4ubYZ4i2VPCcm6$)hcV&OYF#>3CmwQ#;@?6%SpKfQ#&KK$b_2_m4kQ{ zQH{`uguQc)>$_g=@Tr*WaLa5|wtL9WnjTMGnzs(R`Y1$1xoJr^Zk8_?cuf3`eByIf zhf~XtiY|t6#QhIZ<#o0i?As^rU2cvT2yef2OL!;Yfe6!~sSKxSuw<-dGy7K9PVn2idhfj+8QHEq8JfeKd~6m4XnNP&YVppZAoTjz zD$Gs1w$yHz}hnk0!G4PnmWEHK}+ac7hz0Xu}n;$1ChR*P9U82CLE_`K7@(|I>-u?*j=n8IpnwAj$GQoN#D|DU>uqom7 z3IKKCWOeb6G$WEe+a6x-4vI3C_FLa9rO#wv9l`ldP1_FQt)cnh9=YR=e5?*TmJw!k zz?$YlHv|5e!}Uw0NAs?Pt&{tw+K8j{CUM2HQ``Ga9-3Tjj}0x}A~#b;?d~CZg;2z= zSP!e)q^Qd-9r{kN-^f=D*?GSeKiNd33FZmJ=s;0K8o9Vmar8fp z3`NW554qGfa{1G$+T!V?30h4u-h_rers&4)f4|Ewr&-A%nN`pEy5o~Ep)n?`YQDXB zku;4t#?)N&1AAa`*=NfZN-Tgy z4ZUOW52TANnEM53;GonEjV9g(m(b2u(ly6`_Rf-2mus83_E7D(Ypo*wpplOUZxBY% zaZ*GW40iVUJUlQ#-yzG$Vk?gB5zC%qxDRvM52tUrYOd(8UgDNn#8k4}Y)Ba?_?*4d z%E(UkqD||zhzUN(#9i)zorR^n9wX**j+;;y1Q+~l%*Zb#p|dPN(J|QX>+()BH@Nim zsejniXjQ2Gj-kF$1T~)5SUP+3`ixb=H`j{`Cva$Cgfl7N!Ud&w)rCuX%Vjsv6j}K7 zTH1_%yzGB>G-P&WfvZ51c`&dWLj9|wF$RW#E9vQ582y}TH%Bd*Z7~8pFmR8Ef_I@t zd*jp7R4Z0s{*%GEkANWJC4EOmTr#WG&yF@pbPJViv^2gEFNyct_le0%s$>&sn#dxm zoax9R^~T0D4vHIu3`av`BI^aK3Z|@O=iXqg+MW&d2SbaWp!(;`DKUy{j`>2-BS~Mleja9 z78;=9V3Tu`o_W)2#chrabC$)T?KC}204U!bi-N~2gy|V7AU*`OHg!mN*2f`mShF=(o=3d*wHvmPt!2kNchn=hRut{=Y zK(Px9008t({jv7?%O@x70^A#5Xk_?PD01SKqOyJpMV;B2_h3taFEb+ByWA{IowH*w zL&2Eoh!13x54TGNUlorvzHp?xGI%*Repp=%C$>i%Rx8~`cm06HC8ZE+(~z9SPk7V; z&c#l_Oq3X95hOF5b+<4^XzkAtR)Fd=uv{93_%SCGRyv<@#2#PI!gLV4;zP%Gc@@hm zB9E{cry;5?e`c6l?J-_m(`?Er%w8>2*SDD3w%7Avao7r9>cPVprN0dF|3Q5!w}s5R1-YZ3_Q$sEG(@-kWK>k=$5 z40VIhQC%eqdIH09I!J(G@lk%)-YYc5mGVwx`ZhOC(3#M;J~=EK5OA^Ov|`jjNm(tC z6UzMc2a-nf9su9q=c6o{>Y*fCqf)wxjQ{BPP@6zt=*%632%K%OAq z<}@3LtAhT2h7;}(Y8h)YOBk-t)_= zWS)R^9~TE0GZUdTE#_MIjk#cntU@(mCySLkZPx({SNpDtzHk(rhc;UOKX~ zp@E<{F+b`5Kz7$|$JExERAU5Jge69mZWxI%MwTRDr2w0&V8Q++DGDFs7?s zrC{rk9Ufk9e4w72TqjFzN)7DJuojxn@t)deh}d;?zMZ0^MaZ0LmM98j zR|~1p8mZ=ne|UcvUeKJFiOn=2n5(rQ+wP%Y-+2GCw>bYt4@jxdl1H4W>`c2|!oW;I z7A~e{(Rp$2JP$4RAh(^$m^P8^Lm?=_-Y%o7Wg_$_;YS$BAm>PpfC9v!MJ}`SbOO7K zmzs&E|Jks*`FxXez=mB00|1bLq0YdFQ#m_p8+!&l8=IfX3fvFyf8otQflE>Buv%h- zZ$X;p$8O{Zs-njR74C`9-`CRw>$V(AlA`D|6pxtCSeOg1e51PHlq7o=IXy)}6`UNu zsv`Pzz@^Uk#$uG(X0fjKWbQcg-tOj`GEA&_>D$Z6o94g_HtN9Y*j#Xdn^y}al{^_cTn{*_tsn&ydFY&13dx~ykB?2Pi%%kuVd#dw z2@UxYB3u(rji<9BEp=R7S)O@oly3pru?D({sHAhEl6ZXfwTR@?b%n$(7M*iPAtAK@ z!D14=GV37Gn;{YAk8e;#7O<;xcrQTWjL6vM7Pg`3;h>mZ-g+EZ)f25aPrrHTvd~V# z=Lu1PU5TmM&l z;mllr8SX^T&9HZ0*^j2w>jH1JuAGV63i>WgVsGr&^Q~P3_po{W4j7`^fFl3jy-c`_`nN5GB{@oZbq?S(2K$M0E>= z^wupB$So}9(S>-rHz5SD@ceLcetmtmbGCfPA17=BFz}DU2_o?UjgYb>;=pm!r0c|i zEzXXb5)|*3L%X#!y#h3KvxT$IZZougguPRK#fZkFU`6`k%23p_;3+teOg^sV4IZ>p1-0G0b4 zTK^(8t(vr+RnO`y4i#k#<*nJnz3_{^%ljH_HuTB{46vB|6f&85&`We(LBfUSIS$6T z*vIa^VSwYHgJAj(ma~k~n2lK)-{B=rHDf=gmy5<$fsQ6Pvz5uVT@8R&0e9zG5(5tB z*IlUXso9DAgM*Y6o^~*=fE?~cbyq@uz7kAI!pI3y(mSHW(oIw5G%`OAb`pdKhW1O= zcCW8h0h|qQP{+Plmr z+VEWDs>8bN<%XfH*<&EME@9UjkaHhT%7a52q9JrN_up@^s0!oE1THyYPnl?oKT3!{Cgj|tDxIS z|J%;0Pl*>1yAAsesPWByY>+bsks=<_$jVvVTBO`BYfP16C z&Z~%X{E_TVU2U%jcq#Oj>38yq_)%+@A41utL{2I20he<6TX#o~As14VyN2iYRjZu2 zFiraMLW*0#2R_2reLi_>_PCa{ueRT_bgZH8sWH3=@xo)=)^qm8K;|S(_eShx&wCAs{MA=a(s5xV!rIE zf#Q!;bTcIxz$k*}uV>Q1hJNHP7joxzzQfz-UEw9Gw{;!^SS_!>tc5mlw>8;1=i76f zo*tj{uA~-UEZcp@(1J_M6}FBY;bKtG8PCWldp0G-pjFns-xv@NH`aM~0HSEj&Wm!F z=P%^Bicchwb9&`UbX4|b%$Lhu%P5QTY{@2xZ%Qqu59Awh#})MOr;0&Zl16Ta#LMQFd6v~8|kCTK|PHZrOF3Q{uR@y&eXinNSvEV_NR`31EJ zW5}@Kebc4ln3QT&mV9;4w0N~5YS|!!rB_X4mY1I;xXnQ2ax#2U1p)C78G4gkZcoEj zX_`k4i>^b4qLbq78duzd%vdI*^rxJSqB^n>A@$qoWQT#Rgtqa1LR$^P<`6c;%f~^o zR8p@(?WS*0Q{*1PbLaSBwKOqy(+z6#a8LHF2KeAkPbotL}~#}p62l|^gNGr~uRYjz)u@g?L-m)M+!F#?*jK17;n+eM&O}i*u4D`vsYjc9(;njH#ES%Kdd=LI%LwslY&BKJKT_OZ=MVzsyH0geP?gE0bSKq6#z5jf1j%PkX7e%zI~e8R&YGV}(ZSmM2gNu0GZ67{_W=@tl5w z3BUiIojYkdQYnqJN9t|{@3&R^Lw(LAZk^jIIIG5XOK%C()N%pcr_WMeDO$n?H{fy( zbU)Q9sba|)EqsL_o0uDdgaSB?D?R?o?(La9CN+ATkn=sPuGI~6+q35DX$;q^+QntF z=LuB)DTkwWu)ku`R7Rxz_;X;9MqEX>!JdPGsKY3?lh7Wxp{T>4kcz#MM5W@pJ#y|W z`>0Bi@demX=z0r?6WayY#U?`I-k8)v8UBj~xgSL{{9uPu{a~p+ zWeS386d(grw7zWk!JgO(`rpzE`lDlI3c~*J9*6;p@<+BXbN8Z#XgMMXI;F4g;V_8D z1(lC_2!I%znS$;>jG*9urujox%MpS8%nE~%zzu_;FfRD#tLaOX56(c86uHb6AWDyL zL4?n*s{K{a&-w`cR}~8%D#`=pGI?bR##ezTo5V_D0>5hUcS*pXa)J=MGC&UgEc{cj z>@!;K3G*Y$OL6{9(DNz3_JN2ei}en>Jk*N3a?x?|=^b&3?F%~Vs*6j;VW<@{doYe_ zegPeSQL>h!IQU`USXuNfw}+J5SRnm@JJGat9^mu*3~GuZmj#^b6e_xFpk<_$hL7@| zp5`qZT?ebne4IlpeNnJNw*z>{(qxDpPh~=#k$D#$qOE2aJNyQ8osn@DGNO$pA3J=X zx76s;>{i&8Lv?E_I`U%Y#9O>27)w1EPvu+#%8*i-DzrwoF`4~C5pIwjSsKVQHE!4w zJ?_Dg898G$)h4(Znf69p)D$fQ#?#UON)y!&G08=G`rW{H<)u!tqfS22MIgN^Gsru~ zW?${>*-{3}O9Q}*i-6+2Dk-I`D*&Lv^b#m;5hQRO!^U zLyO`x{sWFndzB@n{{Vk7WILl!RP-l^TA8Ug`#$gwz)N9B1LPR^b_NIrPR+DxrnmnE z3?11@HO&44Dw}Xs>DU_n1%9i9l|RhYIdp-kU?uVSU2{E66a(z!>xSRjRLCyR=REl@ zr-!SOC#lMp*oWFSn~(nc1s)a+w&C(he6`A8GgxKWH%+-Y6&CUtYjwFCMFB@^YC|N{ zUHE|-T{lS0kAODU)R=djAzyVoW3xdK(+2Z#*e&99ifdH^6KTa%PFK2BZ}3Yp9^P9y zp6y7+1;5kPgb$3K>XO~lXdb%MHkFN`KNE_cX20JE$YoVczq}Mt)8mUCgGBRHcs3gx z+M`?H>BDHcOwX%r{TlkTa7oPGa3yFR{b+usU!-oe+C$EfUb5DMDdDMdve<@4_*n&3 zSyyF|7Y8|43xmU~syRjI%P761&jmY2s-?NLz2Z_xe5E=uCHg2y=4Zo1GPRrRz`488 z?@&nAeQ`VY-EsBfo6D4?TvEp;^oD8Xrd5N!CDb&ExokF$+oxukpT=nEx5S=oQwKRe zPgsAW(Zp?uLM6ME{e-s`b-CnW`f4V%r|?<->{CISQbmoI3ZKJ-S^D>d1=#r_j=ZON z0*W1T>}w;E*&OoxjDQW5ImR3OgD|G%0iPpvlO};=-kp!LQy&eg&px?-P@A@2{PEG{ zT%5twVez}smyI9$A1fmpM*+cZA5>@RcCrVC#)V-$csY|{%9CDep}y+{U9)L4vH8Dg z&ffT;&ogn{v&a}iCEuc=i+S&$Tu`SstWJBY6Eey$Hue~@;YV@VP?0RzR~uM<0YURB z>|38GAcDXm)?j(J&RQmLyA1!R^Fl(tMO5vXEAiw2Xopp;OP^GP zuDtM98F@~cN5;rtowEsJryfk32kzShbEQ1bWe?F5uJ`;LFW1puaTPUBsD!1n4TsDj zuTzN&b#A|6ut;Q?wXdwzlC%cisY^y2aahh~!1 z9bVHbTxmfn!u+l8D$enpW9P$kzzCR|RtW|Ai|*A@T-D{R|K%w}M%G9u0_T{(E8f23 z`H6{F*j-;V%gI{r^yr{hkx(1obN2Df%&iT0VcVrH@X(F_VPRlcBQ6vGS{Dw0M)LpI zG5<1>iyQu0#pTSdL?tnz1J)l$ukjO>&7mo2h7ZGFotjT?K`M!0tF}#0(LEm_Au4jM zK&+n4;^YwHfTd)Woyc~R9oc6wuXQ838*>~&B15d4nB1$jthx)nG+pj0Y&`u;vA-8}pZL)<3|z$LBn{4= zpE)}6fM}@N+fm*7a;i8;+3H>SkE2+zFU-LTdph3?5SgZ7;0mbV-0RjIys_B;j=O5sI9NZ))`ELUMJ}>4UxPWG$l>g)O znBNiqeUQdK5to5cdjHtF_0NM2e(l}*Nr?dLr;7Zq;}Nibkpf!k06E@rKvnuh>7O)| zl_35&EaA63!v7@u-y4qqNmd#0PqKe$=TB?+FS7q*>+$b=|GrD@Z_b^8Ed7J;e-i%> z-*kZg|1DjAQ;GowIR2vaUmLuBug&jKk$=;OCH|Af?=h0UllVQR Date: Mon, 9 Jun 2025 22:31:23 -0700 Subject: [PATCH 107/131] comments: add Document.comments Provides access to the comments collection from the document object. --- docs/conf.py | 2 ++ src/docx/document.py | 6 ++++++ src/docx/parts/document.py | 6 ++++++ tests/test_document.py | 11 +++++++++++ 4 files changed, 25 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index e37e9be7e..60e28fa4c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,6 +83,8 @@ .. |CharacterStyle| replace:: :class:`.CharacterStyle` +.. |Comments| replace:: :class:`.Comments` + .. |Cm| replace:: :class:`.Cm` .. |ColorFormat| replace:: :class:`.ColorFormat` diff --git a/src/docx/document.py b/src/docx/document.py index 2cf0a1c38..5de03bf9d 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: import docx.types as t + from docx.comments import Comments from docx.oxml.document import CT_Body, CT_Document from docx.parts.document import DocumentPart from docx.settings import Settings @@ -106,6 +107,11 @@ def add_table(self, rows: int, cols: int, style: str | _TableStyle | None = None table.style = style return table + @property + def comments(self) -> Comments: + """A |Comments| object providing access to comments added to the document.""" + return self._part.comments + @property def core_properties(self): """A |CoreProperties| object providing Dublin Core properties of document.""" diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index dea0845f7..78841f47a 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -15,6 +15,7 @@ from docx.shared import lazyproperty if TYPE_CHECKING: + from docx.comments import Comments from docx.enum.style import WD_STYLE_TYPE from docx.opc.coreprops import CoreProperties from docx.settings import Settings @@ -42,6 +43,11 @@ def add_header_part(self): rId = self.relate_to(header_part, RT.HEADER) return header_part, rId + @property + def comments(self) -> Comments: + """|Comments| object providing access to the comments added to this document.""" + raise NotImplementedError + @property def core_properties(self) -> CoreProperties: """A |CoreProperties| object providing read/write access to the core properties diff --git a/tests/test_document.py b/tests/test_document.py index 739813321..0b36017a5 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -9,6 +9,7 @@ import pytest +from docx.comments import Comments from docx.document import Document, _Body from docx.enum.section import WD_SECTION from docx.enum.text import WD_BREAK @@ -164,6 +165,12 @@ def it_can_save_the_document_to_a_file(self, document_part_: Mock): document_part_.save.assert_called_once_with("foobar.docx") + def it_provides_access_to_the_comments(self, document_part_: Mock, comments_: Mock): + document_part_.comments = comments_ + document = Document(cast(CT_Document, element("w:document")), document_part_) + + assert document.comments is comments_ + def it_provides_access_to_its_core_properties( self, document_part_: Mock, core_properties_: Mock ): @@ -281,6 +288,10 @@ def _block_width_prop_(self, request: FixtureRequest): def body_prop_(self, request: FixtureRequest): return property_mock(request, Document, "_body") + @pytest.fixture + def comments_(self, request: FixtureRequest): + return instance_mock(request, Comments) + @pytest.fixture def core_properties_(self, request: FixtureRequest): return instance_mock(request, CoreProperties) From 8f184cc41811995916fa0d0c02d9dab761092a67 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:32:47 -0700 Subject: [PATCH 108/131] comments: add DocumentPart.comments Provide a way to get a `Comments` object from the `DocumentPart`. --- src/docx/parts/comments.py | 15 +++++++++++++++ src/docx/parts/document.py | 11 ++++++++++- tests/parts/test_document.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/docx/parts/comments.py diff --git a/src/docx/parts/comments.py b/src/docx/parts/comments.py new file mode 100644 index 000000000..6258ceed2 --- /dev/null +++ b/src/docx/parts/comments.py @@ -0,0 +1,15 @@ +"""Contains comments added to the document.""" + +from __future__ import annotations + +from docx.comments import Comments +from docx.parts.story import StoryPart + + +class CommentsPart(StoryPart): + """Container part for comments added to the document.""" + + @property + def comments(self) -> Comments: + """A |Comments| proxy object for the `w:comments` root element of this part.""" + raise NotImplementedError diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index 78841f47a..e804647f6 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -6,6 +6,7 @@ from docx.document import Document from docx.opc.constants import RELATIONSHIP_TYPE as RT +from docx.parts.comments import CommentsPart from docx.parts.hdrftr import FooterPart, HeaderPart from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart @@ -46,7 +47,7 @@ def add_header_part(self): @property def comments(self) -> Comments: """|Comments| object providing access to the comments added to this document.""" - raise NotImplementedError + return self._comments_part.comments @property def core_properties(self) -> CoreProperties: @@ -124,6 +125,14 @@ def styles(self): document.""" return self._styles_part.styles + @property + def _comments_part(self) -> CommentsPart: + """A |CommentsPart| object providing access to the comments added to this document. + + Creates a default comments part if one is not present. + """ + raise NotImplementedError + @property def _settings_part(self) -> SettingsPart: """A |SettingsPart| object providing access to the document-level settings for diff --git a/tests/parts/test_document.py b/tests/parts/test_document.py index cfe9e870c..c8b7793f9 100644 --- a/tests/parts/test_document.py +++ b/tests/parts/test_document.py @@ -4,12 +4,14 @@ import pytest +from docx.comments import Comments from docx.enum.style import WD_STYLE_TYPE from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.coreprops import CoreProperties from docx.opc.packuri import PackURI from docx.package import Package +from docx.parts.comments import CommentsPart from docx.parts.document import DocumentPart from docx.parts.hdrftr import FooterPart, HeaderPart from docx.parts.numbering import NumberingPart @@ -109,6 +111,17 @@ def it_can_save_the_package_to_a_file(self, package_: Mock): package_.save.assert_called_once_with("foobar.docx") + def it_provides_access_to_the_comments_added_to_the_document( + self, _comments_part_prop_: Mock, comments_part_: Mock, comments_: Mock, package_: Mock + ): + comments_part_.comments = comments_ + _comments_part_prop_.return_value = comments_part_ + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) + + assert document_part.comments is comments_ + def it_provides_access_to_the_document_settings( self, _settings_part_prop_: Mock, settings_part_: Mock, settings_: Mock, package_: Mock ): @@ -282,6 +295,22 @@ def and_it_creates_a_default_styles_part_if_not_present( # -- fixtures -------------------------------------------------------------------------------- + @pytest.fixture + def comments_(self, request: FixtureRequest) -> Mock: + return instance_mock(request, Comments) + + @pytest.fixture + def CommentsPart_(self, request: FixtureRequest) -> Mock: + return class_mock(request, "docx.parts.document.CommentsPart") + + @pytest.fixture + def comments_part_(self, request: FixtureRequest) -> Mock: + return instance_mock(request, CommentsPart) + + @pytest.fixture + def _comments_part_prop_(self, request: FixtureRequest) -> Mock: + return property_mock(request, DocumentPart, "_comments_part") + @pytest.fixture def core_properties_(self, request: FixtureRequest): return instance_mock(request, CoreProperties) From ae0e82d979a972d758918072c7088fa49fa5eec3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:34:02 -0700 Subject: [PATCH 109/131] comments: add DocumentPart._comments_part Also involves adding `CommentsPart.default`. Because the comments part is optional, we need a mechanism to add a default (empty) comments part when one is not present. This is what `CommentsPart.default()` is for. --- src/docx/oxml/comments.py | 15 +++++++++++ src/docx/parts/comments.py | 26 +++++++++++++++++++ src/docx/parts/document.py | 8 +++++- src/docx/templates/default-comments.xml | 5 ++++ tests/parts/test_comments.py | 25 +++++++++++++++++++ tests/parts/test_document.py | 33 +++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/docx/oxml/comments.py create mode 100644 src/docx/templates/default-comments.xml create mode 100644 tests/parts/test_comments.py diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py new file mode 100644 index 000000000..65624b738 --- /dev/null +++ b/src/docx/oxml/comments.py @@ -0,0 +1,15 @@ +"""Custom element classes related to document comments.""" + +from __future__ import annotations + +from docx.oxml.xmlchemy import BaseOxmlElement + + +class CT_Comments(BaseOxmlElement): + """`w:comments` element, the root element for the comments part. + + Simply contains a collection of `w:comment` elements, each representing a single comment. Each + contained comment is identified by a unique `w:id` attribute, used to reference the comment + from the document text. The offset of the comment in this collection is arbitrary; it is + essentially a _set_ implemented as a list. + """ diff --git a/src/docx/parts/comments.py b/src/docx/parts/comments.py index 6258ceed2..e43f24a8e 100644 --- a/src/docx/parts/comments.py +++ b/src/docx/parts/comments.py @@ -2,7 +2,17 @@ from __future__ import annotations +import os +from typing import cast + +from typing_extensions import Self + from docx.comments import Comments +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.oxml.comments import CT_Comments +from docx.oxml.parser import parse_xml +from docx.package import Package from docx.parts.story import StoryPart @@ -13,3 +23,19 @@ class CommentsPart(StoryPart): def comments(self) -> Comments: """A |Comments| proxy object for the `w:comments` root element of this part.""" raise NotImplementedError + + @classmethod + def default(cls, package: Package) -> Self: + """A newly created comments part, containing a default empty `w:comments` element.""" + partname = PackURI("/word/comments.xml") + content_type = CT.WML_COMMENTS + element = cast("CT_Comments", parse_xml(cls._default_comments_xml())) + return cls(partname, content_type, element, package) + + @classmethod + def _default_comments_xml(cls) -> bytes: + """A byte-string containing XML for a default comments part.""" + path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-comments.xml") + with open(path, "rb") as f: + xml_bytes = f.read() + return xml_bytes diff --git a/src/docx/parts/document.py b/src/docx/parts/document.py index e804647f6..4960264b1 100644 --- a/src/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -131,7 +131,13 @@ def _comments_part(self) -> CommentsPart: Creates a default comments part if one is not present. """ - raise NotImplementedError + try: + return cast(CommentsPart, self.part_related_by(RT.COMMENTS)) + except KeyError: + assert self.package is not None + comments_part = CommentsPart.default(self.package) + self.relate_to(comments_part, RT.COMMENTS) + return comments_part @property def _settings_part(self) -> SettingsPart: diff --git a/src/docx/templates/default-comments.xml b/src/docx/templates/default-comments.xml new file mode 100644 index 000000000..2afdda20b --- /dev/null +++ b/src/docx/templates/default-comments.xml @@ -0,0 +1,5 @@ + + diff --git a/tests/parts/test_comments.py b/tests/parts/test_comments.py new file mode 100644 index 000000000..5e6ef988c --- /dev/null +++ b/tests/parts/test_comments.py @@ -0,0 +1,25 @@ +"""Unit test suite for the docx.parts.hdrftr module.""" + +from __future__ import annotations + +from docx.opc.constants import CONTENT_TYPE as CT +from docx.package import Package +from docx.parts.comments import CommentsPart + + +class DescribeCommentsPart: + """Unit test suite for `docx.parts.comments.CommentsPart` objects.""" + + def it_constructs_a_default_comments_part_to_help(self): + package = Package() + + comments_part = CommentsPart.default(package) + + assert isinstance(comments_part, CommentsPart) + assert comments_part.partname == "/word/comments.xml" + assert comments_part.content_type == CT.WML_COMMENTS + assert comments_part.package is package + assert comments_part.element.tag == ( + "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}comments" + ) + assert len(comments_part.element) == 0 diff --git a/tests/parts/test_document.py b/tests/parts/test_document.py index c8b7793f9..c27990baf 100644 --- a/tests/parts/test_document.py +++ b/tests/parts/test_document.py @@ -227,6 +227,39 @@ def it_can_get_the_id_of_a_style( styles_.get_style_id.assert_called_once_with(style_, WD_STYLE_TYPE.CHARACTER) assert style_id == "BodyCharacter" + def it_provides_access_to_its_comments_part_to_help( + self, package_: Mock, part_related_by_: Mock, comments_part_: Mock + ): + part_related_by_.return_value = comments_part_ + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) + + comments_part = document_part._comments_part + + part_related_by_.assert_called_once_with(document_part, RT.COMMENTS) + assert comments_part is comments_part_ + + def and_it_creates_a_default_comments_part_if_not_present( + self, + package_: Mock, + part_related_by_: Mock, + CommentsPart_: Mock, + comments_part_: Mock, + relate_to_: Mock, + ): + part_related_by_.side_effect = KeyError + CommentsPart_.default.return_value = comments_part_ + document_part = DocumentPart( + PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_ + ) + + comments_part = document_part._comments_part + + CommentsPart_.default.assert_called_once_with(package_) + relate_to_.assert_called_once_with(document_part, comments_part_, RT.COMMENTS) + assert comments_part is comments_part_ + def it_provides_access_to_its_settings_part_to_help( self, part_related_by_: Mock, settings_part_: Mock, package_: Mock ): From 9c8a2e91fa743bf8f19226eb3353c9fa1a6973b3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:36:02 -0700 Subject: [PATCH 110/131] comments: add CommentsPart.comments --- src/docx/comments.py | 10 ++++++++++ src/docx/parts/comments.py | 8 +++++++- tests/parts/test_comments.py | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/docx/comments.py b/src/docx/comments.py index 9165e884d..587837baa 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -2,12 +2,22 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from docx.blkcntnr import BlockItemContainer +if TYPE_CHECKING: + from docx.oxml.comments import CT_Comments + from docx.parts.comments import CommentsPart + class Comments: """Collection containing the comments added to this document.""" + def __init__(self, comments_elm: CT_Comments, comments_part: CommentsPart): + self._comments_elm = comments_elm + self._comments_part = comments_part + class Comment(BlockItemContainer): """Proxy for a single comment in the document. diff --git a/src/docx/parts/comments.py b/src/docx/parts/comments.py index e43f24a8e..111bfb878 100644 --- a/src/docx/parts/comments.py +++ b/src/docx/parts/comments.py @@ -19,10 +19,16 @@ class CommentsPart(StoryPart): """Container part for comments added to the document.""" + def __init__( + self, partname: PackURI, content_type: str, element: CT_Comments, package: Package + ): + super().__init__(partname, content_type, element, package) + self._comments = element + @property def comments(self) -> Comments: """A |Comments| proxy object for the `w:comments` root element of this part.""" - raise NotImplementedError + return Comments(self._comments, self) @classmethod def default(cls, package: Package) -> Self: diff --git a/tests/parts/test_comments.py b/tests/parts/test_comments.py index 5e6ef988c..4cab7783b 100644 --- a/tests/parts/test_comments.py +++ b/tests/parts/test_comments.py @@ -2,14 +2,38 @@ from __future__ import annotations +from typing import cast + +import pytest + +from docx.comments import Comments from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.oxml.comments import CT_Comments from docx.package import Package from docx.parts.comments import CommentsPart +from ..unitutil.cxml import element +from ..unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock + class DescribeCommentsPart: """Unit test suite for `docx.parts.comments.CommentsPart` objects.""" + def it_provides_access_to_its_comments_collection( + self, Comments_: Mock, comments_: Mock, package_: Mock + ): + Comments_.return_value = comments_ + comments_elm = cast(CT_Comments, element("w:comments")) + comments_part = CommentsPart( + PackURI("/word/comments.xml"), CT.WML_COMMENTS, comments_elm, package_ + ) + + comments = comments_part.comments + + Comments_.assert_called_once_with(comments_part.element, comments_part) + assert comments is comments_ + def it_constructs_a_default_comments_part_to_help(self): package = Package() @@ -23,3 +47,17 @@ def it_constructs_a_default_comments_part_to_help(self): "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}comments" ) assert len(comments_part.element) == 0 + + # -- fixtures -------------------------------------------------------------------------------- + + @pytest.fixture + def Comments_(self, request: FixtureRequest) -> Mock: + return class_mock(request, "docx.parts.comments.Comments") + + @pytest.fixture + def comments_(self, request: FixtureRequest) -> Mock: + return instance_mock(request, Comments) + + @pytest.fixture + def package_(self, request: FixtureRequest) -> Mock: + return instance_mock(request, Package) From 595deccd0d4700cc993332f0cde61a98ca9a443b Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:36:50 -0700 Subject: [PATCH 111/131] comments: package-loader loads CommentsPart CommentsPart is loaded as XML-part on document deserialization. --- features/doc-comments.feature | 1 - src/docx/__init__.py | 3 +++ src/docx/parts/comments.py | 6 +++++- tests/parts/test_comments.py | 26 +++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/features/doc-comments.feature b/features/doc-comments.feature index c49edaa77..d23a763a5 100644 --- a/features/doc-comments.feature +++ b/features/doc-comments.feature @@ -5,7 +5,6 @@ Feature: Document.comments And I need methods allowing access to the comments in the collection - @wip Scenario Outline: Access document comments Given a document having comments part Then document.comments is a Comments object diff --git a/src/docx/__init__.py b/src/docx/__init__.py index 205221027..987e8a267 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -25,6 +25,7 @@ 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.comments import CommentsPart from docx.parts.document import DocumentPart from docx.parts.hdrftr import FooterPart, HeaderPart from docx.parts.image import ImagePart @@ -41,6 +42,7 @@ def part_class_selector(content_type: str, reltype: str) -> Type[Part] | None: PartFactory.part_class_selector = part_class_selector PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart +PartFactory.part_type_for[CT.WML_COMMENTS] = CommentsPart PartFactory.part_type_for[CT.WML_DOCUMENT_MAIN] = DocumentPart PartFactory.part_type_for[CT.WML_FOOTER] = FooterPart PartFactory.part_type_for[CT.WML_HEADER] = HeaderPart @@ -51,6 +53,7 @@ def part_class_selector(content_type: str, reltype: str) -> Type[Part] | None: del ( CT, CorePropertiesPart, + CommentsPart, DocumentPart, FooterPart, HeaderPart, diff --git a/src/docx/parts/comments.py b/src/docx/parts/comments.py index 111bfb878..0e4cc7438 100644 --- a/src/docx/parts/comments.py +++ b/src/docx/parts/comments.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import cast +from typing import TYPE_CHECKING, cast from typing_extensions import Self @@ -15,6 +15,10 @@ from docx.package import Package from docx.parts.story import StoryPart +if TYPE_CHECKING: + from docx.oxml.comments import CT_Comments + from docx.package import Package + class CommentsPart(StoryPart): """Container part for comments added to the document.""" diff --git a/tests/parts/test_comments.py b/tests/parts/test_comments.py index 4cab7783b..049c9e737 100644 --- a/tests/parts/test_comments.py +++ b/tests/parts/test_comments.py @@ -8,18 +8,34 @@ from docx.comments import Comments 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.oxml.comments import CT_Comments from docx.package import Package from docx.parts.comments import CommentsPart from ..unitutil.cxml import element -from ..unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock +from ..unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock, method_mock class DescribeCommentsPart: """Unit test suite for `docx.parts.comments.CommentsPart` objects.""" + def it_is_used_by_the_part_loader_to_construct_a_comments_part( + self, package_: Mock, CommentsPart_load_: Mock, comments_part_: Mock + ): + partname = PackURI("/word/comments.xml") + content_type = CT.WML_COMMENTS + reltype = RT.COMMENTS + blob = b"" + CommentsPart_load_.return_value = comments_part_ + + part = PartFactory(partname, content_type, reltype, blob, package_) + + CommentsPart_load_.assert_called_once_with(partname, content_type, blob, package_) + assert part is comments_part_ + def it_provides_access_to_its_comments_collection( self, Comments_: Mock, comments_: Mock, package_: Mock ): @@ -58,6 +74,14 @@ def Comments_(self, request: FixtureRequest) -> Mock: def comments_(self, request: FixtureRequest) -> Mock: return instance_mock(request, Comments) + @pytest.fixture + def comments_part_(self, request: FixtureRequest) -> Mock: + return instance_mock(request, CommentsPart) + + @pytest.fixture + def CommentsPart_load_(self, request: FixtureRequest) -> Mock: + return method_mock(request, CommentsPart, "load", autospec=False) + @pytest.fixture def package_(self, request: FixtureRequest) -> Mock: return instance_mock(request, Package) From 6c0024c52e477707685ba5c373abb2336b9fef36 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:41:05 -0700 Subject: [PATCH 112/131] comments: add Comments.__len__() --- features/doc-comments.feature | 1 - src/docx/comments.py | 4 +++ src/docx/oxml/__init__.py | 27 ++++++++++++------- src/docx/oxml/comments.py | 16 +++++++++++- tests/test_comments.py | 49 +++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 tests/test_comments.py diff --git a/features/doc-comments.feature b/features/doc-comments.feature index d23a763a5..6aaffee68 100644 --- a/features/doc-comments.feature +++ b/features/doc-comments.feature @@ -15,7 +15,6 @@ Feature: Document.comments | no | - @wip Scenario Outline: Comments.__len__() Given a Comments object with comments Then len(comments) == diff --git a/src/docx/comments.py b/src/docx/comments.py index 587837baa..736cbb7ab 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -18,6 +18,10 @@ def __init__(self, comments_elm: CT_Comments, comments_part: CommentsPart): self._comments_elm = comments_elm self._comments_part = comments_part + def __len__(self) -> int: + """The number of comments in this collection.""" + return len(self._comments_elm.comment_lst) + class Comment(BlockItemContainer): """Proxy for a single comment in the document. diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py index 3fbc114ae..37f608cef 100644 --- a/src/docx/oxml/__init__.py +++ b/src/docx/oxml/__init__.py @@ -1,3 +1,5 @@ +# ruff: noqa: E402, I001 + """Initializes oxml sub-package. This including registering custom element classes corresponding to Open XML elements. @@ -84,16 +86,21 @@ # --------------------------------------------------------------------------- # other custom element class mappings -from .coreprops import CT_CoreProperties # noqa +from .comments import CT_Comments, CT_Comment + +register_element_cls("w:comments", CT_Comments) +register_element_cls("w:comment", CT_Comment) + +from .coreprops import CT_CoreProperties register_element_cls("cp:coreProperties", CT_CoreProperties) -from .document import CT_Body, CT_Document # noqa +from .document import CT_Body, CT_Document 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 +from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr register_element_cls("w:abstractNumId", CT_DecimalNumber) register_element_cls("w:ilvl", CT_DecimalNumber) @@ -104,7 +111,7 @@ register_element_cls("w:numbering", CT_Numbering) register_element_cls("w:startOverride", CT_DecimalNumber) -from .section import ( # noqa +from .section import ( CT_HdrFtr, CT_HdrFtrRef, CT_PageMar, @@ -122,11 +129,11 @@ register_element_cls("w:sectPr", CT_SectPr) register_element_cls("w:type", CT_SectType) -from .settings import CT_Settings # noqa +from .settings import CT_Settings register_element_cls("w:settings", CT_Settings) -from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles # noqa +from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles register_element_cls("w:basedOn", CT_String) register_element_cls("w:latentStyles", CT_LatentStyles) @@ -141,7 +148,7 @@ register_element_cls("w:uiPriority", CT_DecimalNumber) register_element_cls("w:unhideWhenUsed", CT_OnOff) -from .table import ( # noqa +from .table import ( CT_Height, CT_Row, CT_Tbl, @@ -178,7 +185,7 @@ register_element_cls("w:vAlign", CT_VerticalJc) register_element_cls("w:vMerge", CT_VMerge) -from .text.font import ( # noqa +from .text.font import ( CT_Color, CT_Fonts, CT_Highlight, @@ -217,11 +224,11 @@ register_element_cls("w:vertAlign", CT_VerticalAlignRun) register_element_cls("w:webHidden", CT_OnOff) -from .text.paragraph import CT_P # noqa +from .text.paragraph import CT_P register_element_cls("w:p", CT_P) -from .text.parfmt import ( # noqa +from .text.parfmt import ( CT_Ind, CT_Jc, CT_PPr, diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index 65624b738..1e818ebfb 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -2,7 +2,7 @@ from __future__ import annotations -from docx.oxml.xmlchemy import BaseOxmlElement +from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore class CT_Comments(BaseOxmlElement): @@ -13,3 +13,17 @@ class CT_Comments(BaseOxmlElement): from the document text. The offset of the comment in this collection is arbitrary; it is essentially a _set_ implemented as a list. """ + + # -- type-declarations to fill in the gaps for metaclass-added methods -- + comment_lst: list[CT_Comment] + + comment = ZeroOrMore("w:comment") + + +class CT_Comment(BaseOxmlElement): + """`w:comment` element, representing a single comment. + + A comment is a so-called "story" and can contain paragraphs and tables much like a table-cell. + While probably most often used for a single sentence or phrase, a comment can contain rich + content, including multiple rich-text paragraphs, hyperlinks, images, and tables. + """ diff --git a/tests/test_comments.py b/tests/test_comments.py new file mode 100644 index 000000000..2bde587c6 --- /dev/null +++ b/tests/test_comments.py @@ -0,0 +1,49 @@ +"""Unit test suite for the docx.comments module.""" + +from __future__ import annotations + +from typing import cast + +import pytest + +from docx.comments import Comments +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.oxml.comments import CT_Comments +from docx.package import Package +from docx.parts.comments import CommentsPart + +from .unitutil.cxml import element +from .unitutil.mock import FixtureRequest, Mock, instance_mock + + +class DescribeComments: + """Unit-test suite for `docx.comments.Comments`.""" + + @pytest.mark.parametrize( + ("cxml", "count"), + [ + ("w:comments", 0), + ("w:comments/w:comment", 1), + ("w:comments/(w:comment,w:comment,w:comment)", 3), + ], + ) + def it_knows_how_many_comments_it_contains(self, cxml: str, count: int, package_: Mock): + comments_elm = cast(CT_Comments, element(cxml)) + comments = Comments( + comments_elm, + CommentsPart( + PackURI("/word/comments.xml"), + CT.WML_COMMENTS, + comments_elm, + package_, + ), + ) + + assert len(comments) == count + + # -- fixtures -------------------------------------------------------------------------------- + + @pytest.fixture + def package_(self, request: FixtureRequest): + return instance_mock(request, Package) From 88ff3cab593bd68440e0d552631f2f80c476447d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:43:23 -0700 Subject: [PATCH 113/131] comments: add Comments.__iter__() --- features/doc-comments.feature | 1 - src/docx/blkcntnr.py | 3 ++- src/docx/comments.py | 15 +++++++++++++-- tests/test_comments.py | 23 ++++++++++++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/features/doc-comments.feature b/features/doc-comments.feature index 6aaffee68..fbe2fd278 100644 --- a/features/doc-comments.feature +++ b/features/doc-comments.feature @@ -25,7 +25,6 @@ Feature: Document.comments | 4 | - @wip Scenario: Comments.__iter__() Given a Comments object with 4 comments Then iterating comments yields 4 Comment objects diff --git a/src/docx/blkcntnr.py b/src/docx/blkcntnr.py index 951e03427..82c7ef727 100644 --- a/src/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -19,6 +19,7 @@ if TYPE_CHECKING: import docx.types as t + from docx.oxml.comments import CT_Comment from docx.oxml.document import CT_Body from docx.oxml.section import CT_HdrFtr from docx.oxml.table import CT_Tc @@ -26,7 +27,7 @@ from docx.styles.style import ParagraphStyle from docx.table import Table -BlockItemElement: TypeAlias = "CT_Body | CT_HdrFtr | CT_Tc" +BlockItemElement: TypeAlias = "CT_Body | CT_Comment | CT_HdrFtr | CT_Tc" class BlockItemContainer(StoryChild): diff --git a/src/docx/comments.py b/src/docx/comments.py index 736cbb7ab..6ccdec83b 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -2,12 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterator from docx.blkcntnr import BlockItemContainer if TYPE_CHECKING: - from docx.oxml.comments import CT_Comments + from docx.oxml.comments import CT_Comment, CT_Comments from docx.parts.comments import CommentsPart @@ -18,6 +18,13 @@ def __init__(self, comments_elm: CT_Comments, comments_part: CommentsPart): self._comments_elm = comments_elm self._comments_part = comments_part + def __iter__(self) -> Iterator[Comment]: + """Iterator over the comments in this collection.""" + return ( + Comment(comment_elm, self._comments_part) + for comment_elm in self._comments_elm.comment_lst + ) + def __len__(self) -> int: """The number of comments in this collection.""" return len(self._comments_elm.comment_lst) @@ -36,3 +43,7 @@ class Comment(BlockItemContainer): Note that certain content like tables may not be displayed in the Word comment sidebar due to space limitations. Such "over-sized" content can still be viewed in the review pane. """ + + def __init__(self, comment_elm: CT_Comment, comments_part: CommentsPart): + super().__init__(comment_elm, comments_part) + self._comment_elm = comment_elm diff --git a/tests/test_comments.py b/tests/test_comments.py index 2bde587c6..b38e429f9 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -6,7 +6,7 @@ import pytest -from docx.comments import Comments +from docx.comments import Comment, Comments from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.packuri import PackURI from docx.oxml.comments import CT_Comments @@ -42,6 +42,27 @@ def it_knows_how_many_comments_it_contains(self, cxml: str, count: int, package_ assert len(comments) == count + def it_is_iterable_over_the_comments_it_contains(self, package_: Mock): + comments_elm = cast(CT_Comments, element("w:comments/(w:comment,w:comment)")) + comments = Comments( + comments_elm, + CommentsPart( + PackURI("/word/comments.xml"), + CT.WML_COMMENTS, + comments_elm, + package_, + ), + ) + + comment_iter = iter(comments) + + comment1 = next(comment_iter) + assert type(comment1) is Comment, "expected a `Comment` object" + comment2 = next(comment_iter) + assert type(comment2) is Comment, "expected a `Comment` object" + with pytest.raises(StopIteration): + next(comment_iter) + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From e2aec420ba2d43991e4fa8acb04f200c65229ad0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:47:47 -0700 Subject: [PATCH 114/131] comments: add Comments.get() To get a comment by id, None when not found. --- src/docx/comments.py | 5 +++++ src/docx/oxml/comments.py | 5 +++++ tests/test_comments.py | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/docx/comments.py b/src/docx/comments.py index 6ccdec83b..4a3da9dae 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -29,6 +29,11 @@ def __len__(self) -> int: """The number of comments in this collection.""" return len(self._comments_elm.comment_lst) + def get(self, comment_id: int) -> Comment | None: + """Return the comment identified by `comment_id`, or |None| if not found.""" + comment_elm = self._comments_elm.get_comment_by_id(comment_id) + return Comment(comment_elm, self._comments_part) if comment_elm is not None else None + class Comment(BlockItemContainer): """Proxy for a single comment in the document. diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index 1e818ebfb..c5d84bc31 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -19,6 +19,11 @@ class CT_Comments(BaseOxmlElement): comment = ZeroOrMore("w:comment") + def get_comment_by_id(self, comment_id: int) -> CT_Comment | None: + """Return the `w:comment` element identified by `comment_id`, or |None| if not found.""" + comment_elms = self.xpath(f"(./w:comment[@w:id='{comment_id}'])[1]") + return comment_elms[0] if comment_elms else None + class CT_Comment(BaseOxmlElement): """`w:comment` element, representing a single comment. diff --git a/tests/test_comments.py b/tests/test_comments.py index b38e429f9..a32f7acbf 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Unit test suite for the docx.comments module.""" from __future__ import annotations @@ -63,6 +65,26 @@ def it_is_iterable_over_the_comments_it_contains(self, package_: Mock): with pytest.raises(StopIteration): next(comment_iter) + def it_can_get_a_comment_by_id(self, package_: Mock): + comments_elm = cast( + CT_Comments, + element("w:comments/(w:comment{w:id=1},w:comment{w:id=2},w:comment{w:id=3})"), + ) + comments = Comments( + comments_elm, + CommentsPart( + PackURI("/word/comments.xml"), + CT.WML_COMMENTS, + comments_elm, + package_, + ), + ) + + comment = comments.get(2) + + assert type(comment) is Comment, "expected a `Comment` object" + assert comment._comment_elm is comments_elm.comment_lst[1] + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From 0eeaa2f0760b61374fef5f2912f63f7ded4bcaeb Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:49:31 -0700 Subject: [PATCH 115/131] comments: add Comment.comment_id --- features/doc-comments.feature | 1 - src/docx/comments.py | 5 +++++ src/docx/oxml/comments.py | 5 ++++- tests/test_comments.py | 18 +++++++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/features/doc-comments.feature b/features/doc-comments.feature index fbe2fd278..944146e5e 100644 --- a/features/doc-comments.feature +++ b/features/doc-comments.feature @@ -30,7 +30,6 @@ Feature: Document.comments Then iterating comments yields 4 Comment objects - @wip Scenario: Comments.get() Given a Comments object with 4 comments When I call comments.get(2) diff --git a/src/docx/comments.py b/src/docx/comments.py index 4a3da9dae..d3f58343f 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -52,3 +52,8 @@ class Comment(BlockItemContainer): def __init__(self, comment_elm: CT_Comment, comments_part: CommentsPart): super().__init__(comment_elm, comments_part) self._comment_elm = comment_elm + + @property + def comment_id(self) -> int: + """The unique identifier of this comment.""" + return self._comment_elm.id diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index c5d84bc31..a24e1dba2 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -2,7 +2,8 @@ from __future__ import annotations -from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore +from docx.oxml.simpletypes import ST_DecimalNumber +from docx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore class CT_Comments(BaseOxmlElement): @@ -32,3 +33,5 @@ class CT_Comment(BaseOxmlElement): While probably most often used for a single sentence or phrase, a comment can contain rich content, including multiple rich-text paragraphs, hyperlinks, images, and tables. """ + + id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType] diff --git a/tests/test_comments.py b/tests/test_comments.py index a32f7acbf..8f9fd473f 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -11,7 +11,7 @@ from docx.comments import Comment, Comments from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.packuri import PackURI -from docx.oxml.comments import CT_Comments +from docx.oxml.comments import CT_Comment, CT_Comments from docx.package import Package from docx.parts.comments import CommentsPart @@ -90,3 +90,19 @@ def it_can_get_a_comment_by_id(self, package_: Mock): @pytest.fixture def package_(self, request: FixtureRequest): return instance_mock(request, Package) + + +class DescribeComment: + """Unit-test suite for `docx.comments.Comment`.""" + + def it_knows_its_comment_id(self, comments_part_: Mock): + comment_elm = cast(CT_Comment, element("w:comment{w:id=42}")) + comment = Comment(comment_elm, comments_part_) + + assert comment.comment_id == 42 + + # -- fixtures -------------------------------------------------------------------------------- + + @pytest.fixture + def comments_part_(self, request: FixtureRequest): + return instance_mock(request, CommentsPart) From 7cf36d648fb18b13979ea7df631724d0dda1bc2c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 22:57:11 -0700 Subject: [PATCH 116/131] xfail: acceptance test for Comment properties --- features/cmt-props.feature | 40 +++++++++++++++++++++++++ features/steps/comments.py | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 features/cmt-props.feature diff --git a/features/cmt-props.feature b/features/cmt-props.feature new file mode 100644 index 000000000..6eead5aa7 --- /dev/null +++ b/features/cmt-props.feature @@ -0,0 +1,40 @@ +Feature: Get comment properties + In order to characterize comments by their metadata + As a developer using python-docx + I need methods to access comment metadata properties + + + Scenario: Comment.id + Given a Comment object + Then comment.comment_id is the comment identifier + + + @wip + Scenario: Comment.author + Given a Comment object + Then comment.author is the author of the comment + + + @wip + Scenario: Comment.initials + Given a Comment object + Then comment.initials is the initials of the comment author + + + @wip + Scenario: Comment.timestamp + Given a Comment object + Then comment.timestamp is the date and time the comment was authored + + + @wip + Scenario: Comment.paragraphs[0].text + Given a Comment object + When I assign para_text = comment.paragraphs[0].text + Then para_text is the text of the first paragraph in the comment + + + @wip + Scenario: Retrieve embedded image from a comment + Given a Comment object containing an embedded image + Then I can extract the image from the comment diff --git a/features/steps/comments.py b/features/steps/comments.py index 81993aeda..14c7d3359 100644 --- a/features/steps/comments.py +++ b/features/steps/comments.py @@ -1,16 +1,29 @@ """Step implementations for document comments-related features.""" +import datetime as dt + from behave import given, then, when from behave.runner import Context from docx import Document from docx.comments import Comment, Comments +from docx.drawing import Drawing from helpers import test_docx # given ==================================================== +@given("a Comment object") +def given_a_comment_object(context: Context): + context.comment = Document(test_docx("comments-rich-para")).comments.get(0) + + +@given("a Comment object containing an embedded image") +def given_a_comment_object_containing_an_embedded_image(context: Context): + context.comment = Document(test_docx("comments-rich-para")).comments.get(1) + + @given("a Comments object with {count} comments") def given_a_comments_object_with_count_comments(context: Context, count: str): testfile_name = {"0": "doc-default", "4": "comments-rich-para"}[count] @@ -30,6 +43,11 @@ def given_a_document_having_no_comments_part(context: Context): # when ===================================================== +@when("I assign para_text = comment.paragraphs[0].text") +def when_I_assign_para_text(context: Context): + context.para_text = context.comment.paragraphs[0].text + + @when("I call comments.get(2)") def when_I_call_comments_get_2(context: Context): context.comment = context.comments.get(2) @@ -38,12 +56,48 @@ def when_I_call_comments_get_2(context: Context): # then ===================================================== +@then("comment.author is the author of the comment") +def then_comment_author_is_the_author_of_the_comment(context: Context): + actual = context.comment.author + assert actual == "Steve Canny", f"expected author 'Steve Canny', got '{actual}'" + + +@then("comment.comment_id is the comment identifier") +def then_comment_comment_id_is_the_comment_identifier(context: Context): + assert context.comment.comment_id == 0 + + +@then("comment.initials is the initials of the comment author") +def then_comment_initials_is_the_initials_of_the_comment_author(context: Context): + initials = context.comment.initials + assert initials == "SJC", f"expected initials 'SJC', got '{initials}'" + + +@then("comment.timestamp is the date and time the comment was authored") +def then_comment_timestamp_is_the_date_and_time_the_comment_was_authored(context: Context): + assert context.comment.timestamp == dt.datetime(2025, 6, 7, 11, 20, 0, tzinfo=dt.timezone.utc) + + @then("document.comments is a Comments object") def then_document_comments_is_a_Comments_object(context: Context): document = context.document assert type(document.comments) is Comments +@then("I can extract the image from the comment") +def then_I_can_extract_the_image_from_the_comment(context: Context): + paragraph = context.comment.paragraphs[0] + run = paragraph.runs[2] + drawing = next(d for d in run.iter_inner_content() if isinstance(d, Drawing)) + assert drawing.has_picture + + image = drawing.image + + assert image.content_type == "image/jpeg", f"got {image.content_type}" + assert image.filename == "image.jpg", f"got {image.filename}" + assert image.sha1 == "1be010ea47803b00e140b852765cdf84f491da47", f"got {image.sha1}" + + @then("iterating comments yields {count} Comment objects") def then_iterating_comments_yields_count_comments(context: Context, count: str): comment_iter = iter(context.comments) @@ -62,6 +116,13 @@ def then_len_comments_eq_count(context: Context, count: str): assert actual == expected, f"expected len(comments) of {expected}, got {actual}" +@then("para_text is the text of the first paragraph in the comment") +def then_para_text_is_the_text_of_the_first_paragraph_in_the_comment(context: Context): + actual = context.para_text + expected = "Text with hyperlink https://google.com embedded." + assert actual == expected, f"expected para_text '{expected}', got '{actual}'" + + @then("the result is a Comment object with id 2") def then_the_result_is_a_comment_object_with_id_2(context: Context): comment = context.comment From 8af46fe57a84299da4ffc3e1528eecc35accca92 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:05:07 -0700 Subject: [PATCH 117/131] comments: add Comment.author --- features/cmt-props.feature | 1 - src/docx/comments.py | 5 +++++ src/docx/oxml/comments.py | 3 ++- tests/test_comments.py | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/features/cmt-props.feature b/features/cmt-props.feature index 6eead5aa7..95fe17746 100644 --- a/features/cmt-props.feature +++ b/features/cmt-props.feature @@ -9,7 +9,6 @@ Feature: Get comment properties Then comment.comment_id is the comment identifier - @wip Scenario: Comment.author Given a Comment object Then comment.author is the author of the comment diff --git a/src/docx/comments.py b/src/docx/comments.py index d3f58343f..a107f7b0b 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -53,6 +53,11 @@ def __init__(self, comment_elm: CT_Comment, comments_part: CommentsPart): super().__init__(comment_elm, comments_part) self._comment_elm = comment_elm + @property + def author(self) -> str: + """The recorded author of this comment.""" + return self._comment_elm.author + @property def comment_id(self) -> int: """The unique identifier of this comment.""" diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index a24e1dba2..1aa71add5 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -2,7 +2,7 @@ from __future__ import annotations -from docx.oxml.simpletypes import ST_DecimalNumber +from docx.oxml.simpletypes import ST_DecimalNumber, ST_String from docx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore @@ -35,3 +35,4 @@ class CT_Comment(BaseOxmlElement): """ id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType] + author: str = RequiredAttribute("w:author", ST_String) # pyright: ignore[reportAssignmentType] diff --git a/tests/test_comments.py b/tests/test_comments.py index 8f9fd473f..7b0e3588c 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -101,6 +101,12 @@ def it_knows_its_comment_id(self, comments_part_: Mock): assert comment.comment_id == 42 + def it_knows_its_author(self, comments_part_: Mock): + comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:author=Steve Canny}")) + comment = Comment(comment_elm, comments_part_) + + assert comment.author == "Steve Canny" + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From cab50c5e65da92e31f64401f535efa0d9d0d8e84 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:06:01 -0700 Subject: [PATCH 118/131] comments: add Comment.initials --- features/cmt-props.feature | 1 - src/docx/comments.py | 9 +++++++++ src/docx/oxml/comments.py | 5 ++++- tests/test_comments.py | 6 ++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/features/cmt-props.feature b/features/cmt-props.feature index 95fe17746..f1a7fbc4c 100644 --- a/features/cmt-props.feature +++ b/features/cmt-props.feature @@ -14,7 +14,6 @@ Feature: Get comment properties Then comment.author is the author of the comment - @wip Scenario: Comment.initials Given a Comment object Then comment.initials is the initials of the comment author diff --git a/src/docx/comments.py b/src/docx/comments.py index a107f7b0b..cc1a86161 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -62,3 +62,12 @@ def author(self) -> str: def comment_id(self) -> int: """The unique identifier of this comment.""" return self._comment_elm.id + + @property + def initials(self) -> str | None: + """Read/write. The recorded initials of the comment author. + + This attribute is optional in the XML, returns |None| if not set. Assigning |None| removes + any existing initials from the XML. + """ + return self._comment_elm.initials diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index 1aa71add5..b841cdfe9 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -3,7 +3,7 @@ from __future__ import annotations from docx.oxml.simpletypes import ST_DecimalNumber, ST_String -from docx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore +from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore class CT_Comments(BaseOxmlElement): @@ -36,3 +36,6 @@ class CT_Comment(BaseOxmlElement): id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType] author: str = RequiredAttribute("w:author", ST_String) # pyright: ignore[reportAssignmentType] + initials: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:initials", ST_String + ) diff --git a/tests/test_comments.py b/tests/test_comments.py index 7b0e3588c..9e4f64d68 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -107,6 +107,12 @@ def it_knows_its_author(self, comments_part_: Mock): assert comment.author == "Steve Canny" + def it_knows_the_initials_of_its_author(self, comments_part_: Mock): + comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:initials=SJC}")) + comment = Comment(comment_elm, comments_part_) + + assert comment.initials == "SJC" + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From cfb87e7708f561f09e6d4f3e7289c659c226eafb Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:06:54 -0700 Subject: [PATCH 119/131] comments: add Comment.timestamp --- features/cmt-props.feature | 1 - src/docx/comments.py | 9 ++++++ src/docx/oxml/comments.py | 7 ++++- src/docx/oxml/simpletypes.py | 53 ++++++++++++++++++++++++++++++++++++ tests/test_comments.py | 10 +++++++ 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/features/cmt-props.feature b/features/cmt-props.feature index f1a7fbc4c..ab5450dfa 100644 --- a/features/cmt-props.feature +++ b/features/cmt-props.feature @@ -19,7 +19,6 @@ Feature: Get comment properties Then comment.initials is the initials of the comment author - @wip Scenario: Comment.timestamp Given a Comment object Then comment.timestamp is the date and time the comment was authored diff --git a/src/docx/comments.py b/src/docx/comments.py index cc1a86161..e5d25fd79 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -2,6 +2,7 @@ from __future__ import annotations +import datetime as dt from typing import TYPE_CHECKING, Iterator from docx.blkcntnr import BlockItemContainer @@ -71,3 +72,11 @@ def initials(self) -> str | None: any existing initials from the XML. """ return self._comment_elm.initials + + @property + def timestamp(self) -> dt.datetime | None: + """The date and time this comment was authored. + + This attribute is optional in the XML, returns |None| if not set. + """ + return self._comment_elm.date diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index b841cdfe9..612a51f8a 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -2,7 +2,9 @@ from __future__ import annotations -from docx.oxml.simpletypes import ST_DecimalNumber, ST_String +import datetime as dt + +from docx.oxml.simpletypes import ST_DateTime, ST_DecimalNumber, ST_String from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore @@ -39,3 +41,6 @@ class CT_Comment(BaseOxmlElement): initials: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:initials", ST_String ) + date: dt.datetime | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w:date", ST_DateTime + ) diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index 69d4b65d4..a0fc87d3f 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -9,6 +9,7 @@ from __future__ import annotations +import datetime as dt from typing import TYPE_CHECKING, Any, Tuple from docx.exceptions import InvalidXmlError @@ -213,6 +214,58 @@ def validate(cls, value: Any) -> None: cls.validate_int_in_range(value, -27273042329600, 27273042316900) +class ST_DateTime(BaseSimpleType): + @classmethod + def convert_from_xml(cls, str_value: str) -> dt.datetime: + """Convert an xsd:dateTime string to a datetime object.""" + + def parse_xsd_datetime(dt_str: str) -> dt.datetime: + # -- handle trailing 'Z' (Zulu/UTC), common in Word files -- + if dt_str.endswith("Z"): + try: + # -- optional fractional seconds case -- + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace( + tzinfo=dt.timezone.utc + ) + except ValueError: + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=dt.timezone.utc + ) + + # -- handles explicit offsets like +00:00, -05:00, or naive datetimes -- + try: + return dt.datetime.fromisoformat(dt_str) + except ValueError: + # -- fall-back to parsing as naive datetime (with or without fractional seconds) -- + try: + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f") + except ValueError: + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S") + + try: + # -- parse anything reasonable, but never raise, just use default epoch time -- + return parse_xsd_datetime(str_value) + except Exception: + return dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) + + @classmethod + def convert_to_xml(cls, value: dt.datetime) -> str: + # -- convert naive datetime to timezon-aware assuming local timezone -- + if value.tzinfo is None: + value = value.astimezone() + + # -- convert to UTC if not already -- + value = value.astimezone(dt.timezone.utc) + + # -- format with 'Z' suffix for UTC -- + return value.strftime("%Y-%m-%dT%H:%M:%SZ") + + @classmethod + def validate(cls, value: Any) -> None: + if not isinstance(value, dt.datetime): + raise TypeError("only a datetime.datetime object may be assigned, got '%s'" % value) + + class ST_DecimalNumber(XsdInt): pass diff --git a/tests/test_comments.py b/tests/test_comments.py index 9e4f64d68..ea9e97c96 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -4,6 +4,7 @@ from __future__ import annotations +import datetime as dt from typing import cast import pytest @@ -113,6 +114,15 @@ def it_knows_the_initials_of_its_author(self, comments_part_: Mock): assert comment.initials == "SJC" + def it_knows_the_date_and_time_it_was_authored(self, comments_part_: Mock): + comment_elm = cast( + CT_Comment, + element("w:comment{w:id=42,w:date=2023-10-01T12:34:56Z}"), + ) + comment = Comment(comment_elm, comments_part_) + + assert comment.timestamp == dt.datetime(2023, 10, 1, 12, 34, 56, tzinfo=dt.timezone.utc) + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From 19175adf57d91f314e95fa72972c6065f72b4dff Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:09:32 -0700 Subject: [PATCH 120/131] comments: add Comment.paragraphs Actual implementation is primarily inherited from `BlockItemContainer`, but support for those operations must be present in `CT_Comment` and it's worth testing explicitly. --- features/cmt-props.feature | 1 - src/docx/oxml/comments.py | 23 +++++++++++++++++++++++ tests/test_comments.py | 12 ++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/features/cmt-props.feature b/features/cmt-props.feature index ab5450dfa..f5c636196 100644 --- a/features/cmt-props.feature +++ b/features/cmt-props.feature @@ -24,7 +24,6 @@ Feature: Get comment properties Then comment.timestamp is the date and time the comment was authored - @wip Scenario: Comment.paragraphs[0].text Given a Comment object When I assign para_text = comment.paragraphs[0].text diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index 612a51f8a..0ebd7e200 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -3,10 +3,15 @@ from __future__ import annotations import datetime as dt +from typing import TYPE_CHECKING, Callable from docx.oxml.simpletypes import ST_DateTime, ST_DecimalNumber, ST_String from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore +if TYPE_CHECKING: + from docx.oxml.table import CT_Tbl + from docx.oxml.text.paragraph import CT_P + class CT_Comments(BaseOxmlElement): """`w:comments` element, the root element for the comments part. @@ -36,6 +41,7 @@ class CT_Comment(BaseOxmlElement): content, including multiple rich-text paragraphs, hyperlinks, images, and tables. """ + # -- attributes on `w:comment` -- id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType] author: str = RequiredAttribute("w:author", ST_String) # pyright: ignore[reportAssignmentType] initials: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] @@ -44,3 +50,20 @@ class CT_Comment(BaseOxmlElement): date: dt.datetime | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "w:date", ST_DateTime ) + + # -- children -- + + p = ZeroOrMore("w:p", successors=()) + tbl = ZeroOrMore("w:tbl", successors=()) + + # -- type-declarations for methods added by metaclass -- + + add_p: Callable[[], CT_P] + p_lst: list[CT_P] + tbl_lst: list[CT_Tbl] + _insert_tbl: Callable[[CT_Tbl], CT_Tbl] + + @property + def inner_content_elements(self) -> list[CT_P | CT_Tbl]: + """Generate all `w:p` and `w:tbl` elements in this comment.""" + return self.xpath("./w:p | ./w:tbl") diff --git a/tests/test_comments.py b/tests/test_comments.py index ea9e97c96..2a0615a79 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -123,6 +123,18 @@ def it_knows_the_date_and_time_it_was_authored(self, comments_part_: Mock): assert comment.timestamp == dt.datetime(2023, 10, 1, 12, 34, 56, tzinfo=dt.timezone.utc) + def it_provides_access_to_the_paragraphs_it_contains(self, comments_part_: Mock): + comment_elm = cast( + CT_Comment, + element('w:comment{w:id=42}/(w:p/w:r/w:t"First para",w:p/w:r/w:t"Second para")'), + ) + comment = Comment(comment_elm, comments_part_) + + paragraphs = comment.paragraphs + + assert len(paragraphs) == 2 + assert [para.text for para in paragraphs] == ["First para", "Second para"] + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From 432dd15eb343476024167a3ffec39e2b86d5585c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:11:42 -0700 Subject: [PATCH 121/131] drawing: add image extraction from Drawing --- features/cmt-props.feature | 1 - src/docx/drawing/__init__.py | 39 +++++++++++++++++++ tests/test_comments.py | 4 +- tests/test_drawing.py | 74 ++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/test_drawing.py diff --git a/features/cmt-props.feature b/features/cmt-props.feature index f5c636196..e4e620828 100644 --- a/features/cmt-props.feature +++ b/features/cmt-props.feature @@ -30,7 +30,6 @@ Feature: Get comment properties Then para_text is the text of the first paragraph in the comment - @wip Scenario: Retrieve embedded image from a comment Given a Comment object containing an embedded image Then I can extract the image from the comment diff --git a/src/docx/drawing/__init__.py b/src/docx/drawing/__init__.py index f40205747..00d1f51bb 100644 --- a/src/docx/drawing/__init__.py +++ b/src/docx/drawing/__init__.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: import docx.types as t + from docx.image.image import Image class Drawing(Parented): @@ -18,3 +19,41 @@ def __init__(self, drawing: CT_Drawing, parent: t.ProvidesStoryPart): super().__init__(parent) self._parent = parent self._drawing = self._element = drawing + + @property + def has_picture(self) -> bool: + """True when `drawing` contains an embedded picture. + + A drawing can contain a picture, but it can also contain a chart, SmartArt, or a + drawing canvas. Methods related to a picture, like `.image`, will raise when the drawing + does not contain a picture. Use this value to determine whether image methods will succeed. + + This value is `False` when a linked picture is present. This should be relatively rare and + the image would only be retrievable from the filesystem. + + Note this does not distinguish between inline and floating images. The presence of either + one will cause this value to be `True`. + """ + xpath_expr = ( + # -- an inline picture -- + "./wp:inline/a:graphic/a:graphicData/pic:pic" + # -- a floating picture -- + " | ./wp:anchor/a:graphic/a:graphicData/pic:pic" + ) + # -- xpath() will return a list, empty if there are no matches -- + return bool(self._drawing.xpath(xpath_expr)) + + @property + def image(self) -> Image: + """An `Image` proxy object for the image in this (picture) drawing. + + Raises `ValueError` when this drawing does contains something other than a picture. Use + `.has_picture` to qualify drawing objects before using this property. + """ + picture_rIds = self._drawing.xpath(".//pic:blipFill/a:blip/@r:embed") + if not picture_rIds: + raise ValueError("drawing does not contain a picture") + rId = picture_rIds[0] + doc_part = self.part + image_part = doc_part.related_parts[rId] + return image_part.image diff --git a/tests/test_comments.py b/tests/test_comments.py index 2a0615a79..a4be3dbb4 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,6 +1,6 @@ # pyright: reportPrivateUsage=false -"""Unit test suite for the docx.comments module.""" +"""Unit test suite for the `docx.comments` module.""" from __future__ import annotations @@ -21,7 +21,7 @@ class DescribeComments: - """Unit-test suite for `docx.comments.Comments`.""" + """Unit-test suite for `docx.comments.Comments` objects.""" @pytest.mark.parametrize( ("cxml", "count"), diff --git a/tests/test_drawing.py b/tests/test_drawing.py new file mode 100644 index 000000000..c8fedb1a4 --- /dev/null +++ b/tests/test_drawing.py @@ -0,0 +1,74 @@ +# pyright: reportPrivateUsage=false + +"""Unit test suite for the `docx.drawing` module.""" + +from __future__ import annotations + +from typing import cast + +import pytest + +from docx.drawing import Drawing +from docx.image.image import Image +from docx.oxml.drawing import CT_Drawing +from docx.parts.document import DocumentPart +from docx.parts.image import ImagePart + +from .unitutil.cxml import element +from .unitutil.mock import FixtureRequest, Mock, instance_mock + + +class DescribeDrawing: + """Unit-test suite for `docx.drawing.Drawing` objects.""" + + @pytest.mark.parametrize( + ("cxml", "expected_value"), + [ + ("w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic", True), + ("w:drawing/wp:anchor/a:graphic/a:graphicData/pic:pic", True), + ("w:drawing/wp:inline/a:graphic/a:graphicData/a:grpSp", False), + ("w:drawing/wp:anchor/a:graphic/a:graphicData/a:chart", False), + ], + ) + def it_knows_when_it_contains_a_Picture( + self, cxml: str, expected_value: bool, document_part_: Mock + ): + drawing = Drawing(cast(CT_Drawing, element(cxml)), document_part_) + assert drawing.has_picture == expected_value + + def it_provides_access_to_the_image_in_a_Picture_drawing( + self, document_part_: Mock, image_part_: Mock, image_: Mock + ): + image_part_.image = image_ + document_part_.part.related_parts = {"rId1": image_part_} + cxml = ( + "w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip{r:embed=rId1}" + ) + drawing = Drawing(cast(CT_Drawing, element(cxml)), document_part_) + + image = drawing.image + + assert image is image_ + + def but_it_raises_when_the_drawing_does_not_contain_a_Picture(self, document_part_: Mock): + drawing = Drawing( + cast(CT_Drawing, element("w:drawing/wp:inline/a:graphic/a:graphicData/a:grpSp")), + document_part_, + ) + + with pytest.raises(ValueError, match="drawing does not contain a picture"): + drawing.image + + # -- fixtures -------------------------------------------------------------------------------- + + @pytest.fixture + def document_part_(self, request: FixtureRequest): + return instance_mock(request, DocumentPart) + + @pytest.fixture + def image_(self, request: FixtureRequest): + return instance_mock(request, Image) + + @pytest.fixture + def image_part_(self, request: FixtureRequest): + return instance_mock(request, ImagePart) From d360409273a9fdfd2d6a26a7f35b8f3bfc781f04 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:20:17 -0700 Subject: [PATCH 122/131] xfail: acceptance test for Comment mutations --- features/cmt-mutations.feature | 66 +++++++++ features/steps/comments.py | 131 ++++++++++++++++++ .../steps/test_files/comments-rich-para.docx | Bin 19974 -> 20023 bytes 3 files changed, 197 insertions(+) create mode 100644 features/cmt-mutations.feature diff --git a/features/cmt-mutations.feature b/features/cmt-mutations.feature new file mode 100644 index 000000000..634e7c1bc --- /dev/null +++ b/features/cmt-mutations.feature @@ -0,0 +1,66 @@ +Feature: Comment mutations + In order to add and modify the content of a comment + As a developer using python-docx + I need mutation methods on Comment objects + + + @wip + Scenario: Comments.add_comment() + Given a Comments object with 0 comments + When I assign comment = comments.add_comment() + Then comment.comment_id == 0 + And len(comment.paragraphs) == 1 + And comment.paragraphs[0].style.name == "CommentText" + And len(comments) == 1 + And comments.get(0) == comment + + + @wip + Scenario: Comments.add_comment() specifying author and initials + Given a Comments object with 0 comments + When I assign comment = comments.add_comment(author="John Doe", initials="JD") + Then comment.author == "John Doe" + And comment.initials == "JD" + + + @wip + Scenario: Comment.add_paragraph() specifying text and style + Given a default Comment object + When I assign paragraph = comment.add_paragraph(text, style) + Then len(comment.paragraphs) == 2 + And paragraph.text == text + And paragraph.style == style + And comment.paragraphs[-1] == paragraph + + + @wip + Scenario: Comment.add_paragraph() not specifying text or style + Given a default Comment object + When I assign paragraph = comment.add_paragraph() + Then len(comment.paragraphs) == 2 + And paragraph.text == "" + And paragraph.style == "CommentText" + And comment.paragraphs[-1] == paragraph + + + @wip + Scenario: Add image to comment + Given a default Comment object + When I assign paragraph = comment.add_paragraph() + And I assign run = paragraph.add_run() + And I call run.add_picture() + Then run.iter_inner_content() yields a single Picture drawing + + + @wip + Scenario: update Comment.author + Given a Comment object + When I assign "Jane Smith" to comment.author + Then comment.author == "Jane Smith" + + + @wip + Scenario: update Comment.initials + Given a Comment object + When I assign "JS" to comment.initials + Then comment.initials == "JS" diff --git a/features/steps/comments.py b/features/steps/comments.py index 14c7d3359..2bca6d5a6 100644 --- a/features/steps/comments.py +++ b/features/steps/comments.py @@ -30,6 +30,11 @@ def given_a_comments_object_with_count_comments(context: Context, count: str): context.comments = Document(test_docx(testfile_name)).comments +@given("a default Comment object") +def given_a_default_comment_object(context: Context): + context.comment = Document(test_docx("comments-rich-para")).comments.add_comment() + + @given("a document having a comments part") def given_a_document_having_a_comments_part(context: Context): context.document = Document(test_docx("comments-rich-para")) @@ -43,11 +48,48 @@ def given_a_document_having_no_comments_part(context: Context): # when ===================================================== +@when('I assign "{author}" to comment.author') +def when_I_assign_author_to_comment_author(context: Context, author: str): + context.comment.author = author + + +@when("I assign comment = comments.add_comment()") +def when_I_assign_comment_eq_add_comment(context: Context): + context.comment = context.comments.add_comment() + + +@when('I assign comment = comments.add_comment(author="John Doe", initials="JD")') +def when_I_assign_comment_eq_comments_add_comment_with_author_and_initials(context: Context): + context.comment = context.comments.add_comment(author="John Doe", initials="JD") + + +@when('I assign "{initials}" to comment.initials') +def when_I_assign_initials(context: Context, initials: str): + context.comment.initials = initials + + @when("I assign para_text = comment.paragraphs[0].text") def when_I_assign_para_text(context: Context): context.para_text = context.comment.paragraphs[0].text +@when("I assign paragraph = comment.add_paragraph()") +def when_I_assign_default_add_paragraph(context: Context): + context.paragraph = context.comment.add_paragraph() + + +@when("I assign paragraph = comment.add_paragraph(text, style)") +def when_I_assign_add_paragraph_with_text_and_style(context: Context): + context.para_text = text = "Comment text" + context.para_style = style = "Normal" + context.paragraph = context.comment.add_paragraph(text, style) + + +@when("I assign run = paragraph.add_run()") +def when_I_assign_paragraph_add_run(context: Context): + context.run = context.paragraph.add_run() + + @when("I call comments.get(2)") def when_I_call_comments_get_2(context: Context): context.comment = context.comments.get(2) @@ -62,6 +104,17 @@ def then_comment_author_is_the_author_of_the_comment(context: Context): assert actual == "Steve Canny", f"expected author 'Steve Canny', got '{actual}'" +@then('comment.author == "{author}"') +def then_comment_author_eq_author(context: Context, author: str): + actual = context.comment.author + assert actual == author, f"expected author '{author}', got '{actual}'" + + +@then("comment.comment_id == 0") +def then_comment_id_is_0(context: Context): + assert context.comment.comment_id == 0 + + @then("comment.comment_id is the comment identifier") def then_comment_comment_id_is_the_comment_identifier(context: Context): assert context.comment.comment_id == 0 @@ -73,11 +126,42 @@ def then_comment_initials_is_the_initials_of_the_comment_author(context: Context assert initials == "SJC", f"expected initials 'SJC', got '{initials}'" +@then('comment.initials == "{initials}"') +def then_comment_initials_eq_initials(context: Context, initials: str): + actual = context.comment.initials + assert actual == initials, f"expected initials '{initials}', got '{actual}'" + + +@then("comment.paragraphs[{idx}] == paragraph") +def then_comment_paragraphs_idx_eq_paragraph(context: Context, idx: str): + actual = context.comment.paragraphs[int(idx)]._p + expected = context.paragraph._p + assert actual == expected, "paragraphs do not compare equal" + + +@then('comment.paragraphs[{idx}].style.name == "{style}"') +def then_comment_paragraphs_idx_style_name_eq_style(context: Context, idx: str, style: str): + actual = context.comment.paragraphs[int(idx)]._p.style + expected = style + assert actual == expected, f"expected style name '{expected}', got '{actual}'" + + @then("comment.timestamp is the date and time the comment was authored") def then_comment_timestamp_is_the_date_and_time_the_comment_was_authored(context: Context): assert context.comment.timestamp == dt.datetime(2025, 6, 7, 11, 20, 0, tzinfo=dt.timezone.utc) +@then("comments.get({id}) == comment") +def then_comments_get_comment_id_eq_comment(context: Context, id: str): + comment_id = int(id) + comment = context.comments.get(comment_id) + + assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}" + assert comment.comment_id == comment_id, ( + f"expected comment_id '{comment_id}', got '{comment.comment_id}'" + ) + + @then("document.comments is a Comments object") def then_document_comments_is_a_Comments_object(context: Context): document = context.document @@ -109,6 +193,13 @@ def then_iterating_comments_yields_count_comments(context: Context, count: str): assert len(remaining) == int(count) - 1, "iterating comments did not yield the expected count" +@then("len(comment.paragraphs) == {count}") +def then_len_comment_paragraphs_eq_count(context: Context, count: str): + actual = len(context.comment.paragraphs) + expected = int(count) + assert actual == expected, f"expected len(comment.paragraphs) of {expected}, got {actual}" + + @then("len(comments) == {count}") def then_len_comments_eq_count(context: Context, count: str): actual = len(context.comments) @@ -123,6 +214,46 @@ def then_para_text_is_the_text_of_the_first_paragraph_in_the_comment(context: Co assert actual == expected, f"expected para_text '{expected}', got '{actual}'" +@then("paragraph.style == style") +def then_paragraph_style_eq_known_style(context: Context): + actual = context.paragraph.style.name + expected = context.para_style + assert actual == expected, f"expected paragraph.style '{expected}', got '{actual}'" + + +@then('paragraph.style == "{style}"') +def then_paragraph_style_eq_style(context: Context, style: str): + actual = context.paragraph._p.style + expected = style + assert actual == expected, f"expected paragraph.style '{expected}', got '{actual}'" + + +@then("paragraph.text == text") +def then_paragraph_text_eq_known_text(context: Context): + actual = context.paragraph.text + expected = context.para_text + assert actual == expected, f"expected paragraph.text '{expected}', got '{actual}'" + + +@then('paragraph.text == ""') +def then_paragraph_text_eq_text(context: Context): + actual = context.paragraph.text + expected = "" + assert actual == expected, f"expected paragraph.text '{expected}', got '{actual}'" + + +@then("run.iter_inner_content() yields a single Picture drawing") +def then_run_iter_inner_content_yields_a_single_picture_drawing(context: Context): + inner_content = list(context.run.iter_inner_content()) + + assert len(inner_content) == 1, ( + f"expected a single inner content element, got {len(inner_content)}" + ) + inner_content_item = inner_content[0] + assert isinstance(inner_content_item, Drawing) + assert inner_content_item.has_picture + + @then("the result is a Comment object with id 2") def then_the_result_is_a_comment_object_with_id_2(context: Context): comment = context.comment diff --git a/features/steps/test_files/comments-rich-para.docx b/features/steps/test_files/comments-rich-para.docx index e63db413e871466da97e906aef754bc06b2f9601..245c17224c22d0a6d72958be917d9ef1a899bad6 100644 GIT binary patch delta 5354 zcmZu#bzGEf)}Eo1kPwh&=yH&j5|FMTrKLkkq@?4aQvn$yhZN}$M7leryOAzo=>G8C zeRub_`~7j^zRtPNb^f}a=bR7(xC#OOW`v7t>Cvy3fT)RHKnH>7DByOult6&0qSYU9 zamNOM(N4f15E>B(1lrS8vi~B7-%wg{2X=Hj=QRGnL^h2-_=ZnUSN^oT3b*1BMaFBT z*{B~gca7uTQ^q)*KlQbeB`GuN47D44H4UbW8hshcDmtK%a-JNvZUxZgF=p|&lHY4$ zMkMsFetRvnRM0l@cnk|VQn@Y$G7JBF+0 z+3|?j%4WalPyNN^0mIAVMNrxpvn5(i{ zjCK6qUIk1sl>y#)$}`RHx7bt+#n%WN!mb$aflXwP+v5LLT&3e1srs!tpHggZfB8kxsMaJ$K(9%#sDwBbs@u)%>Q_`Nx9gRHpC4iych^k(e9xEqAmW(bhREa(E zlR)^9Pv=i@3hKu!Y#fyKWoR2fElD(LJ6KKI#W`D#-UV4;9aSP!aG5~*aU@KZC4|P% z*A4raGQG7RM3O*K#m}C7-C6PMjngergur0>%*-4oj^fhGKEfA)^aX-iU8k!D-EB`R z!acmqbpbDpXRo34=ApCKV6WvXQaOi8md1p6x_xa2ajhsjmY|Jb(htdv#~!W0PKXb5kjh|ggh^!wGB8YTM*2lut4Iel1eGmxWxly9 z9I6fW!r~RTC$&I)6XDH3Vq%V$K$DE`)T)>kfG=|^Ye5A(<~Lf!bH-bgD$?C}bdub1 zpZI8ZNF(d5IwvBO^Ml<)k{&BBIN&cWm87)l^GFM^hI$L78|sNP)H>M|{ctYe$>haT zw_GP52pX}B%ol#%^)U$fB;Eox$f)6LuIqKolevva{569PDvhc4OHt58us04$Mq3y; z48#reh6}nV*rUjeOx8O^7G5lGUoCIf(~@5+>)m%53WPD=3@AagqX+++b-Re-E`tVq6b=xJqwS4X6R~>El(riU4y5*3_tHj>B9KO{8C%wgWOn- zSoW`fvZB@tU!Lkld$yk9%Z`NnNJ;A(s#t3LhfE0VGuEHnLxw`=ZGM&KxybUB;-9piytOUhP;T@~ zoJzJ!jtt4ulzi2-AgIPK>!eo4({{S+t2Vk`$RV?`L3smw*%ac>U5>A7M2kO)&^V`x}wz|`R`9h>a5djEpfCc|hL#=Q!Cz$@s054YXN~g)O+q^!)}1SU!QAT`4a{k$z8ACUcgOpOcF7{a zFHVSPlow@gJ?uYQ>{Kx`=QphoC>r(gMUVG?cB|r56Uk@(69`h4K^Q7J~EN9a73K)dD=nYdxXNvSP=rE6LSQk7K!ooIe zm#Xi;=>!pBihau0e^WZ%5yv>f^4WW65|CcT<=ny`MQI)*5$Er$Fh}IYVKA@FbHcIU zqSDuvgOS%&kbSeHZ_;^k1vW2ST+%SSq`ABlsMB)M9?RVUu^iJ({pvn)5ZF67(%7R& zsP?8xz=8c2^n`k`e2ip8nOI}(3Fha+0ck`;noyf=vO#tLUz+|Jz?*hB+AnZzh?eE1 zJ}7)sbvcdqB<(E3%>7;b%wPz#vGgbMe7vegSNYeF#G2=VSx@0bu)n*Tsg~4*QA4=~ z_0C=yoy9FscauCJ-4iFc9xOY~agO|Q3~}ci91A}(Y|^H@K3#rJz^x|w;&+3`D{91% z+*}yVQrTgfJ0r@iPscBNRy@}(pkcz04e#8Eu&_xs7rs!^ZbGhqb9^CmmV2qxkDR$f zFk>I_Qj|Rn2)Qnlz^(jj_N7Rmua3?B8=hw%+M^@jeZJ$(!0LdZ9Zt;jN5kiJhkbAO zJ*IbaI8x@f`rf#mmQOc$B<)I39;65Rn7r}xp&VE-VHhw~V;ER@d2c!IE#%(fF$`q+ zG7Qk||Ez#rKHnpkA=5=T;WYTXaLTO06MJ@iI@}q&AX;5LG~4x<#Pe!sS;8K|s=v7h zprXbb!Y~`~db1p5R)@aE=|Xw-Ev}h~;T;iqUz8MVL=TWb2jxA+(&@U&IAMvRqjy!d z))HKO-AJo(xS~DlK0l^P__Kg6F?W{9IWzH$yw#~7%anF?S$Adz?;b+xE5ris*rsax zTqOT+0Ibi}RRy-{cmN&ObHuDmEPez`F}+^HAT-wsOH7w5*CnRkw-b$sE5LP*JM$$B zLByRWsmCVE5u;m^YT{>`XmG`Zs0F{2sTsiIvy;#0akt#nv31u)GHb?}2ni zc!B6|sVB&r>EVnW&n;*it3j0z$r;zHS40K0rTT5B?{r)D2`ddfE)4u806{wLsxxkP z42BzyGpfML_*d{6wtkb*2*n4Z_W{A`SQb$RyMuj)z1a4Zxg=K3QGDE}g~T;|B_ zW57Cej)>5Ot*QKj!YAP){YVDJN>v^N>@5Ar`ts3Mkc+lyfy928M(5n`-)Wh7jEP0< zna2>8y6co&J{66a%a`qSK)eYH#N*{as%1E|!rGOl&7=!=l})cyhfN){E%YcqZT>*C zjBl86Aq3VQXXkaoW=eOQw;{mqmM-e)=9C(`>=CSHy4VMW6L8h&f4=E+Ydn8yVZ!14 zwTF3Rr-{ZOWVAbphGGRP4M)zyrWa}|E|6opEp|RCQO=NIzQ9xuxbL$4%AOO9oS-ch z*8F{y=lQ&x43TpSXQVR%zWi9Dq8-ucR^pFbtX3KS>>E$Lj5U{_lrtH8LX@`M<4+!E zL-#6BUCH!)WT6SGw#f9_WQIn9QQ(N-E!5nat%!e-D!+nQRV_UiBM)UImQ6AV%1`Uk zp{28uT>PzF^J$wBK$Lszl99ivnUO#E3Uzg5M5CNhP3gvq4rL{0L~h3QUS#g&$w;@Q z6?Fk~9}dI#FAI#=ZL;qu@)6y=e_`VV=?YT1lFcc7B4iUDJy4%-kkdy0P8h?KP*XaTLsCiBfI!lZ z6cRZ#;04%hF{h5SUFUY?EOwy&IL`fZ7!>*n@ju?%uQl(H{Nk-V`=jb(5Ia&tWMw!8 zYgi}9mEhedfB1QYfj{4s2T5c4A|zmMsw31@fkbkKbw1VF(xoadb$qzcRkhB#2??RI ztG{v>BP5+=_Gz}_-JkNhtNldgE@e7(!Z(Bm*hrA(&e>*vtX)_yIdey8W%t2eMif;RRnRWk^26XQGFtRe9to^Q%OI%t;O2|SvOpna z%Uzo63)+X)z8gtVR6(3#egcV*@|g9|f1&~n?THX|&h=2D`U_fYd*5z}WJmz;@AyNB z@?(0JW{-!hW1vzqxnbQLQs`Kjckt9~>HDzImk5Ho^%ts%uW4OgRO9(#y)dv7=1 z+X4>44~CIiq*^GhH0s!{8Idd*){a>Yd`+o=gy^dRVJOY|(qR9nEWei|YRx62%vjwm z;jJ!K-<&C>z{Vx_u&mn4`%6op$oupqDX^e`S`3V`h*SF>{=z~tBu(V79r3Tw%cwvW zP`@S-{w537!}kQ}IxfJ=2oxuWEI&#F3r&|e5on$5K0t(yRv9T#YFg77hk?dZ%o}UQ zge&9JKV-}x+ZBh=#+t(`pv=YiRx=@SC%)>K=+n#?8w85n(U%mt{7?gn z58J5qiz`97+}ACrc+Q6xIf!*!L`O@P>7f4SXRk=o?PW~sZ%BzCVjL}(q4!UKXj0wE z`r1*ih)f^6vm@^8&h6_^27USB8y(Rg&IK$i`_mfd`znTg6qgvdy(iY)lO0Wbk?8U@d&QHR(9=e zr#+|FJ6}?LvD{$bxVT19S74aU1{@uDa{GR{B1n}x2A^nX+Q`jlQEuUBufe1}6Xd&bE? zcOd~%%``s=8GaR)jxF-GkU#I~5OcY-&Sa6$0qxKEQ=R?2*ZL|4@bMJG8{l1M+X3P1 zEQSz#C%#?!L2H8UVV(Hh?h*${2Xkz+F_Y*1D8>KYtPcf^~>fxOqwR7FJuc{4`<(@O-Jnxpf99W;n9A4w&xJ66+Yp0*OMnERH!6&CN!_tG?@;Q{odl+ zRE3i8DlS9evwgK`GVXM$T_NDc&zJv(X<&SgGuWf?a(_alnzyw=tR}htR3>Wt$wnw* z>k`?gW)0u+>4~S)uq?$$m`q4`Z^s2!yQR9CH!B=a8m^XJ0Ibb+JAb$MAA0$bH>Fr+ zTQKhYmi#eMy6~*DZ)^uTN^eq4{SXS$TUIAu9zVp0QQWu4$ zS5Y@YyDZL%eF9azkUS(Fman02P=n^t_~gu|LATNqwN8~UGdpkWY&&z?dUkK{MdTtbfh);P zR|4IQ=hnrudy5waA$OoZQy&os<9^x$foMS-i*yCzC?L?*cO}_C)c;-+E-0{}-meJZ z*os16U$~5-JU9g&qbP}jpo5puk-&Eq*}$1_5|}6$3$6tdLm}mavvX3wk74-mRv0(gGeu2awEZh?;5D1qF1OhD>t0$L;P;}HaUt@V`qHjOMRX=5q9CfQxKvW`6 z8-;~MBHce^c%>#8Tr9P?J&c5rr{?##kioI$PRvezSDOuoo}H(Z4T77P7|%Lf=C`V& z&@egW&UVA=&^%Hz{l_AMkJ3_Umk@xdS*0aI_~qJAAF*=Aa_&7M>7OwzMavyv9}P)$ zj_&)8m90amu#_myUv!*bKii~48_iMNbuRbVM7?}Uxl$Z)8Jbq7U1Z?7toW=szKHZb zv9v0&?I)3|8Qc4YFJ@+qe2~)Xq|y{c&b~#lhO(mBht)WC3%Ixzez4s+SQ0L zsD75wu4mIFIwd28prWTzf$?-ZVa>w++01w1ZyjGc&k^se!d7_Hu_wb-W`GQ8r$1bkQ;eHw| zPA1-ybk%&SwH&^hQ|SzIJ?4BU81Ws;v%9pS^ya4B;~yF$b${$w?Sji^94_>h#@9Bc zP_I&`7fM(PWJ6{*u2{Rcr58khX5TIFS!Yq8QjxhEtT~j+dz=xaSjdP6*cE=hqcw=u zllxLN&dEO5X|5%dFXFt(FGO#0lqIblv>f<~y7d}eb3cg!UrtweFLIyccmiP^SiZk? zZfPSla3{ZzDk|`7foFn6D#n~EnJG3OwDelNB8f&#Pn1YLcS!qNmPy7M0j*O|v7@J4 zOWgT5-CitqyEGy#z;XQ*P~>9T#y8iIR)=1f7Cr86d0f9zqa9p#&T@M!Fp}oj#ArGj zG|PxRgTog+giWMyG3~uaquDCrRaeN4EB3et&%yPLR691!AnS4iCFHTgV>3?5go0a( zJa(w#8C^R9la~fK#mt1 z_WM)I>`N;R2V202-1H^IFl|4WK{XmP+|GCG!z`29vt`RNFivSX7&D~3ZZ}F`Yz8fN zAtM{EG7P%&+`D5CP(={1$dVXe59nur)UgLpxK~_$)%;ma8b)VF1u)nzT znvKzt@6CNh((Y$pm+lvPQI6i{JV+xkifOmE?G2#p{KinW3B1VfGQGU*- zvFzS2S`!x#vLv-L@wy)8Q%a-F0_*Bi0j{=#VjQXAAm0rUiA$s3E^(ox8^BM!2dWW2 zWk<%jrap(lqM5rca#rj&e8oUE@YTdlr_A55hY)8ON9FANS%Lv-PuW3^=N75!N3Do8 znEw)SxVL;qIUw;JPzxyDvGox-u@2;+(qjC5P3DU;OZ3bOl6y8n7Anxiw{f5706T5nz! z=K++ctL(#O#jM}Yf7>z>i8`n-^49L?ob@E%F7Y#lmRHOf#(Vw5ujLnV#An!k^v%;f z%c#Vg7^r%M|)lm75y(UtVK)r{5)H8(j0q3%7}qw1}OE)^l=L zKi+;K*Q>YbAyd6|%THjDDDaY8OBLtlP2Utvb;S&)=OqAo`t&r@NI@WUBnU))^92Zi zc-!&#_y)Pz`)~)kyY1$g`^>$+9eW@?dHrERKP1JdkxP7lwt)Yg+qZmMajBF^Gg)T4 zt`KQUPW7YV&7OJ#-+kC2KtrRC<=lEJ4GWU~^^< z;UT{#z<8$ESz0wCx?wCsbi@}jud0M;HVG#PYGlUCX~?36gk1D2Pe|J~a#KkaG{N%G zp6TPwT9^=;S9mEEXHi_}1k9HhOm~yh>m%5D8?fn1nb{s)@B4~|4qL0c8@dr{5lWRe z!KK4)Ed{RbN)5!hzY4klc~UhJ^ox9fN?JR6dMETiIn=H)#=kv>MO)S~=;o&wb1K<5 zVNW2XBiFi6R$U1{;sXOUHteYHwf)Xoa6B)$D!oGfbWXwNu~edeig^fr6i;~P5HTs> z2;eltrM8QV&^XVx!2p4MLUi{`Xdjc%3|w*en#*4b{>!c&FmiwW`R>%aUgoej?vy*S`;ZGMA~S}0lKS+TT$}I{ZZvhK4ABE*Ire{77&87C)5A~ zZE||oip2Xs(k&eqrCs%xv>oEk6hn}@ne`uozb&cVO8egogg<|&#{ALgK~Z+H%&uzU z{H8m<&)eyM6RxR9vc(i(-xm1i#}@cHSp`84GZc>BQZY%86axDz zMpNs!csb2RT=MQuj>x=bFDJ|fDaV=-P>Sa<2n zOwFlpezrWIoa|sBwuPnZAS8MFv>b?$3tOaPR4Ls@2Qlu{MU1@=4KcGXWCi=TdNi&hLZm$3C?H%$t>OIJEuzUT0YEG3Dm}9M zn1_U<;0(;9juz0#UU&W=Ff!SiezfH}|L4RxyTWU-`DzvOA;G9D&}W)&JAB`R(kDo& zJwUvCmyg8;YaDn#v7z=NBsHZU%6TtR0+@#iUO**BX_6PC@ELq&vwymp*U8v11&@5u zzs9PW2z}`~f@ZE09D@TNzP4XcBB_V$+N6;v-7z)8ref{pgem1_UCO#aJa^!2N-+@a zj_WzlIG;n3-9XRuOM~07Ogzl4Q=Q{N*sW)q2eJ3&=i4CgR#7@1dBN|}Wkd_hh@3HI zD#v$VHLGj!(GHndnMbcv zPl)CnNMoiCVtQ%Ms`@%pRPQLf0>ZD+_?di9s${K~1MfsGH`UIVTTH@9^=I$B6hPul zEY(2)vn{F}#sg?e-u!P=kfNt|HNhB7zW|9tZ->pDYOM{1X&po3i=Gop)+x`+OoG82 zn_$?{PEnJf+4bmG&rUJ%m&*D4JDQcF5|#@!ft?p~UB4zJ=+SPS374c+c*jCf)Z=p$ zsKE5nwlR8+{C+Zy{DZ4t5}IDLr;5hWRZ7F>9Db_dOn~hM9^5Q)s$m4qxn+^R@v^4c-FTtMu#z#g z(cP@ls-%|AVC&NIMQCuK@!|1FReR zS_EC>-6wK|F|)R1R*HquM8kQ}M30?{WN=L?DQ+@m5zEm;dtNdz7;c#us(VE;M1RF^ z6hycGq9wV8Ab>Ww79$>)Uc59hHPg6^{!TCbjYOnKCgetvkocdu7=mUkM(jUSLh3AW zLTa6HnZH9*CSH2P!^-pjAs`ST@-2d}fecn(H#Ay{tYE+Id-e^{s z^pu|e)5iamy1CSn!CTk3>EZ9{PKlbAcgpsjUfjNVQ*n}Tx&O?3OCgLOvL>XBzo4y; z*nP8q%9QJMMJ2Ep7+ARd%7a2Ek;bs1lIs~F$IMR|cROyZ4vrBIRSO=AiIc@<5M&d+!*5ZRB(^?KjhDhRRjSug^l(}Z4y!*AW?2d-A zX*mdMn@=~f`dWX@QU5470;_LfnqqC&CEnXdFn6%NHWv2{f)Q(HJO552_3Hjj*k4et ztqsh*|L>HGlBc)ehgKxGIMwdsZr6vSp^~m%# zr^?7QJNbk4R{1RJ_#7CiteLkE30*Zy&uoLqQ-Y*v%47XCAG5c1Vk0+wNQxIB9_->b47rTWUm-F~{tdgeefp~Ee}O;nS4&>gItawykh(D4 z>QXiEA0WwQ$8pp1O?3JO2KHUd-cJ4eFR=3ytTl1@H#8!_hFpSe{{{c!!Xt(deMnFV zILkCL9iFzga-{MQeYswaUYdliON5teN9=Des_y0Kqwa5;d#+r>Y*vQ4wRt6L>q))S z$CxRZ#gr%2YljRRq z&+nP2ssT;Nf?$rkar`W@$w1tV)I_}HyGFLW+l28TAd79{$qk*I?5cb)OCet$^~$6_z5a6!#d?_HHJgT2`1Y)`Iix>h?d(%V>-fq%d#$M2?jE;YzN>T7 z;ES3k9J*zKo_;^~U5cE>IJsAqetYE&iiA%2Q0HqGwGEwc zn**oHe7%OZfPs|ngiAry_wPi&3_`g4_Y6JTn(O^z`$u0(St|--mkqx0p3rS2@wE>` z?HD^EpXW%fS$&`ue#7DZf&i`{@sXy649~D%nh%p^3pk zDwD98yL7i&{1%S*6`U=3I9X9`CLH{G?|Pn#W3pWJ z%ERVE6>~+Up9xz` zHoUxE6UneeSu0dg>3+2m5HdA8KOtF_;O z8LVtLubLDV4jiKP4C^+$PE7&Z<0(Al>0LOf`V*{vxQM#s?I|G;$PQww0C@OU#n{(bm~{?{n^hYdea2V)7ri8LM&DdK`aRmy*;{{V_vOcVeB From 8ac9fc4f6b50b9b7f208974e853f1995d63a834a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:24:17 -0700 Subject: [PATCH 123/131] comments: add Comments.add_comment() Only with `text` parameter so far. Author and initials parameters to follow. --- features/cmt-mutations.feature | 5 --- src/docx/comments.py | 60 ++++++++++++++++++++++++++ src/docx/oxml/comments.py | 57 ++++++++++++++++++++++++- tests/oxml/test_comments.py | 31 ++++++++++++++ tests/test_comments.py | 78 ++++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 tests/oxml/test_comments.py diff --git a/features/cmt-mutations.feature b/features/cmt-mutations.feature index 634e7c1bc..6fda8810b 100644 --- a/features/cmt-mutations.feature +++ b/features/cmt-mutations.feature @@ -4,7 +4,6 @@ Feature: Comment mutations I need mutation methods on Comment objects - @wip Scenario: Comments.add_comment() Given a Comments object with 0 comments When I assign comment = comments.add_comment() @@ -15,7 +14,6 @@ Feature: Comment mutations And comments.get(0) == comment - @wip Scenario: Comments.add_comment() specifying author and initials Given a Comments object with 0 comments When I assign comment = comments.add_comment(author="John Doe", initials="JD") @@ -23,7 +21,6 @@ Feature: Comment mutations And comment.initials == "JD" - @wip Scenario: Comment.add_paragraph() specifying text and style Given a default Comment object When I assign paragraph = comment.add_paragraph(text, style) @@ -33,7 +30,6 @@ Feature: Comment mutations And comment.paragraphs[-1] == paragraph - @wip Scenario: Comment.add_paragraph() not specifying text or style Given a default Comment object When I assign paragraph = comment.add_paragraph() @@ -43,7 +39,6 @@ Feature: Comment mutations And comment.paragraphs[-1] == paragraph - @wip Scenario: Add image to comment Given a default Comment object When I assign paragraph = comment.add_paragraph() diff --git a/src/docx/comments.py b/src/docx/comments.py index e5d25fd79..7fd39d54a 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: from docx.oxml.comments import CT_Comment, CT_Comments from docx.parts.comments import CommentsPart + from docx.styles.style import ParagraphStyle + from docx.text.paragraph import Paragraph class Comments: @@ -30,6 +32,48 @@ def __len__(self) -> int: """The number of comments in this collection.""" return len(self._comments_elm.comment_lst) + def add_comment(self, text: str = "", author: str = "", initials: str | None = "") -> Comment: + """Add a new comment to the document and return it. + + The comment is added to the end of the comments collection and is assigned a unique + comment-id. + + If `text` is provided, it is added to the comment. This option provides for the common + case where a comment contains a modest passage of plain text. Multiple paragraphs can be + added using the `text` argument by separating their text with newlines (`"\\\\n"`). + Between newlines, text is interpreted as it is in `Document.add_paragraph(text=...)`. + + The default is to place a single empty paragraph in the comment, which is the same + behavior as the Word UI when you add a comment. New runs can be added to the first + paragraph in the empty comment with `comments.paragraphs[0].add_run()` to adding more + complex text with emphasis or images. Additional paragraphs can be added using + `.add_paragraph()`. + + `author` is a required attribute, set to the empty string by default. + + `initials` is an optional attribute, set to the empty string by default. Passing |None| + for the `initials` parameter causes that attribute to be omitted from the XML. + """ + comment_elm = self._comments_elm.add_comment() + comment_elm.author = author + comment_elm.initials = initials + comment_elm.date = dt.datetime.now(dt.timezone.utc) + comment = Comment(comment_elm, self._comments_part) + + if text == "": + return comment + + para_text_iter = iter(text.split("\n")) + + first_para_text = next(para_text_iter) + first_para = comment.paragraphs[0] + first_para.add_run(first_para_text) + + for s in para_text_iter: + comment.add_paragraph(text=s) + + return comment + def get(self, comment_id: int) -> Comment | None: """Return the comment identified by `comment_id`, or |None| if not found.""" comment_elm = self._comments_elm.get_comment_by_id(comment_id) @@ -54,6 +98,22 @@ def __init__(self, comment_elm: CT_Comment, comments_part: CommentsPart): super().__init__(comment_elm, comments_part) self._comment_elm = comment_elm + def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = None) -> Paragraph: + """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`. + When `style` is |None| or ommitted, the "CommentText" paragraph style is applied, which is + the default style for comments. + """ + paragraph = super().add_paragraph(text, style) + + # -- have to assign style directly to element because `paragraph.style` raises when + # -- a style is not present in the styles part + if style is None: + paragraph._p.style = "CommentText" # pyright: ignore[reportPrivateUsage] + + return paragraph + @property def author(self) -> str: """The recorded author of this comment.""" diff --git a/src/docx/oxml/comments.py b/src/docx/oxml/comments.py index 0ebd7e200..ad9821759 100644 --- a/src/docx/oxml/comments.py +++ b/src/docx/oxml/comments.py @@ -3,8 +3,10 @@ from __future__ import annotations import datetime as dt -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, cast +from docx.oxml.ns import nsdecls +from docx.oxml.parser import parse_xml from docx.oxml.simpletypes import ST_DateTime, ST_DecimalNumber, ST_String from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore @@ -27,11 +29,64 @@ class CT_Comments(BaseOxmlElement): comment = ZeroOrMore("w:comment") + def add_comment(self) -> CT_Comment: + """Return newly added `w:comment` child of this `w:comments`. + + The returned `w:comment` element is the minimum valid value, having a `w:id` value unique + within the existing comments and the required `w:author` attribute present but set to the + empty string. It's content is limited to a single run containing the necessary annotation + reference but no text. Content is added by adding runs to this first paragraph and by + adding additional paragraphs as needed. + """ + next_id = self._next_available_comment_id() + comment = cast( + CT_Comment, + parse_xml( + f'' + f" " + f" " + f' ' + f" " + f" " + f" " + f' ' + f" " + f" " + f" " + f" " + f"" + ), + ) + self.append(comment) + return comment + def get_comment_by_id(self, comment_id: int) -> CT_Comment | None: """Return the `w:comment` element identified by `comment_id`, or |None| if not found.""" comment_elms = self.xpath(f"(./w:comment[@w:id='{comment_id}'])[1]") return comment_elms[0] if comment_elms else None + def _next_available_comment_id(self) -> int: + """The next available comment id. + + According to the schema, this can be any positive integer, as big as you like, and the + default mechanism is to use `max() + 1`. However, if that yields a value larger than will + fit in a 32-bit signed integer, we take a more deliberate approach to use the first + ununsed integer starting from 0. + """ + used_ids = [int(x) for x in self.xpath("./w:comment/@w:id")] + + next_id = max(used_ids, default=-1) + 1 + + if next_id <= 2**31 - 1: + return next_id + + # -- fall-back to enumerating all used ids to find the first unused one -- + for expected, actual in enumerate(sorted(used_ids)): + if expected != actual: + return expected + + return len(used_ids) + class CT_Comment(BaseOxmlElement): """`w:comment` element, representing a single comment. diff --git a/tests/oxml/test_comments.py b/tests/oxml/test_comments.py new file mode 100644 index 000000000..8fc116144 --- /dev/null +++ b/tests/oxml/test_comments.py @@ -0,0 +1,31 @@ +# pyright: reportPrivateUsage=false + +"""Unit-test suite for `docx.oxml.comments` module.""" + +from __future__ import annotations + +from typing import cast + +import pytest + +from docx.oxml.comments import CT_Comments + +from ..unitutil.cxml import element + + +class DescribeCT_Comments: + """Unit-test suite for `docx.oxml.comments.CT_Comments`.""" + + @pytest.mark.parametrize( + ("cxml", "expected_value"), + [ + ("w:comments", 0), + ("w:comments/(w:comment{w:id=1})", 2), + ("w:comments/(w:comment{w:id=4},w:comment{w:id=2147483646})", 2147483647), + ("w:comments/(w:comment{w:id=1},w:comment{w:id=2147483647})", 0), + ("w:comments/(w:comment{w:id=1},w:comment{w:id=2},w:comment{w:id=3})", 4), + ], + ) + def it_finds_the_next_available_comment_id_to_help(self, cxml: str, expected_value: int): + comments_elm = cast(CT_Comments, element(cxml)) + assert comments_elm._next_available_comment_id() == expected_value diff --git a/tests/test_comments.py b/tests/test_comments.py index a4be3dbb4..8f5be2d1e 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -13,6 +13,7 @@ from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.packuri import PackURI from docx.oxml.comments import CT_Comment, CT_Comments +from docx.oxml.ns import qn from docx.package import Package from docx.parts.comments import CommentsPart @@ -86,8 +87,85 @@ def it_can_get_a_comment_by_id(self, package_: Mock): assert type(comment) is Comment, "expected a `Comment` object" assert comment._comment_elm is comments_elm.comment_lst[1] + def but_it_returns_None_when_no_comment_with_that_id_exists(self, package_: Mock): + comments_elm = cast( + CT_Comments, + element("w:comments/(w:comment{w:id=1},w:comment{w:id=2},w:comment{w:id=3})"), + ) + comments = Comments( + comments_elm, + CommentsPart( + PackURI("/word/comments.xml"), + CT.WML_COMMENTS, + comments_elm, + package_, + ), + ) + + comment = comments.get(4) + + assert comment is None, "expected None when no comment with that id exists" + + def it_can_add_a_new_comment(self, package_: Mock): + comments_elm = cast(CT_Comments, element("w:comments")) + comments_part = CommentsPart( + PackURI("/word/comments.xml"), + CT.WML_COMMENTS, + comments_elm, + package_, + ) + now_before = dt.datetime.now(dt.timezone.utc).replace(microsecond=0) + comments = Comments(comments_elm, comments_part) + + comment = comments.add_comment() + + now_after = dt.datetime.now(dt.timezone.utc).replace(microsecond=0) + # -- a comment is unconditionally added, and returned for any further adjustment -- + assert isinstance(comment, Comment) + # -- it is "linked" to the comments part so it can add images and hyperlinks, etc. -- + assert comment.part is comments_part + # -- comment numbering starts at 0, and is incremented for each new comment -- + assert comment.comment_id == 0 + # -- author is a required attribut, but is the empty string by default -- + assert comment.author == "" + # -- initials is an optional attribute, but defaults to the empty string, same as Word -- + assert comment.initials == "" + # -- timestamp is also optional, but defaults to now-UTC -- + assert comment.timestamp is not None + assert now_before <= comment.timestamp <= now_after + # -- by default, a new comment contains a single empty paragraph -- + assert [p.text for p in comment.paragraphs] == [""] + # -- that paragraph has the "CommentText" style, same as Word applies -- + comment_elm = comment._comment_elm + assert len(comment_elm.p_lst) == 1 + p = comment_elm.p_lst[0] + assert p.style == "CommentText" + # -- and that paragraph contains a single run with the necessary annotation reference -- + assert len(p.r_lst) == 1 + r = comment_elm.p_lst[0].r_lst[0] + assert r.style == "CommentReference" + assert r[-1].tag == qn("w:annotationRef") + + def and_it_can_add_text_to_the_comment_when_adding_it(self, comments: Comments, package_: Mock): + comment = comments.add_comment(text="para 1\n\npara 2") + + assert len(comment.paragraphs) == 3 + assert [p.text for p in comment.paragraphs] == ["para 1", "", "para 2"] + assert all(p._p.style == "CommentText" for p in comment.paragraphs) + # -- fixtures -------------------------------------------------------------------------------- + @pytest.fixture + def comments(self, package_: Mock) -> Comments: + comments_elm = cast(CT_Comments, element("w:comments")) + comments_part = CommentsPart( + PackURI("/word/comments.xml"), + CT.WML_COMMENTS, + comments_elm, + package_, + ) + return Comments(comments_elm, comments_part) + @pytest.fixture def package_(self, request: FixtureRequest): return instance_mock(request, Package) From 761f4ccd7751afeeaa5fff5c6f47325c3e0970fa Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 23:26:14 -0700 Subject: [PATCH 124/131] comments: add Comment.author, .initials setters - allow setting on construction - allow update with property setters --- features/cmt-mutations.feature | 2 -- src/docx/comments.py | 13 ++++++++++++- src/docx/shared.py | 2 +- tests/test_comments.py | 35 ++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/features/cmt-mutations.feature b/features/cmt-mutations.feature index 6fda8810b..1ef9ad2db 100644 --- a/features/cmt-mutations.feature +++ b/features/cmt-mutations.feature @@ -47,14 +47,12 @@ Feature: Comment mutations Then run.iter_inner_content() yields a single Picture drawing - @wip Scenario: update Comment.author Given a Comment object When I assign "Jane Smith" to comment.author Then comment.author == "Jane Smith" - @wip Scenario: update Comment.initials Given a Comment object When I assign "JS" to comment.initials diff --git a/src/docx/comments.py b/src/docx/comments.py index 7fd39d54a..f0b359ee7 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -116,9 +116,16 @@ def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = Non @property def author(self) -> str: - """The recorded author of this comment.""" + """Read/write. The recorded author of this comment. + + This field is required but can be set to the empty string. + """ return self._comment_elm.author + @author.setter + def author(self, value: str): + self._comment_elm.author = value + @property def comment_id(self) -> int: """The unique identifier of this comment.""" @@ -133,6 +140,10 @@ def initials(self) -> str | None: """ return self._comment_elm.initials + @initials.setter + def initials(self, value: str | None): + self._comment_elm.initials = value + @property def timestamp(self) -> dt.datetime | None: """The date and time this comment was authored. diff --git a/src/docx/shared.py b/src/docx/shared.py index 1d561227b..6c12dc91e 100644 --- a/src/docx/shared.py +++ b/src/docx/shared.py @@ -328,7 +328,7 @@ def __init__(self, parent: t.ProvidesXmlPart): self._parent = parent @property - def part(self): + def part(self) -> XmlPart: """The package part containing this object.""" return self._parent.part diff --git a/tests/test_comments.py b/tests/test_comments.py index 8f5be2d1e..bdc38af9a 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -153,6 +153,14 @@ def and_it_can_add_text_to_the_comment_when_adding_it(self, comments: Comments, assert [p.text for p in comment.paragraphs] == ["para 1", "", "para 2"] assert all(p._p.style == "CommentText" for p in comment.paragraphs) + def and_it_sets_the_author_and_their_initials_when_adding_a_comment_when_provided( + self, comments: Comments, package_: Mock + ): + comment = comments.add_comment(author="Steve Canny", initials="SJC") + + assert comment.author == "Steve Canny" + assert comment.initials == "SJC" + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture @@ -213,6 +221,33 @@ def it_provides_access_to_the_paragraphs_it_contains(self, comments_part_: Mock) assert len(paragraphs) == 2 assert [para.text for para in paragraphs] == ["First para", "Second para"] + def it_can_update_the_comment_author(self, comments_part_: Mock): + comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:author=Old Author}")) + comment = Comment(comment_elm, comments_part_) + + comment.author = "New Author" + + assert comment.author == "New Author" + + @pytest.mark.parametrize( + "initials", + [ + # -- valid initials -- + "XYZ", + # -- empty string is valid + "", + # -- None is valid, removes existing initials + None, + ], + ) + def it_can_update_the_comment_initials(self, initials: str | None, comments_part_: Mock): + comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:initials=ABC}")) + comment = Comment(comment_elm, comments_part_) + + comment.initials = initials + + assert comment.initials == initials + # -- fixtures -------------------------------------------------------------------------------- @pytest.fixture From 66da52204db395466cc7ea033af0f5bffd228953 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 20:46:35 -0700 Subject: [PATCH 125/131] xfail: acceptance test for Document.add_comment() --- features/doc-add-comment.feature | 14 ++++++++++++++ features/steps/comments.py | 31 +++++++++++++++++++++++++++---- features/steps/settings.py | 17 +++++++++++------ 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 features/doc-add-comment.feature diff --git a/features/doc-add-comment.feature b/features/doc-add-comment.feature new file mode 100644 index 000000000..73560044a --- /dev/null +++ b/features/doc-add-comment.feature @@ -0,0 +1,14 @@ +Feature: Add a comment to a document + In order add a comment to a document + As a developer using python-docx + I need a way to add a comment specifying both its content and its reference + + + @wip + Scenario: Document.add_comment(runs, text, author, initials) + Given a document having a comments part + When I assign comment = document.add_comment(runs, "A comment", "John Doe", "JD") + Then comment is a Comment object + And comment.text == "A comment" + And comment.author == "John Doe" + And comment.initials == "JD" diff --git a/features/steps/comments.py b/features/steps/comments.py index 2bca6d5a6..39680f257 100644 --- a/features/steps/comments.py +++ b/features/steps/comments.py @@ -63,6 +63,17 @@ def when_I_assign_comment_eq_comments_add_comment_with_author_and_initials(conte context.comment = context.comments.add_comment(author="John Doe", initials="JD") +@when('I assign comment = document.add_comment(runs, "A comment", "John Doe", "JD")') +def when_I_assign_comment_eq_document_add_comment(context: Context): + runs = list(context.document.paragraphs[0].runs) + context.comment = context.document.add_comment( + runs=runs, + text="A comment", + author="John Doe", + initials="JD", + ) + + @when('I assign "{initials}" to comment.initials') def when_I_assign_initials(context: Context, initials: str): context.comment.initials = initials @@ -98,10 +109,9 @@ def when_I_call_comments_get_2(context: Context): # then ===================================================== -@then("comment.author is the author of the comment") -def then_comment_author_is_the_author_of_the_comment(context: Context): - actual = context.comment.author - assert actual == "Steve Canny", f"expected author 'Steve Canny', got '{actual}'" +@then("comment is a Comment object") +def then_comment_is_a_Comment_object(context: Context): + assert type(context.comment) is Comment @then('comment.author == "{author}"') @@ -110,6 +120,12 @@ def then_comment_author_eq_author(context: Context, author: str): assert actual == author, f"expected author '{author}', got '{actual}'" +@then("comment.author is the author of the comment") +def then_comment_author_is_the_author_of_the_comment(context: Context): + actual = context.comment.author + assert actual == "Steve Canny", f"expected author 'Steve Canny', got '{actual}'" + + @then("comment.comment_id == 0") def then_comment_id_is_0(context: Context): assert context.comment.comment_id == 0 @@ -146,6 +162,13 @@ def then_comment_paragraphs_idx_style_name_eq_style(context: Context, idx: str, assert actual == expected, f"expected style name '{expected}', got '{actual}'" +@then('comment.text == "{text}"') +def then_comment_text_eq_text(context: Context, text: str): + actual = context.comment.text + expected = text + assert actual == expected, f"expected text '{expected}', got '{actual}'" + + @then("comment.timestamp is the date and time the comment was authored") def then_comment_timestamp_is_the_date_and_time_the_comment_was_authored(context: Context): assert context.comment.timestamp == dt.datetime(2025, 6, 7, 11, 20, 0, tzinfo=dt.timezone.utc) diff --git a/features/steps/settings.py b/features/steps/settings.py index 1b03661eb..882f5ded3 100644 --- a/features/steps/settings.py +++ b/features/steps/settings.py @@ -1,6 +1,7 @@ """Step implementations for document settings-related features.""" from behave import given, then, when +from behave.runner import Context from docx import Document from docx.settings import Settings @@ -11,17 +12,19 @@ @given("a document having a settings part") -def given_a_document_having_a_settings_part(context): +def given_a_document_having_a_settings_part(context: Context): context.document = Document(test_docx("doc-word-default-blank")) @given("a document having no settings part") -def given_a_document_having_no_settings_part(context): +def given_a_document_having_no_settings_part(context: Context): context.document = Document(test_docx("set-no-settings-part")) @given("a Settings object {with_or_without} odd and even page headers as settings") -def given_a_Settings_object_with_or_without_odd_and_even_hdrs(context, with_or_without): +def given_a_Settings_object_with_or_without_odd_and_even_hdrs( + context: Context, with_or_without: str +): testfile_name = {"with": "doc-odd-even-hdrs", "without": "sct-section-props"}[with_or_without] context.settings = Document(test_docx(testfile_name)).settings @@ -30,7 +33,9 @@ def given_a_Settings_object_with_or_without_odd_and_even_hdrs(context, with_or_w @when("I assign {bool_val} to settings.odd_and_even_pages_header_footer") -def when_I_assign_value_to_settings_odd_and_even_pages_header_footer(context, bool_val): +def when_I_assign_value_to_settings_odd_and_even_pages_header_footer( + context: Context, bool_val: str +): context.settings.odd_and_even_pages_header_footer = eval(bool_val) @@ -38,13 +43,13 @@ def when_I_assign_value_to_settings_odd_and_even_pages_header_footer(context, bo @then("document.settings is a Settings object") -def then_document_settings_is_a_Settings_object(context): +def then_document_settings_is_a_Settings_object(context: Context): document = context.document assert type(document.settings) is Settings @then("settings.odd_and_even_pages_header_footer is {bool_val}") -def then_settings_odd_and_even_pages_header_footer_is(context, bool_val): +def then_settings_odd_and_even_pages_header_footer_is(context: Context, bool_val: str): actual = context.settings.odd_and_even_pages_header_footer expected = eval(bool_val) assert actual == expected, "settings.odd_and_even_pages_header_footer is %s" % actual From af3b973dd2c938f6851537978fe76f4f5e91dcc9 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 16:31:54 -0700 Subject: [PATCH 126/131] comments: add Document.add_comment() --- src/docx/document.py | 50 +++++++++++++++++++++++++++++++++++++-- src/docx/oxml/shared.py | 3 +-- src/docx/oxml/xmlchemy.py | 3 +-- src/docx/text/run.py | 7 ++++++ tests/test_document.py | 34 +++++++++++++++++++++++++- 5 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/docx/document.py b/src/docx/document.py index 5de03bf9d..1168c4ae8 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -5,17 +5,18 @@ from __future__ import annotations -from typing import IO, TYPE_CHECKING, Iterator, List +from typing import IO, TYPE_CHECKING, Iterator, List, Sequence 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, Inches, Length +from docx.text.run import Run if TYPE_CHECKING: import docx.types as t - from docx.comments import Comments + from docx.comments import Comment, Comments from docx.oxml.document import CT_Body, CT_Document from docx.parts.document import DocumentPart from docx.settings import Settings @@ -37,6 +38,51 @@ def __init__(self, element: CT_Document, part: DocumentPart): self._part = part self.__body = None + def add_comment( + self, runs: Run | Sequence[Run], text: str, author: str = "", initials: str | None = "" + ) -> Comment: + """Add a comment to the document, anchored to the specified runs. + + `runs` can be a single `Run` object or a non-empty sequence of `Run` objects. Only the + first and last run of a sequence are used, it's just more convenient to pass a whole + sequence when that's what you have handy, like `paragraph.runs` for example. When `runs` + contains a single `Run` object, that run serves as both the first and last run. + + A comment can be anchored only on an even run boundary, meaning the text the comment + "references" must be a non-zero integer number of consecutive runs. The runs need not be + _contiguous_ per se, like the first can be in one paragraph and the last in the next + paragraph, but all runs between the first and the last will be included in the reference. + + The comment reference range is delimited by placing a `w:commentRangeStart` element before + the first run and a `w:commentRangeEnd` element after the last run. This is why only the + first and last run are required and why a single run can serve as both first and last. + Word works out which text to highlight in the UI based on these range markers. + + `text` allows the contents of a simple comment to be provided in the call, providing for + the common case where a comment is a single phrase or sentence without special formatting + such as bold or italics. More complex comments can be added using the returned `Comment` + object in much the same way as a `Document` or (table) `Cell` object, using methods like + `.add_paragraph()`, .add_run()`, etc. + + The `author` and `initials` parameters allow that metadata to be set for the comment. + `author` is a required attribute on a comment and is the empty string by default. + `initials` is optional on a comment and may be omitted by passing |None|, but Word adds an + `initials` attribute by default and we follow that convention by using the empty string + when no `initials` argument is provided. + """ + # -- normalize `runs` to a sequence of runs -- + runs = [runs] if isinstance(runs, Run) else runs + first_run = runs[0] + last_run = runs[-1] + + # -- Note that comments can only appear in the document part -- + comment = self.comments.add_comment(text=text, author=author, initials=initials) + + # -- let the first run orchestrate placement of the comment range start and end -- + first_run.mark_comment_range(last_run, comment.comment_id) + + return comment + def add_heading(self, text: str = "", level: int = 1): """Return a heading paragraph newly added to the end of the document. diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py index 8c2ebc9a9..8cfcd2be1 100644 --- a/src/docx/oxml/shared.py +++ b/src/docx/oxml/shared.py @@ -46,8 +46,7 @@ class CT_String(BaseOxmlElement): @classmethod def new(cls, nsptagname: str, val: str): - """Return a new ``CT_String`` element with tagname `nsptagname` and ``val`` - attribute set to `val`.""" + """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/src/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py index df75ee18c..e2c54b392 100644 --- a/src/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -423,8 +423,7 @@ def _new_method_name(self): 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): diff --git a/src/docx/text/run.py b/src/docx/text/run.py index d35988370..d49876eaf 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -173,6 +173,13 @@ def iter_inner_content(self) -> Iterator[str | Drawing | RenderedPageBreak]: elif isinstance(item, CT_Drawing): # pyright: ignore[reportUnnecessaryIsInstance] yield Drawing(item, self) + def mark_comment_range(self, last_run: Run, comment_id: int) -> None: + """Mark the range of runs from this run to `last_run` (inclusive) as belonging to a comment. + + `comment_id` identfies the comment that references this range. + """ + raise NotImplementedError + @property def style(self) -> CharacterStyle: """Read/write. diff --git a/tests/test_document.py b/tests/test_document.py index 0b36017a5..53efacf8d 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -9,7 +9,7 @@ import pytest -from docx.comments import Comments +from docx.comments import Comment, Comments from docx.document import Document, _Body from docx.enum.section import WD_SECTION from docx.enum.text import WD_BREAK @@ -39,6 +39,26 @@ class DescribeDocument: """Unit-test suite for `docx.document.Document`.""" + def it_can_add_a_comment( + self, + document_part_: Mock, + comments_prop_: Mock, + comments_: Mock, + comment_: Mock, + run_mark_comment_range_: Mock, + ): + comment_.comment_id = 42 + comments_.add_comment.return_value = comment_ + comments_prop_.return_value = comments_ + document = Document(cast(CT_Document, element("w:document/w:body/w:p/w:r")), document_part_) + run = document.paragraphs[0].runs[0] + + comment = document.add_comment(run, "Comment text.") + + comments_.add_comment.assert_called_once_with("Comment text.", "", "") + run_mark_comment_range_.assert_called_once_with(run, run, 42) + assert comment is comment_ + @pytest.mark.parametrize( ("level", "style"), [(0, "Title"), (1, "Heading 1"), (2, "Heading 2"), (9, "Heading 9")] ) @@ -288,10 +308,18 @@ def _block_width_prop_(self, request: FixtureRequest): def body_prop_(self, request: FixtureRequest): return property_mock(request, Document, "_body") + @pytest.fixture + def comment_(self, request: FixtureRequest): + return instance_mock(request, Comment) + @pytest.fixture def comments_(self, request: FixtureRequest): return instance_mock(request, Comments) + @pytest.fixture + def comments_prop_(self, request: FixtureRequest): + return property_mock(request, Document, "comments") + @pytest.fixture def core_properties_(self, request: FixtureRequest): return instance_mock(request, CoreProperties) @@ -325,6 +353,10 @@ def picture_(self, request: FixtureRequest): def run_(self, request: FixtureRequest): return instance_mock(request, Run) + @pytest.fixture + def run_mark_comment_range_(self, request: FixtureRequest): + return method_mock(request, Run, "mark_comment_range") + @pytest.fixture def Section_(self, request: FixtureRequest): return class_mock(request, "docx.document.Section") From e3a321d26195fdd6e368f59b63be06b1277dac14 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 17:51:50 -0700 Subject: [PATCH 127/131] comments: add Run.mark_comment_range() --- src/docx/oxml/text/run.py | 33 ++++++++++++++++++++++++++++++++- src/docx/text/run.py | 7 ++++++- tests/text/test_run.py | 13 +++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py index 88efae83c..7496aa616 100644 --- a/src/docx/oxml/text/run.py +++ b/src/docx/oxml/text/run.py @@ -2,10 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Iterator, List +from typing import TYPE_CHECKING, Callable, Iterator, List, cast from docx.oxml.drawing import CT_Drawing from docx.oxml.ns import qn +from docx.oxml.parser import OxmlElement 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 @@ -87,6 +88,19 @@ def iter_items() -> Iterator[str | CT_Drawing | CT_LastRenderedPageBreak]: return list(iter_items()) + def insert_comment_range_end_and_reference_below(self, comment_id: int) -> None: + """Insert a `w:commentRangeEnd` and `w:commentReference` element after this run. + + The `w:commentRangeEnd` element is the immediate sibling of this `w:r` and is followed by + a `w:r` containing the `w:commentReference` element. + """ + self.addnext(self._new_comment_reference_run(comment_id)) + self.addnext(OxmlElement("w:commentRangeEnd", attrs={qn("w:id"): str(comment_id)})) + + def insert_comment_range_start_above(self, comment_id: int) -> None: + """Insert a `w:commentRangeStart` element with `comment_id` before this run.""" + self.addprevious(OxmlElement("w:commentRangeStart", attrs={qn("w:id"): str(comment_id)})) + @property def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]: """All `w:lastRenderedPageBreaks` descendants of this run.""" @@ -132,6 +146,23 @@ def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr: self.insert(0, rPr) return rPr + def _new_comment_reference_run(self, comment_id: int) -> CT_R: + """Return a new `w:r` element with `w:commentReference` referencing `comment_id`. + + Should look like this: + + + + + + + """ + r = cast(CT_R, OxmlElement("w:r")) + rPr = r.get_or_add_rPr() + rPr.style = "CommentReference" + r.append(OxmlElement("w:commentReference", attrs={qn("w:id"): str(comment_id)})) + return r + # ------------------------------------------------------------------------------------ # Run inner-content elements diff --git a/src/docx/text/run.py b/src/docx/text/run.py index d49876eaf..57ea31fa4 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -178,7 +178,12 @@ def mark_comment_range(self, last_run: Run, comment_id: int) -> None: `comment_id` identfies the comment that references this range. """ - raise NotImplementedError + # -- insert `w:commentRangeStart` with `comment_id` before this (first) run -- + self._r.insert_comment_range_start_above(comment_id) + + # -- insert `w:commentRangeEnd` and `w:commentReference` run with `comment_id` after + # -- `last_run` + last_run._r.insert_comment_range_end_and_reference_below(comment_id) @property def style(self) -> CharacterStyle: diff --git a/tests/text/test_run.py b/tests/text/test_run.py index a54120fdd..910f445d1 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -11,6 +11,7 @@ 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.paragraph import CT_P from docx.oxml.text.run import CT_R from docx.parts.document import DocumentPart from docx.shape import InlineShape @@ -122,6 +123,18 @@ def it_can_iterate_its_inner_content_items( actual = [type(item).__name__ for item in inner_content] assert actual == expected, f"expected: {expected}, got: {actual}" + def it_can_mark_a_comment_reference_range(self, paragraph_: Mock): + p = cast(CT_P, element('w:p/w:r/w:t"referenced text"')) + run = last_run = Run(p.r_lst[0], paragraph_) + + run.mark_comment_range(last_run, comment_id=42) + + assert p.xml == xml( + 'w:p/(w:commentRangeStart{w:id=42},w:r/w:t"referenced text"' + ",w:commentRangeEnd{w:id=42}" + ",w:r/(w:rPr/w:rStyle{w:val=CommentReference},w:commentReference{w:id=42}))" + ) + def it_knows_its_character_style( self, part_prop_: Mock, document_part_: Mock, paragraph_: Mock ): From a809d6cc8aec18648850d8b94d554f05621e433a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 9 Jun 2025 18:05:15 -0700 Subject: [PATCH 128/131] comments: add Comment.text --- features/doc-add-comment.feature | 1 - src/docx/comments.py | 10 ++++++++++ tests/test_comments.py | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/features/doc-add-comment.feature b/features/doc-add-comment.feature index 73560044a..36f46244a 100644 --- a/features/doc-add-comment.feature +++ b/features/doc-add-comment.feature @@ -4,7 +4,6 @@ Feature: Add a comment to a document I need a way to add a comment specifying both its content and its reference - @wip Scenario: Document.add_comment(runs, text, author, initials) Given a document having a comments part When I assign comment = document.add_comment(runs, "A comment", "John Doe", "JD") diff --git a/src/docx/comments.py b/src/docx/comments.py index f0b359ee7..9b69cbcec 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -144,6 +144,16 @@ def initials(self) -> str | None: def initials(self, value: str | None): self._comment_elm.initials = value + @property + def text(self) -> str: + """The text content of this comment as a string. + + Only content in paragraphs is included and of course all emphasis and styling is stripped. + + Paragraph boundaries are indicated with a newline ("\n") + """ + return "\n".join(p.text for p in self.paragraphs) + @property def timestamp(self) -> dt.datetime | None: """The date and time this comment was authored. diff --git a/tests/test_comments.py b/tests/test_comments.py index bdc38af9a..0f292ec8a 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -209,6 +209,26 @@ def it_knows_the_date_and_time_it_was_authored(self, comments_part_: Mock): assert comment.timestamp == dt.datetime(2023, 10, 1, 12, 34, 56, tzinfo=dt.timezone.utc) + @pytest.mark.parametrize( + ("cxml", "expected_value"), + [ + ("w:comment{w:id=42}", ""), + ('w:comment{w:id=42}/w:p/w:r/w:t"Comment text."', "Comment text."), + ( + 'w:comment{w:id=42}/(w:p/w:r/w:t"First para",w:p/w:r/w:t"Second para")', + "First para\nSecond para", + ), + ( + 'w:comment{w:id=42}/(w:p/w:r/w:t"First para",w:p,w:p/w:r/w:t"Second para")', + "First para\n\nSecond para", + ), + ], + ) + def it_can_summarize_its_content_as_text( + self, cxml: str, expected_value: str, comments_part_: Mock + ): + assert Comment(cast(CT_Comment, element(cxml)), comments_part_).text == expected_value + def it_provides_access_to_the_paragraphs_it_contains(self, comments_part_: Mock): comment_elm = cast( CT_Comment, From 4fbe1f684e08aa7eebb0ce6bfedfce512b5c95a2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 11 Jun 2025 21:07:55 -0700 Subject: [PATCH 129/131] docs: add Comments docs - developer/analysis docs - user docs - API docs --- docs/_static/img/comment-parts.png | Bin 0 -> 30058 bytes docs/api/comments.rst | 27 ++ docs/conf.py | 6 +- docs/dev/analysis/features/comments.rst | 419 ++++++++++++++++++++++++ docs/dev/analysis/index.rst | 1 + docs/index.rst | 2 + docs/user/comments.rst | 168 ++++++++++ pyproject.toml | 26 +- src/docx/comments.py | 2 +- src/docx/document.py | 6 +- src/docx/templates/default-comments.xml | 11 +- uv.lock | 303 +++++++++++------ 12 files changed, 846 insertions(+), 125 deletions(-) create mode 100644 docs/_static/img/comment-parts.png create mode 100644 docs/api/comments.rst create mode 100644 docs/dev/analysis/features/comments.rst create mode 100644 docs/user/comments.rst diff --git a/docs/_static/img/comment-parts.png b/docs/_static/img/comment-parts.png new file mode 100644 index 0000000000000000000000000000000000000000..c7db1be54bbbb04abf5286053f9aaea94c26eab9 GIT binary patch literal 30058 zcma%j1zc3y`ZnFAh;$2xASvBaBBeCaDUH-HbW15IU4kOr&Cs19B00>^AUSjm@omq& z_uO;t{hxC`elxO|z1LoAz3W|XJkL8uzED#n#G}GPK|vu@dM2lVf`SPL+A%m-z`xvW zRA?wDsQR|DvM-cmWf@+$I$PU1SfQXijQ5C>Q0{(l@9lzJ?Oc@mx6#}Tld5OM_g#v~ zLtcdnn-{39jwH(<@{}ScF>3tk%!Nv%M$3_~zK5Du2 zrc4)mCz20^_+M1Cvpl^B{*>tdBIf4^D5!&^s(FL26WQY@_*mZe!Hms*@dMJTO#5PLRYO6~lM33NQwdCa5#>2$k-#U_5wbFOahp+Z4%B|MvhM^g zp$OKSeZ(PudD;In?I8(wS8xo4rFmF0d+I%*8$Yr3b&8JTd+)bS?Wy@YdRfcPmZ!07 z{Xb(EvZY$vOOX?T6KcqGsc6bkX!|qU%TPMaKAsL-_=FwJv46WEF!aiierP7EZn_=_ z$XdrrPsv(U73C4o#z8?1w?)AKTByLA3V5TSpeMXV!3KWc1>SNW(f)N66aEqXUu~3_ zUpLBV$|@-Vzcnpft*o5fY@FTSpS6+#kD9jC(sS2SRS~stcH}a%bT+r*0z1C>)dfWy zEDAIot=!ERz>W@1ZlYibrr+)m1=_zp=4N8}?G|@?2_`+&7Ywq_u2u|!Ts&MnOpR`4<9!l-(%p8$8KIu z?q=Y}PHxQqc*wt=BWLAi;cENF-PYNO;n#D`%$+^lC776g4fOlZKjvu#w*B`=PHz8P z7O+6>Ust$!xp=sLe>Tun{MVkJD5}J^*6g39sTf#fl=l z2VnxqV>(1cMLm(1L4ApO=fk7c5cklm+w2UlSrw2qk_$hxgkQ-R_-Py5;o5*@rraAR z!NQG0`O`}d_Z_X6jwop78QlYEw12-UbxAuGdOfx$lRm66B_t#~7#z=kmhgauExNEa zJM+VXJFw)>dTB;ye9rH+F0^$A0dr~7GKyH^8`dO0KRO-+iSji@B^ehr|sL|;HmDTNNe4~hB9P+ zmjU&!UqP)|(j6LDQQl7$5iymSo}=#l0dTp}%kr>$@J}Bb|E??W)F2A9>^7%7(H;-( zO08U&U~8mytH%f1$D;|{cPDU!{(kF#tM39Y8cWXUCTHqYgeqytR?UP~yjn{Zovdbv zHL3nfoBmUR(!OY4nicuyd4q~?h^uJ1NoR8FGN>w)xlghv5eG`uAO3d#Or6;lcdX(@ zh^}d$iiyu(yOJ%yL|4xuaF1Tf_&bO9Z?pZ4*nX{@2i3VtOWpkpJ@-<|Q5;)=2xG`oGhPq4O^1LHz55 z_)a6|86U>|A*DYaFlk&Zx~rC`z2d@>8KQXFU~f6iS;nSd4`7hq33 z@7d|~`P2rd?5o9kqIV1Zwe|WIjK!w@{|381{T~?N=UGZQaSgu9!^LP8KCwPY$E6?V zGc2je&)shumJ$K|_pJW#2`B)qNQ}AKa!-Ns8t$tvY#*{SU$pF_QT)SJUqLk6$biVE z@z%*RKJ|%qI8j`WQ~YN%v8g4#kSowTu8#!q_p(gMw(r4Zf@JUC#17q4k-bFk8MB_5--T zRzFpkXIRJItN8$B&~1JG(9XitfO-yh(j^~fbAMnPPBY(>SG%jB`aFfZjoMhda`?rJKrD=J;UzH0qf+J-30x-MgxA6r%`H<( zVXQgna!V2saofK?_5W<>GY6`iN%d7E;L4^9tb05XEV!q=azy+6i?X(@pP|((yMg6w zbzMR_p5k#2D)+g3^!>q|*&0&as6BR#&kue4v)JFu?}B@Ng5mva=R?upNrJ$7L8 zF&9r(i*0D}e603;Q19e@mrr?T&EHzFnPD7Lrpbjqu0$95$MIPPnxFNm%oR-gW3mpn zeE*A}h!e&%$C)po-5b5?kxrfZ9Fvk{kNK&U~^psaz6g>ewGc%lb_{ zjZc2A)|MHGFbO2B-?GxHheU0En|@46*M#`C6pvfM-NG!Bt;3HiDSiL%!^0jn99&G*>x z&tss*NYF^o)|e zpH4*E1`%4;ZbwEWO*g$7zNa*Kh^LV|E@%rc?sN*6esABFvrBD z@xLD0ZCKzcGiXT0#J@k_cLrmNmAtgMZ(vquB(vDOU7pzNu|2dBE5*g3Q#wOtH1l{& zzgDmM$@YtvFhqP3aX~i0b0}M2+GVu=DW3P0lxE9j$x2sDJ9jqbbdLZ#ok5Si# zZ|khfZ;+A0Pw4DM%Xd7P?4Er0kNSzQFbFIFmfnAKlF<1X+zyfA`D$`fCEe$+}q zsMYGnHEnattFZ_9=Dj8AORyLF@Bcy*fY7ee`^3)c_WEpBBYyU3T#r0 zY$w~(DG4mfuQJFUY4IFfPZ>B1;3tZU+@T&MN5iIh6ZoS@v%eaCw1(jW{v@yM>-^@8 z$fmooYLOv%+isDan5Nd3>&oYCUa!kJFSi#c(9G^gTE8}fMht?dzoreu?0~peXum_8 zywLG=zIfOOXwFE=|E0<@NZ~RJ`)yA>K}M6C`0}c&VI!$RNnu8x>g*;H3bMVQ6N&Bc z==NAKvQ~~sRweq~UoOAjTu%0~;;i}c(7-{;RPxYC)NuW-ffcl#mTzSv>2T7Z#tMGy z!Rtp9jhKbS*mBYfwPc>!^g*(*V}$qLG|ZnCesh{d41je}>9)ZuwL=%@xs|qm;oyV1 zWKjfpxZAI)pV&{Sm=e~wZ)#9V`L|qMZda`LXJ=%KF16Z>=QY{B?cXy;V`mo?dC1(4 z!Lwxd{xW|x6PiA-qkoj|eX=#B;~M{fUHiV^6Oyvloj1@KLU`P0HjRgakSgtRGaIC& zt$YX)iSWmDU8n=y4LgG^)>x7MRnT%6*zqk_b}c91=C@OuEa7uns}p~6GiP66g(S!j zbWD&5!J2hR)gwDNTL>85u4s4KuH@dFZ>lL<*^QODi5rtds(evNGuJrPcaUiFr4xgmS^Tf!qx-C`}Jo3r-Y z3uvT{!h0%~5NzUlB~!CH*To?0Az)VQz)n`t`0jV|hFeYDrVcfuPVLv{hWiVZKNSnPFJ6szcW(=ocq7?&u&?$S>E8zgRnZr}4GxWp}-lD%R;;}n7$$I$SN4l0I zhg{G6O%dRFzJF1r7Fh}<-8p|3GrZgOdG*0;`%@Qor1Q%n!xFjX^$rH+ubV{AUc`|* zKClt3rzqMTN1~M|rO(Ff@7+@mJsYw9*ts3hEJY?(7eer#y2XPwaoIvasa51<`6?&n zS?E8{@ToAGBCUs(oZmFFLA^s{VI}qWj(cK1Fw^!3>yFn*?{S6Qxw>7l$t5P_9;CwL zOCDrloqtT66!55EIn1en{1juVB8~j0ENZK3vAt#458)77#-|tU>+OD`G7 z@!YUOhOO*9;JUv`tylHaMHim$GaabLe_jAS@bO>|JZ!3w0;OG7d9jg5<`pT_N77IrD3aU~|=E3PT@e5CDi zt3=w-`2=ub2TdPyM4#Pzc?T9#1Oz@9E&I)D$p%*z9&yaB$K#3yw1(5FLa=)xKR<>z z7?i~^(Fa^rnFvYgT7S)?d>IcjZUnu#MYP{CK^vUM5>tq_&dq*H!B)e`vmNLd zi>8HSv2}Ou!D}W0YEPEJ>0eRrLK|X#YNAewW9S{vMna`-&OZgrRkYvC4GCAcADV_tAj{Q)Ib)8gC<;|3>KYPbLS5;nC;M_@3usZ1CU zj9gBKBlosSDq>6*(NKe+uxJXyH>jP+dAG5;6`sc6pv)^G0xCyn{o@5=OqLdIJszoQ zn_RXQ?rFp*9vMux?A2Q2UdborYk0YFB5Rk0#3?1=btvWk8Jhq4u(IF|_~v3mz9uT) z=WOpL-w1HVH=->I+7ZVMM|PN?C!Py_4W{3}LY*L^&N?+o@?l`8&P%Oqzf*k!ZiD*L zxF(4N>;8^<&&jh9o znbX5|=@Vf$x4=Hg$HqZ+!Bi>a zhQX9MWQ-(t7IxS}sy-tM*8-E1ZOYf2jy7)qRv^i|S6u2K$NzG$BAo1pri<~hEn3F0 z;LpwSr!Jz()HI3OI)xc8-_eMC2ZcDt;qL^KRIcM$NYTAb8 zXa4l77(yJ{x7sOMXr|&G*fq5O*5|fqBb(HZsaw){v_9oMwa_K9DFGGry6R(WFqkY4 zpSn}JNwpd;gi`Nohn)g`Ro>b?IKSzT(O(H3B895*xp<1rbv^C%WunA#ag6XX&P(Kd zh;&_PoO9a+>lpWa$F2&R#PX+s;R8Fy=^i^d#9h_+=Otn?=Vc9W*WP*uSaV%FVY~pv zzSuOU)6o%8a=(FR*W<7F?QbHkxW1`JTt)1h(l1MullM0`LZ<|5l?mLX#lt)NE;kDd zC|kD4xeRT#55&JZx1Q*j$c4ut(7Ng@+`o!!qzdnzc2=}nkEYib-4`gINxpm%_;~VC zVX>2*ppG#Ch>@_P9VALhztd7YYP>oeR(;rhI%j_^K{6%4F<(C!)f)_JH<|M`{ zH+bw@`g54OonVje4jm@Rs>>KAdWeMGiiLc7FE_pKzRa65r-p2LbgKgfX-gq=R->mj zXSE(=b9LV)mdf7l)>J`$q8P`6-&ei+i&!oYW-ElOf|FDGO;sITEt^Zi{t5A zUe=tE;Wzr5sVHQL(OBrvRADmImp?G&>`w%M^!B`~=cOf=*d1+&b|Zm|H%IY`bX9(- z9qzyhKm4xDeSzeE)!9=-lFjH{BJfVSLwoq{%Uchr61x7^4Yvb5ii_UeK@PrkQjzRL zL?DJ5F&fkH&q|~(OBhIxc_V@eJTWkAaHbFP0C%lJzzfBkiYJVDVL+}+Q$r9vKa-}d z!%NsZE4*7ffx+FACm-!_*H~pM+Jjt%SAT;zuKnh6m_GH5MM)o;;!p2a=>El_L=FCJ7NZnr19AeezleQ;VSG;wY%>V#$?UHxumHz_G zs2s+XP7V7Q6G*0{5VT`|qC}39?L{{5S&;!9sy}R*@2EE<4W1sPc8nxAP)cMS1T4U7 zG!YCiPT5v3*Xt~gWCkd${x=}Z0PCbThz_Vpo&3;L{RVIGs$uv&?W4Qf1h(-nKZK26nlv_XsjO>F*q1GD!#m%!T&nuXHWK2jM|mmaA})W zn6}PbFc~pt`2GWQEI+z{D;JCTn%86Q;oH8b!9jUz!zs{>@5an{ix!>S24;pvgZnL9j~&9>Wb%as zjBNx+<-0Ne6QTPjgx5j$Z1HGJiX10lz&l)z9I+B2DJL+(mDr~jl?382R`2dE*K-3 zGgjg>RX@FB8-`g8=RL3mArYZ2JOth|5KtA3kW;1x+$?kjbkPLTbU8%OGsJ zRF+%s+m3I~ziqMcXOIzeH4Z%D(3Kj$dnz+9-*1SnE^+b!@shrjo+eX9=Vq}p#8D_) zJW${TfaF+x`zRx*{#%}%^ho=Vxc~JUU&j(LagZoY49gI2zl=ZDo~_Cj9tSglVB<89 zovME9tLyU_%@9%tBJJgA&;F*hjv#bQb?#c5@ux#6+&MWZrj9;SypO)v%e^rW4xmgO zlD3S6KO))C_L;WlTcN~5p!EF}_&0NeqXGa*eeWolb&Wi<--5_fP3!!Sr&uUV9HFf{ zZ%I``f*;S}s7*yGDKE{^63}4tw%Sg7=`=TJKmM-lh>J=CpWkQkOiXB2;&Zu@IGeXH zK5)AG&7H89JyqBxZ{DS!_9mN3z%KRU_FPz8Eax@g7|aXtaV}GP_$db5+c&>ttbWi` zt6H{*5$Rjl6?AXS~AJ`$8jh`!DkmWvtkuK!+^RLjp`!f^t2k6s>CI~Q8Gv%9^ZeItF=jK1B z6yhxGe%w#r?0nT}bvL|qbg2_o?`U9;mbR;>_*S$U!uW(4pNg{cz`>lsyn7h~wUg>T z57#Hk+O;cj}lK-%X8YFQ4;Iyn2n zp7HW)me{mli2vrp0X&XrwU+#Cj?*P7m|&^i{)ez=?Gl|qSz?I?KpuNnyey~` zgfo*WPENqs_bH}k%cZ|5AF@&P9@M4o+*m+j>ax;u2&=lNlIs(b6q{LGTq}+4+UP>L zTk6g;6C(3aJS#8=!_yEP$@cV3?EPm9-A2gp4z!HQM48Chp_#jDGq$&%Lc1Uurhmn3 z?8sp71g+PMNpi@B6V;WtlG?F0%SHHun*wK|37~S>E9Kb+PkBIzQA{P2X{THaL~Z+B z_^jD^4%e3_tRLyUf93*tYKq>J0N+>$Q?uB8WtHlvKqI0p?C!|jN_rV8sGzD+YDVNPXl1F(wWbOobKdeZdfjtR`v(ECAS zl(VaqR9&G3*z~@YRv#?rtILT+e==;sTg3!-4b!Tsqj2GG{3@elq*eL_t0%-jI}}Gx z6t<_zFpus^>w(l27mEljk=@U+96ONo=HgeQE&+{^I^%+K-HBv4Dt-7tv@{2_9S^!I z#h!{$T+M1PwK+RQjuGWh?ga2G{Su0&_VjZ0H0iXq<8;Sa=ft!J)ZfXpNbET2b5zS- zeTuG9McjXJ6bw9NW6|+4N$J~%ENG1XfWU)b$$_t`0eI%Bje$q#h{oNo0-SKk*pDvx++8NNT>g$e~rK8UKb zpH_HsBy_kKLj0v9kI7%IcRJ$L=x1n(e$Zu~;uEBBu;ja218A%qHoEy%UM@ZSHj@M-WfmW!3a zlzjczz3*42_DcT(G@dcU;xZs9C=XKa-KCsLoqtjjs4K)*qy0=(*P|!m4qB|YWx;DU z)Au1NZ_c}zkEqj@A^?C!sq9tVUBRTR8|FmH*_nv~b#k&a%v0e?g29L1*u-T}O2{k+ znbGHGw=LZb)lQcpvCbQqk!`9Of*RG9N*6$0xl`kEDs3ZFP`Z4Ad!M&bv-34&<_;O% zp6I~c#NGEJ0hXoA=ydv9m8n6K1T%r8SMc_$LuDU3G?2jFCXT1^KpXfjdJcECbfCve z5Q21C$M|u7F^psnb-o5i?{bl@vpyqLsN!o96!hiHPhY3bmdRslDxPUj{(%UKzD9!P zj}0S7OG7&`zWGTltcjv@;+99d$&9f(o_L6+YaFonpCYmy5sZf|2$39?irB!S%aFPZ{~4nLc;`}5uxfYfhL z=FsX0ZM}A$4#YL*wS8ko?EGCz9o!FLtAqG@s*IFUB1l$Fq)OJb^Y@Ksr1Vbzrpm|^ z!GyxwYCm1hXVFJ6jw`uK#`=+TIlbR!<7-wo1aG%q&D`D`xyR5b7V&MI`KvG%eAl=b z61kIowZ~~auhHvOmdH(s;OJ9NAd^ya^;=2!SE^>p&G^FoR^xEtlF(;x@{mu-bnHQ; z>(#2nQ5RVI-w|dd^&J~{OOxtz8J;OWMR$AFHM&14$(6UUEem;z)Uv3ab7Jpz| zbkz(XV*{54Vc&k7iZ_kkjVrW4h3nyS9x46(N)O6wO+9ITz6 zC@v2E3$XpAyFswbzzyDQTolMfekJmsa(`ii!;m6TTv5_R)Zlr|y~Tk}O*IVauQ2i1 z%($QXq01-t0%!KlAE*6a?Vv6>lrOyS55=N`c5(%J+S)Ky%`iG=H1QtLvt8q#z9nBe zHNl>8jSv2o`u$Zv#`8hui&!c2Q*m>0YP(OZaYC|e=-C&e$)`p@4VeO}^ScZYRh=t{ zv7j@wxWcj8uRq{>(;DOV!dLIn_Hfk|f6Jxg7BKEt=(7ZHzGAp7eZ{CeU6#E1*^*r| z@AH!)W6c<+sYmVVW2gThCP1WU%E_GZK$@igi&O%Fgv9Up%F0cyYo7s*`+K#OG5}1U zJGrv#u;T7ep)qfK9@-jZx^;3ZuQ@=X9(*(?xi{;XQjB(h?9=$CSRQI&aHheAZ4~Pv z%1-^xuO75yOlFk%ke22V=pRCkb$5U=CI==JlGV1}C`~Xi+LGumYr36N*oGKu@_>G)OP9N)z_xorOY2nU*hXT(+g6Q&YDMa$9T2#2}sy5!i>EY8tKUIBWR zyEs!!zxH?jj$X5S(L6vm9^N1k!vM%l-cTj#u{AK?Z)`ayPHqVRBf7drYj4pR?;Z?d`7%~xpqP9(jTP^NcM^)aOV^NYG>un z0K#)9;tum_2RiYwx;Fr>&t9HPn;=qL@=bAQKr?35zxdLPEYH<0d~VLv#J#*4fWgA% zbN~6`b_YmQPo_Ojo?H7NtwF_{3qS7#9=A$BysDoRrLwl<%svD#P50Dgg z<}N^>Q~lt3_Yf-!i+q&atpplJ1`r892Y{52(aY`I8)St(`~$ZMoIAQ9!DJU}EX;rT zrM2FX<^83bMPU{5XeIkjL1TcSd|>PnGqkuMr`5k@~ z#+z1R*DhSPMx%O_ zNfCP{Y>sW_$9ZQo?P ze|LzEbN=YjN%%BzOM*M!Tnna~2pvD@{AqRQAW!1uF|J*nCMT><3VYUBg zX%qFG)YGvR@y*?84|^5&vBbW^rg&iUF5U%?z7Fch2DSU1Pn=b=takfoB_``LRQc$# zbLOn)IfaD_THTtH#X z9yOx=GQngxd%tR2S}xWzPMdm4u-0`AUBY(L0WP~t);*S77y+<4WwTbcg6xgSK>XXM z)sI4?_i_2E+W zLJlyd-st;vl2&KuE#iMrD{`ih5n6_i=bLT66KzMjE;91+N=EaEd7rW%BwQyZHYXzv4Uk(gCZ zUOLl<;Q>7AOs0xGauvPHgRvFMqf^If{hIUopRU^hP}K**MIK@zEiP`ICw`J^Y!{9< zH2Q7+W%_MTD)iZMG4E}umy|bB}O#Ar}TWhhf?Vgk6S1yj{^K|0FN4Z%L^CI7JsepA(6FaGrEv;gv z0vg{`h7~xbwRo>ro(FT2!Tw!C#fF0#8a2KwB86(`wt98m$$V@_%Wfmlh>f?7 z=OGL$`?ryM6_$oMKEJ{z1}U7Y^Oact8j-3~>9@$Dg6u~C=9UD+8>tlUc03|VR{CR8 zfGkYO)Ffo^l~D%=9p&OLu$N56W)DCZt>l?MQRQ)9Sm1O4=tVN0)zAQ;(7Y+-#IN$5 zD*)L~eoQ2D3h=wtM|rtdcGD-+Y4fRXP64tpMbL3>0N}wN45x56$bRDR4c)|Yz@tzC zDk%l{rBf)bP$fghGifg}0iGhdo?wcydPg37i09_{fL zwzrN8kD$lI{mfJiah*c263Y4QwIy|UVyr(ETZzyOhidWkhZSORIUZcI6+3J_YLcYv z%VpV%7uzGo^)sPI*4RIi6~|hitc%~^qH`?XE~zgJmV9bYkRpG~GleWE{P8rbrkmg2VTMr%fZ?zmGlo_A$z}Wewmq!?l_h7xjMDWh|uCr!E zuE9wPJ>0i_k%p_<=ekkdg#IW=Ox#*z{}S?9Cv0fl!0(Xs%**7n^!uYE*#_pAQ5O?* z)n22~tyb#$vTm0!_Mb?>PsqIqC8?na6MxS35v0NCV;vPy0Y?_LdOPM6kLNze@e^&ztQk$&aK*X@-S7DWK|K4lv9~?#$O-ssE~3Vu>i>l?YmtC44t)7@10^V?q3I; zi?7EnEYmKhyl>oa5vf+f4-jM9u)FP0<+Fl2v=cUbFV>NuJnBev9TwvrhO8*eq2xlt z*MvN!tlK&Ew-M~E1=+XWg@n==+o973N503`wt1@H04K@*On<3p&*;%bg44~LmtvN$ zSV8=;GzJf{`h5@wjcr`lofSGB{`L9)s2@~e2IYaq|acs~SKsV#$^ zcT@t;Vln50SQI~+J~N~0C?DyMf^CS4><;g zWu}zzB7Nem)5+-${pfCxgv!zSsNXm(+L^l4wIw>%OgP)!4!SO3R_9)PzwSV6v^5~F zYp^P_phhQ5WK-`e-f39yw`N3-W+0z7_9+93Q5PKN?gn217^z*3(th)sG74~zUTlXZ zIZzN$@6nR$46g2&C2pdn*q6@O%kG>YqW3$FhtV@D#(Y?b5&NoLcL-EuP1NJ(ISbz@ z-~Mq{kMw)PArZ^P)JrxATksCSZ18v%&uquL20K{#7mCEk#M6oxt}a$3Rj%z=pgaBE zZl7=*_F3*4vy>YNB#~r<6U!%r%f4SHzLXV>EX1|WoXje&7PQ~i4v~y7h1P0K(36ieB1Z=TPA9ZLwpl(R}WV$`W>>g zcR%SvNx|_enDmFwAGOG0+%ER>w#`6Hs*YnDpQ40+!^oZMjS6U0l!6KNY;K80I=uL( zcz=tSzhJm&OyaIZPH?2`!R1y%B;RH{xz*BElB;VD@(HzDn*GnJ3ZY zBqw9umu5KuyHQDNi_L^dD>)#i%-dmhso}*8L?V{x&qXw*rO!<@mYZ_||H4{=d|cISU4`pOXX(11@Pa--j0##&wNbl|I(7`{2@J^a>>zE(Ac zrO%pJB4mO+#qee98*0H)E-Jq+wsJ%bOxyHEeJspjQ`SS;Znux{Fq{rs<|Z*xYQXQc zp((oSUZb4DKL6Ic0`FOd^pfZT&V8fp!}C_Y!#2sgG(1DwNX(E}3WnzWUN&>R5A(0- z_4Cm28rqOiR$-8~CM$@y?d@VuAT{`D97vsFk^Fhelw-b1WWwQ6D8jMhE>lOP3F|YH z>l=&8yFsW51Ps>qZ1DA0Bjz)=PfDC?lrq{cV94-BF*XVsC^DuI9Sf|1CWU^7*#A7{ zddIbY$p`i__OU_U2e<*Ld8sRv?pY{}u29R!=0<7;;&mf&=dO-+qBeMel5_bt%;ny5 zw0(BZmj!CH4e9wT)uW8f%v?>YcTL>;1$WGNY_|{k>7Cn&85M|WMejj4PsbOcE6am$ zT7W!iIK{-Dy)ssYL6$0QdVon$+9rJT9loW#*pkGKSdPhVmoiPB7}^H$Wi`Fq2=BTd zQSRk?A$Y_cc=t7>q;Dg_B$fbV2?7e(R{C%#_ho|SjGPsQX+@n}V)2>-N_x|7^ci&N zGMU%}o(rCGP0;q6_+PBCW?Rtug{u-@%`D{;cT$`RsktlCS~g-ky1lBkS;9vM6N&Ta zEu+yPIy+GC_6Q?>p1%tT0%iS-aRrZIqh}wPjnh&do36=uM%*^GfAj2VSm=}7441(s zzkms4VqbSGSdo&;TXuD6Q8qAW=fIC|E*2_%6W*on67uA7vFTu9{wpcA%n4kpR>btU z!u(j0{lkfa;zu9CKp<^A3vPmQv2zfkvNDmqWBar|dzP<7s_g_dKx4$?Quvngnoa}t zhImJfVgO6L4iUmy{^`4m-OK%@$7dPRSTAh~)#|-Ahy5^57^*ujSF4V=)TiNR0c!j* z1cEq~O&8Cg!fk7er{O~*EcmL0ou9L~_OVBXQx+b+fAZ;ui_D+2*VeA#TdtXAk%ym< z4D=wcDe6ep=lDak$<~U&^N!}FRmsMfJlFOpZ^C17`preuP|luGmS~kXS87N?9ic36 zbEG!Oyvfa}TS>@vfaOo@&FcLs(qw!#TOGmbuXh@<+>;5PFE*Q6y*cG49)30|my^qM zD^MZ9@kxus-KD|0GiP!S$JPa@UH+`xf0uHblrI26H}$dx)}X%*AuW`e7&9yOZt4z3 z3u41+L(God;Iy1%-L&C=CVAP4UU=6oaOo6(P;%D2Q~G9_-DvIU=!0f*Ia~Y3-PkdsK=yzfXLoZ3`AkeR|h$q<}FFPBqstg*$<3U{HB25jjs)OXp_%2U! z^>ow4PI+EGQJg%;4PKlZk;G7{sp*hB^wPEqS$BEFV3QV%+V5Eo(afQXmp|U&zb;D3a5FPK&XkS)F(#!0?r#m0>HgA8kDlGh z=9I2ek&kYJoSD$2;32@uW@ugtohZdY&wlUJ^y4-yrofLcD$_Tn>Du4=M-Gud;_6YF zQni!))q)Zw?Xcf3g#1MuM&_GHHzs8Xu^>(mImu9WP$X9t11==tjd0GJA8vV&64H@C z$&E}m!>Y$-HE1%_v%4iaWl^)?@2_m8t$aRRFRo_ID12-ufydXy_~!<283x3NA3ig2 z_e2Iy<3^|L^cmK1U)X!hA<`>u)RIWI6juk?a^BhE8E<#usO90C&3olRM9B@`1R2z^ z^qiWS2J5i3tavZA(i221lfdpQ`2qM2zK$weONgfM{s$B+Rzrpi+7PUtVw~1I{ejW- zj?g(x+!?Njwx%QV*?NV9HVw-cU;h_CK*CNzuIvO&Zbey7s5|ZC4E|?B(5w>+$&zT z<@x&e%kqMV2Qq!vhBC~1VBi$oTs+F%2=rQ7wq^_J+344HtbEm-j87PisScXlr$;=0 zc%bG|c2$w~ISb!ti7a&pIyLAw8+y5u52v2sf8q$2t6}b5>vW75X;hebZg~#?k`FK+ zTSj#|QzYY;SG+DUojS;+^S71?HNvs|^kcaLSIUT3Y^Z1FhPk~`^IDig^%yT3xw({c zo`*DhL?M}%mw=DcKW6A&R@=7yYWVQlWs06pCP!LYf_xw_y#Ih|^h(~8VX8YiAmss^Hqnyru`RhlMpN*tf-PStS9?dH147O>gfS%J{OwR;6f19RG z8>FmD%-F7|4WJCN8jWvtx?`!!?Cp0|ehlH+r6+9FijbEsl@}C{^TfDJgS9O;?h8N= zZ=CjEE$4e5oiV)@-Xg>zHY6tV*0}2UTv2_BG%qDhGblJizC|H=?bA_r*J%P@EF9N! z_;M7vJR%AY^PDk;m4Y5{U7ENgayNe>IP(-5{i3KKo;Oy>*tYM4d3oOwC?c6BUVtMT zB~(L1f0APbVZEo1R;AvlPWowJyufX9KX`9hpd_TA3c-t2Vk(3#NF2cTg6`rNySRr>H!%zAJ|TYP1j?2_#HwIP8>fagZ08T+?8)=Q z#8-J6)AK@jthl9MV~62M6z1l~o9vzh$(84>K{#J@f zC5VD;w>SFvmI>lsPou;SVQOjQC+>uM8~^as_t9h)-b575Mt-Cqyz_)+z(!GoQ~R5s zcvnc;*K$b*)h<+OD)IwX-(LTSVlMQ#NkXopYS?03SFEH>O|V-RP_EW88DB>q00ovP50K$zZdHy8>V5xRI+W#E_BLVT`rMQ`oWT1o|R(h-5IzGNp1qZ-E-|{T%i+G6b6l^19iCTc;{Dc+%kA! znBzjXtK(!>2DgZxrq7s1L5jjlFx6VBvZ3EA8l~^!KG?3%N;%l04PL0aR3;C&>d;h} z_E7HiEM-2E$m8>seBIJ6d1)_33Fng7I;R+i?FJ5Db#_^)LR=eqnBAO#%d)y)=B2FQ(F#dYH)l?SpnFef^!KMzvnB3Qt6!Vr8`X%Uh|2 zEE~$Px2%Tbv%8((+qXWxBEO9O~)$SgD1 z=VV34siH8pHQJy#b4ll#1!ModfIX6K5OFOmssL7$p8+>Y*W0I3W4s_xQPEO&>=}yd zjS&+R8)jD`TOItY^Qg&3Xxk-T-c^2eZ0AWl+w?-Mf+}t1Pp-;onU|?m zWS-a-!vy7hl}}%8qedI)-~%9&D){hTSK!N;e%k&{&=4+SATvBnR+MzL#+E4HXvQoSr*OBPJoNl4k zYmE`J0jl=X#@h=k@OM)ct#_cXr5yFIuSRzJt5vo3K~`QKT;Q8<^!w(BB3MiY&Dms$ zQ&pt(%Q3>KQ6lltSxUHi=+eZz2NZc)z?okWxmJDuX^aRA_%;F4bfQE)fc0~?dO z<<#?UZE{|A46(AsPk=%a1B`*1HOg$NDG~kmna1Jv?Z&eYYPfEcW(YmiPQPxm z#{Z0R?emtjgbMq2U<-t6Y6>NtW32RP*JZ5n+2eaC(+1(H!hhuV=*$1B>nfn4+SWha z0)ik&3xWcYQc|LXAfYHAl9D2wLk=mO(h`HT5`#3%&>&qR-JQb>Imi(I!+rO@`|f*x z*J80|4YOy*_npJu-!G1Czfue+l&z&3qi#LIEFH-}qmrb47p{?s0rfS$B#)ja=TwxI z=bAdR3q}))OZ^D+50fx>pv^`cU?t9-z->Euu;?V*OL6o2B)JSXgJlg(os#r7e;f2z z)d(IKMAp7zobJ^1wna@Z0V zG9%6G3do=|T-ogDLL#B8d<*BN8)(TH&g82^1qkeT!lM;wd(1ad z-o|TwGwE;{uXsZJiV~=O^ptO8md1(W>;8ht4!ljJz4QL2?hKzDd`~efxncKsz4M17 zSxrWElZikV%;kx^$_+It)zXJcdH8~(t&ctX)Mtq-E5zQfNN22VwP+G)+^lWBF#hiQ zdWM{2oz9_!<6Dbg2rR-uRLaJrHITh)ZobwrN3uPW)4>_1d0RTC3)#3hQ)`JrCr327 zP`2X-9qVF?ah$>=h4cXw`j6d?I#FSk9gbuYK%Tj9d!o#Jd`ICY4c-)t3s9P`e62{n zA66G|>yyu?yDDa}zvvGqzVCIJsNFz;+*&d4#9ouHb;Sxd#)|YsEapq69tB&$n^^-$ z&1@c$QtJep-^)n2OJ|28uOs?QdOOhZS+hF(Ynclq{l%J4#`6a`g`Zf97*ad=G}*t{ ziFEcJDSxAvuXPkt8MYxtdU%`<8hB<%`wM_NG<)6LXt`2Ug@%pg-S}m4tOqt5(sbrC zP*p4IJ`6H9tEn>v)m$PTru-B&#B1V`d^w`=K*HH^3T@W@(`Y4No#okhA9;on8(pvP zNv7GVWI(kOM!I_gEQd*vl3+{*J#4vnOUp-LM{((D;xIBMcwLlJlsRYe0Mjcihc`sjEZ2E44$gUvCym=S*)kI~UoRACc_Kw`yG(&g>f1r%pnOH(z*oYW9HLBQOK;0zOu7dE zn9#YOT zpIW~m75ulmLhAMBBC`)P`mM*c&J-1*VdgE%&#Tg2gtZIJf1{vB(UA;_Ei$>0@v$-! zR!|t!=@C@u8KmkFOv~wd95HI#D2sK(hPpKhE zyIXdGAAJmDs^@KH!DlJTQ?&5!)^w&C7+~Y-{4SXfUNO>*O*oc7@=xal*o5hbywx=X z4vdfI8ZOi~CyGMs=dtyuR5Md`;)X@m59j)ekm}k{LH^z8;!M(`)0FBL{?RrGIw?=vE_bN!Dxgwf0`tqLocg%4AQbgCTx3Qr4g-y*S|6YQUII3bD zrPz;Zz& zaIVDDzZ70ZZkbn-&3P3jkIdWBX9QxdSQW+~R;&m*RroIeV{S7&TA0a$n)&?&;O-K^NX9;fx8$>xu|YO+QjK;!N- z=dnIn1)%Tk?dX6Fy&4IqUN<@%_l@2npLivqIV2?F^rU&iOkW;@|7b9Qd9)H@6do^xkQ9r z*y*&IYY#Hmu@-cGn0B0=z3tnrtsK|%vFB+=7p;VbtwnZCNzunmAt;o^O(@0Yx-K%5 z{=GR~T6|enid84otFDbt&YsA%Q4p70@(7LPkxbV{quTg80{nZ}47u{70iptnUPQRg zax90Ie2ru93F@lVp5@6`bv|K9y5{j4Ck`G!4&u$Cuy8U}tTfO49E1g@%hE|?@*^|K z+FD*Jiu7nzu41q9Y*0TL<)xO|aAvXDC=j1Kw%rmT(H8V7I(s<&_S%o2u$1`klYWV> zJgVqmTcegPV3NV@d95dxo@9*1xMYN5D!qm67m+8hWnn$qL$q+kg@f!L94!?bu^ERz z07)O@?An=y)ug*Gg$Wu?P9M(G0DCN*dOkyk7W^L-mKp~RzQ_i&YKrWFU*$aLN;AWR zJA4RWnlTd0@s4(rOLyFYgg?D-L`+q3WE+2faJ&c#qJF!`z!G`(9-SS*S8i?a&E&&X zB_GdFpE}4bbe*!4?Z%Uoi-W;RuKDHx1FlSNBBe6kKDLb}3c+8yFl4o=m;3f$rnKIG z_;RF!b2Wh8?-*ZRiD`bk@D@$UJ-m|64c?kuzw!YH;u7v)?Uh(hH%%9uph_2~4e%Kv z;k2O{$yOkp6hoOgymqIUp1`|rJ>+55oI_qp)4Ak)0m$hi(E!q5JsyG`E#q_A1xPKe zHLSePkH+^8M>M9_{V)W5aYu&FeJa!y=0)vv=1+YaT;i`V%@CPdiB7dj6<;(fcj=!j z%tzHk94%yQlNhE5P(`BPCKK*#W=W(jY+GhS;}e#>^)F7i&b^nkmSujm`11?$x=1-e zN#5^2$zDG-p5MB*ooBN7x)%qOnYH?;R{h9n)%>DoRhx#nQ6lORN0d~;HitF3NSCdD znd&hkC-}on=eMC*cbTEGm@mngs5mQkKK8b*!VGvLSXF#e9^^ve#xQH>uFomGAK)Qp zIq_mP^@Wk6jpBIvh?f|&Q8LwFdKaiCIa4O?xsc(s^4MsRJXu)-6RY>_l&(_JGzQ|v zl}gp>f4x8~y6=A6E`LCGU5d}}yO;a^DE#4g`{Xxzmkp)q)2P7NZkO|RZQGUcPNR>z zP+9)ApDJ8*CmDg=QEjbWiu2}ei=FiObL#8a};$Hk8>fD3P9v`D8HaLBk%A%*gLRrp+P&iB%%H$gE2Ac*J( zOzu&BHF96XXTboRA+D}gx6~hHCBHUuV$RO5j6j%TcD&6#m`-~{=NM7G8(u$qf0OCn zi|in28VA5vWy<49F>Hz#=MMOr1d!S4qXFK?Dq7Oemz6+LGEu~OtV<)KewB&Y&Afg;R(y|gZo1Mc{<(WdEEs^*uh_%Iwq{@mK<>Nnh^6!Q zPY7Z5?CObSiz#3h$npa&D=s|UeadeDl5cI^)fn=Wh51o;R}Vg6@CP(t>Ck(5ftHn6 z3S}=gIB75)`rZLZneh1KF~^8ipv+!&1Q;}=>cAP^)m;JZP~#ae1`*K0%(Zq?ex6rQ z?b6V3FFAb2jugC`^7_1~(AuPJfAPT$(2YN~t$)apQmH~oHcu3aG+`GZ^R=JWN+Uu! zgFj6~JKf#=`Xh(H?S0f)SlLZX`l?~R1H7VSHf2JXGMz75tGjEmD-wiZ8N%3nOB+4)ArvWqew9EbG3K}4$Ho$ zkgN5MY7;I#PH-+km41ok(^}VhTcSspv@b_+Weu}37C)e>bCfpERlw8xUQQ9h-UQLj z>+%FPXkMh)M2!IQm~;gb2F*A6VBRS10cz*>*^<2SvHgq#rTg7b5W9jGe zp53Ge43Ny#*xRPUilgt8*Vw}lqQJk(thcjEyw9Q~*66N?n`h@`O)WwBM6ahFakv3u zdVuj+(+A|@c*ZuM^%Y^eMZdb>?+jQvb?|zY)-I9gQK<8Vw-A~|!XB;31X(1@MRO4V ztg%(M=>??}BtXx<3#c&4AM)swK8R}qWb;B6eRP+#D3T!8iMD3XI~xi{c`7tspRm`1 z9wFdEY|KwyDTzwjIHAxT0CLJrN9oUknY4u52Xt{oSxt+N+`I(>#$#Gi$`r_Dbj)U8 z&6dQ2?&CKTlJDzjqgFb);@E93uoL_l2v{8;_*}0#s0ruvS>G+ta4dt6&b41i)tQG zRN5DuFC{~=bR$~|4ZekYUlv%rur{VrP%TXE62e*X6p)k>4`Yw+Z2#6zQ5dBq%Q|)6 zFkaN$dxCvfi;PqYxSy%^z8o6lj%}eV4m?_r(f1W-osmgB$vG7NRcxG?;%e5>Nj({` z9xB0Fc71xjIl6hP5nFbbhV5C%qbgOMHc`1Q&EQ0J@%^n+-}Ix9nd4ooURp3>YdTl0 zA$UKS4<_XZ*}K%Qcx%Kc$XiO`qK&z=>*X$4fVNAKQ*V;o&^Sg0PGAJx)mo}s(IrJU zi>dWNm~W8{$R!BByAAx7>*0DUfa2;^Y$%#7aRV77~mby2AP)Zhu$2a9f%)+Y~A6ivg5g`21C?hY%r|Oj1g{u zhL}>`89TSZqu!Y&4jWTir2gUo{Gn&9)Wbtuo{RlxrCh71PvAsDOCw%>?}JnuotU=( z#i(F1zpgwoN%x%bU<*@cGb!Yi%jEOe;&waEAGq00DITjyy{fZWW1BC7?pj&6zn>m2 z3T=kE1>CS7{Q}66ywu)+phBDfodk7pgey_2`yCRqSS}Kxk;Qq}Qa<@*08OZhxg!q_ zMU+*xm{pc(Vb^3WY;OU#^$!6$+08ErQt_qQ**x3E86S(n*OoJQX2{#Eh%Sxsw;9gB zU*A-{uYJ=ex?_L}IKMU!GDr#WRg4?Pdh~05l^U#Fs;1w2WmD5CD+4maSE5e0lt;5!lEA-1|>ky&VAiL&aT4B9tLzg8Xq!MAm|TIk$O_@=Y3$ zJ{-Ng;bKC19F;Drz`De~P~hzbW4E1cFmU?mXs5-Rdw9WthHWo(CU10w<}J*R~>c{nyL5Xas-xNs{t zN~RO&;UzDf7MRV=wU7MuaBW=}yapHAf3|t}oI*EO!`pcF+Pmy?cTwTDzM!AIY0mxf zF9ok7^#y5MOe4w5jQ3x`zxRdqe{7Sc1c&E4R^hg)nbl^D4hljWA!X`?2C(Xe*wyYo z6eQ9gu;kTVH#Mu^r~U|&M=u;bmhX^Hp%2?_IfWfObZ^ear(e{)-sqzK&1Aj^tHQm0QiJcjf<_uKGsZ(VMCHO=hJ6$!mRg>uF=JXmtRI+_~@3purEOLTEkgopd$XQ~n%b4?`6 zCyBPm(GeNy!1oi*8jVYjRyRWI#g1FBQ;ab!u6@*$eGa~;Hjr7NjG8~$j0}V#4!0h{ z4^clIChzV)GNoMq$;BctZ$H6#Bqgam0caD{jGU<4;5q&a$ngTWm{2zUgX5MT!tp-&J_Uo+qND_(JM%cBc@Tf* zS&H#Nt$4i_Jrum;GcOnzHbs(mJ)OJJ5!_NHiIiIQz zW5T%LooRmJ`~yeMGs^nHEXG8~d*40>?pPA?VaZy0fe-EakhBRPk+Trz!Zs*HmuDqa*t`J!t`*pQ6+~Ycbn014=Mq6blbD>Y|5p=!vfYk-$kl z#-yF&Rq!m~X|${XYtoZ4Uj+rvgtwTsG^E(#_b~sLYv*|^^5qXo!G3Bs;k+uLry9P$ z!pxuNsF``RRTHGA3mkX1v#d0-*>@7fG`%CRKJOXjjbP|hJFyO)*;n@^KAp`WFE;r! zTKIvRwhPXio!Pl;p5GQ~l*6@e)I6T}{18(T zPA#xHmafg=FWZo)n7t0jxmqr;BIe`pc-&jDu_2o=U}i38^ewy8;!zi>*-fC{pZdQypw=V|=yGoYpc2>a@@BvCIe_u$(jr=@cCL}2FHl)T2JE|~!Q8#ZbJ z#)pyQr46EMJeQl{i4O%2!hOf58;Bi$D!@+F9$0&Q-95-mX&Fp}g44{<>z$tVYU#qg zGL}*v2i>;P2u!4@;KGtmf$VWbt|QA?-3iM!;RJi^b?`I{+9g!w>?Ejbs~Nk;*r-z` zD~9eJQeH-QAqE1?*ONlJHf<&GBB)9sKkGBD1qUROp6Sc`3Suo0&~PJ+6bbZQBYej7 zr6W(~_6K@5Gl|@DxAQbh&F<;+8?CMBzr#~)(;z?G^$bfG_x~Y}?KnHxmqjkaf3~v$ zO~q)CG4P(uE@j&Qw5zo$>7DhDJiP-#JP=d6CWyit(K6zV89ZOWxzTd^9gB?9B$1LhQ0yq>h2S4lgw`j@0|rA{05~5MYj| zjM>h;!*O=d{LywxG_eLY&u3XJ{n*%Yq5KV-NzBW^)%;QG4%IGD=oD-ESbpGHN$Lh* zO|cl>r=VII;ufbhiAjI9av{tY%2SU^)Q+$VVaZuSar=~nUAJYGfjCdR5IXtpmFLRH ze@dWLl$Is@L_vgTfGG5kGJ?<$lw4Ud|8!0p+tc6TfSt7`YS?Bt=Tl{+%`Yu(vkcUP zbmN9y;^DRP5RrjBLS_?%&f`Ymj5`!T-#w;w6_l8*T}yq=B}`YQnw>I^i8Zdzc&LmU ze`B1htz#^rZ=l2#46_|l#2zP$J3VLEjeZ+N5UU(5=V$zA<>C$3E%;v0i0Vu`R`V!S zdOKpbbeDsM+uAD0*5PqywkJR1EIjd-6``%SK=YLptyt~yyX&<(ua^w{POkA=C$IVg zPciAhXxRA-wcCTf$XnLm=9Ip@foo5z(p@ zB0cIx29V|Z&d$iSHQ37`G~x+_tkOS$6_;$RS#zU)SwnExYk?wX6;Qt67fo1VlV0~` zaYUZyo1Ax?N)RAY9Iyxqzi#^de0%NFN#~OrjVFZz#|Kz}x-B+dXAA9S8H4)XB~pbN z5;u4>+hxhCIZ&q*;DR#hKrG5j1J3ijbI3PA z_@tn%VfHvJ|F%@Vrb|xHg=!ls9l?-sKkgvLc%eIS_f!Jzp+;e400TQDs~SBpBGd}$ zNLXzAK*!AQwFx zNSV_1g|2XvA4sgNVTczS0I_$uM}^ooG3HfSK#V55BlH1Hw&%bFkY*CFK7i&(uH1Nn zKA@qN1wH8^*Y73WcIsfeC*)@M^{nnkZy)TdS~w?+=EB;9;?J^fR@rrjRxf&*xj3>< z#VV3iB@KCEVeiZB($2_-XU*JgzV1X{YSp?-J;^kfuJ&&msroR&8h^utP|7)wzJRi9 ztjNJnnH2pJo96a=cbqndz^;#q1Xhk2J1DFp3X1BRrD^XNY@6%zi%INVd9?Zu(@U9% z7N1oPQf9K=g%KGM2K_?i29)lWzJ45(4%ICx^Zb&>j4FHH{WP}+DzkLvE`*gWDP-w< zi_Rgn$}BFd=>CHd32`<%xF~4#$g&2ycgn)syV(#Sy1xXY-s#P(CSPRISa@XCDIE?` z!e{dEXy8+LB$1{rURH;?MP!8y@2P)GG3uEd0l z<8e?3T};a>GQ0=HZ`ypb+4Q3B6sE@y3QEooF3r|+h&wNtn6*itHi&ubVj{82snDv3N>nCkkx3%GZt2l;}#G&8aS2>-g z2slZusnc$q#>Q_ubkep{(XY}OH(To{j+U?DL1zdEL}H`TM<)`;A+wDZZfZcCXV}gz zy1&@StM%f?dq*1YH>NuJF9G(BL<0d4g6Wn>?~g^`BrIYejpop(XL2MdcPQnRxS$YZAzlizF|0OQm0S8A1fj4+4O&}@$WnU*jzOd2%m-UWW9F~;A^o(kK%8rFlpp$#E9{rZbm~fvve*%P4 zQsHX4ACjAmp55#W-F2VY8X{0z$!3pEjELDP*3kQ9+VTN?`}jv@A}?paO_QdLa{1xP za;jk=D8c{##DJ_?E`!j!d9Z(s7=>owS$-jtrw>G~l2YaE_MIrhJ9!;zy9JpRlpM~Y({9DpD| z^&e&m|A=-O7qtU*QT}R$q$5)p;`!VndN*u0`iXqS!aca;t-tX5|LFZ+q51Roqu573 zB?%@G*Q=5QVe~H-Z9|gJOCL+_FKGqpM1Xe8zI#!!jsJNU{tsjHe;+}wUa&@CwY!9F zovLX1ODog3KiwWc&$_q)(|EpqA7hZRm#G+Q{f{C4PqK2vDmDwvx{e4aN`LnBb((iYCihOQBDEF`?|BjK>au7QA^Hp{E>Vz5<^O8)*Gw|C zSsP|z+;*)v;syENG<@GV$R2%o_`)CaszK+@*+}@W@5Fz7^lI(?{+8xJJk3=@e@~DN zQgN5MsUSnu=_G6_#-UMc|0W(%R`@9L{~v9p4(l(^hPzc?XLvN40P9Tkow-e~{;CSu z!O!m%Ms#leD@K3!^Vhd5sAu7#rC$yD+-%UPmf1TM9OSr)iC^WERk{bx%z zfAt?+uYrDu7b-SxG_HZmWDzxGhI~-0-%Jb3ABq$Ih0Cg`LA;6@T|5$CVIT?8s7O8Z z*U<6~4o5pW{6|dx(HEEh?wp|(f~MHsBf(4>^2Tzj0W1ZpN-Z-)84?lvnS1|e0c7LJ z?dOZ0pfu%sUg+d6xzgR9H3?s0rHQ?`XSa(6M}<1^kVYJ~|5*3G*253WpK8NTnrhZ1 zKZ&|oqf5qX^7QGHjPKUYZFh>)->t_0&WL!v(2>eivigK`_Q&IrH~nVn$A*wQ%4ah! z-LP=9IQ<{j$bXL$$Y1|QmRnlGnaKs5)<|#Z0})pZDF_`nW_mki;T${n{%_(IJLIEQ z(Z9*HHuP|qL2TXy8rICS=-hk|68BF zWFXX~LFbDDw4f@P65MOJyUp$%6ZZQxG-v_CABfQGI$DRw7rgNc=VVrD z|GRR3yraT8y8A4-QUjiuQR)XZf74@@i?0SrH=ie+Gd1~Lfa4#PAKrh9DL;x2+Eubn ztxV{C3QOJPA1T7yPYXxSw#xsevt7LW&#JA*U)~}k8@Oi+CeH6VLfnL3o*0hX(!l5M zz4={#23fb!UqOR~b)?hVLSiEQ@s+8{(%zk@x>wxLQPn=O-^3X8D643uVqT2q+jzS7 zc_GM16B|O-$am1*tJP-yyT15wm~ug_nPv^J29SamxjHQMVVo1Xgk0+YZFV5>Wl8$+ z@2(kf;#{k~pWl34cIN2Mw1+*NjyZPflzYIH8DGqu+*`bCem71Bs;4pNu$dSV^h1Zr zq{%_PS)a8HdkE1O+^?NH5Q(uEmVIT}W4U$X#*>9GyjC4lmSY-iC**J3i3Z~No zK6KJJ$$XEN;de(LJo!zinMoOSG3y1r4~a2jKIR`g*Lh4+b&FC5^>=@FrKl}z%z~%C z91Cr7#mgQkFEn-{T$5{xElgqBt-cptzil@{nw#WfSmybv3RHt8o)}qZP!db|0t;U# zB9XDRjHZ2n!KVJrYOoIBbpMq78rkr5#@?uEd~~;lK$v(&n5xd+6~m&=eoXQTWL|vr PY)xeab@|fA#sU8aWL~ea literal 0 HcmV?d00001 diff --git a/docs/api/comments.rst b/docs/api/comments.rst new file mode 100644 index 000000000..a54ecc9ce --- /dev/null +++ b/docs/api/comments.rst @@ -0,0 +1,27 @@ + +.. _comments_api: + +Comment-related objects +======================= + +.. currentmodule:: docx.comments + + +|Comments| objects +------------------ + +.. autoclass:: Comments() + :members: + :inherited-members: + :exclude-members: + part + + +|Comment| objects +------------------ + +.. autoclass:: Comment() + :members: + :inherited-members: + :exclude-members: + part diff --git a/docs/conf.py b/docs/conf.py index 60e28fa4c..883ecb81d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,8 +83,6 @@ .. |CharacterStyle| replace:: :class:`.CharacterStyle` -.. |Comments| replace:: :class:`.Comments` - .. |Cm| replace:: :class:`.Cm` .. |ColorFormat| replace:: :class:`.ColorFormat` @@ -93,6 +91,10 @@ .. |_Columns| replace:: :class:`._Columns` +.. |Comment| replace:: :class:`.Comment` + +.. |Comments| replace:: :class:`.Comments` + .. |CoreProperties| replace:: :class:`.CoreProperties` .. |datetime| replace:: :class:`.datetime.datetime` diff --git a/docs/dev/analysis/features/comments.rst b/docs/dev/analysis/features/comments.rst new file mode 100644 index 000000000..153079caf --- /dev/null +++ b/docs/dev/analysis/features/comments.rst @@ -0,0 +1,419 @@ + +Comments +======== + +Word allows *comments* to be added to a document. This is an aspect of the *reviewing* +feature-set and is typically used by a second party to provide feedback to the author +without changing the document itself. + +The procedure is simple: + +- You select some range of text with the mouse or Shift+Arrow keys +- You press the *New Comment* button (Review toolbar) +- You type or paste in your comment + +.. image:: /_static/img/comment-parts.png + +**Comment Anatomy.** Each comment has two parts, the *comment-reference* and the +*comment-content*: + +The *comment-refererence*, sometimes *comment-anchor*, is the text you selected before +pressing the *New Comment* button. It is a *range* in the document content delimited by +a start marker and an end marker, and containing the *id* of the comment that refers to +it. + +The *comment-content* is whatever content you typed or pasted in. The content for each +comment is stored in the separate *comments-part* (part-name ``word/comments.xml``) as a +distinct comment object. Each comment has a unique id, allowing a comment reference to +be associated with its content and vice versa. + +**Comment Reference.** The comment-reference is a *range*. A range must both start and +end at an even *run* boundary. Intuitively, a range corresponds to a *selection* of text +in the Word UI, one formed by dragging with the mouse or using the *Shift-Arrow* keys. + +In general a range can span "run containers", such as paragraphs, such that the range +begins in one paragraph and ends in a later paragraph. However, a range must enclose +*contiguous* runs, such that a range that contains only two vertically adjacent cells in +a multi-column table is not possible (even though such a selection with the mouse is +possible). + +**Comment Content.** Interestingly, although commonly used to contain a single line of +plain text, the comment-content can contain essentially any content that can appear in +the document body. This includes rich text with emphasis, runs with a different typeface +and size, both paragraph and character styles, hyperlinks, images, and tables. Note that +tables do not appear in the comment as displayed in the *comment-sidebar* although they +do apper in the *reviewing-pane*. + +**Comment Metadata.** Each comment can be assigned *author*, *initals*, and *date* +metadata. In Word, these fields are assigned automatically based on values in ``Settings +> User`` of the installed Word application. These may be configured automatically in an +enterprise installation, based on the user account, but by default they are empty. + +*author* metadata is required, although silently assigned the empty string by Word if +the user name is not configured. *initials* is optional, but always set by Word, to the +empty string if not configured. *date* is also optional, but always set by Word to the +date and time the comment was added (seconds resolution, UTC). + +**Additional Features.** Later versions of Word allow a comment to be *resolved*. A +comment in this state will appear grayed-out in the Word UI. Later versions of Word also +allow a comment to be *replied to*, forming a *comment thread*. Neither of these +features is supported by the initial implementation of comments in *python-docx*. + +The resolved-status and replies features are implemented as *extensions* and involve two +additional comment-related parts: + +- `commentsExtended.xml` - contains completion (resolved) status and parent-id for + threading comment responses; keys to `w15:paraId` of comment paragraph in + `comments.xml` +- `commentsIds.xml` - maps `w16cid:paraId` to `w16cid:durableId`, not sure what that is + exactly. + +**Applicability.** Note that comments cannot be added to a header or footer and cannot +be nested inside a comment itself. In general the *python-docx* API will not allow these +operations but if you outsmart it then the resulting comment will either be silently +removed or trigger a repair error when the document is loaded by Word. + + +Word Behavior +------------- + +- A DOCX package does not contain a ``comments.xml`` part by default. It is added to the + package when the first comment is added to the document. + +- A newly-created comment contains a single paragraph + +- Word starts `w:id` at 0 and increments from there. It appears to use a + `max(comment_ids) + 1` algorithm rather than aggressively filling in id numbering + gaps. + +- Word-behavior: looks like Word doesn't allow a "zero-length" comment reference; if you + insert a comment when no text is selected, the word prior to the insertion-point is + selected. + +- Word allows a comment to be applied to a range that starts before any character and + ends after any later character. However, the XML range-markers can only be placed + between runs. Word accommodates this be breaking runs as necessary to start and stop + at the desired character positions. + + +MS API +------ + +.. highlight:: python + +**Document**:: + + Document.Comments + +**Comments** + +https://learn.microsoft.com/en-us/office/vba/api/word.comments:: + + Comments.Add(Range, Text) -> Comment + + # -- retrieve comment by array idx, not comment_id key -- + Comments.Item(idx: Long) -> Comment + + Comments.Count() -> Long + + # -- restrict visible comments to those by a particular reviewer + Comments.ShowBy = "Travis McGuillicuddy" + +**Comment** + +https://learn.microsoft.com/en-us/office/vba/api/word.comment:: + + # -- delete comment and all replies to it -- + Comment.DeleteRecursively() -> void + + # -- open OLE object embedded in comment for editing -- + Comment.Edit() -> void + + # -- get the "parent" comment when this comment is a reply -- + Comment.Ancestor() -> Comment | Nothing + + # -- author of this comment, with email and name fields -- + Comment.Contact -> CoAuthor + + Comment.Date -> Date + Comment.Done -> bool + Comment.IsInk -> bool + + # -- content of the comment, contrast with `Reference` below -- + Comment.Range -> Range + + # -- content within document this comment refers to -- + Comment.Reference -> Range + + Comment.Replies -> Comments + + # -- described in API docs like the same thing as `Reference` -- + Comment.Scope -> Range + + +Candidate Protocol +------------------ + +.. highlight:: python + +The critical required reference for adding a comment is the *range* referred to by the +comment; i.e. the "selection" of text that is being commented on. Because this range +must start and end at an even run boundary, it is enough to specify the first and last +run in the range, where a single run can be both the start and end run:: + + >>> paragraph = document.add_paragraph("Hello, world!") + >>> document.add_comment( + ... runs=paragraph.runs, + ... text="I have this to say about that" + ... author="Steve Canny", + ... initials="SC", + ... ) + + +A single run can be provided when that is more convenient:: + + >>> paragraph = document.add_paragraph("Summary: ") + >>> run = paragraph.add_run("{{place-summary-here}} + >>> document.add_comment( + ... run, text="The AI model will replace this placeholder with a summary" + ... ) + + +Note that `author` and `initials` are optional parameters; both default to the empty +string. + +`text` is also an optional parameter and also defaults to the empty string. Omitting a +`text` argument (or passing `text=""`) produces a comment containing a single paragraph +you can immediately add runs to and add additional paragraphs after: + + >>> paragraph = document.add_paragraph("Summary: ") + >>> run = paragraph.add_run("{{place-summary-here}}") + >>> comment = document.add_comment(run) + >>> paragraph = comment.paragraphs[0] + >>> paragraph.add_run("The ") + >>> paragraph.add_run("AI model").bold = True + >>> paragraph.add_run(" will replace this placeholder with a ") + >>> paragraph.add_run("summary").bold = True + + +A method directly on |Run| may also be convenient, since you will always have the first +run of the range in hand when adding a comment but may not have ready access to the +``document`` object:: + + >>> runs = find_sequence_of_one_or_more_runs_to_comment_on() + >>> runs[0].add_comment( + ... last_run=runs[-1], + ... text="The AI model will replace this placeholder with a summary", + ... ) + + +However, in this situation we would need to qualify the runs as being inside the +document part and not in a header or footer or comment, and perhaps other invalid +comment locations. I believe comments can be applied to footnotes and endnotes though. + + +Specimen XML +------------ + +.. highlight:: xml + +``comments.xml`` (namespace declarations may vary):: + + + + > + + + + + + + + + + I have this to say about that + + + + + + +Comment reference in document body:: + + + + + Hello, world! + + + + + + + + + + + +**Notes** + +- `w:comment` is a *block-item* container, and can contain any content that can appear + in a document body or table cell, including both paragraphs and tables (and whatever + can go inside those, like images, hyperlinks, etc. + +- Word places the `w:annotationRef`-containing run as the first run in the first + paragraph of the comment. I haven't been able to detect any behavior change caused by + leaving this out or placing it elsewhere in the comment content. + +- Relationships referenced from within `w:comment` content are relationships *from the + comments part* to the image part, hyperlink, etc. + +- `w:commentRangeStart` and `w:commentRangeEnd` elements are *optional*. The + authoritative position of the comment is the required `w:commentReference` element. + This means the *ending* location of a comment anchor can be efficiently found using + XPath. + + +Schema Excerpt +-------------- + +**Notes:** + +- `commentRangeStart` and `commentRangeEnd` are both type `CT_MarkupRange` and both + belong to `EG_RunLevelElts` (peers of `w:r`) which gives them their positioning in the + document structure. + +- These two markers can occur at the *block* level, at the *run* level, or at the *table + row* or *cell* level. However Word only seems to use them as peers of `w:r`. These can + occur as a sibling to: + + - a *paragraph* (`w:p`) + - a *table* (`w:tbl`) + - a *run* (`w:r`) + - a *table row* (`w:tr`) + - a *table cell* (`w:tc`) + +.. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dev/analysis/index.rst b/docs/dev/analysis/index.rst index b32bf5cc1..25bf5fb4e 100644 --- a/docs/dev/analysis/index.rst +++ b/docs/dev/analysis/index.rst @@ -10,6 +10,7 @@ Feature Analysis .. toctree:: :titlesonly: + features/comments features/header features/settings features/text/index diff --git a/docs/index.rst b/docs/index.rst index 1b1029787..aee0acfbf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,6 +81,7 @@ User Guide user/api-concepts user/styles-understanding user/styles-using + user/comments user/shapes @@ -96,6 +97,7 @@ API Documentation api/text api/table api/section + api/comments api/shape api/dml api/shared diff --git a/docs/user/comments.rst b/docs/user/comments.rst new file mode 100644 index 000000000..869d6f5f1 --- /dev/null +++ b/docs/user/comments.rst @@ -0,0 +1,168 @@ +.. _comments: + +Working with Comments +===================== + +Word allows *comments* to be added to a document. This is an aspect of the *reviewing* +feature-set and is typically used by a second party to provide feedback to the author +without changing the document itself. + +The procedure is simple: + +- You select some range of text with the mouse or Shift+Arrow keys +- You press the *New Comment* button (Review toolbar) +- You type or paste in your comment + +.. image:: /_static/img/comment-parts.png + +A comment can only be added to the main document. A comment cannot be added in a header, +a footer, or within a comment. A comment _can_ be added to a footnote or endnote, but +those are not yet supported by *python-docx*. + +**Comment Anatomy.** Each comment has two parts, the *comment-reference* and the +*comment-content*: + +The **comment-refererence**, sometimes *comment-anchor*, is the text in the main +document you selected before pressing the *New Comment* button. It is a so-called +*range* in the main document that starts at the first selected character and ends after +the last one. + +The **comment-content**, sometimes just *comment*, is whatever content you typed or +pasted in. The content for each comment is stored in a separate comment object, and +these comment objects are stored in a separate *comments-part* (part-name +``word/comments.xml``), not in the main document. Each comment is assigned a unique id +when it is created, allowing the comment reference to be associated with its content and +vice versa. + +**Comment Reference.** The comment-reference is a *range*. A range must both start and +end at an even *run* boundary. Intuitively, a range corresponds to a *selection* of text +in the Word UI, one formed by dragging with the mouse or using the *Shift-Arrow* keys. + +In the XML, this range is delimited by a start marker `` and an +end marker ``, both of which contain the *id* of the comment they +delimit. The start marker appears before the run starting with the first character of +the range and the end marker appears immediately after the run ending with the last +character of the range. Adding a comment that references an arbitrary range of text in +an existing document may require splitting runs on the desired character boundaries. + +In general a range can span paragraphs, such that the range begins in one paragraph and +ends in a later paragraph. However, a range must enclose *contiguous* runs, such that a +range that contains only two vertically adjacent cells in a multi-column table is not +possible (even though Word allows such a selection with the mouse). + +**Comment Content.** Interestingly, although commonly used to contain a single line of +plain text, the comment-content can contain essentially any content that can appear in +the document body. This includes rich text with emphasis, runs with a different typeface +and size, both paragraph and character styles, hyperlinks, images, and tables. Note that +tables do not appear in the comment as displayed in the *comment-sidebar* although they +do apper in the *reviewing-pane*. + +**Comment Metadata.** Each comment can be assigned *author*, *initals*, and *date* +metadata. In Word, these fields are assigned automatically based on values in ``Settings +> User`` of the installed Word application. These might be configured automatically in +an enterprise installation, based on the user account, but by default they are empty. + +*author* metadata is required, although silently assigned the empty string by Word if +the user name is not configured. *initials* is optional, but always set by Word, to the +empty string if not configured. *date* is also optional, but always set by Word to the +UTC date and time the comment was added, with seconds resolution (no milliseconds or +microseconds). + +**Additional Features.** Later versions of Word allow a comment to be *resolved*. A +comment in this state will appear grayed-out in the Word UI. Later versions of Word also +allow a comment to be *replied to*, forming a *comment thread*. Neither of these +features is supported by the initial implementation of comments in *python-docx*. + +**Applicability.** Note that comments cannot be added to a header or footer and cannot +be nested inside a comment itself. In general the *python-docx* API will not allow these +operations but if you outsmart it then the resulting comment will either be silently +removed or trigger a repair error when the document is loaded by Word. + + +Adding a Comment +---------------- + +A simple example is adding a comment to a paragraph:: + + >>> from docx import Document + >>> document = Document() + >>> paragraph = document.add_paragraph("Hello, world!") + + >>> comment = document.add_comment( + ... runs=paragraph.runs, + ... text="I have this to say about that" + ... author="Steve Canny", + ... initials="SC", + ... ) + >>> comment + + >>> comment.id + 0 + >>> comment.author + 'Steve Canny' + >>> comment.initials + 'SC' + >>> comment.date + datetime.datetime(2025, 6, 11, 20, 42, 30, 0, tzinfo=datetime.timezone.utc) + >>> comment.text + 'I have this to say about that' + +The API documentation for :meth:`.Document.add_comment` provides further details. + + +Accessing and using the Comments collection +------------------------------------------- + +The comments collection is accessed via the :attr:`.Document.comments` property:: + + >>> comments = document.comments + >>> comments + + >>> len(comments) + 1 + +The comments collection supports random access to a comment by its id:: + + >>> comment = comments.get(0) + >>> comment + + + +Adding rich content to a comment +-------------------------------- + +A comment is a _block-item container_, just like the document body or a table cell, so +it can contain any content that can appear in those places. It does not contain +page-layout sections and cannot contain a comment reference, but it can contain multiple +paragraphs and/or tables, and runs within paragraphs can have emphasis such as bold or +italic, and have images or hyperlinks. + +A comment created with `text=""` will contain a single paragraph with a single empty run +containing the so-called *annotation reference* but no text. It's probably best to leave +this run as it is but you can freely add additional runs to the paragraph that contain +whatever content you like. + +The methods for adding this content are the same as those used for the document and +table cells:: + + >>> paragraph = document.add_paragraph("The rain in Spain.") + >>> comment = document.add_comment( + ... runs=paragraph.runs, + ... text="", + ... ) + >>> cmt_para = comment.paragraphs[0] + >>> cmt_para.add_run("Please finish this thought. I believe it should be ") + >>> cmt_para.add_run("falls mainly in the plain.").bold = True + + +Updating comment metadata +------------------------- + +The author and initials metadata can be updated as desired:: + + >>> comment.author = "John Smith" + >>> comment.initials = "JS" + >>> comment.author + 'John Smith' + >>> comment.initials + 'JS' diff --git a/pyproject.toml b/pyproject.toml index 7c343f2e2..bb347f8d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,22 @@ dynamic = ["version"] keywords = ["docx", "office", "openxml", "word"] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.9" +# requires-python = ">=3.9" +requires-python = ">=3.9,<3.10" + +[dependency-groups] +dev = [ + "Jinja2==2.11.3", + "MarkupSafe==0.23", + "Sphinx==1.8.6", + "alabaster<0.7.14", + "behave>=1.2.6", + "pyparsing>=3.2.3", + "pyright>=1.1.401", + "pytest>=8.4.0", + "ruff>=0.11.13", + "types-lxml-multi-subclass>=2025.3.30", +] [project.urls] Changelog = "https://github.com/python-openxml/python-docx/blob/master/HISTORY.rst" @@ -109,12 +124,3 @@ known-local-folder = ["helpers"] [tool.setuptools.dynamic] version = {attr = "docx.__version__"} -[dependency-groups] -dev = [ - "behave>=1.2.6", - "pyparsing>=3.2.3", - "pyright>=1.1.401", - "pytest>=8.4.0", - "ruff>=0.11.13", - "types-lxml-multi-subclass>=2025.3.30", -] diff --git a/src/docx/comments.py b/src/docx/comments.py index 9b69cbcec..8ea195224 100644 --- a/src/docx/comments.py +++ b/src/docx/comments.py @@ -150,7 +150,7 @@ def text(self) -> str: Only content in paragraphs is included and of course all emphasis and styling is stripped. - Paragraph boundaries are indicated with a newline ("\n") + Paragraph boundaries are indicated with a newline (`"\\\\n"`) """ return "\n".join(p.text for p in self.paragraphs) diff --git a/src/docx/document.py b/src/docx/document.py index 1168c4ae8..73757b46d 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -39,7 +39,11 @@ def __init__(self, element: CT_Document, part: DocumentPart): self.__body = None def add_comment( - self, runs: Run | Sequence[Run], text: str, author: str = "", initials: str | None = "" + self, + runs: Run | Sequence[Run], + text: str | None = "", + author: str = "", + initials: str | None = "", ) -> Comment: """Add a comment to the document, anchored to the specified runs. diff --git a/src/docx/templates/default-comments.xml b/src/docx/templates/default-comments.xml index 2afdda20b..2a36ca987 100644 --- a/src/docx/templates/default-comments.xml +++ b/src/docx/templates/default-comments.xml @@ -1,5 +1,12 @@ + xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" + xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" + xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" +/> diff --git a/uv.lock b/uv.lock index bbef867c8..da04bfabf 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,24 @@ version = 1 revision = 1 -requires-python = ">=3.9" +requires-python = "==3.9.*" + +[[package]] +name = "alabaster" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857 }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] [[package]] name = "beautifulsoup4" @@ -29,6 +47,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/6c/ec9169548b6c4cb877aaa6773408ca08ae2a282805b958dbc163cb19822d/behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c", size = 136779 }, ] +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -47,18 +96,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786 }, ] +[[package]] +name = "docutils" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/17/559b4d020f4b46e0287a2eddf2d8ebf76318fd3bd495f1625414b052fdc9/docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", size = 2016138 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5e/6003a0d1f37725ec2ebd4046b657abb9372202655f96e76795dca8c0063c/docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61", size = 575533 }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, ] +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -68,80 +144,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] +[[package]] +name = "jinja2" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6", size = 257589 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/c2/1eece8c95ddbc9b1aeb64f5783a9e07a286de42191b7204d67b7496ddf35/Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", size = 125699 }, +] + [[package]] name = "lxml" version = "5.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838 }, - { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827 }, - { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098 }, - { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261 }, - { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621 }, - { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231 }, - { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279 }, - { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405 }, - { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169 }, - { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691 }, - { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503 }, - { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346 }, - { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139 }, - { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609 }, - { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285 }, - { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507 }, - { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104 }, - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240 }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685 }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164 }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206 }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144 }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124 }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520 }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016 }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884 }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690 }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418 }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092 }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231 }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798 }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195 }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243 }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197 }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086 }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613 }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008 }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915 }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890 }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817 }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916 }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274 }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757 }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028 }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487 }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688 }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043 }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569 }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270 }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606 }, { url = "https://files.pythonhosted.org/packages/1e/04/acd238222ea25683e43ac7113facc380b3aaf77c53e7d88c4f544cef02ca/lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e", size = 8082189 }, { url = "https://files.pythonhosted.org/packages/d6/4e/cc7fe9ccb9999cc648492ce970b63c657606aefc7d0fba46b17aa2ba93fb/lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40", size = 4384950 }, { url = "https://files.pythonhosted.org/packages/56/bf/acd219c489346d0243a30769b9d446b71e5608581db49a18c8d91a669e19/lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729", size = 5209823 }, @@ -153,12 +173,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/53/979165f50a853dab1cf3b9e53105032d55f85c5993f94afc4d9a61a22877/lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142", size = 5192346 }, { url = "https://files.pythonhosted.org/packages/17/2b/f37b5ae28949143f863ba3066b30eede6107fc9a503bd0d01677d4e2a1e0/lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6", size = 3478275 }, { url = "https://files.pythonhosted.org/packages/9a/d5/b795a183680126147665a8eeda8e802c180f2f7661aa9a550bba5bcdae63/lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1", size = 3806275 }, - { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319 }, - { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614 }, - { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273 }, - { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552 }, - { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091 }, - { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862 }, { url = "https://files.pythonhosted.org/packages/ad/fb/d19b67e4bb63adc20574ba3476cf763b3514df1a37551084b890254e4b15/lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530", size = 3891034 }, { url = "https://files.pythonhosted.org/packages/c9/5d/6e1033ee0cdb2f9bc93164f9df14e42cb5bbf1bbed3bf67f687de2763104/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6", size = 4207420 }, { url = "https://files.pythonhosted.org/packages/f3/4b/23ac79efc32d913259d66672c5f93daac7750a3d97cdc1c1a9a5d1c1b46c/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877", size = 4305106 }, @@ -167,6 +181,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/25/d381abcfd00102d3304aa191caab62f6e3bcbac93ee248771db6be153dfd/lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987", size = 3486416 }, ] +[[package]] +name = "markupsafe" +version = "0.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-0.23.tar.gz", hash = "sha256:a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3", size = 13416 } + [[package]] name = "nodeenv" version = "1.9.1" @@ -253,12 +273,12 @@ version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } wheels = [ @@ -275,11 +295,15 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "alabaster" }, { name = "behave" }, + { name = "jinja2" }, + { name = "markupsafe" }, { name = "pyparsing" }, { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, + { name = "sphinx" }, { name = "types-lxml-multi-subclass" }, ] @@ -291,14 +315,33 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "alabaster", specifier = "<0.7.14" }, { name = "behave", specifier = ">=1.2.6" }, + { name = "jinja2", specifier = "==2.11.3" }, + { name = "markupsafe", specifier = "==0.23" }, { name = "pyparsing", specifier = ">=3.2.3" }, { name = "pyright", specifier = ">=1.1.401" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "ruff", specifier = ">=0.11.13" }, + { name = "sphinx", specifier = "==1.8.6" }, { name = "types-lxml-multi-subclass", specifier = ">=2025.3.30" }, ] +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, +] + [[package]] name = "ruff" version = "0.11.13" @@ -324,6 +367,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, ] +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, +] + [[package]] name = "six" version = "1.17.0" @@ -333,6 +385,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274 }, +] + [[package]] name = "soupsieve" version = "2.7" @@ -342,42 +403,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, ] +[[package]] +name = "sphinx" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "six" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-websupport" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/74/5cef400220b2f22a4c85540b9ba20234525571b8b851be8a9ac219326a11/Sphinx-1.8.6.tar.gz", hash = "sha256:e096b1b369dbb0fcb95a31ba8c9e1ae98c588e601f08eada032248e1696de4b1", size = 5816141 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/da/e1b65da61267aeb92a76b6b6752430bcc076d98b723687929eb3d2e0d128/Sphinx-1.8.6-py2.py3-none-any.whl", hash = "sha256:5973adbb19a5de30e15ab394ec8bc05700317fa83f122c349dd01804d983720f", size = 3110177 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "sphinxcontrib-websupport" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/aa/b03a3f569a52b6f21a579d168083a27036c1f606269e34abdf5b70fe3a2c/sphinxcontrib-websupport-1.2.4.tar.gz", hash = "sha256:4edf0223a0685a7c485ae5a156b6f529ba1ee481a1417817935b20bde1956232", size = 602360 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e5/2a547830845e6e6e5d97b3246fc1e3ec74cba879c9adc5a8e27f1291bca3/sphinxcontrib_websupport-1.2.4-py2.py3-none-any.whl", hash = "sha256:6fc9287dfc823fe9aa432463edd6cea47fa9ebbf488d7f289b322ffcfca075c7", size = 39924 }, +] + [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] @@ -398,7 +474,7 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "cssselect" }, { name = "types-html5lib" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b7/3a/7f6d1d3b921404efef20ed1ddc2b6f1333e3f0bc5b91da37874e786ff835/types_lxml_multi_subclass-2025.3.30.tar.gz", hash = "sha256:7ac7a78e592fdba16951668968b21511adda49bbefbc0f130e55501b70e068b4", size = 153188 } wheels = [ @@ -413,3 +489,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d0 wheels = [ { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, ] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] From 1fe660198aab18a421c95d019f53b2aa22d2fe2f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 12 Jun 2025 20:57:40 -0700 Subject: [PATCH 130/131] build: small adjustments for tox --- pyproject.toml | 4 +- tox.ini | 2 +- uv.lock | 262 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 260 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bb347f8d3..3650ce4d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,7 @@ dynamic = ["version"] keywords = ["docx", "office", "openxml", "word"] license = { text = "MIT" } readme = "README.md" -# requires-python = ">=3.9" -requires-python = ">=3.9,<3.10" +requires-python = ">=3.9" [dependency-groups] dev = [ @@ -44,6 +43,7 @@ dev = [ "pyright>=1.1.401", "pytest>=8.4.0", "ruff>=0.11.13", + "tox>=4.26.0", "types-lxml-multi-subclass>=2025.3.30", ] diff --git a/tox.ini b/tox.ini index 37acaa5fa..1f4741b6f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38, py39, py310, py311, py312 +envlist = py39, py310, py311, py312, py313 [testenv] deps = -rrequirements-test.txt diff --git a/uv.lock b/uv.lock index da04bfabf..675fe6777 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 1 -requires-python = "==3.9.*" +requires-python = ">=3.9" [[package]] name = "alabaster" @@ -47,6 +47,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/6c/ec9169548b6c4cb877aaa6773408ca08ae2a282805b958dbc163cb19822d/behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c", size = 136779 }, ] +[[package]] +name = "cachetools" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/b0/f539a1ddff36644c28a61490056e5bae43bd7386d9f9c69beae2d7e7d6d1/cachetools-6.0.0.tar.gz", hash = "sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf", size = 30160 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c3/8bb087c903c95a570015ce84e0c23ae1d79f528c349cbc141b5c4e250293/cachetools-6.0.0-py3-none-any.whl", hash = "sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e", size = 10964 }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -56,12 +65,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, ] +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, @@ -96,6 +166,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786 }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + [[package]] name = "docutils" version = "0.17.1" @@ -110,13 +189,22 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, ] +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + [[package]] name = "idna" version = "3.10" @@ -162,6 +250,74 @@ version = "5.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838 }, + { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827 }, + { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098 }, + { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261 }, + { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621 }, + { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231 }, + { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279 }, + { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405 }, + { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169 }, + { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691 }, + { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503 }, + { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346 }, + { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139 }, + { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609 }, + { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285 }, + { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507 }, + { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104 }, + { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240 }, + { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685 }, + { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164 }, + { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206 }, + { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144 }, + { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124 }, + { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520 }, + { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016 }, + { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884 }, + { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690 }, + { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418 }, + { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092 }, + { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231 }, + { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798 }, + { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195 }, + { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243 }, + { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197 }, + { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, + { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, + { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, + { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, + { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, + { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, + { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, + { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, + { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, + { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, + { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, + { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, + { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, + { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, + { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, + { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, + { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086 }, + { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613 }, + { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008 }, + { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915 }, + { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890 }, + { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817 }, + { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916 }, + { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274 }, + { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757 }, + { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028 }, + { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487 }, + { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688 }, + { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043 }, + { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569 }, + { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270 }, + { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606 }, { url = "https://files.pythonhosted.org/packages/1e/04/acd238222ea25683e43ac7113facc380b3aaf77c53e7d88c4f544cef02ca/lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e", size = 8082189 }, { url = "https://files.pythonhosted.org/packages/d6/4e/cc7fe9ccb9999cc648492ce970b63c657606aefc7d0fba46b17aa2ba93fb/lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40", size = 4384950 }, { url = "https://files.pythonhosted.org/packages/56/bf/acd219c489346d0243a30769b9d446b71e5608581db49a18c8d91a669e19/lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729", size = 5209823 }, @@ -173,6 +329,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/53/979165f50a853dab1cf3b9e53105032d55f85c5993f94afc4d9a61a22877/lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142", size = 5192346 }, { url = "https://files.pythonhosted.org/packages/17/2b/f37b5ae28949143f863ba3066b30eede6107fc9a503bd0d01677d4e2a1e0/lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6", size = 3478275 }, { url = "https://files.pythonhosted.org/packages/9a/d5/b795a183680126147665a8eeda8e802c180f2f7661aa9a550bba5bcdae63/lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1", size = 3806275 }, + { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319 }, + { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273 }, + { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552 }, + { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091 }, + { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862 }, { url = "https://files.pythonhosted.org/packages/ad/fb/d19b67e4bb63adc20574ba3476cf763b3514df1a37551084b890254e4b15/lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530", size = 3891034 }, { url = "https://files.pythonhosted.org/packages/c9/5d/6e1033ee0cdb2f9bc93164f9df14e42cb5bbf1bbed3bf67f687de2763104/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6", size = 4207420 }, { url = "https://files.pythonhosted.org/packages/f3/4b/23ac79efc32d913259d66672c5f93daac7750a3d97cdc1c1a9a5d1c1b46c/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877", size = 4305106 }, @@ -227,6 +389,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/b3/f6cc950042bfdbe98672e7c834d930f85920fb7d3359f59096e8d2799617/parse_type-0.6.4-py2.py3-none-any.whl", hash = "sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c", size = 27442 }, ] +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -254,6 +425,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, ] +[[package]] +name = "pyproject-api" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158 }, +] + [[package]] name = "pyright" version = "1.1.401" @@ -273,12 +457,12 @@ version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } wheels = [ @@ -304,6 +488,7 @@ dev = [ { name = "pytest" }, { name = "ruff" }, { name = "sphinx" }, + { name = "tox" }, { name = "types-lxml-multi-subclass" }, ] @@ -324,6 +509,7 @@ dev = [ { name = "pytest", specifier = ">=8.4.0" }, { name = "ruff", specifier = ">=0.11.13" }, { name = "sphinx", specifier = "==1.8.6" }, + { name = "tox", specifier = ">=4.26.0" }, { name = "types-lxml-multi-subclass", specifier = ">=2025.3.30" }, ] @@ -454,9 +640,61 @@ version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] +[[package]] +name = "tox" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/dcec0c00321a107f7f697fd00754c5112572ea6dcacb40b16d8c3eea7c37/tox-4.26.0.tar.gz", hash = "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca", size = 197260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/14/f58b4087cf248b18c795b5c838c7a8d1428dfb07cb468dad3ec7f54041ab/tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", size = 172761 }, +] + [[package]] name = "types-html5lib" version = "1.1.11.20250516" @@ -474,7 +712,7 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "cssselect" }, { name = "types-html5lib" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b7/3a/7f6d1d3b921404efef20ed1ddc2b6f1333e3f0bc5b91da37874e786ff835/types_lxml_multi_subclass-2025.3.30.tar.gz", hash = "sha256:7ac7a78e592fdba16951668968b21511adda49bbefbc0f130e55501b70e068b4", size = 153188 } wheels = [ @@ -498,3 +736,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] + +[[package]] +name = "virtualenv" +version = "20.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 }, +] From e45454602b53e8e572b179ccf1c91093ec9f4ed7 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 16 Jun 2025 13:45:03 -0700 Subject: [PATCH 131/131] release: prepare v1.2.0 release --- HISTORY.rst | 8 + Makefile | 17 +-- pyproject.toml | 1 + src/docx/__init__.py | 2 +- uv.lock | 355 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 373 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0dab17d87..69bba4161 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ Release History --------------- +1.2.0 (2025-06-16) +++++++++++++++++++ + +- Add support for comments +- Drop support for Python 3.8, add testing for Python 3.13 + + 1.1.2 (2024-05-01) ++++++++++++++++++ @@ -10,6 +17,7 @@ Release History - Fix #1385 Support use of Part._rels by python-docx-template - Add support and testing for Python 3.12 + 1.1.1 (2024-04-29) ++++++++++++++++++ diff --git a/Makefile b/Makefile index da0d7a4ac..2b2fb4121 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ BEHAVE = behave MAKE = make PYTHON = python -BUILD = $(PYTHON) -m build TWINE = $(PYTHON) -m twine .PHONY: accept build clean cleandocs coverage docs install opendocs sdist test @@ -24,10 +23,10 @@ help: @echo " wheel generate a binary distribution into dist/" accept: - $(BEHAVE) --stop + uv run $(BEHAVE) --stop build: - $(BUILD) + uv build clean: # find . -type f -name \*.pyc -exec rm {} \; @@ -38,7 +37,7 @@ cleandocs: $(MAKE) -C docs clean coverage: - py.test --cov-report term-missing --cov=docx tests/ + uv run pytest --cov-report term-missing --cov=docx tests/ docs: $(MAKE) -C docs html @@ -50,16 +49,16 @@ opendocs: open docs/.build/html/index.html sdist: - $(BUILD) --sdist . + uv build --sdist test: - pytest -x + uv run pytest -x test-upload: sdist wheel - $(TWINE) upload --repository testpypi dist/* + uv run $(TWINE) upload --repository testpypi dist/* upload: clean sdist wheel - $(TWINE) upload dist/* + uv run $(TWINE) upload dist/* wheel: - $(BUILD) --wheel . + uv build --wheel diff --git a/pyproject.toml b/pyproject.toml index 3650ce4d1..b3dc0be02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dev = [ "pytest>=8.4.0", "ruff>=0.11.13", "tox>=4.26.0", + "twine>=6.1.0", "types-lxml-multi-subclass>=2025.3.30", ] diff --git a/src/docx/__init__.py b/src/docx/__init__.py index 987e8a267..fd06c84d2 100644 --- a/src/docx/__init__.py +++ b/src/docx/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from docx.opc.part import Part -__version__ = "1.1.2" +__version__ = "1.2.0" __all__ = ["Document"] diff --git a/uv.lock b/uv.lock index 675fe6777..7888c5298 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + [[package]] name = "beautifulsoup4" version = "4.13.4" @@ -65,6 +74,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, ] +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, +] + [[package]] name = "chardet" version = "5.2.0" @@ -157,6 +215,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cryptography" +version = "45.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335 }, + { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487 }, + { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922 }, + { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433 }, + { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163 }, + { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687 }, + { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623 }, + { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447 }, + { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830 }, + { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746 }, + { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456 }, + { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495 }, + { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540 }, + { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052 }, + { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024 }, + { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442 }, + { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038 }, + { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964 }, + { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732 }, + { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424 }, + { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438 }, + { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622 }, + { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899 }, + { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900 }, + { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422 }, + { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475 }, +] + [[package]] name = "cssselect" version = "1.3.0" @@ -205,6 +300,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, ] +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 }, +] + [[package]] name = "idna" version = "3.10" @@ -223,6 +330,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -232,6 +351,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, +] + [[package]] name = "jinja2" version = "2.11.3" @@ -244,6 +408,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/c2/1eece8c95ddbc9b1aeb64f5783a9e07a286de42191b7204d67b7496ddf35/Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", size = 125699 }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 }, +] + [[package]] name = "lxml" version = "5.4.0" @@ -343,12 +525,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/25/d381abcfd00102d3304aa191caab62f6e3bcbac93ee248771db6be153dfd/lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987", size = 3486416 }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + [[package]] name = "markupsafe" version = "0.23" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-0.23.tar.gz", hash = "sha256:a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3", size = 13416 } +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, +] + +[[package]] +name = "nh3" +version = "0.2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678 }, + { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774 }, + { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012 }, + { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619 }, + { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384 }, + { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908 }, + { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180 }, + { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747 }, + { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908 }, + { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133 }, + { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328 }, + { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020 }, + { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878 }, + { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460 }, + { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369 }, + { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036 }, + { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712 }, + { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559 }, + { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591 }, + { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670 }, + { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093 }, + { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623 }, + { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -407,6 +650,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -489,6 +741,7 @@ dev = [ { name = "ruff" }, { name = "sphinx" }, { name = "tox" }, + { name = "twine" }, { name = "types-lxml-multi-subclass" }, ] @@ -510,9 +763,33 @@ dev = [ { name = "ruff", specifier = ">=0.11.13" }, { name = "sphinx", specifier = "==1.8.6" }, { name = "tox", specifier = ">=4.26.0" }, + { name = "twine", specifier = ">=6.1.0" }, { name = "types-lxml-multi-subclass", specifier = ">=2025.3.30" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "readme-renderer" +version = "43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/b5/536c775084d239df6345dccf9b043419c7e3308bc31be4c7882196abc62e/readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311", size = 31768 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/be/3ea20dc38b9db08387cf97997a85a7d51527ea2057d71118feb0aa8afa55/readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9", size = 13301 }, +] + [[package]] name = "requests" version = "2.32.4" @@ -528,6 +805,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + [[package]] name = "ruff" version = "0.11.13" @@ -553,6 +865,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, ] +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -695,6 +1020,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/14/f58b4087cf248b18c795b5c838c7a8d1428dfb07cb468dad3ec7f54041ab/tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", size = 172761 }, ] +[[package]] +name = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 }, +] + [[package]] name = "types-html5lib" version = "1.1.11.20250516" @@ -750,3 +1096,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +]