diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..6b14f6cd5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: ci + +on: + pull_request: + branches: [ master ] + push: + branches: + - master + - develop + +permissions: + contents: write + +jobs: + + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: Install test dependencies + run: | + pip install . + pip install -r requirements-test.txt + - name: Test with pytest + run: pytest --cov=pptx --cov-report term-missing tests + - name: Acceptance tests with behave + run: behave --stop diff --git a/.gitignore b/.gitignore index 49f59feb9..c043a21c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +/build/ .cache .coverage /.tox/ -*.egg-info +/src/*.egg-info *.pyc /dist/ /docs/.build 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/HISTORY.rst b/HISTORY.rst index f160e7394..e1c4e8faf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,60 @@ Release History --------------- +1.0.2 (2024-08-07) +++++++++++++++++++ + +- fix: #1003 restore read-only enum members + +1.0.1 (2024-08-05) +++++++++++++++++++ + +- fix: #1000 add py.typed + + +1.0.0 (2024-08-03) +++++++++++++++++++ + +- fix: #929 raises on JPEG with image/jpg MIME-type +- fix: #943 remove mention of a Px Length subtype +- fix: #972 next-slide-id fails in rare cases +- fix: #990 do not require strict timestamps for Zip +- Add type annotations + + +0.6.23 (2023-11-02) ++++++++++++++++++++ + +- fix: #912 Pillow<=9.5 constraint entails security vulnerability + + +0.6.22 (2023-08-28) ++++++++++++++++++++ + +- Add #909 Add imgW, imgH params to `shapes.add_ole_object()` +- fix: #754 _Relationships.items() raises +- fix: #758 quote in autoshape name must be escaped +- fix: #746 update Python 3.x support in docs +- fix: #748 setup's `license` should be short string +- fix: #762 AttributeError: module 'collections' has no attribute 'abc' + (Windows Python 3.10+) + + +0.6.21 (2021-09-20) ++++++++++++++++++++ + +- Fix #741 _DirPkgReader must implement .__contains__() + + +0.6.20 (2021-09-14) ++++++++++++++++++++ + +- Fix #206 accommodate NULL target-references in relationships. +- Fix #223 escape image filename that appears as literal in XML. +- Fix #517 option to display chart categories/values in reverse order. +- Major refactoring of ancient package loading code. + + 0.6.19 (2021-05-17) +++++++++++++++++++ diff --git a/MANIFEST.in b/MANIFEST.in index 3872c0e4e..14688f1e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include HISTORY.rst LICENSE README.rst tox.ini recursive-include features * -recursive-include pptx/templates * +recursive-include src/pptx/templates * recursive-include tests *.py recursive-include tests/test_files * diff --git a/Makefile b/Makefile index 1ea294b40..7a0fddb2e 100644 --- a/Makefile +++ b/Makefile @@ -1,44 +1,55 @@ BEHAVE = behave MAKE = make -PYTHON = python -SETUP = $(PYTHON) ./setup.py - -.PHONY: accept clean cleandocs coverage docs readme sdist upload +.PHONY: help 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 cached HTML documentation and start fresh" - @echo " coverage run nosetests with coverage" - @echo " docs build HTML documentation using Sphinx (incremental)" - @echo " opendocs open local HTML documentation in browser" - @echo " readme update README.html from README.rst" - @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 cached HTML documentation and start fresh" + @echo " coverage run nosetests with coverage" + @echo " docs build HTML documentation using Sphinx (incremental)" + @echo " opendocs open local HTML documentation in browser" + @echo " test-upload upload distribution to TestPyPI" + @echo " upload upload distribution tarball to PyPI" + +.PHONY: accept accept: $(BEHAVE) --stop +.PHONY: build +build: + rm -rf dist + python -m build + twine check dist/* + +.PHONY: clean clean: find . -type f -name \*.pyc -exec rm {} \; find . -type f -name .DS_Store -exec rm {} \; rm -rf dist .coverage +.PHONY: cleandocs cleandocs: $(MAKE) -C docs clean +.PHONY: coverage coverage: py.test --cov-report term-missing --cov=pptx --cov=tests +.PHONY: docs docs: $(MAKE) -C docs html +.PHONY: opendocs opendocs: open docs/.build/html/index.html -sdist: - $(SETUP) sdist +.PHONY: test-upload +test-upload: build + twine upload --repository testpypi dist/* -upload: - $(SETUP) sdist upload +.PHONY: upload +upload: clean build + twine upload dist/* diff --git a/README.rst b/README.rst index 3573a8d36..24d657b37 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,17 @@ -.. image:: https://travis-ci.org/scanny/python-pptx.svg?branch=master - :target: https://travis-ci.org/scanny/python-pptx - -*python-pptx* is a Python library for creating and updating PowerPoint (.pptx) +*python-pptx* is a Python library for creating, reading, and updating PowerPoint (.pptx) files. -A typical use would be generating a customized PowerPoint presentation from -database content, downloadable by clicking a link in a web application. -Several developers have used it to automate production of presentation-ready -engineering status reports based on information held in their work management -system. It could also be used for making bulk updates to a library of -presentations or simply to automate the production of a slide or two that -would be tedious to get right by hand. +A typical use would be generating a PowerPoint presentation from dynamic content such as +a database query, analytics output, or a JSON payload, perhaps in response to an HTTP +request and downloading the generated PPTX file in response. It runs on any Python +capable platform, including macOS and Linux, and does not require the PowerPoint +application to be installed or licensed. + +It can also be used to analyze PowerPoint files from a corpus, perhaps to extract search +indexing text and images. + +In can also be used to simply automate the production of a slide or two that would be +tedious to get right by hand, which is how this all got started. More information is available in the `python-pptx documentation`_. diff --git a/docs/conf.py b/docs/conf.py index e1117d803..536221211 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 pptx import __version__ # noqa: E402 @@ -30,8 +30,8 @@ def _warn_node(self, msg, node, **kwargs): - if not msg.startswith('nonlocal image URI found:'): - self._warnfunc(msg, '%s:%s' % get_source_line(node), **kwargs) + if not msg.startswith("nonlocal image URI found:"): + self._warnfunc(msg, "%s:%s" % get_source_line(node), **kwargs) sphinx.environment.BuildEnvironment.warn_node = _warn_node @@ -45,31 +45,31 @@ def _warn_node(self, msg, node, **kwargs): # 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.doctest', - 'sphinx.ext.inheritance_diagram', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode' + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.ifconfig", + "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-pptx' -copyright = u'2012, 2013, Steve Canny' +project = u"python-pptx" +copyright = u"2012, 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 @@ -300,7 +300,7 @@ def _warn_node(self, msg, node, **kwargs): .. |_Relationship| replace:: :class:`._Relationship` -.. |RelationshipCollection| replace:: :class:`RelationshipCollection` +.. |_Relationships| replace:: :class:`_Relationships` .. |RGBColor| replace:: :class:`.RGBColor` @@ -381,7 +381,7 @@ def _warn_node(self, msg, node, **kwargs): # 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. @@ -399,7 +399,7 @@ def _warn_node(self, msg, node, **kwargs): # 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 = [] @@ -409,7 +409,7 @@ def _warn_node(self, msg, node, **kwargs): # 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 @@ -417,7 +417,7 @@ def _warn_node(self, msg, node, **kwargs): # 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". @@ -438,7 +438,7 @@ def _warn_node(self, msg, node, **kwargs): # 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. @@ -450,8 +450,7 @@ def _warn_node(self, msg, node, **kwargs): # Custom sidebar templates, maps document names to template names. 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 @@ -485,7 +484,7 @@ def _warn_node(self, msg, node, **kwargs): # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-pptxdoc' +htmlhelp_basename = "python-pptxdoc" # -- Options for LaTeX output ----------------------------------------------- @@ -493,10 +492,8 @@ def _warn_node(self, msg, node, **kwargs): 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': '', } @@ -505,8 +502,13 @@ def _warn_node(self, msg, node, **kwargs): # (source start file, target name, title, author, # documentclass [howto/manual]). latex_documents = [ - ('index', 'python-pptx.tex', u'python-pptx Documentation', - u'Steve Canny', 'manual'), + ( + "index", + "python-pptx.tex", + u"python-pptx Documentation", + u"Steve Canny", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -535,8 +537,7 @@ def _warn_node(self, msg, node, **kwargs): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'python-pptx', u'python-pptx Documentation', - [u'Steve Canny'], 1) + ("index", "python-pptx", u"python-pptx Documentation", [u"Steve Canny"], 1) ] # If true, show URL addresses after external links. @@ -549,9 +550,15 @@ def _warn_node(self, msg, node, **kwargs): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-pptx', u'python-pptx Documentation', - u'Steve Canny', 'python-pptx', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "python-pptx", + u"python-pptx Documentation", + u"Steve Canny", + "python-pptx", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. diff --git a/docs/dev/analysis/cht-access-xlsx.rst b/docs/dev/analysis/cht-access-xlsx.rst index 20706b460..646b14285 100644 --- a/docs/dev/analysis/cht-access-xlsx.rst +++ b/docs/dev/analysis/cht-access-xlsx.rst @@ -55,60 +55,6 @@ Workbook chart. Read-only Object. -Code sketches -------------- - -``ChartPart.xlsx_blob = blob``:: - - @xlsx_blob.setter - def xlsx_blob(self, blob): - xlsx_part = self.xlsx_part - if xlsx_part: - xlsx_part.blob = blob - else: - xlsx_part = EmbeddedXlsxPart.new(blob, self.package) - rId = self.relate_to(xlsx_part, RT.PACKAGE) - externalData = self._element.get_or_add_externalData - externalData.rId = rId - -``@classmethod EmbeddedXlsxPart.new(cls, blob, package)``:: - - partname = cls.next_partname(package) - content_type = CT.SML_SHEET - xlsx_part = EmbeddedXlsxPart(partname, content_type, blob, package) - return xlsx_part - - -``ChartPart.add_or_replace_xlsx(xlsx_stream)``:: - - xlsx_part = self.get_or_add_xlsx_part() - xlsx_stream.seek(0) - xlsx_bytes = xlsx_stream.read() - xlsx_part.blob = xlsx_bytes - - -``ChartPart.xlsx_part``:: - - externalData = self._element.externalData - if externalData is None: - raise ValueError("chart has no embedded worksheet") - rId = externalData.rId - xlsx_part = self.related_parts[rId] - return xlsx_part - - # later ... - - xlsx_stream = BytesIO(xlsx_part.blob) - xlsx_package = OpcPackage.open(xlsx_stream) - workbook_part = xlsx_package.main_document - - -* Maybe can implement just a few Excel parts, enough to access and manipulate - the data necessary. Like Workbook (start part I think) and Worksheet. - -* What about linked rather than embedded Worksheet? - - XML specimens ------------- diff --git a/docs/dev/analysis/cht-axes.rst b/docs/dev/analysis/cht-axes.rst index 3952161f1..e9ab79eb8 100644 --- a/docs/dev/analysis/cht-axes.rst +++ b/docs/dev/analysis/cht-axes.rst @@ -17,6 +17,40 @@ depending upon the chart type. Likewise for a value axis. PowerPoint behavior ------------------- +Reverse-order +~~~~~~~~~~~~~ + +Normally, categories appear left-to-right in the order specified and values appear +vertically in increasing order. This default ordering can be reversed when desired. + +One common case is for the categories in a "horizontal" bar-chart (as opposed to the +"vertical" column-chart). Because the value axis appears at the bottom, categories +appear from bottom-to-top on the categories axis. For many readers this is odd, perhaps +because we read top-to-bottom. + +The axis "direction" can be switched using the `Axis.reverse_order` property. This +controls the value of the `c:xAx/c:scaling/c:orientation{val=minMax|maxMin}` XML +element/attribute. The default is False. + +MS API protocol:: + + >>> axis = Chart.Axes(xlCategory) + >>> axis.ReversePlotOrder + False + >>> axis.ReversePlotOrder = True + >>> axis.ReversePlotOrder + True + +Proposed python-pptx protocol:: + + >>> axis = chart.category_axis + >>> axis.reverse_order + False + >>> axis.reverse_order = True + >>> axis.reverse_order + True + + Tick label position ~~~~~~~~~~~~~~~~~~~ @@ -288,6 +322,10 @@ Related Schema Definitions + + + + @@ -312,6 +350,13 @@ Related Schema Definitions + + + + + + + diff --git a/docs/dev/resources/about_relationships.rst b/docs/dev/resources/about_relationships.rst index 99f57f305..e6e12c445 100644 --- a/docs/dev/resources/about_relationships.rst +++ b/docs/dev/resources/about_relationships.rst @@ -151,7 +151,8 @@ How will dynamic parts (like Slide) interact with its relationship list? ? Should it just add items to the relationship list when it creates new things? -? Does it need some sort of lookup capability in order to delete? Or just have a delete relationship method on RelationshipCollection or something like that. +? Does it need some sort of lookup capability in order to delete? Or just have a delete +relationship method on _Relationships or something like that. Need to come up with a plausible set of use cases to think about a design. Right now the only use case is loading a template into a presentation and diff --git a/docs/index.rst b/docs/index.rst index cadc8a7ad..79ad6c369 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,10 +7,21 @@ Release v\ |version| (:ref:`Installation `) .. include:: ../README.rst +Philosophy +---------- + +|pp| aims to broadly support the PowerPoint format (PPTX, PowerPoint 2007 and later), +but its primary commitment is to be *industrial-grade*, that is, suitable for use in a +commercial setting. Maintaining this robustness requires a high engineering standard +which includes a comprehensive two-level (e2e + unit) testing regimen. This discipline +comes at a cost in development effort/time, but we consider reliability to be an +essential requirement. + + Feature Support --------------- -|pp| has the following capabilities, with many more on the roadmap: +|pp| has the following capabilities: * Round-trip any Open XML presentation (.pptx file) including all its elements * Add slides @@ -21,11 +32,18 @@ Feature Support * Add auto shapes (e.g. polygons, flowchart shapes, etc.) to a slide * Add and manipulate column, bar, line, and pie charts * Access and change core document properties such as title and subject +* And many others ... + +Even with all |pp| does, the PowerPoint document format is very rich and there are still +features |pp| does not support. + + +New features/releases +--------------------- -Additional capabilities are actively being developed and added on a release -cadence of roughly once per month. If you find a feature you need that |pp| -doesn't yet have, reach out via the mailing list or issue tracker and we'll see -if we can jump the queue for you to pop it in there :) +New features are generally added via sponsorship. If there's a new feature you need for +your use case, feel free to reach out at the email address on the github.com/scanny +profile page. Many of the most used features such as charts were added this way. User Guide diff --git a/docs/user/install.rst b/docs/user/install.rst index f402bcc68..b376d58e8 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -13,13 +13,13 @@ the Python Imaging Library (``PIL``). The charting features depend on satisfying these dependencies for you, but if you use the ``setup.py`` installation method you will need to install the dependencies yourself. -Currently |pp| requires Python 2.7, 3.3, 3.4, or 3.6. The tests are run against 2.7 and -3.6 on Travis CI. +Currently |pp| requires Python 2.7 or 3.3 or later. The tests are run against 2.7 and +3.8 on Travis CI. Dependencies ------------ -* Python 2.6, 2.7, 3.3, 3.4, or 3.6 +* Python 2.6, 2.7, 3.3 or later * lxml * Pillow * XlsxWriter (to use charting features) diff --git a/features/act-action.feature b/features/act-action.feature index e206344a8..a05ce19ae 100644 --- a/features/act-action.feature +++ b/features/act-action.feature @@ -25,6 +25,7 @@ Feature: Get and set click action properties | OLE action | OLE_VERB | | run macro | RUN_MACRO | | run program | RUN_PROGRAM | + | play media | NONE | Scenario Outline: Get ActionSetting.hyperlink diff --git a/features/act-hyperlink.feature b/features/act-hyperlink.feature index 666c158c6..9ffea20ae 100644 --- a/features/act-hyperlink.feature +++ b/features/act-hyperlink.feature @@ -9,16 +9,16 @@ Feature: Get or set an external hyperlink on a shape or text run Then click_action.hyperlink.address is Examples: Click actions - | action | value | - | none | None | - | first slide | None | - | last slide viewed | None | - | named slide | slide3.xml | - | hyperlink | http://yahoo.com | - | custom slide show | None | - | OLE action | None | - | run macro | None | - | run program | /Applications/Calculator.app | + | action | value | + | none | None | + | first slide | None | + | last slide viewed | None | + | named slide | slide3.xml | + | hyperlink | http://yahoo.com | + | custom slide show | None | + | OLE action | None | + | run macro | None | + | run program | file:////Applications/Calculator.app | Scenario Outline: Add hyperlink @@ -51,4 +51,3 @@ Feature: Get or set an external hyperlink on a shape or text run | OLE action | | run macro | | run program | - diff --git a/features/cht-axis-props.feature b/features/cht-axis-props.feature index fecdca00b..e8ef5e96d 100644 --- a/features/cht-axis-props.feature +++ b/features/cht-axis-props.feature @@ -70,6 +70,18 @@ Feature: Axis properties | automatic | None | + Scenario Outline: Get Axis.format + Given a axis + Then axis.format is a ChartFormat object + And axis.format.fill is a FillFormat object + And axis.format.line is a LineFormat object + + Examples: axis types + | axis-type | + | category | + | value | + + Scenario Outline: Get Axis.has_[major/minor]_gridlines Given an axis gridlines Then axis.has__gridlines is @@ -152,13 +164,24 @@ Feature: Axis properties Then axis.major_gridlines is a MajorGridlines object - Scenario Outline: Get Axis.format - Given a axis - Then axis.format is a ChartFormat object - And axis.format.fill is a FillFormat object - And axis.format.line is a LineFormat object + Scenario Outline: Get Axis.reverse_order + Given an axis having reverse-order turned + Then axis.reverse_order is - Examples: axis types - | axis-type | - | category | - | value | + Examples: axis unit cases + | status | expected-value | + | on | True | + | off | False | + + + Scenario Outline: Set Axis.reverse_order + Given an axis having reverse-order turned + When I assign to axis.reverse_order + Then axis.reverse_order is + + Examples: major/minor_unit assignment cases + | status | value | expected-value | + | off | False | False | + | off | True | True | + | on | False | False | + | on | True | True | diff --git a/features/prs-open-save.feature b/features/prs-open-save.feature index 344412581..d60b2c242 100644 --- a/features/prs-open-save.feature +++ b/features/prs-open-save.feature @@ -15,6 +15,12 @@ Feature: Round-trip a presentation And I save the presentation Then I see the pptx file in the working directory + Scenario: Start presentation from package extracted into directory + Given a clean working directory + When I open a presentation extracted into a directory + And I save the presentation + Then I see the pptx file in the working directory + Scenario: Save presentation to package stream Given a clean working directory When I open a basic PowerPoint presentation @@ -26,3 +32,8 @@ Feature: Round-trip a presentation Given a presentation with external relationships When I save and reload the presentation Then the external relationships are still there + And the package has the expected number of .rels parts + + Scenario: Load presentation with invalid image/jpg MIME-type + Given a presentation with an image/jpg MIME-type + Then I can access the JPEG image diff --git a/features/steps/action.py b/features/steps/action.py index 8051da750..2a2da135a 100644 --- a/features/steps/action.py +++ b/features/steps/action.py @@ -1,27 +1,21 @@ -# encoding: utf-8 +"""Gherkin step implementations for click action-related features.""" -""" -Gherkin step implementations for click action-related features. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations from behave import given, then, when +from helpers import test_file from pptx import Presentation from pptx.action import Hyperlink from pptx.enum.action import PP_ACTION -from helpers import test_pptx - - # given =================================================== @given("an ActionSetting object having action {action} as click_action") def given_an_ActionSetting_object_as_click_action(context, action): shape_idx = {"NONE": 0, "NAMED_SLIDE": 6}[action] - slides = Presentation(test_pptx("act-props")).slides + slides = Presentation(test_file("act-props.pptm")).slides context.slides = slides context.click_action = slides[2].shapes[shape_idx].click_action @@ -49,8 +43,9 @@ def given_a_shape_having_click_action_action(context, action): "OLE action", "run macro", "run program", + "play media", ).index(action) - slides = Presentation(test_pptx("act-props")).slides + slides = Presentation(test_file("act-props.pptm")).slides context.slides = slides context.click_action = slides[2].shapes[shape_idx].click_action @@ -90,8 +85,10 @@ def then_click_action_hyperlink_is_a_Hyperlink_object(context): def then_click_action_hyperlink_address_is_value(context, value): expected_value = None if value == "None" else value hyperlink = context.click_action.hyperlink - print("expected value %s != %s" % (expected_value, hyperlink.address)) - assert hyperlink.address == expected_value + assert hyperlink.address == expected_value, "expected %s, got %s" % ( + expected_value, + hyperlink.address, + ) @then("click_action.target_slide is {value}") diff --git a/features/steps/axis.py b/features/steps/axis.py index 22c815862..59697461b 100644 --- a/features/steps/axis.py +++ b/features/steps/axis.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - """Gherkin step implementations for chart axis features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.chart import XL_AXIS_CROSSES, XL_CATEGORY_TYPE -from helpers import test_pptx - - # given =================================================== @@ -19,9 +15,7 @@ def given_a_axis_type_axis(context, axis_type): prs = Presentation(test_pptx("cht-axis-props")) chart = prs.slides[0].shapes[0].chart - context.axis = {"category": chart.category_axis, "value": chart.value_axis}[ - axis_type - ] + context.axis = {"category": chart.category_axis, "value": chart.value_axis}[axis_type] @given("a major gridlines") @@ -33,9 +27,7 @@ def given_a_major_gridlines(context): @given("a value axis having category axis crossing of {crossing}") def given_a_value_axis_having_cat_ax_crossing_of(context, crossing): - slide_idx = {"automatic": 0, "maximum": 2, "minimum": 3, "2.75": 4, "-1.5": 5}[ - crossing - ] + slide_idx = {"automatic": 0, "maximum": 2, "minimum": 3, "2.75": 4, "-1.5": 5}[crossing] prs = Presentation(test_pptx("cht-axis-props")) context.value_axis = prs.slides[slide_idx].shapes[0].chart.value_axis @@ -69,6 +61,13 @@ def given_an_axis_having_major_or_minor_unit_of_value(context, major_or_minor, v context.axis = chart.value_axis +@given("an axis having reverse-order turned {status}") +def given_an_axis_having_reverse_order_turned_on_or_off(context, status): + prs = Presentation(test_pptx("cht-axis-props")) + chart = prs.slides[0].shapes[0].chart + context.axis = {"on": chart.value_axis, "off": chart.category_axis}[status] + + @given("an axis of type {cls_name}") def given_an_axis_of_type_cls_name(context, cls_name): slide_idx = {"CategoryAxis": 0, "DateAxis": 6}[cls_name] @@ -115,9 +114,7 @@ def when_I_assign_value_to_axis_has_title(context, value): @when("I assign {value} to axis.has_{major_or_minor}_gridlines") -def when_I_assign_value_to_axis_has_major_or_minor_gridlines( - context, value, major_or_minor -): +def when_I_assign_value_to_axis_has_major_or_minor_gridlines(context, value, major_or_minor): axis = context.axis propname = "has_%s_gridlines" % major_or_minor new_value = {"True": True, "False": False}[value] @@ -132,6 +129,11 @@ def when_I_assign_value_to_axis_major_or_minor_unit(context, value, major_or_min setattr(axis, propname, new_value) +@when("I assign {value} to axis.reverse_order") +def when_I_assign_value_to_axis_reverse_order(context, value): + context.axis.reverse_order = {"True": True, "False": False}[value] + + @when("I assign {value} to axis_title.has_text_frame") def when_I_assign_value_to_axis_title_has_text_frame(context, value): context.axis_title.has_text_frame = {"True": True, "False": False}[value] @@ -198,9 +200,7 @@ def then_axis_has_title_is_value(context, value): @then("axis.has_{major_or_minor}_gridlines is {value}") -def then_axis_has_major_or_minor_gridlines_is_expected_value( - context, major_or_minor, value -): +def then_axis_has_major_or_minor_gridlines_is_expected_value(context, major_or_minor, value): axis = context.axis actual_value = { "major": axis.has_major_gridlines, @@ -221,12 +221,18 @@ def then_axis_major_or_minor_unit_is_value(context, major_or_minor, value): axis = context.axis propname = "%s_unit" % major_or_minor actual_value = getattr(axis, propname) - expected_value = {"20.0": 20.0, "8.4": 8.4, "5.0": 5.0, "4.2": 4.2, "None": None}[ - value - ] + expected_value = {"20.0": 20.0, "8.4": 8.4, "5.0": 5.0, "4.2": 4.2, "None": None}[value] assert actual_value == expected_value, "got %s" % actual_value +@then("axis.reverse_order is {value}") +def then_axis_reverse_order_is_value(context, value): + axis = context.axis + actual_value = axis.reverse_order + expected_value = {"True": True, "False": False}[value] + assert actual_value is expected_value, "got %s" % actual_value + + @then("axis_title.format is a ChartFormat object") def then_axis_title_format_is_a_ChartFormat_object(context): class_name = type(context.axis_title.format).__name__ diff --git a/features/steps/background.py b/features/steps/background.py index 596a3a665..b629cea74 100644 --- a/features/steps/background.py +++ b/features/steps/background.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for slide background-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== diff --git a/features/steps/category.py b/features/steps/category.py index 3a119f960..2c4a10ce3 100644 --- a/features/steps/category.py +++ b/features/steps/category.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for chart category features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== diff --git a/features/steps/chart.py b/features/steps/chart.py index fd4edefc2..ced211f32 100644 --- a/features/steps/chart.py +++ b/features/steps/chart.py @@ -1,16 +1,12 @@ -# encoding: utf-8 +"""Gherkin step implementations for chart features.""" -""" -Gherkin step implementations for chart features. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import hashlib - from itertools import islice from behave import given, then, when +from helpers import count, test_pptx from pptx import Presentation from pptx.chart.chart import Legend @@ -19,9 +15,6 @@ from pptx.parts.embeddedpackage import EmbeddedXlsxPart from pptx.util import Inches -from helpers import count, test_pptx - - # given =================================================== diff --git a/features/steps/chartdata.py b/features/steps/chartdata.py index c116a0cb3..82e88ff5a 100644 --- a/features/steps/chartdata.py +++ b/features/steps/chartdata.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Gherkin step implementations for chart data features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import datetime @@ -12,7 +10,6 @@ from pptx.enum.chart import XL_CHART_TYPE from pptx.util import Inches - # given =================================================== diff --git a/features/steps/color.py b/features/steps/color.py index 590cabf79..43bb3cc08 100644 --- a/features/steps/color.py +++ b/features/steps/color.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for ColorFormat-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from behave import given, when, then +from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.dml import MSO_THEME_COLOR -from helpers import test_pptx - - # given ==================================================== diff --git a/features/steps/coreprops.py b/features/steps/coreprops.py index bda998b71..9989c2e01 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -1,20 +1,14 @@ -# encoding: utf-8 +"""Gherkin step implementations for core properties-related features.""" -""" -Gherkin step implementations for core properties-related features. -""" - -from __future__ import absolute_import +from __future__ import annotations from datetime import datetime, timedelta -from behave import given, when, then +from behave import given, then, when +from helpers import no_core_props_pptx_path, saved_pptx_path from pptx import Presentation -from helpers import saved_pptx_path, no_core_props_pptx_path - - # given =================================================== @@ -49,7 +43,7 @@ def step_when_set_core_doc_props_to_valid_values(context): ("revision", 9), ("subject", "Subject"), # --- exercise unicode-text case for Python 2.7 --- - ("title", u"åß∂Title°"), + ("title", "åß∂Title°"), ("version", "Version"), ) for name, value in context.propvals: diff --git a/features/steps/datalabel.py b/features/steps/datalabel.py index dc56de4e4..bb4f474aa 100644 --- a/features/steps/datalabel.py +++ b/features/steps/datalabel.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - """Gherkin step implementations for chart data label features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.chart import XL_DATA_LABEL_POSITION -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/effect.py b/features/steps/effect.py index f319545fc..c9e2806cc 100644 --- a/features/steps/effect.py +++ b/features/steps/effect.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for ShadowFormat-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given ==================================================== diff --git a/features/steps/fill.py b/features/steps/fill.py index fea93ec8f..cbdad36a1 100644 --- a/features/steps/fill.py +++ b/features/steps/fill.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - """Gherkin step implementations for FillFormat-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.dml import MSO_FILL, MSO_PATTERN # noqa -from helpers import test_pptx - - # given ==================================================== @@ -23,9 +19,7 @@ def given_a_FillFormat_object_as_fill(context): @given("a FillFormat object as fill having {pattern} fill") def given_a_FillFormat_object_as_fill_having_pattern(context, pattern): - shape_idx = {"no pattern": 0, "MSO_PATTERN.DIVOT": 1, "MSO_PATTERN.WAVE": 2}[ - pattern - ] + shape_idx = {"no pattern": 0, "MSO_PATTERN.DIVOT": 1, "MSO_PATTERN.WAVE": 2}[pattern] slide = Presentation(test_pptx("dml-fill")).slides[1] fill = slide.shapes[shape_idx].fill context.fill = fill @@ -102,18 +96,14 @@ def when_I_call_fill_solid(context): def then_fill_back_color_is_a_ColorFormat_object(context): actual_value = context.fill.back_color.__class__.__name__ expected_value = "ColorFormat" - assert actual_value == expected_value, ( - "fill.back_color is a %s object" % actual_value - ) + assert actual_value == expected_value, "fill.back_color is a %s object" % actual_value @then("fill.fore_color is a ColorFormat object") def then_fill_fore_color_is_a_ColorFormat_object(context): actual_value = context.fill.fore_color.__class__.__name__ expected_value = "ColorFormat" - assert actual_value == expected_value, ( - "fill.fore_color is a %s object" % actual_value - ) + assert actual_value == expected_value, "fill.fore_color is a %s object" % actual_value @then("fill.gradient_angle == {value}") @@ -127,9 +117,7 @@ def then_fill_gradient_angle_eq_value(context, value): def then_fill_gradient_stops_is_a_GradientStops_object(context): expected_value = "_GradientStops" actual_value = context.fill.gradient_stops.__class__.__name__ - assert actual_value == expected_value, ( - "fill.gradient_stops is a %s object" % actual_value - ) + assert actual_value == expected_value, "fill.gradient_stops is a %s object" % actual_value @then("fill.pattern is {value}") diff --git a/features/steps/font.py b/features/steps/font.py index 2a4c279c5..a9ea45c6b 100644 --- a/features/steps/font.py +++ b/features/steps/font.py @@ -1,20 +1,14 @@ -# encoding: utf-8 +"""Step implementations for run property (font)-related features.""" -""" -Step implementations for run property (font)-related features -""" - -from __future__ import absolute_import +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import MSO_UNDERLINE -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/font_color.py b/features/steps/font_color.py index e336978af..53872dff1 100644 --- a/features/steps/font_color.py +++ b/features/steps/font_color.py @@ -1,20 +1,14 @@ -# encoding: utf-8 +"""Gherkin step implementations for font color features.""" -""" -Gherkin step implementations for font color features -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR -from helpers import test_pptx - - font_color_pptx_path = test_pptx("font-color") @@ -31,9 +25,7 @@ def step_given_font_with_color_type(context, color_type): @given("a font with a color brightness setting of {setting}") def step_font_with_color_brightness(context, setting): - textbox_idx = {"no brightness adjustment": 2, "25% darker": 3, "40% lighter": 4}[ - setting - ] + textbox_idx = {"no brightness adjustment": 2, "25% darker": 3, "40% lighter": 4}[setting] context.prs = Presentation(font_color_pptx_path) textbox = context.prs.slides[0].shapes[textbox_idx] context.font = textbox.text_frame.paragraphs[0].runs[0].font diff --git a/features/steps/helpers.py b/features/steps/helpers.py index bd6d7a330..67a29439a 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -1,13 +1,9 @@ -# encoding: utf-8 - -""" -Helper methods and variables for acceptance tests. -""" +"""Helper methods and variables for acceptance tests.""" import os -def absjoin(*paths): +def absjoin(*paths: str) -> str: return os.path.abspath(os.path.join(*paths)) @@ -17,9 +13,7 @@ def absjoin(*paths): test_pptx_dir = absjoin(thisdir, "test_files") # legacy test pptx files --------------- -no_core_props_pptx_path = absjoin( - thisdir, "../../tests/test_files", "no-core-props.pptx" -) +no_core_props_pptx_path = absjoin(thisdir, "../../tests/test_files", "no-core-props.pptx") # scratch test pptx file --------------- saved_pptx_path = absjoin(scratch_dir, "test_out.pptx") @@ -27,41 +21,31 @@ def absjoin(*paths): test_text = "python-pptx was here!" -def cls_qname(obj): +def cls_qname(obj: object) -> str: module_name = obj.__module__ cls_name = obj.__class__.__name__ qname = "%s.%s" % (module_name, cls_name) return qname -def count(start=0, step=1): - """ - Local implementation of `itertools.count()` to allow v2.6 compatibility. - """ +def count(start: int = 0, step: int = 1): + """Local implementation of `itertools.count()` to allow v2.6 compatibility.""" n = start while True: yield n n += step -def test_file(filename): - """ - Return the absolute path to the file having *filename* in acceptance - test_files directory. - """ +def test_file(filename: str) -> str: + """Return the absolute path to the file having *filename* in acceptance test_files directory.""" return absjoin(thisdir, "test_files", filename) -def test_image(filename): - """ - Return the absolute path to image file having *filename* in test_files - directory. - """ +def test_image(filename: str): + """Return the absolute path to image file having *filename* in test_files directory.""" return absjoin(thisdir, "test_files", filename) -def test_pptx(name): - """ - Return the absolute path to test .pptx file with root name *name*. - """ +def test_pptx(name: str) -> str: + """Return the absolute path to test .pptx file with root name *name*.""" return absjoin(thisdir, "test_files", "%s.pptx" % name) diff --git a/features/steps/legend.py b/features/steps/legend.py index f8385f12a..7c35cd7f7 100644 --- a/features/steps/legend.py +++ b/features/steps/legend.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for chart legend features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.chart import XL_LEGEND_POSITION from pptx.text.text import Font -from helpers import test_pptx - - # given =================================================== @@ -31,9 +27,7 @@ def given_a_legend_having_horizontal_offset_of_value(context, value): @given("a legend positioned {location} the chart") def given_a_legend_positioned_location_the_chart(context, location): - slide_idx = {"at an unspecified location of": 0, "below": 1, "to the right of": 2}[ - location - ] + slide_idx = {"at an unspecified location of": 0, "below": 1, "to the right of": 2}[location] prs = Presentation(test_pptx("cht-legend-props")) context.legend = prs.slides[slide_idx].shapes[0].chart.legend diff --git a/features/steps/line.py b/features/steps/line.py index 5489b03ed..fb1cb1bb3 100644 --- a/features/steps/line.py +++ b/features/steps/line.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Step implementations for LineFormat-related features.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.dml import MSO_LINE from pptx.util import Length, Pt -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/picture.py b/features/steps/picture.py index 282d21743..2fce7f2ca 100644 --- a/features/steps/picture.py +++ b/features/steps/picture.py @@ -1,22 +1,17 @@ -# encoding: utf-8 +"""Gherkin step implementations for picture-related features.""" -""" -Gherkin step implementations for picture-related features. -""" +from __future__ import annotations -from __future__ import absolute_import +import io -from behave import given, when, then +from behave import given, then, when +from helpers import saved_pptx_path, test_image, test_pptx from pptx import Presentation -from pptx.compat import BytesIO from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE from pptx.package import Package from pptx.util import Inches -from helpers import saved_pptx_path, test_image, test_pptx - - # given =================================================== @@ -46,7 +41,7 @@ def when_I_add_the_image_filename_using_shapes_add_picture(context, filename): def when_I_add_the_stream_image_filename_using_add_picture(context, filename): shapes = context.slide.shapes with open(test_image(filename), "rb") as f: - stream = BytesIO(f.read()) + stream = io.BytesIO(f.read()) shapes.add_picture(stream, Inches(1.25), Inches(1.25)) @@ -60,12 +55,10 @@ def when_I_assign_member_to_picture_auto_shape_type(context, member): @then("a {ext} image part appears in the pptx file") def step_then_a_ext_image_part_appears_in_the_pptx_file(context, ext): - pkg = Package().open(saved_pptx_path) - partnames = [part.partname for part in pkg.parts] + pkg = Package.open(saved_pptx_path) + partnames = frozenset(p.partname for p in pkg.iter_parts()) image_partname = "/ppt/media/image1.%s" % ext - assert image_partname in partnames, "got %s" % [ - p for p in partnames if "image" in p - ] + assert image_partname in partnames, "got %s" % [p for p in partnames if "image" in p] @then("picture.auto_shape_type == MSO_AUTO_SHAPE_TYPE.{member}") diff --git a/features/steps/placeholder.py b/features/steps/placeholder.py index 2ea14f492..43638373d 100644 --- a/features/steps/placeholder.py +++ b/features/steps/placeholder.py @@ -1,14 +1,11 @@ -# encoding: utf-8 +"""Gherkin step implementations for placeholder-related features.""" -""" -Gherkin step implementations for placeholder-related features. -""" - -from __future__ import absolute_import +from __future__ import annotations import hashlib -from behave import given, when, then +from behave import given, then, when +from helpers import saved_pptx_path, test_file, test_pptx, test_text from pptx import Presentation from pptx.chart.data import CategoryChartData @@ -16,9 +13,6 @@ from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.shapes.base import _PlaceholderFormat -from helpers import saved_pptx_path, test_file, test_pptx, test_text - - # given =================================================== @@ -32,9 +26,7 @@ def given_a_bullet_body_placeholder(context): @given("a known {placeholder_type} placeholder shape") def given_a_known_placeholder_shape(context, placeholder_type): - context.execute_steps( - "given an unpopulated %s placeholder shape" % placeholder_type - ) + context.execute_steps("given an unpopulated %s placeholder shape" % placeholder_type) @given("a layout placeholder having directly set position and size") diff --git a/features/steps/plot.py b/features/steps/plot.py index 98feb88e5..0a3636717 100644 --- a/features/steps/plot.py +++ b/features/steps/plot.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for chart plot features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== @@ -95,9 +91,7 @@ def when_I_assign_value_to_plot_vary_by_categories(context, value): def then_bubble_plot_bubble_scale_is_value(context, value): expected_value = int(value) bubble_plot = context.bubble_plot - assert bubble_plot.bubble_scale == expected_value, ( - "got %s" % bubble_plot.bubble_scale - ) + assert bubble_plot.bubble_scale == expected_value, "got %s" % bubble_plot.bubble_scale @then("len(plot.categories) is {count}") diff --git a/features/steps/presentation.py b/features/steps/presentation.py index 9b02c5f29..11a259545 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -1,54 +1,60 @@ -# encoding: utf-8 +"""Gherkin step implementations for presentation-level features.""" -""" -Gherkin step implementations for presentation-level features. -""" - -from __future__ import absolute_import +from __future__ import annotations +import io import os +import zipfile +from typing import TYPE_CHECKING, cast -from behave import given, when, then +from behave import given, then, when +from behave.runner import Context +from helpers import saved_pptx_path, test_file, test_pptx from pptx import Presentation -from pptx.compat import BytesIO from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.util import Inches -from helpers import saved_pptx_path, test_pptx - +if TYPE_CHECKING: + from pptx import presentation + from pptx.shapes.picture import Picture # given =================================================== @given("a clean working directory") -def given_clean_working_dir(context): +def given_clean_working_dir(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) @given("a presentation") -def given_a_presentation(context): +def given_a_presentation(context: Context): context.presentation = Presentation(test_pptx("prs-properties")) @given("a presentation having a notes master") -def given_a_presentation_having_a_notes_master(context): +def given_a_presentation_having_a_notes_master(context: Context): context.prs = Presentation(test_pptx("prs-notes")) @given("a presentation having no notes master") -def given_a_presentation_having_no_notes_master(context): +def given_a_presentation_having_no_notes_master(context: Context): context.prs = Presentation(test_pptx("prs-properties")) +@given("a presentation with an image/jpg MIME-type") +def given_prs_with_image_jpg_MIME_type(context): + context.prs = Presentation(test_pptx("test-image-jpg-mime")) + + @given("a presentation with external relationships") -def given_prs_with_ext_rels(context): +def given_prs_with_ext_rels(context: Context): context.prs = Presentation(test_pptx("ext-rels")) @given("an initialized pptx environment") -def given_initialized_pptx_env(context): +def given_initialized_pptx_env(context: Context): pass @@ -56,32 +62,37 @@ def given_initialized_pptx_env(context): @when("I change the slide width and height") -def when_change_slide_width_and_height(context): +def when_change_slide_width_and_height(context: Context): presentation = context.presentation presentation.slide_width = Inches(4) presentation.slide_height = Inches(3) @when("I construct a Presentation instance with no path argument") -def when_construct_default_prs(context): +def when_construct_default_prs(context: Context): context.prs = Presentation() @when("I open a basic PowerPoint presentation") -def when_open_basic_pptx(context): +def when_open_basic_pptx(context: Context): context.prs = Presentation(test_pptx("test")) +@when("I open a presentation extracted into a directory") +def when_I_open_a_presentation_extracted_into_a_directory(context: Context): + context.prs = Presentation(test_file("extracted-pptx")) + + @when("I open a presentation contained in a stream") -def when_open_presentation_stream(context): +def when_open_presentation_stream(context: Context): with open(test_pptx("test"), "rb") as f: - stream = BytesIO(f.read()) + stream = io.BytesIO(f.read()) context.prs = Presentation(stream) stream.close() @when("I save and reload the presentation") -def when_save_and_reload_prs(context): +def when_save_and_reload_prs(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) context.prs.save(saved_pptx_path) @@ -89,7 +100,7 @@ def when_save_and_reload_prs(context): @when("I save that stream to a file") -def when_save_stream_to_a_file(context): +def when_save_stream_to_a_file(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) context.stream.seek(0) @@ -98,15 +109,15 @@ def when_save_stream_to_a_file(context): @when("I save the presentation") -def when_save_presentation(context): +def when_save_presentation(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) context.prs.save(saved_pptx_path) @when("I save the presentation to a stream") -def when_save_presentation_to_stream(context): - context.stream = BytesIO() +def when_save_presentation_to_stream(context: Context): + context.stream = io.BytesIO() context.prs.save(context.stream) @@ -114,7 +125,7 @@ def when_save_presentation_to_stream(context): @then("I receive a presentation based on the default template") -def then_receive_prs_based_on_def_tmpl(context): +def then_receive_prs_based_on_def_tmpl(context: Context): prs = context.prs assert prs is not None slide_masters = prs.slide_masters @@ -126,19 +137,19 @@ def then_receive_prs_based_on_def_tmpl(context): @then("its slide height matches its known value") -def then_slide_height_matches_known_value(context): +def then_slide_height_matches_known_value(context: Context): presentation = context.presentation assert presentation.slide_height == 6858000 @then("its slide width matches its known value") -def then_slide_width_matches_known_value(context): +def then_slide_width_matches_known_value(context: Context): presentation = context.presentation assert presentation.slide_width == 9144000 @then("I see the pptx file in the working directory") -def then_see_pptx_file_in_working_dir(context): +def then_see_pptx_file_in_working_dir(context: Context): assert os.path.isfile(saved_pptx_path) minimum = 30000 actual = os.path.getsize(saved_pptx_path) @@ -146,7 +157,7 @@ def then_see_pptx_file_in_working_dir(context): @then("len(notes_master.shapes) is {shape_count}") -def then_len_notes_master_shapes_is_shape_count(context, shape_count): +def then_len_notes_master_shapes_is_shape_count(context: Context, shape_count: str): notes_master = context.prs.notes_master expected = int(shape_count) actual = len(notes_master.shapes) @@ -154,25 +165,25 @@ def then_len_notes_master_shapes_is_shape_count(context, shape_count): @then("prs.notes_master is a NotesMaster object") -def then_prs_notes_master_is_a_NotesMaster_object(context): +def then_prs_notes_master_is_a_NotesMaster_object(context: Context): prs = context.prs assert type(prs.notes_master).__name__ == "NotesMaster" @then("prs.slides is a Slides object") -def then_prs_slides_is_a_Slides_object(context): +def then_prs_slides_is_a_Slides_object(context: Context): prs = context.presentation assert type(prs.slides).__name__ == "Slides" @then("prs.slide_masters is a SlideMasters object") -def then_prs_slide_masters_is_a_SlideMasters_object(context): +def then_prs_slide_masters_is_a_SlideMasters_object(context: Context): prs = context.presentation assert type(prs.slide_masters).__name__ == "SlideMasters" @then("the external relationships are still there") -def then_ext_rels_are_preserved(context): +def then_ext_rels_are_preserved(context: Context): prs = context.prs sld = prs.slides[0] rel = sld.part._rels["rId2"] @@ -181,13 +192,31 @@ def then_ext_rels_are_preserved(context): assert rel.target_ref == "https://github.com/scanny/python-pptx" +@then("the package has the expected number of .rels parts") +def then_the_package_has_the_expected_number_of_rels_parts(context: Context): + with zipfile.ZipFile(saved_pptx_path, "r") as z: + member_count = len(z.namelist()) + assert member_count == 18, "expected 18, got %d" % member_count + + +@then("I can access the JPEG image") +def then_I_can_access_the_JPEG_image(context): + prs = cast("presentation.Presentation", context.prs) + slide = prs.slides[0] + picture = cast("Picture", slide.shapes[0]) + try: + picture.image + except AttributeError: + raise AssertionError("JPEG image not recognized") + + @then("the slide height matches the new value") -def then_slide_height_matches_new_value(context): +def then_slide_height_matches_new_value(context: Context): presentation = context.presentation assert presentation.slide_height == Inches(3) @then("the slide width matches the new value") -def then_slide_width_matches_new_value(context): +def then_slide_width_matches_new_value(context: Context): presentation = context.presentation assert presentation.slide_width == Inches(4) diff --git a/features/steps/series.py b/features/steps/series.py index 3b900e746..35965fe94 100644 --- a/features/steps/series.py +++ b/features/steps/series.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - """Gherkin step implementations for chart plot features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from ast import literal_eval from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.chart import XL_MARKER_STYLE from pptx.enum.dml import MSO_FILL_TYPE, MSO_THEME_COLOR -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/shape.py b/features/steps/shape.py index c5154a45b..b10ad0659 100644 --- a/features/steps/shape.py +++ b/features/steps/shape.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - """Gherkin step implementations for shape-related features.""" -from __future__ import unicode_literals +from __future__ import annotations import hashlib -from behave import given, when, then +from behave import given, then, when +from helpers import cls_qname, test_file, test_pptx from pptx import Presentation -from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE from pptx.action import ActionSetting +from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE from pptx.util import Emu -from helpers import cls_qname, test_file, test_pptx - - # given =================================================== @@ -222,9 +218,7 @@ def given_a_shape_of_known_position_and_size(context): @when("I add a {cx} x {cy} shape at ({x}, {y})") def when_I_add_a_cx_cy_shape_at_x_y(context, cx, cy, x, y): - context.shape.shapes.add_shape( - MSO_SHAPE.ROUNDED_RECTANGLE, int(x), int(y), int(cx), int(cy) - ) + context.shape.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, int(x), int(y), int(cx), int(cy)) @when("I assign 0.15 to shape.adjustments[0]") @@ -272,9 +266,7 @@ def when_I_assign_value_to_connector_end_y(context, value): @when("I assign {value} to picture.crop_{side}") def when_I_assign_value_to_picture_crop_side(context, value, side): - new_value = ( - None if value == "None" else float(value) if "." in value else int(value) - ) + new_value = None if value == "None" else float(value) if "." in value else int(value) setattr(context.picture, "crop_%s" % side, new_value) @@ -336,9 +328,7 @@ def then_accessing_shape_click_action_raises_TypeError(context): except TypeError: return except Exception as e: - raise AssertionError( - "Accessing GroupShape.click_action raised %s" % type(e).__name__ - ) + raise AssertionError("Accessing GroupShape.click_action raised %s" % type(e).__name__) raise AssertionError("Accessing GroupShape.click_action did not raise") @@ -658,9 +648,7 @@ def then_shape_shape_id_equals(context, value_str): def then_shape_shape_type_is_MSO_SHAPE_TYPE_member(context, member_name): expected_shape_type = getattr(MSO_SHAPE_TYPE, member_name) actual_shape_type = context.shape.shape_type - assert actual_shape_type == expected_shape_type, ( - "shape.shape_type == %s" % actual_shape_type - ) + assert actual_shape_type == expected_shape_type, "shape.shape_type == %s" % actual_shape_type @then("shape.text == {value}") diff --git a/features/steps/shapes.py b/features/steps/shapes.py index 53a081ce6..57d5f2bb0 100644 --- a/features/steps/shapes.py +++ b/features/steps/shapes.py @@ -1,10 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for shape collections.""" +from __future__ import annotations + import io from behave import given, then, when +from helpers import saved_pptx_path, test_file, test_image, test_pptx from pptx import Presentation from pptx.chart.data import CategoryChartData @@ -13,9 +14,6 @@ from pptx.shapes.base import BaseShape from pptx.util import Emu, Inches -from helpers import saved_pptx_path, test_file, test_image, test_pptx - - # given =================================================== @@ -174,9 +172,7 @@ def when_I_assign_shapes_add_ole_object_to_shape(context): @when("I assign shapes.add_picture() to shape") def when_I_assign_shapes_add_picture_to_shape(context): - context.shape = context.shapes.add_picture( - test_image("sonic.gif"), Inches(1), Inches(2) - ) + context.shape = context.shapes.add_picture(test_image("sonic.gif"), Inches(1), Inches(2)) @when("I assign shapes.add_shape() to shape") @@ -188,9 +184,7 @@ def when_I_assign_shapes_add_shape_to_shape(context): @when("I assign shapes.add_textbox() to shape") def when_I_assign_shapes_add_textbox_to_shape(context): - context.shape = context.shapes.add_textbox( - Inches(1), Inches(2), Inches(3), Inches(0.5) - ) + context.shape = context.shapes.add_textbox(Inches(1), Inches(2), Inches(3), Inches(0.5)) @when("I assign shapes.build_freeform() to builder") @@ -229,9 +223,7 @@ def when_I_assign_True_to_shapes_turbo_add_enabled(context): @when("I call shapes.add_chart({type_}, chart_data)") def when_I_call_shapes_add_chart(context, type_): chart_type = getattr(XL_CHART_TYPE, type_) - context.chart = context.shapes.add_chart( - chart_type, 0, 0, 0, 0, context.chart_data - ).chart + context.chart = context.shapes.add_chart(chart_type, 0, 0, 0, 0, context.chart_data).chart @when("I call shapes.add_connector(MSO_CONNECTOR.STRAIGHT, 1, 2, 3, 4)") @@ -252,9 +244,7 @@ def when_I_call_shapes_add_movie(context): @then("iterating shapes produces {count} objects of type {class_name}") -def then_iterating_shapes_produces_count_objects_of_type_class_name( - context, count, class_name -): +def then_iterating_shapes_produces_count_objects_of_type_class_name(context, count, class_name): shapes = context.shapes expected_count, expected_class_name = int(count), class_name idx = -1 @@ -268,17 +258,13 @@ def then_iterating_shapes_produces_count_objects_of_type_class_name( @then("iterating shapes produces {count} objects that subclass BaseShape") -def then_iterating_shapes_produces_count_objects_that_subclass_BaseShape( - context, count -): +def then_iterating_shapes_produces_count_objects_that_subclass_BaseShape(context, count): shapes = context.shapes expected_count = int(count) idx = -1 for idx, shape in enumerate(shapes): class_name = shape.__class__.__name__ - assert isinstance(shape, BaseShape), ( - "%s does not subclass BaseShape" % class_name - ) + assert isinstance(shape, BaseShape), "%s does not subclass BaseShape" % class_name actual_count = idx + 1 assert actual_count == expected_count, "got %d items" % actual_count @@ -294,9 +280,7 @@ def then_len_shapes_eq_value(context, value): def then_shape_is_a_type_object(context, clsname): actual_class_name = context.shape.__class__.__name__ expected_class_name = clsname - assert actual_class_name == expected_class_name, ( - "shape is a %s object" % actual_class_name - ) + assert actual_class_name == expected_class_name, "shape is a %s object" % actual_class_name @then("shapes[-1] == shape") diff --git a/features/steps/slide.py b/features/steps/slide.py index 3d13c7d64..a7527f36d 100644 --- a/features/steps/slide.py +++ b/features/steps/slide.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for slide-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== @@ -144,9 +140,7 @@ def then_slide_background_is_a_Background_object(context): def then_slide_follow_master_background_is_value(context, value): expected_value = {"True": True, "False": False}[value] actual_value = context.slide.follow_master_background - assert actual_value is expected_value, ( - "slide.follow_master_background is %s" % actual_value - ) + assert actual_value is expected_value, "slide.follow_master_background is %s" % actual_value @then("slide.has_notes_slide is {value}") @@ -173,18 +167,14 @@ def then_slide_notes_slide_is_a_NotesSlide_object(context): def then_slide_placeholders_is_a_clsname_object(context, clsname): actual_clsname = context.slide.placeholders.__class__.__name__ expected_clsname = clsname - assert actual_clsname == expected_clsname, ( - "slide.placeholders is a %s object" % actual_clsname - ) + assert actual_clsname == expected_clsname, "slide.placeholders is a %s object" % actual_clsname @then("slide.shapes is a {clsname} object") def then_slide_shapes_is_a_clsname_object(context, clsname): actual_clsname = context.slide.shapes.__class__.__name__ expected_clsname = clsname - assert actual_clsname == expected_clsname, ( - "slide.shapes is a %s object" % actual_clsname - ) + assert actual_clsname == expected_clsname, "slide.shapes is a %s object" % actual_clsname @then("slide.slide_id is 256") diff --git a/features/steps/slides.py b/features/steps/slides.py index 16283057c..42ef66885 100644 --- a/features/steps/slides.py +++ b/features/steps/slides.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for slide collection-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals - -from behave import given, when, then - -from pptx import Presentation +from __future__ import annotations +from behave import given, then, when from helpers import test_pptx +from pptx import Presentation # given =================================================== diff --git a/features/steps/table.py b/features/steps/table.py index 3aec6671e..8cbf43afd 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for table-related features""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from behave import given, when, then +from behave import given, then, when +from helpers import test_pptx from pptx import Presentation -from pptx.enum.text import MSO_ANCHOR # noqa +from pptx.enum.text import MSO_ANCHOR # noqa # pyright: ignore[reportUnusedImport] from pptx.util import Inches -from helpers import test_pptx - - # given =================================================== @@ -57,9 +53,7 @@ def given_a_Cell_object_with_known_margins_as_cell(context): @given("a _Cell object with {setting} vertical alignment as cell") def given_a_Cell_object_with_setting_vertical_alignment(context, setting): - cell_coordinates = {"inherited": (0, 1), "middle": (0, 2), "bottom": (0, 3)}[ - setting - ] + cell_coordinates = {"inherited": (0, 1), "middle": (0, 2), "bottom": (0, 3)}[setting] prs = Presentation(test_pptx("tbl-cell")) context.cell = prs.slides[0].shapes[0].table.cell(*cell_coordinates) diff --git a/features/steps/test_files/act-props.pptm b/features/steps/test_files/act-props.pptm new file mode 100644 index 000000000..0a499f56d Binary files /dev/null and b/features/steps/test_files/act-props.pptm differ diff --git a/features/steps/test_files/act-props.pptx b/features/steps/test_files/act-props.pptx deleted file mode 100644 index 4ef5fbc50..000000000 Binary files a/features/steps/test_files/act-props.pptx and /dev/null differ diff --git a/features/steps/test_files/cht-axis-props.pptx b/features/steps/test_files/cht-axis-props.pptx index 7dd230eb2..4b80fbd6b 100644 Binary files a/features/steps/test_files/cht-axis-props.pptx and b/features/steps/test_files/cht-axis-props.pptx differ diff --git a/features/steps/test_files/extracted-pptx/[Content_Types].xml b/features/steps/test_files/extracted-pptx/[Content_Types].xml new file mode 100644 index 000000000..4af2894e0 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/[Content_Types].xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/_rels/.rels b/features/steps/test_files/extracted-pptx/_rels/.rels new file mode 100644 index 000000000..cbaca35bd --- /dev/null +++ b/features/steps/test_files/extracted-pptx/_rels/.rels @@ -0,0 +1,7 @@ + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/docProps/app.xml b/features/steps/test_files/extracted-pptx/docProps/app.xml new file mode 100644 index 000000000..d040c0468 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/docProps/app.xml @@ -0,0 +1,42 @@ + + + 2 + 5 + Microsoft Macintosh PowerPoint + On-screen Show (4:3) + 2 + 1 + 0 + 0 + 0 + false + + + + Theme + + + 1 + + + Slide Titles + + + 1 + + + + + + Office Theme + Presentation Title Text + + + + neopraxis.org + false + false + + false + 14.0000 + diff --git a/features/steps/test_files/extracted-pptx/docProps/core.xml b/features/steps/test_files/extracted-pptx/docProps/core.xml new file mode 100644 index 000000000..7803bae14 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/docProps/core.xml @@ -0,0 +1,13 @@ + + + Presentation + + python-pptx + + + Steve Canny + 4 + 2012-11-17T11:07:40Z + 2012-12-17T06:54:44Z + + diff --git a/features/steps/test_files/extracted-pptx/docProps/thumbnail.jpeg b/features/steps/test_files/extracted-pptx/docProps/thumbnail.jpeg new file mode 100644 index 000000000..1f16c0ad4 Binary files /dev/null and b/features/steps/test_files/extracted-pptx/docProps/thumbnail.jpeg differ diff --git a/features/steps/test_files/extracted-pptx/ppt/_rels/presentation.xml.rels b/features/steps/test_files/extracted-pptx/ppt/_rels/presentation.xml.rels new file mode 100644 index 000000000..a5cace9d1 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/_rels/presentation.xml.rels @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/presProps.xml b/features/steps/test_files/extracted-pptx/ppt/presProps.xml new file mode 100644 index 000000000..3889a5131 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/presProps.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/presentation.xml b/features/steps/test_files/extracted-pptx/ppt/presentation.xml new file mode 100644 index 000000000..bb01bf331 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/presentation.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/printerSettings/printerSettings1.bin b/features/steps/test_files/extracted-pptx/ppt/printerSettings/printerSettings1.bin new file mode 100644 index 000000000..38b1fba1a Binary files /dev/null and b/features/steps/test_files/extracted-pptx/ppt/printerSettings/printerSettings1.bin differ diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml new file mode 100644 index 000000000..401fcb64c --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master subtitle style + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml new file mode 100644 index 000000000..05b32f9b5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml new file mode 100644 index 000000000..7de50b1e1 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml new file mode 100644 index 000000000..c08a4aeae --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml new file mode 100644 index 000000000..9a5a9b143 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml new file mode 100644 index 000000000..9ca055ff2 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml new file mode 100644 index 000000000..9f58fd925 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml @@ -0,0 +1,417 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml new file mode 100644 index 000000000..220940510 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml new file mode 100644 index 000000000..432903279 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml new file mode 100644 index 000000000..e3d1f6bfe --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml new file mode 100644 index 000000000..bc9867476 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels new file mode 100644 index 000000000..bf3e45829 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml b/features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml new file mode 100644 index 000000000..98b610fa9 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels new file mode 100644 index 000000000..a1233b58c --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml b/features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml new file mode 100644 index 000000000..2b2508b61 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Presentation Title Text + + + + + + + + + + + + + + + + + + + + + + + Subtitle Text + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/tableStyles.xml b/features/steps/test_files/extracted-pptx/ppt/tableStyles.xml new file mode 100644 index 000000000..179ee8a62 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/tableStyles.xml @@ -0,0 +1,2 @@ + + diff --git a/features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml b/features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml new file mode 100644 index 000000000..8eef13149 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/viewProps.xml b/features/steps/test_files/extracted-pptx/ppt/viewProps.xml new file mode 100644 index 000000000..7f36cea5a --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/viewProps.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/test-image-jpg-mime.pptx b/features/steps/test_files/test-image-jpg-mime.pptx new file mode 100644 index 000000000..857bd7ca2 Binary files /dev/null and b/features/steps/test_files/test-image-jpg-mime.pptx differ diff --git a/features/steps/text.py b/features/steps/text.py index 78c515191..5c3692b5d 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for text-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from behave import given, when, then +from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.text import PP_ALIGN from pptx.util import Emu -from helpers import test_pptx - - # given =================================================== @@ -38,9 +34,7 @@ def given_a_paragraph_having_line_spacing_of_setting(context, setting): @given("a paragraph having space {before_after} of {setting}") -def given_a_paragraph_having_space_before_after_of_setting( - context, before_after, setting -): +def given_a_paragraph_having_space_before_after_of_setting(context, before_after, setting): slide_idx = {"before": 0, "after": 1}[before_after] paragraph_idx = {"no explicit setting": 0, "6 pt": 1}[setting] prs = Presentation(test_pptx("txt-paragraph-spacing")) @@ -126,9 +120,7 @@ def when_I_assign_value_to_paragraph_line_spacing(context, value_str): @when("I assign {value_str} to paragraph.space_{before_after}") -def when_I_assign_value_to_paragraph_space_before_after( - context, value_str, before_after -): +def when_I_assign_value_to_paragraph_space_before_after(context, value_str, before_after): value = {"76200": 76200, "38100": 38100, "None": None}[value_str] attr_name = {"before": "space_before", "after": "space_after"}[before_after] paragraph = context.paragraph diff --git a/features/steps/text_frame.py b/features/steps/text_frame.py index fe2096e3a..49fd44092 100644 --- a/features/steps/text_frame.py +++ b/features/steps/text_frame.py @@ -1,26 +1,20 @@ -# encoding: utf-8 - """Step implementations for text frame-related features""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.text import MSO_AUTO_SIZE from pptx.util import Inches, Pt -from helpers import test_pptx - - # given =================================================== @given("a TextFrame object as text_frame") def given_a_text_frame(context): - context.text_frame = ( - Presentation(test_pptx("txt-text")).slides[0].shapes[0].text_frame - ) + context.text_frame = Presentation(test_pptx("txt-text")).slides[0].shapes[0].text_frame @given("a TextFrame object containing {value} as text_frame") @@ -126,9 +120,10 @@ def then_text_frame_word_wrap_is_value(context, value): assert text_frame.word_wrap is expected_value -@then("the size of the text is 10pt") +@then("the size of the text is 10pt or 11pt") def then_the_size_of_the_text_is_10pt(context): + """Size depends on Pillow version, probably algorithm isn't quite right either.""" text_frame = context.text_frame for paragraph in text_frame.paragraphs: for run in paragraph.runs: - assert run.font.size == Pt(10.0), "got %s" % run.font.size.pt + assert run.font.size in (Pt(10.0), Pt(11.0)), "got %s" % run.font.size.pt diff --git a/features/txt-fit-text.feature b/features/txt-fit-text.feature index 5cee76c7c..dd5313b56 100644 --- a/features/txt-fit-text.feature +++ b/features/txt-fit-text.feature @@ -8,4 +8,4 @@ Feature: Resize text to fit shape When I call TextFrame.fit_text() Then text_frame.auto_size is MSO_AUTO_SIZE.NONE And text_frame.word_wrap is True - And the size of the text is 10pt + And the size of the text is 10pt or 11pt diff --git a/pptx/compat/__init__.py b/pptx/compat/__init__.py deleted file mode 100644 index 3e7715fe5..000000000 --- a/pptx/compat/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# encoding: utf-8 - -""" -Provides Python 2/3 compatibility objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys - -import collections - -try: - Sequence = collections.abc.Sequence -except AttributeError: - Sequence = collections.Sequence - -if sys.version_info >= (3, 0): - from .python3 import ( # noqa - BytesIO, - is_integer, - is_string, - is_unicode, - to_unicode, - Unicode, - ) -else: - from .python2 import ( # noqa - BytesIO, - is_integer, - is_string, - is_unicode, - to_unicode, - Unicode, - ) diff --git a/pptx/compat/python2.py b/pptx/compat/python2.py deleted file mode 100644 index 34e2535d5..000000000 --- a/pptx/compat/python2.py +++ /dev/null @@ -1,41 +0,0 @@ -# encoding: utf-8 - -"""Provides Python 2 compatibility objects.""" - -from StringIO import StringIO as BytesIO # noqa - - -def is_integer(obj): - """Return True if *obj* is an integer (int, long), False otherwise.""" - return isinstance(obj, (int, long)) # noqa F821 - - -def is_string(obj): - """Return True if *obj* is a string, False otherwise.""" - return isinstance(obj, basestring) # noqa F821 - - -def is_unicode(obj): - """Return True if *obj* is a unicode string, False otherwise.""" - return isinstance(obj, unicode) # noqa F821 - - -def to_unicode(text): - """Return *text* as a unicode string. - - *text* can be a 7-bit ASCII string, a UTF-8 encoded 8-bit string, or unicode. String - values are converted to unicode assuming UTF-8 encoding. Unicode values are returned - unchanged. - """ - # both str and unicode inherit from basestring - if not isinstance(text, basestring): # noqa F821 - tmpl = "expected unicode or UTF-8 (or ASCII) encoded str, got %s value %s" - raise TypeError(tmpl % (type(text), text)) - # return unicode strings unchanged - if isinstance(text, unicode): # noqa F821 - return text - # otherwise assume UTF-8 encoding, which also works for ASCII - return unicode(text, "utf-8") # noqa F821 - - -Unicode = unicode # noqa F821 diff --git a/pptx/compat/python3.py b/pptx/compat/python3.py deleted file mode 100644 index 85fce2376..000000000 --- a/pptx/compat/python3.py +++ /dev/null @@ -1,43 +0,0 @@ -# encoding: utf-8 - -"""Provides Python 3 compatibility objects.""" - -from io import BytesIO # noqa - - -def is_integer(obj): - """ - Return True if *obj* is an int, False otherwise. - """ - return isinstance(obj, int) - - -def is_string(obj): - """ - Return True if *obj* is a string, False otherwise. - """ - return isinstance(obj, str) - - -def is_unicode(obj): - """ - Return True if *obj* is a unicode string, False otherwise. - """ - return isinstance(obj, str) - - -def to_unicode(text): - """Return *text* as a (unicode) str. - - *text* can be str or bytes. A bytes object is assumed to be encoded as UTF-8. - If *text* is a str object it is returned unchanged. - """ - if isinstance(text, str): - return text - try: - return text.decode("utf-8") - except AttributeError: - raise TypeError("expected unicode string, got %s value %s" % (type(text), text)) - - -Unicode = str diff --git a/pptx/enum/action.py b/pptx/enum/action.py deleted file mode 100644 index 8bbf9189a..000000000 --- a/pptx/enum/action.py +++ /dev/null @@ -1,46 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations that describe click action settings -""" - -from __future__ import absolute_import - -from .base import alias, Enumeration, EnumMember - - -@alias("PP_ACTION") -class PP_ACTION_TYPE(Enumeration): - """ - Specifies the type of a mouse action (click or hover action). - - Alias: ``PP_ACTION`` - - Example:: - - from pptx.enum.action import PP_ACTION - - assert shape.click_action.action == PP_ACTION.HYPERLINK - """ - - __ms_name__ = "PpActionType" - - __url__ = "https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx" - - __members__ = ( - EnumMember("END_SHOW", 6, "Slide show ends."), - EnumMember("FIRST_SLIDE", 3, "Returns to the first slide."), - EnumMember("HYPERLINK", 7, "Hyperlink."), - EnumMember("LAST_SLIDE", 4, "Moves to the last slide."), - EnumMember("LAST_SLIDE_VIEWED", 5, "Moves to the last slide viewed."), - EnumMember("NAMED_SLIDE", 101, "Moves to slide specified by slide number."), - EnumMember("NAMED_SLIDE_SHOW", 10, "Runs the slideshow."), - EnumMember("NEXT_SLIDE", 1, "Moves to the next slide."), - EnumMember("NONE", 0, "No action is performed."), - EnumMember("OPEN_FILE", 102, "Opens the specified file."), - EnumMember("OLE_VERB", 11, "OLE Verb."), - EnumMember("PLAY", 12, "Begins the slideshow."), - EnumMember("PREVIOUS_SLIDE", 2, "Moves to the previous slide."), - EnumMember("RUN_MACRO", 8, "Runs a macro."), - EnumMember("RUN_PROGRAM", 9, "Runs a program."), - ) diff --git a/pptx/enum/base.py b/pptx/enum/base.py deleted file mode 100644 index c57e15b33..000000000 --- a/pptx/enum/base.py +++ /dev/null @@ -1,364 +0,0 @@ -# encoding: utf-8 - -""" -Base classes and other objects used by enumerations -""" - -from __future__ import absolute_import, print_function - -import sys -import textwrap - - -def alias(*aliases): - """ - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to - be referenced by each of the names provided as arguments. - """ - - def decorator(cls): - # alias must be set in globals from caller's frame - caller = sys._getframe(1) - globals_dict = caller.f_globals - for alias in aliases: - globals_dict[alias] = cls - return cls - - return decorator - - -class _DocsPageFormatter(object): - """ - Formats a RestructuredText documention page (string) for the enumeration - class parts passed to the constructor. An immutable one-shot service - object. - """ - - def __init__(self, clsname, clsdict): - self._clsname = clsname - self._clsdict = clsdict - - @property - def page_str(self): - """ - The RestructuredText documentation page for the enumeration. This is - the only API member for the class. - """ - tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s" - components = ( - self._ms_name, - self._page_title, - self._intro_text, - self._member_defs, - ) - return tmpl % components - - @property - def _intro_text(self): - """ - The docstring of the enumeration, formatted for use at the top of the - documentation page - """ - try: - cls_docstring = self._clsdict["__doc__"] - except KeyError: - cls_docstring = "" - - if cls_docstring is None: - return "" - - return textwrap.dedent(cls_docstring).strip() - - def _member_def(self, member): - """ - Return an individual member definition formatted as an RST glossary - entry, wrapped to fit within 78 columns. - """ - member_docstring = textwrap.dedent(member.docstring).strip() - member_docstring = textwrap.fill( - member_docstring, - width=78, - initial_indent=" " * 4, - subsequent_indent=" " * 4, - ) - return "%s\n%s\n" % (member.name, member_docstring) - - @property - def _member_defs(self): - """ - A single string containing the aggregated member definitions section - of the documentation page - """ - members = self._clsdict["__members__"] - member_defs = [ - self._member_def(member) for member in members if member.name is not None - ] - return "\n".join(member_defs) - - @property - def _ms_name(self): - """ - The Microsoft API name for this enumeration - """ - return self._clsdict["__ms_name__"] - - @property - def _page_title(self): - """ - The title for the documentation page, formatted as code (surrounded - in double-backtics) and underlined with '=' characters - """ - title_underscore = "=" * (len(self._clsname) + 4) - return "``%s``\n%s" % (self._clsname, title_underscore) - - -class MetaEnumeration(type): - """ - The metaclass for Enumeration and its subclasses. Adds a name for each - named member and compiles state needed by the enumeration class to - respond to other attribute gets - """ - - def __new__(meta, clsname, bases, clsdict): - meta._add_enum_members(clsdict) - meta._collect_valid_settings(clsdict) - meta._generate_docs_page(clsname, clsdict) - return type.__new__(meta, clsname, bases, clsdict) - - @classmethod - def _add_enum_members(meta, clsdict): - """ - Dispatch ``.add_to_enum()`` call to each member so it can do its - thing to properly add itself to the enumeration class. This - delegation allows member sub-classes to add specialized behaviors. - """ - enum_members = clsdict["__members__"] - for member in enum_members: - member.add_to_enum(clsdict) - - @classmethod - def _collect_valid_settings(meta, clsdict): - """ - Return a sequence containing the enumeration values that are valid - assignment values. Return-only values are excluded. - """ - enum_members = clsdict["__members__"] - valid_settings = [] - for member in enum_members: - valid_settings.extend(member.valid_settings) - clsdict["_valid_settings"] = valid_settings - - @classmethod - def _generate_docs_page(meta, clsname, clsdict): - """ - Return the RST documentation page for the enumeration. - """ - clsdict["__docs_rst__"] = _DocsPageFormatter(clsname, clsdict).page_str - - -class EnumerationBase(object): - """ - Base class for all enumerations, used directly for enumerations requiring - only basic behavior. It's __dict__ is used below in the Python 2+3 - compatible metaclass definition. - """ - - __members__ = () - __ms_name__ = "" - - @classmethod - def validate(cls, value): - """ - Raise |ValueError| if *value* is not an assignable value. - """ - if value not in cls._valid_settings: - raise ValueError( - "%s not a member of %s enumeration" % (value, cls.__name__) - ) - - -Enumeration = MetaEnumeration("Enumeration", (object,), dict(EnumerationBase.__dict__)) - - -class XmlEnumeration(Enumeration): - """ - Provides ``to_xml()`` and ``from_xml()`` methods in addition to base - enumeration features - """ - - __members__ = () - __ms_name__ = "" - - @classmethod - def from_xml(cls, xml_val): - """ - Return the enumeration member corresponding to the XML value - *xml_val*. - """ - return cls._xml_to_member[xml_val] - - @classmethod - def to_xml(cls, enum_val): - """ - Return the XML value of the enumeration value *enum_val*. - """ - cls.validate(enum_val) - 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 "{0:s} ({1:d})".format(self._member_name, self) - - -class ReturnValueOnlyEnumMember(EnumMember): - """ - Used to define a member of an enumeration that is only valid as a query - result and is not valid as a setting, e.g. MSO_VERTICAL_ANCHOR.MIXED (-2) - """ - - @property - def valid_settings(self): - """ - No settings are valid for a return-only value. - """ - return () - - -class XmlMappedEnumMember(EnumMember): - """ - Used to define a member whose value maps to an XML attribute value. - """ - - def __init__(self, name, value, xml_value, docstring): - super(XmlMappedEnumMember, self).__init__(name, value, docstring) - self._xml_value = xml_value - - def add_to_enum(self, clsdict): - """ - Compile XML mappings in addition to base add behavior. - """ - super(XmlMappedEnumMember, self).add_to_enum(clsdict) - self.register_xml_mapping(clsdict) - - def register_xml_mapping(self, clsdict): - """ - Add XML mappings to the enumeration class state for this member. - """ - member_to_xml = self._get_or_add_member_to_xml(clsdict) - member_to_xml[self.value] = self.xml_value - xml_to_member = self._get_or_add_xml_to_member(clsdict) - xml_to_member[self.xml_value] = self.value - - @property - def xml_value(self): - """ - The XML attribute value that corresponds to this enumeration value - """ - return self._xml_value - - @staticmethod - def _get_or_add_member_to_xml(clsdict): - """ - Add the enum -> xml value mapping to the enumeration class state - """ - if "_member_to_xml" not in clsdict: - clsdict["_member_to_xml"] = dict() - return clsdict["_member_to_xml"] - - @staticmethod - def _get_or_add_xml_to_member(clsdict): - """ - Add the xml -> enum value mapping to the enumeration class state - """ - if "_xml_to_member" not in clsdict: - clsdict["_xml_to_member"] = dict() - return clsdict["_xml_to_member"] diff --git a/pptx/enum/chart.py b/pptx/enum/chart.py deleted file mode 100644 index 26cd3e5fc..000000000 --- a/pptx/enum/chart.py +++ /dev/null @@ -1,363 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations used by charts and related objects -""" - -from __future__ import absolute_import - -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - - -class XL_AXIS_CROSSES(XmlEnumeration): - """ - Specifies the point on the specified axis where the other axis crosses. - - Example:: - - from pptx.enum.chart import XL_AXIS_CROSSES - - value_axis.crosses = XL_AXIS_CROSSES.MAXIMUM - """ - - __ms_name__ = "XlAxisCrosses" - - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff745402.aspx" - - __members__ = ( - XmlMappedEnumMember( - "AUTOMATIC", - -4105, - "autoZero", - "The axis crossing point is set " "automatically, often at zero.", - ), - ReturnValueOnlyEnumMember( - "CUSTOM", - -4114, - "The .crosses_at property specifies the axis cr" "ossing point.", - ), - XmlMappedEnumMember( - "MAXIMUM", 2, "max", "The axis crosses at the maximum value." - ), - XmlMappedEnumMember( - "MINIMUM", 4, "min", "The axis crosses at the minimum value." - ), - ) - - -class XL_CATEGORY_TYPE(Enumeration): - """ - Specifies the type of the category axis. - - Example:: - - from pptx.enum.chart import XL_CATEGORY_TYPE - - date_axis = chart.category_axis - assert date_axis.category_type == XL_CATEGORY_TYPE.TIME_SCALE - """ - - __ms_name__ = "XlCategoryType" - - __url__ = "https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx" - - __members__ = ( - EnumMember( - "AUTOMATIC_SCALE", -4105, "The application controls the axis " "type." - ), - EnumMember( - "CATEGORY_SCALE", 2, "Axis groups data by an arbitrary set of " "categories" - ), - EnumMember( - "TIME_SCALE", - 3, - "Axis groups data on a time scale of days, " "months, or years.", - ), - ) - - -class XL_CHART_TYPE(Enumeration): - """ - Specifies the type of a chart. - - Example:: - - from pptx.enum.chart import XL_CHART_TYPE - - assert chart.chart_type == XL_CHART_TYPE.BAR_STACKED - """ - - __ms_name__ = "XlChartType" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff838409.aspx" - - __members__ = ( - EnumMember("THREE_D_AREA", -4098, "3D Area."), - EnumMember("THREE_D_AREA_STACKED", 78, "3D Stacked Area."), - EnumMember("THREE_D_AREA_STACKED_100", 79, "100% Stacked Area."), - EnumMember("THREE_D_BAR_CLUSTERED", 60, "3D Clustered Bar."), - EnumMember("THREE_D_BAR_STACKED", 61, "3D Stacked Bar."), - EnumMember("THREE_D_BAR_STACKED_100", 62, "3D 100% Stacked Bar."), - EnumMember("THREE_D_COLUMN", -4100, "3D Column."), - EnumMember("THREE_D_COLUMN_CLUSTERED", 54, "3D Clustered Column."), - EnumMember("THREE_D_COLUMN_STACKED", 55, "3D Stacked Column."), - EnumMember("THREE_D_COLUMN_STACKED_100", 56, "3D 100% Stacked Column."), - EnumMember("THREE_D_LINE", -4101, "3D Line."), - EnumMember("THREE_D_PIE", -4102, "3D Pie."), - EnumMember("THREE_D_PIE_EXPLODED", 70, "Exploded 3D Pie."), - EnumMember("AREA", 1, "Area"), - EnumMember("AREA_STACKED", 76, "Stacked Area."), - EnumMember("AREA_STACKED_100", 77, "100% Stacked Area."), - EnumMember("BAR_CLUSTERED", 57, "Clustered Bar."), - EnumMember("BAR_OF_PIE", 71, "Bar of Pie."), - EnumMember("BAR_STACKED", 58, "Stacked Bar."), - EnumMember("BAR_STACKED_100", 59, "100% Stacked Bar."), - EnumMember("BUBBLE", 15, "Bubble."), - EnumMember("BUBBLE_THREE_D_EFFECT", 87, "Bubble with 3D effects."), - EnumMember("COLUMN_CLUSTERED", 51, "Clustered Column."), - EnumMember("COLUMN_STACKED", 52, "Stacked Column."), - EnumMember("COLUMN_STACKED_100", 53, "100% Stacked Column."), - EnumMember("CONE_BAR_CLUSTERED", 102, "Clustered Cone Bar."), - EnumMember("CONE_BAR_STACKED", 103, "Stacked Cone Bar."), - EnumMember("CONE_BAR_STACKED_100", 104, "100% Stacked Cone Bar."), - EnumMember("CONE_COL", 105, "3D Cone Column."), - EnumMember("CONE_COL_CLUSTERED", 99, "Clustered Cone Column."), - EnumMember("CONE_COL_STACKED", 100, "Stacked Cone Column."), - EnumMember("CONE_COL_STACKED_100", 101, "100% Stacked Cone Column."), - EnumMember("CYLINDER_BAR_CLUSTERED", 95, "Clustered Cylinder Bar."), - EnumMember("CYLINDER_BAR_STACKED", 96, "Stacked Cylinder Bar."), - EnumMember("CYLINDER_BAR_STACKED_100", 97, "100% Stacked Cylinder Bar."), - EnumMember("CYLINDER_COL", 98, "3D Cylinder Column."), - EnumMember("CYLINDER_COL_CLUSTERED", 92, "Clustered Cone Column."), - EnumMember("CYLINDER_COL_STACKED", 93, "Stacked Cone Column."), - EnumMember("CYLINDER_COL_STACKED_100", 94, "100% Stacked Cylinder Column."), - EnumMember("DOUGHNUT", -4120, "Doughnut."), - EnumMember("DOUGHNUT_EXPLODED", 80, "Exploded Doughnut."), - EnumMember("LINE", 4, "Line."), - EnumMember("LINE_MARKERS", 65, "Line with Markers."), - EnumMember("LINE_MARKERS_STACKED", 66, "Stacked Line with Markers."), - EnumMember("LINE_MARKERS_STACKED_100", 67, "100% Stacked Line with Markers."), - EnumMember("LINE_STACKED", 63, "Stacked Line."), - EnumMember("LINE_STACKED_100", 64, "100% Stacked Line."), - EnumMember("PIE", 5, "Pie."), - EnumMember("PIE_EXPLODED", 69, "Exploded Pie."), - EnumMember("PIE_OF_PIE", 68, "Pie of Pie."), - EnumMember("PYRAMID_BAR_CLUSTERED", 109, "Clustered Pyramid Bar."), - EnumMember("PYRAMID_BAR_STACKED", 110, "Stacked Pyramid Bar."), - EnumMember("PYRAMID_BAR_STACKED_100", 111, "100% Stacked Pyramid Bar."), - EnumMember("PYRAMID_COL", 112, "3D Pyramid Column."), - EnumMember("PYRAMID_COL_CLUSTERED", 106, "Clustered Pyramid Column."), - EnumMember("PYRAMID_COL_STACKED", 107, "Stacked Pyramid Column."), - EnumMember("PYRAMID_COL_STACKED_100", 108, "100% Stacked Pyramid Column."), - EnumMember("RADAR", -4151, "Radar."), - EnumMember("RADAR_FILLED", 82, "Filled Radar."), - EnumMember("RADAR_MARKERS", 81, "Radar with Data Markers."), - EnumMember("STOCK_HLC", 88, "High-Low-Close."), - EnumMember("STOCK_OHLC", 89, "Open-High-Low-Close."), - EnumMember("STOCK_VHLC", 90, "Volume-High-Low-Close."), - EnumMember("STOCK_VOHLC", 91, "Volume-Open-High-Low-Close."), - EnumMember("SURFACE", 83, "3D Surface."), - EnumMember("SURFACE_TOP_VIEW", 85, "Surface (Top View)."), - EnumMember("SURFACE_TOP_VIEW_WIREFRAME", 86, "Surface (Top View wireframe)."), - EnumMember("SURFACE_WIREFRAME", 84, "3D Surface (wireframe)."), - EnumMember("XY_SCATTER", -4169, "Scatter."), - EnumMember("XY_SCATTER_LINES", 74, "Scatter with Lines."), - EnumMember( - "XY_SCATTER_LINES_NO_MARKERS", - 75, - "Scatter with Lines and No Da" "ta Markers.", - ), - EnumMember("XY_SCATTER_SMOOTH", 72, "Scatter with Smoothed Lines."), - EnumMember( - "XY_SCATTER_SMOOTH_NO_MARKERS", - 73, - "Scatter with Smoothed Lines" " and No Data Markers.", - ), - ) - - -@alias("XL_LABEL_POSITION") -class XL_DATA_LABEL_POSITION(XmlEnumeration): - """ - Specifies where the data label is positioned. - - Example:: - - from pptx.enum.chart import XL_LABEL_POSITION - - data_labels = chart.plots[0].data_labels - data_labels.position = XL_LABEL_POSITION.OUTSIDE_END - """ - - __ms_name__ = "XlDataLabelPosition" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff745082.aspx" - - __members__ = ( - XmlMappedEnumMember( - "ABOVE", 0, "t", "The data label is positioned above the data point." - ), - XmlMappedEnumMember( - "BELOW", 1, "b", "The data label is positioned below the data point." - ), - XmlMappedEnumMember( - "BEST_FIT", 5, "bestFit", "Word sets the position of the data label." - ), - XmlMappedEnumMember( - "CENTER", - -4108, - "ctr", - "The data label is centered on the data point or inside a bar or a pie " - "slice.", - ), - XmlMappedEnumMember( - "INSIDE_BASE", - 4, - "inBase", - "The data label is positioned inside the data point at the bottom edge.", - ), - XmlMappedEnumMember( - "INSIDE_END", - 3, - "inEnd", - "The data label is positioned inside the data point at the top edge.", - ), - XmlMappedEnumMember( - "LEFT", - -4131, - "l", - "The data label is positioned to the left of the data point.", - ), - ReturnValueOnlyEnumMember("MIXED", 6, "Data labels are in multiple positions."), - XmlMappedEnumMember( - "OUTSIDE_END", - 2, - "outEnd", - "The data label is positioned outside the data point at the top edge.", - ), - XmlMappedEnumMember( - "RIGHT", - -4152, - "r", - "The data label is positioned to the right of the data point.", - ), - ) - - -class XL_LEGEND_POSITION(XmlEnumeration): - """ - Specifies the position of the legend on a chart. - - Example:: - - from pptx.enum.chart import XL_LEGEND_POSITION - - chart.has_legend = True - chart.legend.position = XL_LEGEND_POSITION.BOTTOM - """ - - __ms_name__ = "XlLegendPosition" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff745840.aspx" - - __members__ = ( - XmlMappedEnumMember("BOTTOM", -4107, "b", "Below the chart."), - XmlMappedEnumMember( - "CORNER", 2, "tr", "In the upper-right corner of the chart borde" "r." - ), - ReturnValueOnlyEnumMember("CUSTOM", -4161, "A custom position."), - XmlMappedEnumMember("LEFT", -4131, "l", "Left of the chart."), - XmlMappedEnumMember("RIGHT", -4152, "r", "Right of the chart."), - XmlMappedEnumMember("TOP", -4160, "t", "Above the chart."), - ) - - -class XL_MARKER_STYLE(XmlEnumeration): - """ - Specifies the marker style for a point or series in a line chart, scatter - chart, or radar chart. - - Example:: - - from pptx.enum.chart import XL_MARKER_STYLE - - series.marker.style = XL_MARKER_STYLE.CIRCLE - """ - - __ms_name__ = "XlMarkerStyle" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff197219.aspx" - - __members__ = ( - XmlMappedEnumMember("AUTOMATIC", -4105, "auto", "Automatic markers"), - XmlMappedEnumMember("CIRCLE", 8, "circle", "Circular markers"), - XmlMappedEnumMember("DASH", -4115, "dash", "Long bar markers"), - XmlMappedEnumMember("DIAMOND", 2, "diamond", "Diamond-shaped markers"), - XmlMappedEnumMember("DOT", -4118, "dot", "Short bar markers"), - XmlMappedEnumMember("NONE", -4142, "none", "No markers"), - XmlMappedEnumMember("PICTURE", -4147, "picture", "Picture markers"), - XmlMappedEnumMember("PLUS", 9, "plus", "Square markers with a plus sign"), - XmlMappedEnumMember("SQUARE", 1, "square", "Square markers"), - XmlMappedEnumMember("STAR", 5, "star", "Square markers with an asterisk"), - XmlMappedEnumMember("TRIANGLE", 3, "triangle", "Triangular markers"), - XmlMappedEnumMember("X", -4168, "x", "Square markers with an X"), - ) - - -class XL_TICK_MARK(XmlEnumeration): - """ - Specifies a type of axis tick for a chart. - - Example:: - - from pptx.enum.chart import XL_TICK_MARK - - chart.value_axis.minor_tick_mark = XL_TICK_MARK.INSIDE - """ - - __ms_name__ = "XlTickMark" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff193878.aspx" - - __members__ = ( - XmlMappedEnumMember("CROSS", 4, "cross", "Tick mark crosses the axis"), - XmlMappedEnumMember("INSIDE", 2, "in", "Tick mark appears inside the axis"), - XmlMappedEnumMember("NONE", -4142, "none", "No tick mark"), - XmlMappedEnumMember("OUTSIDE", 3, "out", "Tick mark appears outside the axis"), - ) - - -class XL_TICK_LABEL_POSITION(XmlEnumeration): - """ - Specifies the position of tick-mark labels on a chart axis. - - Example:: - - from pptx.enum.chart import XL_TICK_LABEL_POSITION - - category_axis = chart.category_axis - category_axis.tick_label_position = XL_TICK_LABEL_POSITION.LOW - """ - - __ms_name__ = "XlTickLabelPosition" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff822561.aspx" - - __members__ = ( - XmlMappedEnumMember("HIGH", -4127, "high", "Top or right side of the chart."), - XmlMappedEnumMember("LOW", -4134, "low", "Bottom or left side of the chart."), - XmlMappedEnumMember( - "NEXT_TO_AXIS", - 4, - "nextTo", - "Next to axis (where axis is not at" " either side of the chart).", - ), - XmlMappedEnumMember("NONE", -4142, "none", "No tick labels."), - ) diff --git a/pptx/enum/dml.py b/pptx/enum/dml.py deleted file mode 100644 index 765fe2dea..000000000 --- a/pptx/enum/dml.py +++ /dev/null @@ -1,319 +0,0 @@ -# encoding: utf-8 - -"""Enumerations used by DrawingML objects.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - - -class MSO_COLOR_TYPE(Enumeration): - """ - Specifies the color specification scheme - - Example:: - - from pptx.enum.dml import MSO_COLOR_TYPE - - assert shape.fill.fore_color.type == MSO_COLOR_TYPE.SCHEME - """ - - __ms_name__ = "MsoColorType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15" ").aspx" - ) - - __members__ = ( - EnumMember("RGB", 1, "Color is specified by an |RGBColor| value"), - EnumMember("SCHEME", 2, "Color is one of the preset theme colors"), - EnumMember( - "HSL", - 101, - """ - Color is specified using Hue, Saturation, and Luminosity values - """, - ), - EnumMember( - "PRESET", - 102, - """ - Color is specified using a named built-in color - """, - ), - EnumMember( - "SCRGB", - 103, - """ - Color is an scRGB color, a wide color gamut RGB color space - """, - ), - EnumMember( - "SYSTEM", - 104, - """ - Color is one specified by the operating system, such as the - window background color. - """, - ), - ) - - -@alias("MSO_FILL") -class MSO_FILL_TYPE(Enumeration): - """ - Specifies the type of bitmap used for the fill of a shape. - - Alias: ``MSO_FILL`` - - Example:: - - from pptx.enum.dml import MSO_FILL - - assert shape.fill.type == MSO_FILL.SOLID - """ - - __ms_name__ = "MsoFillType" - - __url__ = "http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx" - - __members__ = ( - EnumMember( - "BACKGROUND", - 5, - """ - The shape is transparent, such that whatever is behind the shape - shows through. Often this is the slide background, but if - a visible shape is behind, that will show through. - """, - ), - EnumMember("GRADIENT", 3, "Shape is filled with a gradient"), - EnumMember( - "GROUP", - 101, - "Shape is part of a group and should inherit the " - "fill properties of the group.", - ), - EnumMember("PATTERNED", 2, "Shape is filled with a pattern"), - EnumMember("PICTURE", 6, "Shape is filled with a bitmapped image"), - EnumMember("SOLID", 1, "Shape is filled with a solid color"), - EnumMember("TEXTURED", 4, "Shape is filled with a texture"), - ) - - -@alias("MSO_LINE") -class MSO_LINE_DASH_STYLE(XmlEnumeration): - """Specifies the dash style for a line. - - Alias: ``MSO_LINE`` - - Example:: - - from pptx.enum.dml import MSO_LINE - - shape.line.dash_style = MSO_LINE.DASH_DOT_DOT - """ - - __ms_name__ = "MsoLineDashStyle" - - __url__ = ( - "https://msdn.microsoft.com/en-us/vba/office-shared-vba/articles/mso" - "linedashstyle-enumeration-office" - ) - - __members__ = ( - XmlMappedEnumMember("DASH", 4, "dash", "Line consists of dashes only."), - XmlMappedEnumMember("DASH_DOT", 5, "dashDot", "Line is a dash-dot pattern."), - XmlMappedEnumMember( - "DASH_DOT_DOT", 6, "lgDashDotDot", "Line is a dash-dot-dot patte" "rn." - ), - XmlMappedEnumMember("LONG_DASH", 7, "lgDash", "Line consists of long dashes."), - XmlMappedEnumMember( - "LONG_DASH_DOT", 8, "lgDashDot", "Line is a long dash-dot patter" "n." - ), - XmlMappedEnumMember("ROUND_DOT", 3, "dot", "Line is made up of round dots."), - XmlMappedEnumMember("SOLID", 1, "solid", "Line is solid."), - XmlMappedEnumMember( - "SQUARE_DOT", 2, "sysDash", "Line is made up of square dots." - ), - ReturnValueOnlyEnumMember("DASH_STYLE_MIXED", -2, "Not supported."), - ) - - -@alias("MSO_PATTERN") -class MSO_PATTERN_TYPE(XmlEnumeration): - """Specifies the fill pattern used in a shape. - - Alias: ``MSO_PATTERN`` - - Example:: - - from pptx.enum.dml import MSO_PATTERN - - fill = shape.fill - fill.patterned() - fill.pattern = MSO_PATTERN.WAVE - """ - - __ms_name__ = "MsoPatternType" - - __url__ = ( - "https://msdn.microsoft.com/VBA/Office-Shared-VBA/articles/msopatter" - "ntype-enumeration-office" - ) - - __members__ = ( - XmlMappedEnumMember("CROSS", 51, "cross", "Cross"), - XmlMappedEnumMember( - "DARK_DOWNWARD_DIAGONAL", 15, "dkDnDiag", "Dark Downward Diagona" "l" - ), - XmlMappedEnumMember("DARK_HORIZONTAL", 13, "dkHorz", "Dark Horizontal"), - XmlMappedEnumMember( - "DARK_UPWARD_DIAGONAL", 16, "dkUpDiag", "Dark Upward Diagonal" - ), - XmlMappedEnumMember("DARK_VERTICAL", 14, "dkVert", "Dark Vertical"), - XmlMappedEnumMember( - "DASHED_DOWNWARD_DIAGONAL", 28, "dashDnDiag", "Dashed Downward D" "iagonal" - ), - XmlMappedEnumMember("DASHED_HORIZONTAL", 32, "dashHorz", "Dashed Horizontal"), - XmlMappedEnumMember( - "DASHED_UPWARD_DIAGONAL", 27, "dashUpDiag", "Dashed Upward Diago" "nal" - ), - XmlMappedEnumMember("DASHED_VERTICAL", 31, "dashVert", "Dashed Vertical"), - XmlMappedEnumMember("DIAGONAL_BRICK", 40, "diagBrick", "Diagonal Brick"), - XmlMappedEnumMember("DIAGONAL_CROSS", 54, "diagCross", "Diagonal Cross"), - XmlMappedEnumMember("DIVOT", 46, "divot", "Pattern Divot"), - XmlMappedEnumMember("DOTTED_DIAMOND", 24, "dotDmnd", "Dotted Diamond"), - XmlMappedEnumMember("DOTTED_GRID", 45, "dotGrid", "Dotted Grid"), - XmlMappedEnumMember("DOWNWARD_DIAGONAL", 52, "dnDiag", "Downward Diagonal"), - XmlMappedEnumMember("HORIZONTAL", 49, "horz", "Horizontal"), - XmlMappedEnumMember("HORIZONTAL_BRICK", 35, "horzBrick", "Horizontal Brick"), - XmlMappedEnumMember( - "LARGE_CHECKER_BOARD", 36, "lgCheck", "Large Checker Board" - ), - XmlMappedEnumMember("LARGE_CONFETTI", 33, "lgConfetti", "Large Confetti"), - XmlMappedEnumMember("LARGE_GRID", 34, "lgGrid", "Large Grid"), - XmlMappedEnumMember( - "LIGHT_DOWNWARD_DIAGONAL", 21, "ltDnDiag", "Light Downward Diago" "nal" - ), - XmlMappedEnumMember("LIGHT_HORIZONTAL", 19, "ltHorz", "Light Horizontal"), - XmlMappedEnumMember( - "LIGHT_UPWARD_DIAGONAL", 22, "ltUpDiag", "Light Upward Diagonal" - ), - XmlMappedEnumMember("LIGHT_VERTICAL", 20, "ltVert", "Light Vertical"), - XmlMappedEnumMember("NARROW_HORIZONTAL", 30, "narHorz", "Narrow Horizontal"), - XmlMappedEnumMember("NARROW_VERTICAL", 29, "narVert", "Narrow Vertical"), - XmlMappedEnumMember("OUTLINED_DIAMOND", 41, "openDmnd", "Outlined Diamond"), - XmlMappedEnumMember("PERCENT_10", 2, "pct10", "10% of the foreground color."), - XmlMappedEnumMember("PERCENT_20", 3, "pct20", "20% of the foreground color."), - XmlMappedEnumMember("PERCENT_25", 4, "pct25", "25% of the foreground color."), - XmlMappedEnumMember("PERCENT_30", 5, "pct30", "30% of the foreground color."), - XmlMappedEnumMember("PERCENT_40", 6, "pct40", "40% of the foreground color."), - XmlMappedEnumMember("PERCENT_5", 1, "pct5", "5% of the foreground color."), - XmlMappedEnumMember("PERCENT_50", 7, "pct50", "50% of the foreground color."), - XmlMappedEnumMember("PERCENT_60", 8, "pct60", "60% of the foreground color."), - XmlMappedEnumMember("PERCENT_70", 9, "pct70", "70% of the foreground color."), - XmlMappedEnumMember("PERCENT_75", 10, "pct75", "75% of the foreground color."), - XmlMappedEnumMember("PERCENT_80", 11, "pct80", "80% of the foreground color."), - XmlMappedEnumMember("PERCENT_90", 12, "pct90", "90% of the foreground color."), - XmlMappedEnumMember("PLAID", 42, "plaid", "Plaid"), - XmlMappedEnumMember("SHINGLE", 47, "shingle", "Shingle"), - XmlMappedEnumMember( - "SMALL_CHECKER_BOARD", 17, "smCheck", "Small Checker Board" - ), - XmlMappedEnumMember("SMALL_CONFETTI", 37, "smConfetti", "Small Confetti"), - XmlMappedEnumMember("SMALL_GRID", 23, "smGrid", "Small Grid"), - XmlMappedEnumMember("SOLID_DIAMOND", 39, "solidDmnd", "Solid Diamond"), - XmlMappedEnumMember("SPHERE", 43, "sphere", "Sphere"), - XmlMappedEnumMember("TRELLIS", 18, "trellis", "Trellis"), - XmlMappedEnumMember("UPWARD_DIAGONAL", 53, "upDiag", "Upward Diagonal"), - XmlMappedEnumMember("VERTICAL", 50, "vert", "Vertical"), - XmlMappedEnumMember("WAVE", 48, "wave", "Wave"), - XmlMappedEnumMember("WEAVE", 44, "weave", "Weave"), - XmlMappedEnumMember( - "WIDE_DOWNWARD_DIAGONAL", 25, "wdDnDiag", "Wide Downward Diagona" "l" - ), - XmlMappedEnumMember( - "WIDE_UPWARD_DIAGONAL", 26, "wdUpDiag", "Wide Upward Diagonal" - ), - XmlMappedEnumMember("ZIG_ZAG", 38, "zigZag", "Zig Zag"), - ReturnValueOnlyEnumMember("MIXED", -2, "Mixed pattern."), - ) - - -@alias("MSO_THEME_COLOR") -class MSO_THEME_COLOR_INDEX(XmlEnumeration): - """ - Indicates the Office theme color, one of those shown in the color gallery - on the formatting ribbon. - - Alias: ``MSO_THEME_COLOR`` - - Example:: - - from pptx.enum.dml import MSO_THEME_COLOR - - shape.fill.solid() - shape.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_1 - """ - - __ms_name__ = "MsoThemeColorIndex" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15" ").aspx" - ) - - __members__ = ( - EnumMember("NOT_THEME_COLOR", 0, "Indicates the color is not a theme color."), - XmlMappedEnumMember( - "ACCENT_1", 5, "accent1", "Specifies the Accent 1 theme color." - ), - XmlMappedEnumMember( - "ACCENT_2", 6, "accent2", "Specifies the Accent 2 theme color." - ), - XmlMappedEnumMember( - "ACCENT_3", 7, "accent3", "Specifies the Accent 3 theme color." - ), - XmlMappedEnumMember( - "ACCENT_4", 8, "accent4", "Specifies the Accent 4 theme color." - ), - XmlMappedEnumMember( - "ACCENT_5", 9, "accent5", "Specifies the Accent 5 theme color." - ), - XmlMappedEnumMember( - "ACCENT_6", 10, "accent6", "Specifies the Accent 6 theme color." - ), - XmlMappedEnumMember( - "BACKGROUND_1", 14, "bg1", "Specifies the Background 1 theme " "color." - ), - XmlMappedEnumMember( - "BACKGROUND_2", 16, "bg2", "Specifies the Background 2 theme " "color." - ), - XmlMappedEnumMember("DARK_1", 1, "dk1", "Specifies the Dark 1 theme color."), - XmlMappedEnumMember("DARK_2", 3, "dk2", "Specifies the Dark 2 theme color."), - XmlMappedEnumMember( - "FOLLOWED_HYPERLINK", - 12, - "folHlink", - "Specifies the theme color" " for a clicked hyperlink.", - ), - XmlMappedEnumMember( - "HYPERLINK", 11, "hlink", "Specifies the theme color for a hyper" "link." - ), - XmlMappedEnumMember("LIGHT_1", 2, "lt1", "Specifies the Light 1 theme color."), - XmlMappedEnumMember("LIGHT_2", 4, "lt2", "Specifies the Light 2 theme color."), - XmlMappedEnumMember("TEXT_1", 13, "tx1", "Specifies the Text 1 theme color."), - XmlMappedEnumMember("TEXT_2", 15, "tx2", "Specifies the Text 2 theme color."), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Indicates multiple theme colors are used, such as " "in a group shape.", - ), - ) diff --git a/pptx/enum/lang.py b/pptx/enum/lang.py deleted file mode 100644 index f466218ec..000000000 --- a/pptx/enum/lang.py +++ /dev/null @@ -1,500 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations used for specifying language. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from .base import ( - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - - -class MSO_LANGUAGE_ID(XmlEnumeration): - """ - Specifies the language identifier. - - Example:: - - from pptx.enum.lang import MSO_LANGUAGE_ID - - font.language_id = MSO_LANGUAGE_ID.POLISH - """ - - __ms_name__ = "MsoLanguageId" - - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff862134.aspx" - - __members__ = ( - ReturnValueOnlyEnumMember( - "MIXED", -2, "More than one language in specified range." - ), - EnumMember("NONE", 0, "No language specified."), - XmlMappedEnumMember("AFRIKAANS", 1078, "af-ZA", "The Afrikaans language."), - XmlMappedEnumMember("ALBANIAN", 1052, "sq-AL", "The Albanian language."), - XmlMappedEnumMember("AMHARIC", 1118, "am-ET", "The Amharic language."), - XmlMappedEnumMember("ARABIC", 1025, "ar-SA", "The Arabic language."), - XmlMappedEnumMember( - "ARABIC_ALGERIA", 5121, "ar-DZ", "The Arabic Algeria language." - ), - XmlMappedEnumMember( - "ARABIC_BAHRAIN", 15361, "ar-BH", "The Arabic Bahrain language." - ), - XmlMappedEnumMember( - "ARABIC_EGYPT", 3073, "ar-EG", "The Arabic Egypt language." - ), - XmlMappedEnumMember("ARABIC_IRAQ", 2049, "ar-IQ", "The Arabic Iraq language."), - XmlMappedEnumMember( - "ARABIC_JORDAN", 11265, "ar-JO", "The Arabic Jordan language." - ), - XmlMappedEnumMember( - "ARABIC_KUWAIT", 13313, "ar-KW", "The Arabic Kuwait language." - ), - XmlMappedEnumMember( - "ARABIC_LEBANON", 12289, "ar-LB", "The Arabic Lebanon language." - ), - XmlMappedEnumMember( - "ARABIC_LIBYA", 4097, "ar-LY", "The Arabic Libya language." - ), - XmlMappedEnumMember( - "ARABIC_MOROCCO", 6145, "ar-MA", "The Arabic Morocco language." - ), - XmlMappedEnumMember("ARABIC_OMAN", 8193, "ar-OM", "The Arabic Oman language."), - XmlMappedEnumMember( - "ARABIC_QATAR", 16385, "ar-QA", "The Arabic Qatar language." - ), - XmlMappedEnumMember( - "ARABIC_SYRIA", 10241, "ar-SY", "The Arabic Syria language." - ), - XmlMappedEnumMember( - "ARABIC_TUNISIA", 7169, "ar-TN", "The Arabic Tunisia language." - ), - XmlMappedEnumMember("ARABIC_UAE", 14337, "ar-AE", "The Arabic UAE language."), - XmlMappedEnumMember( - "ARABIC_YEMEN", 9217, "ar-YE", "The Arabic Yemen language." - ), - XmlMappedEnumMember("ARMENIAN", 1067, "hy-AM", "The Armenian language."), - XmlMappedEnumMember("ASSAMESE", 1101, "as-IN", "The Assamese language."), - XmlMappedEnumMember( - "AZERI_CYRILLIC", 2092, "az-AZ", "The Azeri Cyrillic language." - ), - XmlMappedEnumMember( - "AZERI_LATIN", 1068, "az-Latn-AZ", "The Azeri Latin language." - ), - XmlMappedEnumMember("BASQUE", 1069, "eu-ES", "The Basque language."), - XmlMappedEnumMember( - "BELGIAN_DUTCH", 2067, "nl-BE", "The Belgian Dutch language." - ), - XmlMappedEnumMember( - "BELGIAN_FRENCH", 2060, "fr-BE", "The Belgian French language." - ), - XmlMappedEnumMember("BENGALI", 1093, "bn-IN", "The Bengali language."), - XmlMappedEnumMember("BOSNIAN", 4122, "hr-BA", "The Bosnian language."), - XmlMappedEnumMember( - "BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC", - 8218, - "bs-BA", - "The Bosni" "an Bosnia Herzegovina Cyrillic language.", - ), - XmlMappedEnumMember( - "BOSNIAN_BOSNIA_HERZEGOVINA_LATIN", - 5146, - "bs-Latn-BA", - "The Bos" "nian Bosnia Herzegovina Latin language.", - ), - XmlMappedEnumMember( - "BRAZILIAN_PORTUGUESE", - 1046, - "pt-BR", - "The Brazilian Portuguese" " language.", - ), - XmlMappedEnumMember("BULGARIAN", 1026, "bg-BG", "The Bulgarian language."), - XmlMappedEnumMember("BURMESE", 1109, "my-MM", "The Burmese language."), - XmlMappedEnumMember( - "BYELORUSSIAN", 1059, "be-BY", "The Byelorussian language." - ), - XmlMappedEnumMember("CATALAN", 1027, "ca-ES", "The Catalan language."), - XmlMappedEnumMember("CHEROKEE", 1116, "chr-US", "The Cherokee language."), - XmlMappedEnumMember( - "CHINESE_HONG_KONG_SAR", - 3076, - "zh-HK", - "The Chinese Hong Kong S" "AR language.", - ), - XmlMappedEnumMember( - "CHINESE_MACAO_SAR", 5124, "zh-MO", "The Chinese Macao SAR langu" "age." - ), - XmlMappedEnumMember( - "CHINESE_SINGAPORE", 4100, "zh-SG", "The Chinese Singapore langu" "age." - ), - XmlMappedEnumMember("CROATIAN", 1050, "hr-HR", "The Croatian language."), - XmlMappedEnumMember("CZECH", 1029, "cs-CZ", "The Czech language."), - XmlMappedEnumMember("DANISH", 1030, "da-DK", "The Danish language."), - XmlMappedEnumMember("DIVEHI", 1125, "div-MV", "The Divehi language."), - XmlMappedEnumMember("DUTCH", 1043, "nl-NL", "The Dutch language."), - XmlMappedEnumMember("EDO", 1126, "bin-NG", "The Edo language."), - XmlMappedEnumMember("ENGLISH_AUS", 3081, "en-AU", "The English AUS language."), - XmlMappedEnumMember( - "ENGLISH_BELIZE", 10249, "en-BZ", "The English Belize language." - ), - XmlMappedEnumMember( - "ENGLISH_CANADIAN", 4105, "en-CA", "The English Canadian languag" "e." - ), - XmlMappedEnumMember( - "ENGLISH_CARIBBEAN", 9225, "en-CB", "The English Caribbean langu" "age." - ), - XmlMappedEnumMember( - "ENGLISH_INDONESIA", 14345, "en-ID", "The English Indonesia lang" "uage." - ), - XmlMappedEnumMember( - "ENGLISH_IRELAND", 6153, "en-IE", "The English Ireland language." - ), - XmlMappedEnumMember( - "ENGLISH_JAMAICA", 8201, "en-JA", "The English Jamaica language." - ), - XmlMappedEnumMember( - "ENGLISH_NEW_ZEALAND", 5129, "en-NZ", "The English NewZealand la" "nguage." - ), - XmlMappedEnumMember( - "ENGLISH_PHILIPPINES", - 13321, - "en-PH", - "The English Philippines " "language.", - ), - XmlMappedEnumMember( - "ENGLISH_SOUTH_AFRICA", - 7177, - "en-ZA", - "The English South Africa" " language.", - ), - XmlMappedEnumMember( - "ENGLISH_TRINIDAD_TOBAGO", - 11273, - "en-TT", - "The English Trinidad" " Tobago language.", - ), - XmlMappedEnumMember("ENGLISH_UK", 2057, "en-GB", "The English UK language."), - XmlMappedEnumMember("ENGLISH_US", 1033, "en-US", "The English US language."), - XmlMappedEnumMember( - "ENGLISH_ZIMBABWE", 12297, "en-ZW", "The English Zimbabwe langua" "ge." - ), - XmlMappedEnumMember("ESTONIAN", 1061, "et-EE", "The Estonian language."), - XmlMappedEnumMember("FAEROESE", 1080, "fo-FO", "The Faeroese language."), - XmlMappedEnumMember("FARSI", 1065, "fa-IR", "The Farsi language."), - XmlMappedEnumMember("FILIPINO", 1124, "fil-PH", "The Filipino language."), - XmlMappedEnumMember("FINNISH", 1035, "fi-FI", "The Finnish language."), - XmlMappedEnumMember( - "FRANCH_CONGO_DRC", 9228, "fr-CD", "The French Congo DRC languag" "e." - ), - XmlMappedEnumMember("FRENCH", 1036, "fr-FR", "The French language."), - XmlMappedEnumMember( - "FRENCH_CAMEROON", 11276, "fr-CM", "The French Cameroon language" "." - ), - XmlMappedEnumMember( - "FRENCH_CANADIAN", 3084, "fr-CA", "The French Canadian language." - ), - XmlMappedEnumMember( - "FRENCH_COTED_IVOIRE", - 12300, - "fr-CI", - "The French Coted Ivoire " "language.", - ), - XmlMappedEnumMember( - "FRENCH_HAITI", 15372, "fr-HT", "The French Haiti language." - ), - XmlMappedEnumMember( - "FRENCH_LUXEMBOURG", 5132, "fr-LU", "The French Luxembourg langu" "age." - ), - XmlMappedEnumMember("FRENCH_MALI", 13324, "fr-ML", "The French Mali language."), - XmlMappedEnumMember( - "FRENCH_MONACO", 6156, "fr-MC", "The French Monaco language." - ), - XmlMappedEnumMember( - "FRENCH_MOROCCO", 14348, "fr-MA", "The French Morocco language." - ), - XmlMappedEnumMember( - "FRENCH_REUNION", 8204, "fr-RE", "The French Reunion language." - ), - XmlMappedEnumMember( - "FRENCH_SENEGAL", 10252, "fr-SN", "The French Senegal language." - ), - XmlMappedEnumMember( - "FRENCH_WEST_INDIES", - 7180, - "fr-WINDIES", - "The French West Indie" "s language.", - ), - XmlMappedEnumMember( - "FRISIAN_NETHERLANDS", 1122, "fy-NL", "The Frisian Netherlands l" "anguage." - ), - XmlMappedEnumMember("FULFULDE", 1127, "ff-NG", "The Fulfulde language."), - XmlMappedEnumMember( - "GAELIC_IRELAND", 2108, "ga-IE", "The Gaelic Ireland language." - ), - XmlMappedEnumMember( - "GAELIC_SCOTLAND", 1084, "en-US", "The Gaelic Scotland language." - ), - XmlMappedEnumMember("GALICIAN", 1110, "gl-ES", "The Galician language."), - XmlMappedEnumMember("GEORGIAN", 1079, "ka-GE", "The Georgian language."), - XmlMappedEnumMember("GERMAN", 1031, "de-DE", "The German language."), - XmlMappedEnumMember( - "GERMAN_AUSTRIA", 3079, "de-AT", "The German Austria language." - ), - XmlMappedEnumMember( - "GERMAN_LIECHTENSTEIN", - 5127, - "de-LI", - "The German Liechtenstein" " language.", - ), - XmlMappedEnumMember( - "GERMAN_LUXEMBOURG", 4103, "de-LU", "The German Luxembourg langu" "age." - ), - XmlMappedEnumMember("GREEK", 1032, "el-GR", "The Greek language."), - XmlMappedEnumMember("GUARANI", 1140, "gn-PY", "The Guarani language."), - XmlMappedEnumMember("GUJARATI", 1095, "gu-IN", "The Gujarati language."), - XmlMappedEnumMember("HAUSA", 1128, "ha-NG", "The Hausa language."), - XmlMappedEnumMember("HAWAIIAN", 1141, "haw-US", "The Hawaiian language."), - XmlMappedEnumMember("HEBREW", 1037, "he-IL", "The Hebrew language."), - XmlMappedEnumMember("HINDI", 1081, "hi-IN", "The Hindi language."), - XmlMappedEnumMember("HUNGARIAN", 1038, "hu-HU", "The Hungarian language."), - XmlMappedEnumMember("IBIBIO", 1129, "ibb-NG", "The Ibibio language."), - XmlMappedEnumMember("ICELANDIC", 1039, "is-IS", "The Icelandic language."), - XmlMappedEnumMember("IGBO", 1136, "ig-NG", "The Igbo language."), - XmlMappedEnumMember("INDONESIAN", 1057, "id-ID", "The Indonesian language."), - XmlMappedEnumMember("INUKTITUT", 1117, "iu-Cans-CA", "The Inuktitut language."), - XmlMappedEnumMember("ITALIAN", 1040, "it-IT", "The Italian language."), - XmlMappedEnumMember("JAPANESE", 1041, "ja-JP", "The Japanese language."), - XmlMappedEnumMember("KANNADA", 1099, "kn-IN", "The Kannada language."), - XmlMappedEnumMember("KANURI", 1137, "kr-NG", "The Kanuri language."), - XmlMappedEnumMember("KASHMIRI", 1120, "ks-Arab", "The Kashmiri language."), - XmlMappedEnumMember( - "KASHMIRI_DEVANAGARI", - 2144, - "ks-Deva", - "The Kashmiri Devanagari" " language.", - ), - XmlMappedEnumMember("KAZAKH", 1087, "kk-KZ", "The Kazakh language."), - XmlMappedEnumMember("KHMER", 1107, "kh-KH", "The Khmer language."), - XmlMappedEnumMember("KIRGHIZ", 1088, "ky-KG", "The Kirghiz language."), - XmlMappedEnumMember("KONKANI", 1111, "kok-IN", "The Konkani language."), - XmlMappedEnumMember("KOREAN", 1042, "ko-KR", "The Korean language."), - XmlMappedEnumMember("KYRGYZ", 1088, "ky-KG", "The Kyrgyz language."), - XmlMappedEnumMember("LAO", 1108, "lo-LA", "The Lao language."), - XmlMappedEnumMember("LATIN", 1142, "la-Latn", "The Latin language."), - XmlMappedEnumMember("LATVIAN", 1062, "lv-LV", "The Latvian language."), - XmlMappedEnumMember("LITHUANIAN", 1063, "lt-LT", "The Lithuanian language."), - XmlMappedEnumMember( - "MACEDONINAN_FYROM", 1071, "mk-MK", "The Macedonian FYROM langua" "ge." - ), - XmlMappedEnumMember( - "MALAY_BRUNEI_DARUSSALAM", - 2110, - "ms-BN", - "The Malay Brunei Daru" "ssalam language.", - ), - XmlMappedEnumMember("MALAYALAM", 1100, "ml-IN", "The Malayalam language."), - XmlMappedEnumMember("MALAYSIAN", 1086, "ms-MY", "The Malaysian language."), - XmlMappedEnumMember("MALTESE", 1082, "mt-MT", "The Maltese language."), - XmlMappedEnumMember("MANIPURI", 1112, "mni-IN", "The Manipuri language."), - XmlMappedEnumMember("MAORI", 1153, "mi-NZ", "The Maori language."), - XmlMappedEnumMember("MARATHI", 1102, "mr-IN", "The Marathi language."), - XmlMappedEnumMember( - "MEXICAN_SPANISH", 2058, "es-MX", "The Mexican Spanish language." - ), - XmlMappedEnumMember("MONGOLIAN", 1104, "mn-MN", "The Mongolian language."), - XmlMappedEnumMember("NEPALI", 1121, "ne-NP", "The Nepali language."), - XmlMappedEnumMember("NO_PROOFING", 1024, "en-US", "No proofing."), - XmlMappedEnumMember( - "NORWEGIAN_BOKMOL", 1044, "nb-NO", "The Norwegian Bokmol languag" "e." - ), - XmlMappedEnumMember( - "NORWEGIAN_NYNORSK", 2068, "nn-NO", "The Norwegian Nynorsk langu" "age." - ), - XmlMappedEnumMember("ORIYA", 1096, "or-IN", "The Oriya language."), - XmlMappedEnumMember("OROMO", 1138, "om-Ethi-ET", "The Oromo language."), - XmlMappedEnumMember("PASHTO", 1123, "ps-AF", "The Pashto language."), - XmlMappedEnumMember("POLISH", 1045, "pl-PL", "The Polish language."), - XmlMappedEnumMember("PORTUGUESE", 2070, "pt-PT", "The Portuguese language."), - XmlMappedEnumMember("PUNJABI", 1094, "pa-IN", "The Punjabi language."), - XmlMappedEnumMember( - "QUECHUA_BOLIVIA", 1131, "quz-BO", "The Quechua Bolivia language" "." - ), - XmlMappedEnumMember( - "QUECHUA_ECUADOR", 2155, "quz-EC", "The Quechua Ecuador language" "." - ), - XmlMappedEnumMember( - "QUECHUA_PERU", 3179, "quz-PE", "The Quechua Peru language." - ), - XmlMappedEnumMember( - "RHAETO_ROMANIC", 1047, "rm-CH", "The Rhaeto Romanic language." - ), - XmlMappedEnumMember("ROMANIAN", 1048, "ro-RO", "The Romanian language."), - XmlMappedEnumMember( - "ROMANIAN_MOLDOVA", 2072, "ro-MO", "The Romanian Moldova languag" "e." - ), - XmlMappedEnumMember("RUSSIAN", 1049, "ru-RU", "The Russian language."), - XmlMappedEnumMember( - "RUSSIAN_MOLDOVA", 2073, "ru-MO", "The Russian Moldova language." - ), - XmlMappedEnumMember( - "SAMI_LAPPISH", 1083, "se-NO", "The Sami Lappish language." - ), - XmlMappedEnumMember("SANSKRIT", 1103, "sa-IN", "The Sanskrit language."), - XmlMappedEnumMember("SEPEDI", 1132, "ns-ZA", "The Sepedi language."), - XmlMappedEnumMember( - "SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC", - 7194, - "sr-BA", - "The Serbi" "an Bosnia Herzegovina Cyrillic language.", - ), - XmlMappedEnumMember( - "SERBIAN_BOSNIA_HERZEGOVINA_LATIN", - 6170, - "sr-Latn-BA", - "The Ser" "bian Bosnia Herzegovina Latin language.", - ), - XmlMappedEnumMember( - "SERBIAN_CYRILLIC", 3098, "sr-SP", "The Serbian Cyrillic languag" "e." - ), - XmlMappedEnumMember( - "SERBIAN_LATIN", 2074, "sr-Latn-CS", "The Serbian Latin language" "." - ), - XmlMappedEnumMember("SESOTHO", 1072, "st-ZA", "The Sesotho language."), - XmlMappedEnumMember( - "SIMPLIFIED_CHINESE", 2052, "zh-CN", "The Simplified Chinese lan" "guage." - ), - XmlMappedEnumMember("SINDHI", 1113, "sd-Deva-IN", "The Sindhi language."), - XmlMappedEnumMember( - "SINDHI_PAKISTAN", 2137, "sd-Arab-PK", "The Sindhi Pakistan lang" "uage." - ), - XmlMappedEnumMember("SINHALESE", 1115, "si-LK", "The Sinhalese language."), - XmlMappedEnumMember("SLOVAK", 1051, "sk-SK", "The Slovak language."), - XmlMappedEnumMember("SLOVENIAN", 1060, "sl-SI", "The Slovenian language."), - XmlMappedEnumMember("SOMALI", 1143, "so-SO", "The Somali language."), - XmlMappedEnumMember("SORBIAN", 1070, "wen-DE", "The Sorbian language."), - XmlMappedEnumMember("SPANISH", 1034, "es-ES_tradnl", "The Spanish language."), - XmlMappedEnumMember( - "SPANISH_ARGENTINA", 11274, "es-AR", "The Spanish Argentina lang" "uage." - ), - XmlMappedEnumMember( - "SPANISH_BOLIVIA", 16394, "es-BO", "The Spanish Bolivia language" "." - ), - XmlMappedEnumMember( - "SPANISH_CHILE", 13322, "es-CL", "The Spanish Chile language." - ), - XmlMappedEnumMember( - "SPANISH_COLOMBIA", 9226, "es-CO", "The Spanish Colombia languag" "e." - ), - XmlMappedEnumMember( - "SPANISH_COSTA_RICA", 5130, "es-CR", "The Spanish Costa Rica lan" "guage." - ), - XmlMappedEnumMember( - "SPANISH_DOMINICAN_REPUBLIC", - 7178, - "es-DO", - "The Spanish Domini" "can Republic language.", - ), - XmlMappedEnumMember( - "SPANISH_ECUADOR", 12298, "es-EC", "The Spanish Ecuador language" "." - ), - XmlMappedEnumMember( - "SPANISH_EL_SALVADOR", - 17418, - "es-SV", - "The Spanish El Salvador " "language.", - ), - XmlMappedEnumMember( - "SPANISH_GUATEMALA", 4106, "es-GT", "The Spanish Guatemala langu" "age." - ), - XmlMappedEnumMember( - "SPANISH_HONDURAS", 18442, "es-HN", "The Spanish Honduras langua" "ge." - ), - XmlMappedEnumMember( - "SPANISH_MODERN_SORT", 3082, "es-ES", "The Spanish Modern Sort l" "anguage." - ), - XmlMappedEnumMember( - "SPANISH_NICARAGUA", 19466, "es-NI", "The Spanish Nicaragua lang" "uage." - ), - XmlMappedEnumMember( - "SPANISH_PANAMA", 6154, "es-PA", "The Spanish Panama language." - ), - XmlMappedEnumMember( - "SPANISH_PARAGUAY", 15370, "es-PY", "The Spanish Paraguay langua" "ge." - ), - XmlMappedEnumMember( - "SPANISH_PERU", 10250, "es-PE", "The Spanish Peru language." - ), - XmlMappedEnumMember( - "SPANISH_PUERTO_RICO", - 20490, - "es-PR", - "The Spanish Puerto Rico " "language.", - ), - XmlMappedEnumMember( - "SPANISH_URUGUAY", 14346, "es-UR", "The Spanish Uruguay language" "." - ), - XmlMappedEnumMember( - "SPANISH_VENEZUELA", 8202, "es-VE", "The Spanish Venezuela langu" "age." - ), - XmlMappedEnumMember("SUTU", 1072, "st-ZA", "The Sutu language."), - XmlMappedEnumMember("SWAHILI", 1089, "sw-KE", "The Swahili language."), - XmlMappedEnumMember("SWEDISH", 1053, "sv-SE", "The Swedish language."), - XmlMappedEnumMember( - "SWEDISH_FINLAND", 2077, "sv-FI", "The Swedish Finland language." - ), - XmlMappedEnumMember( - "SWISS_FRENCH", 4108, "fr-CH", "The Swiss French language." - ), - XmlMappedEnumMember( - "SWISS_GERMAN", 2055, "de-CH", "The Swiss German language." - ), - XmlMappedEnumMember( - "SWISS_ITALIAN", 2064, "it-CH", "The Swiss Italian language." - ), - XmlMappedEnumMember("SYRIAC", 1114, "syr-SY", "The Syriac language."), - XmlMappedEnumMember("TAJIK", 1064, "tg-TJ", "The Tajik language."), - XmlMappedEnumMember( - "TAMAZIGHT", 1119, "tzm-Arab-MA", "The Tamazight language." - ), - XmlMappedEnumMember( - "TAMAZIGHT_LATIN", 2143, "tmz-DZ", "The Tamazight Latin language" "." - ), - XmlMappedEnumMember("TAMIL", 1097, "ta-IN", "The Tamil language."), - XmlMappedEnumMember("TATAR", 1092, "tt-RU", "The Tatar language."), - XmlMappedEnumMember("TELUGU", 1098, "te-IN", "The Telugu language."), - XmlMappedEnumMember("THAI", 1054, "th-TH", "The Thai language."), - XmlMappedEnumMember("TIBETAN", 1105, "bo-CN", "The Tibetan language."), - XmlMappedEnumMember( - "TIGRIGNA_ERITREA", 2163, "ti-ER", "The Tigrigna Eritrea languag" "e." - ), - XmlMappedEnumMember( - "TIGRIGNA_ETHIOPIC", 1139, "ti-ET", "The Tigrigna Ethiopic langu" "age." - ), - XmlMappedEnumMember( - "TRADITIONAL_CHINESE", 1028, "zh-TW", "The Traditional Chinese l" "anguage." - ), - XmlMappedEnumMember("TSONGA", 1073, "ts-ZA", "The Tsonga language."), - XmlMappedEnumMember("TSWANA", 1074, "tn-ZA", "The Tswana language."), - XmlMappedEnumMember("TURKISH", 1055, "tr-TR", "The Turkish language."), - XmlMappedEnumMember("TURKMEN", 1090, "tk-TM", "The Turkmen language."), - XmlMappedEnumMember("UKRAINIAN", 1058, "uk-UA", "The Ukrainian language."), - XmlMappedEnumMember("URDU", 1056, "ur-PK", "The Urdu language."), - XmlMappedEnumMember( - "UZBEK_CYRILLIC", 2115, "uz-UZ", "The Uzbek Cyrillic language." - ), - XmlMappedEnumMember( - "UZBEK_LATIN", 1091, "uz-Latn-UZ", "The Uzbek Latin language." - ), - XmlMappedEnumMember("VENDA", 1075, "ve-ZA", "The Venda language."), - XmlMappedEnumMember("VIETNAMESE", 1066, "vi-VN", "The Vietnamese language."), - XmlMappedEnumMember("WELSH", 1106, "cy-GB", "The Welsh language."), - XmlMappedEnumMember("XHOSA", 1076, "xh-ZA", "The Xhosa language."), - XmlMappedEnumMember("YI", 1144, "ii-CN", "The Yi language."), - XmlMappedEnumMember("YIDDISH", 1085, "yi-Hebr", "The Yiddish language."), - XmlMappedEnumMember("YORUBA", 1130, "yo-NG", "The Yoruba language."), - XmlMappedEnumMember("ZULU", 1077, "zu-ZA", "The Zulu language."), - ) diff --git a/pptx/enum/shapes.py b/pptx/enum/shapes.py deleted file mode 100644 index 0fd1e0100..000000000 --- a/pptx/enum/shapes.py +++ /dev/null @@ -1,885 +0,0 @@ -# encoding: utf-8 - -"""Enumerations used by shapes and related objects.""" - -from pptx.enum.base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) -from pptx.util import lazyproperty - - -@alias("MSO_SHAPE") -class MSO_AUTO_SHAPE_TYPE(XmlEnumeration): - """ - Specifies a type of AutoShape, e.g. DOWN_ARROW - - Alias: ``MSO_SHAPE`` - - Example:: - - from pptx.enum.shapes import MSO_SHAPE - from pptx.util import Inches - - left = top = width = height = Inches(1.0) - slide.shapes.add_shape( - MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height - ) - """ - - __ms_name__ = "MsoAutoShapeType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff862770(v=office.15" ").aspx" - ) - - __members__ = ( - XmlMappedEnumMember( - "ACTION_BUTTON_BACK_OR_PREVIOUS", - 129, - "actionButtonBackPrevious", - "Back or Previous button. Supports " "mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_BEGINNING", - 131, - "actionButtonBeginning", - "Beginning button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_CUSTOM", - 125, - "actionButtonBlank", - "Button with no default picture or text. Supports mouse-click an" - "d mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_DOCUMENT", - 134, - "actionButtonDocument", - "Document button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_END", - 132, - "actionButtonEnd", - "End button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_FORWARD_OR_NEXT", - 130, - "actionButtonForwardNext", - "Forward or Next button. Supports mouse-click and mouse-over act" "ions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_HELP", - 127, - "actionButtonHelp", - "Help button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_HOME", - 126, - "actionButtonHome", - "Home button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_INFORMATION", - 128, - "actionButtonInformation", - "Information button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_MOVIE", - 136, - "actionButtonMovie", - "Movie button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_RETURN", - 133, - "actionButtonReturn", - "Return button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_SOUND", - 135, - "actionButtonSound", - "Sound button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember("ARC", 25, "arc", "Arc"), - XmlMappedEnumMember( - "BALLOON", 137, "wedgeRoundRectCallout", "Rounded Rectangular Callout" - ), - XmlMappedEnumMember( - "BENT_ARROW", - 41, - "bentArrow", - "Block arrow that follows a curved 90-degree angle", - ), - XmlMappedEnumMember( - "BENT_UP_ARROW", - 44, - "bentUpArrow", - "Block arrow that follows a sharp 90-degree angle. Points up by " "default", - ), - XmlMappedEnumMember("BEVEL", 15, "bevel", "Bevel"), - XmlMappedEnumMember("BLOCK_ARC", 20, "blockArc", "Block arc"), - XmlMappedEnumMember("CAN", 13, "can", "Can"), - XmlMappedEnumMember("CHART_PLUS", 182, "chartPlus", "Chart Plus"), - XmlMappedEnumMember("CHART_STAR", 181, "chartStar", "Chart Star"), - XmlMappedEnumMember("CHART_X", 180, "chartX", "Chart X"), - XmlMappedEnumMember("CHEVRON", 52, "chevron", "Chevron"), - XmlMappedEnumMember("CHORD", 161, "chord", "Geometric chord shape"), - XmlMappedEnumMember( - "CIRCULAR_ARROW", - 60, - "circularArrow", - "Block arrow that follows a curved 180-degree angle", - ), - XmlMappedEnumMember("CLOUD", 179, "cloud", "Cloud"), - XmlMappedEnumMember("CLOUD_CALLOUT", 108, "cloudCallout", "Cloud callout"), - XmlMappedEnumMember("CORNER", 162, "corner", "Corner"), - XmlMappedEnumMember("CORNER_TABS", 169, "cornerTabs", "Corner Tabs"), - XmlMappedEnumMember("CROSS", 11, "plus", "Cross"), - XmlMappedEnumMember("CUBE", 14, "cube", "Cube"), - XmlMappedEnumMember( - "CURVED_DOWN_ARROW", 48, "curvedDownArrow", "Block arrow that curves down" - ), - XmlMappedEnumMember( - "CURVED_DOWN_RIBBON", 100, "ellipseRibbon", "Ribbon banner that curves down" - ), - XmlMappedEnumMember( - "CURVED_LEFT_ARROW", 46, "curvedLeftArrow", "Block arrow that curves left" - ), - XmlMappedEnumMember( - "CURVED_RIGHT_ARROW", - 45, - "curvedRightArrow", - "Block arrow that curves right", - ), - XmlMappedEnumMember( - "CURVED_UP_ARROW", 47, "curvedUpArrow", "Block arrow that curves up" - ), - XmlMappedEnumMember( - "CURVED_UP_RIBBON", 99, "ellipseRibbon2", "Ribbon banner that curves up" - ), - XmlMappedEnumMember("DECAGON", 144, "decagon", "Decagon"), - XmlMappedEnumMember("DIAGONAL_STRIPE", 141, "diagStripe", "Diagonal Stripe"), - XmlMappedEnumMember("DIAMOND", 4, "diamond", "Diamond"), - XmlMappedEnumMember("DODECAGON", 146, "dodecagon", "Dodecagon"), - XmlMappedEnumMember("DONUT", 18, "donut", "Donut"), - XmlMappedEnumMember("DOUBLE_BRACE", 27, "bracePair", "Double brace"), - XmlMappedEnumMember("DOUBLE_BRACKET", 26, "bracketPair", "Double bracket"), - XmlMappedEnumMember("DOUBLE_WAVE", 104, "doubleWave", "Double wave"), - XmlMappedEnumMember( - "DOWN_ARROW", 36, "downArrow", "Block arrow that points down" - ), - XmlMappedEnumMember( - "DOWN_ARROW_CALLOUT", - 56, - "downArrowCallout", - "Callout with arrow that points down", - ), - XmlMappedEnumMember( - "DOWN_RIBBON", - 98, - "ribbon", - "Ribbon banner with center area below ribbon ends", - ), - XmlMappedEnumMember("EXPLOSION1", 89, "irregularSeal1", "Explosion"), - XmlMappedEnumMember("EXPLOSION2", 90, "irregularSeal2", "Explosion"), - XmlMappedEnumMember( - "FLOWCHART_ALTERNATE_PROCESS", - 62, - "flowChartAlternateProcess", - "Alternate process flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_CARD", 75, "flowChartPunchedCard", "Card flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_COLLATE", 79, "flowChartCollate", "Collate flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_CONNECTOR", - 73, - "flowChartConnector", - "Connector flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_DATA", 64, "flowChartInputOutput", "Data flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DECISION", 63, "flowChartDecision", "Decision flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DELAY", 84, "flowChartDelay", "Delay flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DIRECT_ACCESS_STORAGE", - 87, - "flowChartMagneticDrum", - "Direct access storage flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_DISPLAY", 88, "flowChartDisplay", "Display flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DOCUMENT", 67, "flowChartDocument", "Document flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_EXTRACT", 81, "flowChartExtract", "Extract flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_INTERNAL_STORAGE", - 66, - "flowChartInternalStorage", - "Internal storage flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MAGNETIC_DISK", - 86, - "flowChartMagneticDisk", - "Magnetic disk flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MANUAL_INPUT", - 71, - "flowChartManualInput", - "Manual input flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MANUAL_OPERATION", - 72, - "flowChartManualOperation", - "Manual operation flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MERGE", 82, "flowChartMerge", "Merge flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_MULTIDOCUMENT", - 68, - "flowChartMultidocument", - "Multi-document flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_OFFLINE_STORAGE", - 139, - "flowChartOfflineStorage", - "Offline Storage", - ), - XmlMappedEnumMember( - "FLOWCHART_OFFPAGE_CONNECTOR", - 74, - "flowChartOffpageConnector", - "Off-page connector flowchart symbol", - ), - XmlMappedEnumMember("FLOWCHART_OR", 78, "flowChartOr", '"Or" flowchart symbol'), - XmlMappedEnumMember( - "FLOWCHART_PREDEFINED_PROCESS", - 65, - "flowChartPredefinedProcess", - "Predefined process flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_PREPARATION", - 70, - "flowChartPreparation", - "Preparation flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_PROCESS", 61, "flowChartProcess", "Process flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_PUNCHED_TAPE", - 76, - "flowChartPunchedTape", - "Punched tape flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_SEQUENTIAL_ACCESS_STORAGE", - 85, - "flowChartMagneticTape", - "Sequential access storage flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_SORT", 80, "flowChartSort", "Sort flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_STORED_DATA", - 83, - "flowChartOnlineStorage", - "Stored data flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_SUMMING_JUNCTION", - 77, - "flowChartSummingJunction", - "Summing junction flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_TERMINATOR", - 69, - "flowChartTerminator", - "Terminator flowchart symbol", - ), - XmlMappedEnumMember("FOLDED_CORNER", 16, "foldedCorner", "Folded corner"), - XmlMappedEnumMember("FRAME", 158, "frame", "Frame"), - XmlMappedEnumMember("FUNNEL", 174, "funnel", "Funnel"), - XmlMappedEnumMember("GEAR_6", 172, "gear6", "Gear 6"), - XmlMappedEnumMember("GEAR_9", 173, "gear9", "Gear 9"), - XmlMappedEnumMember("HALF_FRAME", 159, "halfFrame", "Half Frame"), - XmlMappedEnumMember("HEART", 21, "heart", "Heart"), - XmlMappedEnumMember("HEPTAGON", 145, "heptagon", "Heptagon"), - XmlMappedEnumMember("HEXAGON", 10, "hexagon", "Hexagon"), - XmlMappedEnumMember( - "HORIZONTAL_SCROLL", 102, "horizontalScroll", "Horizontal scroll" - ), - XmlMappedEnumMember("ISOSCELES_TRIANGLE", 7, "triangle", "Isosceles triangle"), - XmlMappedEnumMember( - "LEFT_ARROW", 34, "leftArrow", "Block arrow that points left" - ), - XmlMappedEnumMember( - "LEFT_ARROW_CALLOUT", - 54, - "leftArrowCallout", - "Callout with arrow that points left", - ), - XmlMappedEnumMember("LEFT_BRACE", 31, "leftBrace", "Left brace"), - XmlMappedEnumMember("LEFT_BRACKET", 29, "leftBracket", "Left bracket"), - XmlMappedEnumMember( - "LEFT_CIRCULAR_ARROW", 176, "leftCircularArrow", "Left Circular Arrow" - ), - XmlMappedEnumMember( - "LEFT_RIGHT_ARROW", - 37, - "leftRightArrow", - "Block arrow with arrowheads that point both left and right", - ), - XmlMappedEnumMember( - "LEFT_RIGHT_ARROW_CALLOUT", - 57, - "leftRightArrowCallout", - "Callout with arrowheads that point both left and right", - ), - XmlMappedEnumMember( - "LEFT_RIGHT_CIRCULAR_ARROW", - 177, - "leftRightCircularArrow", - "Left Right Circular Arrow", - ), - XmlMappedEnumMember( - "LEFT_RIGHT_RIBBON", 140, "leftRightRibbon", "Left Right Ribbon" - ), - XmlMappedEnumMember( - "LEFT_RIGHT_UP_ARROW", - 40, - "leftRightUpArrow", - "Block arrow with arrowheads that point left, right, and up", - ), - XmlMappedEnumMember( - "LEFT_UP_ARROW", - 43, - "leftUpArrow", - "Block arrow with arrowheads that point left and up", - ), - XmlMappedEnumMember("LIGHTNING_BOLT", 22, "lightningBolt", "Lightning bolt"), - XmlMappedEnumMember( - "LINE_CALLOUT_1", - 109, - "borderCallout1", - "Callout with border and horizontal callout line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_1_ACCENT_BAR", - 113, - "accentCallout1", - "Callout with vertical accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR", - 121, - "accentBorderCallout1", - "Callout with border and vertical accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_1_NO_BORDER", 117, "callout1", "Callout with horizontal line" - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2", - 110, - "borderCallout2", - "Callout with diagonal straight line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2_ACCENT_BAR", - 114, - "accentCallout2", - "Callout with diagonal callout line and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR", - 122, - "accentBorderCallout2", - "Callout with border, diagonal straight line, and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2_NO_BORDER", - 118, - "callout2", - "Callout with no border and diagonal callout line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3", 111, "borderCallout3", "Callout with angled line" - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3_ACCENT_BAR", - 115, - "accentCallout3", - "Callout with angled callout line and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR", - 123, - "accentBorderCallout3", - "Callout with border, angled callout line, and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3_NO_BORDER", - 119, - "callout3", - "Callout with no border and angled callout line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4", - 112, - "borderCallout3", - "Callout with callout line segments forming a U-shape.", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4_ACCENT_BAR", - 116, - "accentCallout3", - "Callout with accent bar and callout line segments forming a U-s" "hape.", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR", - 124, - "accentBorderCallout3", - "Callout with border, accent bar, and callout line segments form" - "ing a U-shape.", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4_NO_BORDER", - 120, - "callout3", - "Callout with no border and callout line segments forming a U-sh" "ape.", - ), - XmlMappedEnumMember("LINE_INVERSE", 183, "lineInv", "Straight Connector"), - XmlMappedEnumMember("MATH_DIVIDE", 166, "mathDivide", "Division"), - XmlMappedEnumMember("MATH_EQUAL", 167, "mathEqual", "Equal"), - XmlMappedEnumMember("MATH_MINUS", 164, "mathMinus", "Minus"), - XmlMappedEnumMember("MATH_MULTIPLY", 165, "mathMultiply", "Multiply"), - XmlMappedEnumMember("MATH_NOT_EQUAL", 168, "mathNotEqual", "Not Equal"), - XmlMappedEnumMember("MATH_PLUS", 163, "mathPlus", "Plus"), - XmlMappedEnumMember("MOON", 24, "moon", "Moon"), - XmlMappedEnumMember( - "NON_ISOSCELES_TRAPEZOID", - 143, - "nonIsoscelesTrapezoid", - "Non-isosceles Trapezoid", - ), - XmlMappedEnumMember( - "NOTCHED_RIGHT_ARROW", - 50, - "notchedRightArrow", - "Notched block arrow that points right", - ), - XmlMappedEnumMember("NO_SYMBOL", 19, "noSmoking", '"No" symbol'), - XmlMappedEnumMember("OCTAGON", 6, "octagon", "Octagon"), - XmlMappedEnumMember("OVAL", 9, "ellipse", "Oval"), - XmlMappedEnumMember( - "OVAL_CALLOUT", 107, "wedgeEllipseCallout", "Oval-shaped callout" - ), - XmlMappedEnumMember("PARALLELOGRAM", 2, "parallelogram", "Parallelogram"), - XmlMappedEnumMember("PENTAGON", 51, "homePlate", "Pentagon"), - XmlMappedEnumMember("PIE", 142, "pie", "Pie"), - XmlMappedEnumMember("PIE_WEDGE", 175, "pieWedge", "Pie"), - XmlMappedEnumMember("PLAQUE", 28, "plaque", "Plaque"), - XmlMappedEnumMember("PLAQUE_TABS", 171, "plaqueTabs", "Plaque Tabs"), - XmlMappedEnumMember( - "QUAD_ARROW", - 39, - "quadArrow", - "Block arrows that point up, down, left, and right", - ), - XmlMappedEnumMember( - "QUAD_ARROW_CALLOUT", - 59, - "quadArrowCallout", - "Callout with arrows that point up, down, left, and right", - ), - XmlMappedEnumMember("RECTANGLE", 1, "rect", "Rectangle"), - XmlMappedEnumMember( - "RECTANGULAR_CALLOUT", 105, "wedgeRectCallout", "Rectangular callout" - ), - XmlMappedEnumMember("REGULAR_PENTAGON", 12, "pentagon", "Pentagon"), - XmlMappedEnumMember( - "RIGHT_ARROW", 33, "rightArrow", "Block arrow that points right" - ), - XmlMappedEnumMember( - "RIGHT_ARROW_CALLOUT", - 53, - "rightArrowCallout", - "Callout with arrow that points right", - ), - XmlMappedEnumMember("RIGHT_BRACE", 32, "rightBrace", "Right brace"), - XmlMappedEnumMember("RIGHT_BRACKET", 30, "rightBracket", "Right bracket"), - XmlMappedEnumMember("RIGHT_TRIANGLE", 8, "rtTriangle", "Right triangle"), - XmlMappedEnumMember("ROUNDED_RECTANGLE", 5, "roundRect", "Rounded rectangle"), - XmlMappedEnumMember( - "ROUNDED_RECTANGULAR_CALLOUT", - 106, - "wedgeRoundRectCallout", - "Rounded rectangle-shaped callout", - ), - XmlMappedEnumMember( - "ROUND_1_RECTANGLE", 151, "round1Rect", "Round Single Corner Rectangle" - ), - XmlMappedEnumMember( - "ROUND_2_DIAG_RECTANGLE", - 153, - "round2DiagRect", - "Round Diagonal Corner Rectangle", - ), - XmlMappedEnumMember( - "ROUND_2_SAME_RECTANGLE", - 152, - "round2SameRect", - "Round Same Side Corner Rectangle", - ), - XmlMappedEnumMember("SMILEY_FACE", 17, "smileyFace", "Smiley face"), - XmlMappedEnumMember( - "SNIP_1_RECTANGLE", 155, "snip1Rect", "Snip Single Corner Rectangle" - ), - XmlMappedEnumMember( - "SNIP_2_DIAG_RECTANGLE", - 157, - "snip2DiagRect", - "Snip Diagonal Corner Rectangle", - ), - XmlMappedEnumMember( - "SNIP_2_SAME_RECTANGLE", - 156, - "snip2SameRect", - "Snip Same Side Corner Rectangle", - ), - XmlMappedEnumMember( - "SNIP_ROUND_RECTANGLE", - 154, - "snipRoundRect", - "Snip and Round Single Corner Rectangle", - ), - XmlMappedEnumMember("SQUARE_TABS", 170, "squareTabs", "Square Tabs"), - XmlMappedEnumMember("STAR_10_POINT", 149, "star10", "10-Point Star"), - XmlMappedEnumMember("STAR_12_POINT", 150, "star12", "12-Point Star"), - XmlMappedEnumMember("STAR_16_POINT", 94, "star16", "16-point star"), - XmlMappedEnumMember("STAR_24_POINT", 95, "star24", "24-point star"), - XmlMappedEnumMember("STAR_32_POINT", 96, "star32", "32-point star"), - XmlMappedEnumMember("STAR_4_POINT", 91, "star4", "4-point star"), - XmlMappedEnumMember("STAR_5_POINT", 92, "star5", "5-point star"), - XmlMappedEnumMember("STAR_6_POINT", 147, "star6", "6-Point Star"), - XmlMappedEnumMember("STAR_7_POINT", 148, "star7", "7-Point Star"), - XmlMappedEnumMember("STAR_8_POINT", 93, "star8", "8-point star"), - XmlMappedEnumMember( - "STRIPED_RIGHT_ARROW", - 49, - "stripedRightArrow", - "Block arrow that points right with stripes at the tail", - ), - XmlMappedEnumMember("SUN", 23, "sun", "Sun"), - XmlMappedEnumMember("SWOOSH_ARROW", 178, "swooshArrow", "Swoosh Arrow"), - XmlMappedEnumMember("TEAR", 160, "teardrop", "Teardrop"), - XmlMappedEnumMember("TRAPEZOID", 3, "trapezoid", "Trapezoid"), - XmlMappedEnumMember("UP_ARROW", 35, "upArrow", "Block arrow that points up"), - XmlMappedEnumMember( - "UP_ARROW_CALLOUT", - 55, - "upArrowCallout", - "Callout with arrow that points up", - ), - XmlMappedEnumMember( - "UP_DOWN_ARROW", 38, "upDownArrow", "Block arrow that points up and down" - ), - XmlMappedEnumMember( - "UP_DOWN_ARROW_CALLOUT", - 58, - "upDownArrowCallout", - "Callout with arrows that point up and down", - ), - XmlMappedEnumMember( - "UP_RIBBON", - 97, - "ribbon2", - "Ribbon banner with center area above ribbon ends", - ), - XmlMappedEnumMember( - "U_TURN_ARROW", 42, "uturnArrow", "Block arrow forming a U shape" - ), - XmlMappedEnumMember( - "VERTICAL_SCROLL", 101, "verticalScroll", "Vertical scroll" - ), - XmlMappedEnumMember("WAVE", 103, "wave", "Wave"), - ) - - -@alias("MSO_CONNECTOR") -class MSO_CONNECTOR_TYPE(XmlEnumeration): - """ - Specifies a type of connector. - - Alias: ``MSO_CONNECTOR`` - - Example:: - - from pptx.enum.shapes import MSO_CONNECTOR - from pptx.util import Cm - - shapes = prs.slides[0].shapes - connector = shapes.add_connector( - MSO_CONNECTOR.STRAIGHT, Cm(2), Cm(2), Cm(10), Cm(10) - ) - assert connector.left.cm == 2 - """ - - __ms_name__ = "MsoConnectorType" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff860918.aspx" - - __members__ = ( - XmlMappedEnumMember("CURVE", 3, "curvedConnector3", "Curved connector."), - XmlMappedEnumMember("ELBOW", 2, "bentConnector3", "Elbow connector."), - XmlMappedEnumMember("STRAIGHT", 1, "line", "Straight line connector."), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates a combination of othe" "r states.", - ), - ) - - -@alias("MSO") -class MSO_SHAPE_TYPE(Enumeration): - """ - Specifies the type of a shape - - Alias: ``MSO`` - - Example:: - - from pptx.enum.shapes import MSO_SHAPE_TYPE - - assert shape.type == MSO_SHAPE_TYPE.PICTURE - """ - - __ms_name__ = "MsoShapeType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15" ").aspx" - ) - - __members__ = ( - EnumMember("AUTO_SHAPE", 1, "AutoShape"), - EnumMember("CALLOUT", 2, "Callout shape"), - EnumMember("CANVAS", 20, "Drawing canvas"), - EnumMember("CHART", 3, "Chart, e.g. pie chart, bar chart"), - EnumMember("COMMENT", 4, "Comment"), - EnumMember("DIAGRAM", 21, "Diagram"), - EnumMember("EMBEDDED_OLE_OBJECT", 7, "Embedded OLE object"), - EnumMember("FORM_CONTROL", 8, "Form control"), - EnumMember("FREEFORM", 5, "Freeform"), - EnumMember("GROUP", 6, "Group shape"), - EnumMember("IGX_GRAPHIC", 24, "SmartArt graphic"), - EnumMember("INK", 22, "Ink"), - EnumMember("INK_COMMENT", 23, "Ink Comment"), - EnumMember("LINE", 9, "Line"), - EnumMember("LINKED_OLE_OBJECT", 10, "Linked OLE object"), - EnumMember("LINKED_PICTURE", 11, "Linked picture"), - EnumMember("MEDIA", 16, "Media"), - EnumMember("OLE_CONTROL_OBJECT", 12, "OLE control object"), - EnumMember("PICTURE", 13, "Picture"), - EnumMember("PLACEHOLDER", 14, "Placeholder"), - EnumMember("SCRIPT_ANCHOR", 18, "Script anchor"), - EnumMember("TABLE", 19, "Table"), - EnumMember("TEXT_BOX", 17, "Text box"), - EnumMember("TEXT_EFFECT", 15, "Text effect"), - EnumMember("WEB_VIDEO", 26, "Web video"), - ReturnValueOnlyEnumMember("MIXED", -2, "Mixed shape types"), - ) - - -class PP_MEDIA_TYPE(Enumeration): - """ - Indicates the OLE media type. - - Example:: - - from pptx.enum.shapes import PP_MEDIA_TYPE - - movie = slide.shapes[0] - assert movie.media_type == PP_MEDIA_TYPE.MOVIE - """ - - __ms_name__ = "PpMediaType" - - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff746008.aspx" - - __members__ = ( - EnumMember("MOVIE", 3, "Video media such as MP4."), - EnumMember("OTHER", 1, "Other media types"), - EnumMember("SOUND", 1, "Audio media such as MP3."), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates multiple media types," - " typically for a collection of shapes. May not be applicable in" - " python-pptx.", - ), - ) - - -@alias("PP_PLACEHOLDER") -class PP_PLACEHOLDER_TYPE(XmlEnumeration): - """ - Specifies one of the 18 distinct types of placeholder. - - Alias: ``PP_PLACEHOLDER`` - - Example:: - - from pptx.enum.shapes import PP_PLACEHOLDER - - placeholder = slide.placeholders[0] - assert placeholder.type == PP_PLACEHOLDER.TITLE - """ - - __ms_name__ = "PpPlaceholderType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15" ").aspx" - ) - - __members__ = ( - XmlMappedEnumMember("BITMAP", 9, "clipArt", "Clip art placeholder"), - XmlMappedEnumMember("BODY", 2, "body", "Body"), - XmlMappedEnumMember("CENTER_TITLE", 3, "ctrTitle", "Center Title"), - XmlMappedEnumMember("CHART", 8, "chart", "Chart"), - XmlMappedEnumMember("DATE", 16, "dt", "Date"), - XmlMappedEnumMember("FOOTER", 15, "ftr", "Footer"), - XmlMappedEnumMember("HEADER", 14, "hdr", "Header"), - XmlMappedEnumMember("MEDIA_CLIP", 10, "media", "Media Clip"), - XmlMappedEnumMember("OBJECT", 7, "obj", "Object"), - XmlMappedEnumMember( - "ORG_CHART", - 11, - "dgm", - "SmartArt placeholder. Organization char" "t is a legacy name.", - ), - XmlMappedEnumMember("PICTURE", 18, "pic", "Picture"), - XmlMappedEnumMember("SLIDE_IMAGE", 101, "sldImg", "Slide Image"), - XmlMappedEnumMember("SLIDE_NUMBER", 13, "sldNum", "Slide Number"), - XmlMappedEnumMember("SUBTITLE", 4, "subTitle", "Subtitle"), - XmlMappedEnumMember("TABLE", 12, "tbl", "Table"), - XmlMappedEnumMember("TITLE", 1, "title", "Title"), - ReturnValueOnlyEnumMember("VERTICAL_BODY", 6, "Vertical Body"), - ReturnValueOnlyEnumMember("VERTICAL_OBJECT", 17, "Vertical Object"), - ReturnValueOnlyEnumMember("VERTICAL_TITLE", 5, "Vertical Title"), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; multiple placeholders of differ" "ing types.", - ), - ) - - -class _ProgIdEnum(object): - """One-off Enum-like object for progId values. - - Indicates the type of an OLE object in terms of the program used to open it. - - A member of this enumeration can be used in a `SlideShapes.add_ole_object()` call to - specify a Microsoft Office file-type (Excel, PowerPoint, or Word), which will - then not require several of the arguments required to embed other object types. - - Example:: - - from pptx.enum.shapes import PROG_ID - from pptx.util import Inches - - embedded_xlsx_shape = slide.shapes.add_ole_object( - "workbook.xlsx", PROG_ID.XLSX, left=Inches(1), top=Inches(1) - ) - assert embedded_xlsx_shape.ole_format.prog_id == "Excel.Sheet.12" - """ - - class Member(object): - """A particular progID with its attributes.""" - - def __init__(self, name, progId, icon_filename, width, height): - self._name = name - self._progId = progId - self._icon_filename = icon_filename - self._width = width - self._height = height - - def __repr__(self): - return "PROG_ID.%s" % self._name - - @property - def height(self): - return self._height - - @property - def icon_filename(self): - return self._icon_filename - - @property - def progId(self): - return self._progId - - @property - def width(self): - return self._width - - def __contains__(self, item): - return item in ( - self.DOCX, - self.PPTX, - self.XLSX, - ) - - def __repr__(self): - return "%s.PROG_ID" % __name__ - - @lazyproperty - def DOCX(self): - return self.Member("DOCX", "Word.Document.12", "docx-icon.emf", 965200, 609600) - - @lazyproperty - def PPTX(self): - return self.Member( - "PPTX", "PowerPoint.Show.12", "pptx-icon.emf", 965200, 609600 - ) - - @lazyproperty - def XLSX(self): - return self.Member("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600) - - -PROG_ID = _ProgIdEnum() diff --git a/pptx/enum/text.py b/pptx/enum/text.py deleted file mode 100644 index 54297bbd5..000000000 --- a/pptx/enum/text.py +++ /dev/null @@ -1,255 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations used by text and related objects -""" - -from __future__ import absolute_import - -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - - -class MSO_AUTO_SIZE(Enumeration): - """ - Determines the type of automatic sizing allowed. - - The following names can be used to specify the automatic sizing behavior - used to fit a shape's text within the shape bounding box, for example:: - - from pptx.enum.text import MSO_AUTO_SIZE - - shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE - - The word-wrap setting of the text frame interacts with the auto-size - setting to determine the specific auto-sizing behavior. - - Note that ``TextFrame.auto_size`` can also be set to |None|, which removes - the auto size setting altogether. This causes the setting to be inherited, - either from the layout placeholder, in the case of a placeholder shape, or - from the theme. - """ - - NONE = 0 - SHAPE_TO_FIT_TEXT = 1 - TEXT_TO_FIT_SHAPE = 2 - - __ms_name__ = "MsoAutoSize" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15" ").aspx" - ) - - __members__ = ( - EnumMember( - "NONE", - 0, - "No automatic sizing of the shape or text will be don" - "e. Text can freely extend beyond the horizontal and vertical ed" - "ges of the shape bounding box.", - ), - EnumMember( - "SHAPE_TO_FIT_TEXT", - 1, - "The shape height and possibly width are" - " adjusted to fit the text. Note this setting interacts with the" - " TextFrame.word_wrap property setting. If word wrap is turned o" - "n, only the height of the shape will be adjusted; soft line bre" - "aks will be used to fit the text horizontally.", - ), - EnumMember( - "TEXT_TO_FIT_SHAPE", - 2, - "The font size is reduced as necessary t" - "o fit the text within the shape.", - ), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates a combination of auto" - "matic sizing schemes are used.", - ), - ) - - -@alias("MSO_UNDERLINE") -class MSO_TEXT_UNDERLINE_TYPE(XmlEnumeration): - """ - Indicates the type of underline for text. Used with - :attr:`.Font.underline` to specify the style of text underlining. - - Alias: ``MSO_UNDERLINE`` - - Example:: - - from pptx.enum.text import MSO_UNDERLINE - - run.font.underline = MSO_UNDERLINE.DOUBLE_LINE - """ - - __ms_name__ = "MsoTextUnderlineType" - - __url__ = "http://msdn.microsoft.com/en-us/library/aa432699.aspx" - - __members__ = ( - XmlMappedEnumMember("NONE", 0, "none", "Specifies no underline."), - XmlMappedEnumMember( - "DASH_HEAVY_LINE", 8, "dashHeavy", "Specifies a dash underline." - ), - XmlMappedEnumMember("DASH_LINE", 7, "dash", "Specifies a dash line underline."), - XmlMappedEnumMember( - "DASH_LONG_HEAVY_LINE", - 10, - "dashLongHeavy", - "Specifies a long heavy line underline.", - ), - XmlMappedEnumMember( - "DASH_LONG_LINE", 9, "dashLong", "Specifies a dashed long line underline." - ), - XmlMappedEnumMember( - "DOT_DASH_HEAVY_LINE", - 12, - "dotDashHeavy", - "Specifies a dot dash heavy line underline.", - ), - XmlMappedEnumMember( - "DOT_DASH_LINE", 11, "dotDash", "Specifies a dot dash line underline." - ), - XmlMappedEnumMember( - "DOT_DOT_DASH_HEAVY_LINE", - 14, - "dotDotDashHeavy", - "Specifies a dot dot dash heavy line underline.", - ), - XmlMappedEnumMember( - "DOT_DOT_DASH_LINE", - 13, - "dotDotDash", - "Specifies a dot dot dash line underline.", - ), - XmlMappedEnumMember( - "DOTTED_HEAVY_LINE", - 6, - "dottedHeavy", - "Specifies a dotted heavy line underline.", - ), - XmlMappedEnumMember( - "DOTTED_LINE", 5, "dotted", "Specifies a dotted line underline." - ), - XmlMappedEnumMember( - "DOUBLE_LINE", 3, "dbl", "Specifies a double line underline." - ), - XmlMappedEnumMember( - "HEAVY_LINE", 4, "heavy", "Specifies a heavy line underline." - ), - XmlMappedEnumMember( - "SINGLE_LINE", 2, "sng", "Specifies a single line underline." - ), - XmlMappedEnumMember( - "WAVY_DOUBLE_LINE", 17, "wavyDbl", "Specifies a wavy double line underline." - ), - XmlMappedEnumMember( - "WAVY_HEAVY_LINE", 16, "wavyHeavy", "Specifies a wavy heavy line underline." - ), - XmlMappedEnumMember( - "WAVY_LINE", 15, "wavy", "Specifies a wavy line underline." - ), - XmlMappedEnumMember("WORDS", 1, "words", "Specifies underlining words."), - ReturnValueOnlyEnumMember("MIXED", -2, "Specifies a mixed of underline types."), - ) - - -@alias("MSO_ANCHOR") -class MSO_VERTICAL_ANCHOR(XmlEnumeration): - """ - Specifies the vertical alignment of text in a text frame. Used with the - ``.vertical_anchor`` property of the |TextFrame| object. Note that the - ``vertical_anchor`` property can also have the value None, indicating - there is no directly specified vertical anchor setting and its effective - value is inherited from its placeholder if it has one or from the theme. - |None| may also be assigned to remove an explicitly specified vertical - anchor setting. - """ - - __ms_name__ = "MsoVerticalAnchor" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff865255.aspx" - - __members__ = ( - XmlMappedEnumMember( - None, - None, - None, - "Text frame has no vertical anchor specified " - "and inherits its value from its layout placeholder or theme.", - ), - XmlMappedEnumMember("TOP", 1, "t", "Aligns text to top of text frame"), - XmlMappedEnumMember("MIDDLE", 3, "ctr", "Centers text vertically"), - XmlMappedEnumMember("BOTTOM", 4, "b", "Aligns text to bottom of text frame"), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates a combination of the " "other states.", - ), - ) - - -@alias("PP_ALIGN") -class PP_PARAGRAPH_ALIGNMENT(XmlEnumeration): - """ - Specifies the horizontal alignment for one or more paragraphs. - - Alias: ``PP_ALIGN`` - - Example:: - - from pptx.enum.text import PP_ALIGN - - shape.paragraphs[0].alignment = PP_ALIGN.CENTER - """ - - __ms_name__ = "PpParagraphAlignment" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15" ").aspx" - ) - - __members__ = ( - XmlMappedEnumMember("CENTER", 2, "ctr", "Center align"), - XmlMappedEnumMember( - "DISTRIBUTE", - 5, - "dist", - "Evenly distributes e.g. Japanese chara" - "cters from left to right within a line", - ), - XmlMappedEnumMember( - "JUSTIFY", - 4, - "just", - "Justified, i.e. each line both begins and" - " ends at the margin with spacing between words adjusted such th" - "at the line exactly fills the width of the paragraph.", - ), - XmlMappedEnumMember( - "JUSTIFY_LOW", - 7, - "justLow", - "Justify using a small amount of sp" "ace between words.", - ), - XmlMappedEnumMember("LEFT", 1, "l", "Left aligned"), - XmlMappedEnumMember("RIGHT", 3, "r", "Right aligned"), - XmlMappedEnumMember("THAI_DISTRIBUTE", 6, "thaiDist", "Thai distributed"), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates multiple paragraph al" - "ignments are present in a set of paragraphs.", - ), - ) diff --git a/pptx/opc/constants.py b/pptx/opc/constants.py deleted file mode 100644 index 9eef0ee23..000000000 --- a/pptx/opc/constants.py +++ /dev/null @@ -1,541 +0,0 @@ -# encoding: utf-8 - -"""Constant values related to the Open Packaging Convention. - -In particular, this includes content (MIME) types and relationship types. -""" - - -class CONTENT_TYPE(object): - """Content type URIs (like MIME-types) that specify a part's format.""" - - ASF = "video/x-ms-asf" - AVI = "video/avi" - 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_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml" - DML_DIAGRAM_LAYOUT = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" - ) - DML_DIAGRAM_STYLE = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" - ) - GIF = "image/gif" - INK = "application/inkml+xml" - JPEG = "image/jpeg" - MOV = "video/quicktime" - MP4 = "video/mp4" - MPG = "video/mpeg" - MS_PHOTO = "image/vnd.ms-photo" - MS_VIDEO = "video/msvideo" - OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml" - OFC_CHART_EX = "application/vnd.ms-office.chartex+xml" - OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml" - OFC_CUSTOM_PROPERTIES = ( - "application/vnd.openxmlformats-officedocument.custom-properties+xml" - ) - OFC_CUSTOM_XML_PROPERTIES = ( - "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" - ) - OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml" - OFC_EXTENDED_PROPERTIES = ( - "application/vnd.openxmlformats-officedocument.extended-properties+xml" - ) - OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" - OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" - OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml" - OFC_THEME_OVERRIDE = ( - "application/vnd.openxmlformats-officedocument.themeOverride+xml" - ) - OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing" - OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml" - OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( - "application/vnd.openxmlformats-package.digital-signature-certificate" - ) - OPC_DIGITAL_SIGNATURE_ORIGIN = ( - "application/vnd.openxmlformats-package.digital-signature-origin" - ) - OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( - "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" - ) - OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" - PML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" - ) - PML_COMMENT_AUTHORS = ( - "application/vnd.openxmlformats-officedocument.presentationml.commen" - "tAuthors+xml" - ) - PML_HANDOUT_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.handou" - "tMaster+xml" - ) - PML_NOTES_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesM" - "aster+xml" - ) - PML_NOTES_SLIDE = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" - ) - PML_PRESENTATION = ( - "application/vnd.openxmlformats-officedocument.presentationml.presentation" - ) - PML_PRESENTATION_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.presentation.ma" - "in+xml" - ) - PML_PRES_MACRO_MAIN = ( - "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml" - ) - PML_PRES_PROPS = ( - "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" - ) - PML_PRINTER_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.presentationml.printerSettings" - ) - PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" - PML_SLIDESHOW_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+" - "xml" - ) - PML_SLIDE_LAYOUT = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" - ) - PML_SLIDE_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" - ) - PML_SLIDE_UPDATE_INFO = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo" - "+xml" - ) - PML_TABLE_STYLES = ( - "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml" - ) - PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml" - PML_TEMPLATE_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.template.main+x" - "ml" - ) - PML_VIEW_PROPS = ( - "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" - ) - PNG = "image/png" - SML_CALC_CHAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" - ) - SML_CHARTSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" - ) - SML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" - ) - SML_CONNECTIONS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" - ) - SML_CUSTOM_PROPERTY = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" - ) - SML_DIALOGSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" - ) - SML_EXTERNAL_LINK = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" - ) - SML_PIVOT_CACHE_DEFINITION = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefini" - "tion+xml" - ) - SML_PIVOT_CACHE_RECORDS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecord" - "s+xml" - ) - SML_PIVOT_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" - ) - SML_PRINTER_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" - ) - SML_QUERY_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" - ) - SML_REVISION_HEADERS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+" - "xml" - ) - SML_REVISION_LOG = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" - ) - SML_SHARED_STRINGS = ( - "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_METADATA = ( - "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.tableSingleCells" - "+xml" - ) - SML_TEMPLATE_MAIN = ( - "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.volatileDependen" - "cies+xml" - ) - SML_WORKSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" - ) - SWF = "application/x-shockwave-flash" - TIFF = "image/tiff" - VIDEO = "video/unknown" - 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.document.glos" - "sary+xml" - ) - WML_DOCUMENT_MAIN = ( - "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.foot" - "notes+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.printerSettin" - "gs" - ) - 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.webSettings+x" - "ml" - ) - WMV = "video/x-ms-wmv" - XML = "application/xml" - X_EMF = "image/x-emf" - X_FONTDATA = "application/x-fontdata" - X_FONT_TTF = "application/x-font-ttf" - X_MS_VIDEO = "video/x-msvideo" - X_WMF = "image/x-wmf" - - -class NAMESPACE(object): - """Constant values for OPC XML namespaces""" - - DML_WORDPROCESSING_DRAWING = ( - "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" - ) - OFC_RELATIONSHIPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - ) - OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships" - OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types" - WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" - - -class RELATIONSHIP_TARGET_MODE(object): - """Open XML relationship target modes""" - - EXTERNAL = "External" - INTERNAL = "Internal" - - -class RELATIONSHIP_TYPE(object): - AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" - A_F_CHUNK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" - ) - CALC_CHAIN = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" - ) - CERTIFICATE = ( - "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" - "re/certificate" - ) - CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - CHARTSHEET = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" - ) - CHART_COLOR_STYLE = ( - "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle" - ) - CHART_USER_SHAPES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUse" - "rShapes" - ) - COMMENTS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" - ) - COMMENT_AUTHORS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentA" - "uthors" - ) - CONNECTIONS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connecti" - "ons" - ) - CONTROL = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" - ) - CORE_PROPERTIES = ( - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-p" - "roperties" - ) - CUSTOM_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-p" - "roperties" - ) - CUSTOM_PROPERTY = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/customProperty" - ) - CUSTOM_XML = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" - ) - CUSTOM_XML_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXm" - "lProps" - ) - DIAGRAM_COLORS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramC" - "olors" - ) - DIAGRAM_DATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramD" - "ata" - ) - DIAGRAM_LAYOUT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramL" - "ayout" - ) - DIAGRAM_QUICK_STYLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQ" - "uickStyle" - ) - DIALOGSHEET = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsh" - "eet" - ) - DRAWING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - ) - ENDNOTES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" - ) - EXTENDED_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended" - "-properties" - ) - EXTERNAL_LINK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/external" - "Link" - ) - FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" - FONT_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" - ) - FOOTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" - ) - FOOTNOTES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" - ) - GLOSSARY_DOCUMENT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossary" - "Document" - ) - HANDOUT_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutM" - "aster" - ) - HEADER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" - ) - HYPERLINK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlin" - "k" - ) - IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" - MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media" - NOTES_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMas" - "ter" - ) - NOTES_SLIDE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSli" - "de" - ) - NUMBERING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numberin" - "g" - ) - OFFICE_DOCUMENT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDo" - "cument" - ) - OLE_OBJECT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObjec" - "t" - ) - ORIGIN = ( - "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" - "re/origin" - ) - PACKAGE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" - ) - PIVOT_CACHE_DEFINITION = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac" - "heDefinition" - ) - PIVOT_CACHE_RECORDS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh" - "eetml/pivotCacheRecords" - ) - PIVOT_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTab" - "le" - ) - PRES_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProp" - "s" - ) - PRINTER_SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerS" - "ettings" - ) - QUERY_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTab" - "le" - ) - REVISION_HEADERS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revision" - "Headers" - ) - REVISION_LOG = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revision" - "Log" - ) - SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" - ) - SHARED_STRINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedSt" - "rings" - ) - SHEET_METADATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMet" - "adata" - ) - SIGNATURE = ( - "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" - "re/signature" - ) - SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" - SLIDE_LAYOUT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLay" - "out" - ) - SLIDE_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMas" - "ter" - ) - SLIDE_UPDATE_INFO = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpd" - "ateInfo" - ) - 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/tableSin" - "gleCells" - ) - TABLE_STYLES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSty" - "les" - ) - 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/themeOve" - "rride" - ) - THUMBNAIL = ( - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbn" - "ail" - ) - USERNAMES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/username" - "s" - ) - VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video" - VIEW_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProp" - "s" - ) - VML_DRAWING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawi" - "ng" - ) - VOLATILE_DEPENDENCIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile" - "Dependencies" - ) - WEB_SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSetti" - "ngs" - ) - WORKSHEET_SOURCE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/workshee" - "tSource" - ) - XML_MAPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" - ) diff --git a/pptx/opc/oxml.py b/pptx/opc/oxml.py deleted file mode 100644 index 3693b7775..000000000 --- a/pptx/opc/oxml.py +++ /dev/null @@ -1,166 +0,0 @@ -# encoding: utf-8 - -""" -Temporary stand-in for main oxml module that came across with the -PackageReader transplant. Probably much will get replaced with objects from -the pptx.oxml.core and then this module will either get deleted or only hold -the package related custom element classes. -""" - -from __future__ import absolute_import - -from lxml import etree - -from .constants import NAMESPACE as NS, RELATIONSHIP_TARGET_MODE as RTM -from ..oxml import parse_xml, register_element_cls -from ..oxml.simpletypes import ( - ST_ContentType, - ST_Extension, - ST_TargetMode, - XsdAnyUri, - XsdId, -) -from ..oxml.xmlchemy import ( - BaseOxmlElement, - OptionalAttribute, - RequiredAttribute, - ZeroOrMore, -) - - -nsmap = { - "ct": NS.OPC_CONTENT_TYPES, - "pr": NS.OPC_RELATIONSHIPS, - "r": NS.OFC_RELATIONSHIPS, -} - - -def oxml_tostring(elm, encoding=None, pretty_print=False, standalone=None): - return etree.tostring( - elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone - ) - - -def serialize_part_xml(part_elm): - xml = etree.tostring(part_elm, encoding="UTF-8", standalone=True) - return xml - - -class CT_Default(BaseOxmlElement): - """ - ```` element, specifying the default content type to be applied - to a part with the specified extension. - """ - - extension = RequiredAttribute("Extension", ST_Extension) - contentType = RequiredAttribute("ContentType", ST_ContentType) - - -class CT_Override(BaseOxmlElement): - """ - ```` element, specifying the content type to be applied for a - part with the specified partname. - """ - - partName = RequiredAttribute("PartName", XsdAnyUri) - contentType = RequiredAttribute("ContentType", ST_ContentType) - - -class CT_Relationship(BaseOxmlElement): - """ - ```` element, representing a single relationship from a - source to a target part. - """ - - rId = RequiredAttribute("Id", XsdId) - reltype = RequiredAttribute("Type", XsdAnyUri) - target_ref = RequiredAttribute("Target", XsdAnyUri) - targetMode = OptionalAttribute("TargetMode", ST_TargetMode, default=RTM.INTERNAL) - - @classmethod - def new(cls, rId, reltype, target, target_mode=RTM.INTERNAL): - """ - Return a new ```` element. - """ - xml = '' % nsmap["pr"] - relationship = parse_xml(xml) - relationship.rId = rId - relationship.reltype = reltype - relationship.target_ref = target - relationship.targetMode = target_mode - return relationship - - -class CT_Relationships(BaseOxmlElement): - """ - ```` element, the root element in a .rels file. - """ - - relationship = ZeroOrMore("pr:Relationship") - - def add_rel(self, rId, reltype, target, is_external=False): - """ - Add a child ```` element with attributes set according - to parameter values. - """ - target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL - relationship = CT_Relationship.new(rId, reltype, target, target_mode) - self._insert_relationship(relationship) - - @classmethod - def new(cls): - """ - Return a new ```` element. - """ - xml = '' % nsmap["pr"] - relationships = parse_xml(xml) - return relationships - - @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 oxml_tostring(self, encoding="UTF-8", standalone=True) - - -class CT_Types(BaseOxmlElement): - """ - ```` element, the container element for Default and Override - elements in [Content_Types].xml. - """ - - default = ZeroOrMore("ct:Default") - override = ZeroOrMore("ct:Override") - - def add_default(self, ext, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ - return self._add_default(extension=ext, contentType=content_type) - - def add_override(self, partname, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ - return self._add_override(partName=partname, contentType=content_type) - - @classmethod - def new(cls): - """ - Return a new ```` element. - """ - xml = '' % nsmap["ct"] - types = parse_xml(xml) - return types - - -register_element_cls("ct:Default", CT_Default) -register_element_cls("ct:Override", CT_Override) -register_element_cls("ct:Types", CT_Types) - -register_element_cls("pr:Relationship", CT_Relationship) -register_element_cls("pr:Relationships", CT_Relationships) diff --git a/pptx/opc/package.py b/pptx/opc/package.py deleted file mode 100644 index 564f9f112..000000000 --- a/pptx/opc/package.py +++ /dev/null @@ -1,606 +0,0 @@ -# encoding: utf-8 - -"""Fundamental Open Packaging Convention (OPC) objects. - -The :mod:`pptx.packaging` module coheres around the concerns of reading and writing -presentations to and from a .pptx file. -""" - -from pptx.compat import is_string -from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.oxml import CT_Relationships, serialize_part_xml -from pptx.opc.packuri import PACKAGE_URI, PackURI -from pptx.opc.pkgreader import PackageReader -from pptx.opc.pkgwriter import PackageWriter -from pptx.oxml import parse_xml -from pptx.util import lazyproperty - - -class OpcPackage(object): - """ - Main API class for |python-opc|. A new instance is constructed by calling - the :meth:`open` class method with a path to a package file or file-like - object containing one. - """ - - def __init__(self): - super(OpcPackage, self).__init__() - - def after_unmarshal(self): - """ - Called by loading code after all parts and relationships have been - loaded, to afford the opportunity for any required post-processing. - This one does nothing other than catch the call if a subclass - doesn't. - """ - pass - - 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: - continue - part = rel.target_part - if part in visited: - continue - visited.append(part) - yield part - new_source = part - for part in walk_parts(new_source, visited): - yield part - - for part in walk_parts(self): - yield part - - 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(): - yield rel - if rel.is_external: - continue - part = rel.target_part - if part in visited: - continue - visited.append(part) - new_source = part - for rel in walk_rels(new_source, visited): - yield rel - - for rel in walk_rels(self): - yield rel - - 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 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 self.part_related_by(RT.OFFICE_DOCUMENT) - - def next_partname(self, tmpl): - """ - Return a |PackURI| instance representing the next available partname - matching *tmpl*, which 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: '/ppt/slides/slide%d.xml' - """ - partnames = [part.partname for part in self.iter_parts()] - for n in range(1, len(partnames) + 2): - candidate_partname = tmpl % n - if candidate_partname not in partnames: - return PackURI(candidate_partname) - raise Exception("ProgrammingError: ran out of candidate_partnames") - - @classmethod - def open(cls, 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 self.rels.part_with_reltype(reltype) - - @property - def parts(self): - """ - Return a list containing a reference to each of the parts in this - package. - """ - return [part for part in self.iter_parts()] - - 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. - """ - rel = self.rels.get_or_add(reltype, part) - return rel.rId - - @lazyproperty - def rels(self): - """ - Return a reference to the |RelationshipCollection| holding the - relationships for this package. - """ - return RelationshipCollection(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. - """ - for part in self.parts: - part.before_marshal() - PackageWriter.write(pkg_file, self.rels, self.parts) - - -class Part(object): - """ - Base class for package parts. Provides common properties and methods, but - intended to be subclassed in client code to implement specific part - behaviors. - """ - - def __init__(self, partname, content_type, blob=None, package=None): - super(Part, self).__init__() - self._partname = partname - self._content_type = content_type - self._blob = blob - self._package = package - - # load/save interface to OpcPackage ------------------------------ - - def after_unmarshal(self): - """ - Entry point for post-unmarshaling processing, for example to parse - the part XML. May be overridden by subclasses without forwarding call - to super. - """ - # don't place any code here, just catch call if not overridden by - # subclass - pass - - def before_marshal(self): - """ - Entry point for pre-serialization processing, for example to finalize - part naming if necessary. May be overridden by subclasses without - forwarding call to super. - """ - # don't place any code here, just catch call if not overridden by - # subclass - pass - - @property - def blob(self): - """ - Contents of this package part as a sequence of bytes. May be text or - binary. Intended to be overridden by subclasses. Default behavior is - to return load blob. - """ - return self._blob - - @blob.setter - def blob(self, bytes_): - """ - Note that not all subclasses use the part blob as their blob source. - In particular, the |XmlPart| subclass uses its `self._element` to - serialize a blob on demand. This works find for binary parts though. - """ - self._blob = bytes_ - - @property - def content_type(self): - """ - Content type of this part. - """ - return self._content_type - - @classmethod - def load(cls, partname, content_type, blob, package): - return cls(partname, content_type, blob, package) - - def load_rel(self, reltype, target, rId, is_external=False): - """ - Return newly added |_Relationship| instance of *reltype* between this - part and *target* with key *rId*. Target mode is set to - ``RTM.EXTERNAL`` if *is_external* is |True|. Intended for use during - load from a serialized package, where the rId is well known. Other - methods exist for adding a new relationship to a part when - manipulating a part. - """ - return self.rels.add_relationship(reltype, target, rId, is_external) - - @property - def package(self): - """ - |OpcPackage| instance this part belongs to. - """ - return self._package - - @property - def partname(self): - """ - |PackURI| instance holding partname of this part, e.g. - '/ppt/slides/slide1.xml' - """ - return self._partname - - @partname.setter - def partname(self, partname): - if not isinstance(partname, PackURI): - tmpl = "partname must be instance of PackURI, got '%s'" - raise TypeError(tmpl % type(partname).__name__) - self._partname = partname - - # relationship management interface for child objects ------------ - - def drop_rel(self, rId): - """ - Remove the relationship identified by *rId* if its reference count - is less than 2. Relationships with a reference count of 0 are - implicit relationships. - """ - if self._rel_ref_count(rId) < 2: - del self.rels[rId] - - def part_related_by(self, reltype): - """ - Return part to which this part has a relationship of *reltype*. - Raises |KeyError| if no such relationship is found and |ValueError| - if more than one such relationship is found. Provides ability to - resolve implicitly related part, such as Slide -> SlideLayout. - """ - return self.rels.part_with_reltype(reltype) - - def relate_to(self, target, reltype, is_external=False): - """ - Return rId key of relationship of *reltype* to *target*, from an - existing relationship if there is one, otherwise a newly created one. - """ - if is_external: - return self.rels.get_or_add_ext_rel(reltype, target) - else: - rel = self.rels.get_or_add(reltype, target) - return rel.rId - - @property - def related_parts(self): - """ - Dictionary mapping related parts by rId, so child objects can resolve - explicit relationships present in the part XML, e.g. sldIdLst to a - specific |Slide| instance. - """ - return self.rels.related_parts - - @lazyproperty - def rels(self): - """ - |RelationshipCollection| instance holding the relationships for this - part. - """ - return RelationshipCollection(self._partname.baseURI) - - def target_ref(self, rId): - """ - Return URL contained in target ref of relationship identified by - *rId*. - """ - rel = self.rels[rId] - return rel.target_ref - - def _blob_from_file(self, file): - """Return bytes of `file`, which is either a str path or a file-like object.""" - # --- a str `file` is assumed to be a path --- - if is_string(file): - with open(file, "rb") as f: - return f.read() - - # --- otherwise, assume `file` is a file-like object - # --- reposition file cursor if it has one - if callable(getattr(file, "seek")): - file.seek(0) - return file.read() - - def _rel_ref_count(self, rId): - """ - Return the count of references in this part's XML to the relationship - identified by *rId*. - """ - rIds = self._element.xpath("//@r:id") - return len([_rId for _rId in rIds if _rId == rId]) - - -class XmlPart(Part): - """ - Base class for package parts containing an XML payload, which is most of - them. Provides additional methods to the |Part| base class that take care - of parsing and reserializing the XML payload and managing relationships - to other parts. - """ - - def __init__(self, partname, content_type, element, package=None): - super(XmlPart, self).__init__(partname, content_type, package=package) - self._element = element - - @property - def blob(self): - return serialize_part_xml(self._element) - - @classmethod - def load(cls, partname, content_type, blob, package): - element = parse_xml(blob) - return cls(partname, content_type, element, package) - - @property - def part(self): - """ - Part of the parent protocol, "children" of the document will not know - the part that contains them so must ask their parent object. That - chain of delegation ends here for child objects. - """ - return self - - -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. - """ - - part_type_for = {} - default_part_type = Part - - def __new__(cls, partname, content_type, blob, package): - PartClass = cls._part_cls_for(content_type) - return PartClass.load(partname, content_type, blob, package) - - @classmethod - def _part_cls_for(cls, content_type): - """ - Return the custom part class registered for *content_type*, or the - default part class if no custom class is registered for - *content_type*. - """ - if content_type in cls.part_type_for: - return cls.part_type_for[content_type] - return cls.default_part_type - - -class RelationshipCollection(dict): - """ - Collection object for |_Relationship| instances, having list semantics. - """ - - def __init__(self, baseURI): - super(RelationshipCollection, self).__init__() - self._baseURI = baseURI - self._target_parts_by_rId = {} - - def add_relationship(self, reltype, target, rId, is_external=False): - """ - Return a newly added |_Relationship| instance. - """ - rel = _Relationship(rId, reltype, target, self._baseURI, is_external) - self[rId] = rel - if not is_external: - self._target_parts_by_rId[rId] = target - 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. - """ - rel = self._get_matching(reltype, target_part) - if rel is None: - rId = self._next_rId - rel = self.add_relationship(reltype, target_part, rId) - 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. - """ - 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) - 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. - """ - 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. - """ - 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. - """ - 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. - """ - - def matches(rel, reltype, target, is_external): - if rel.reltype != reltype: - return False - 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 - - for rel in self.values(): - if matches(rel, reltype, target, is_external): - return rel - 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. - """ - matching = [rel for rel in self.values() if rel.reltype == reltype] - if len(matching) == 0: - tmpl = "no relationship of type '%s' in collection" - raise KeyError(tmpl % reltype) - if len(matching) > 1: - tmpl = "multiple relationships of type '%s' in collection" - raise ValueError(tmpl % reltype) - return matching[0] - - @property - def _next_rId(self): - """ - Next available rId in collection, starting from 'rId1' and making use - of any gaps in numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']. - """ - for n in range(1, len(self) + 2): - rId_candidate = "rId%d" % n # like 'rId19' - if rId_candidate not in self: - return rId_candidate - - -class Unmarshaller(object): - """ - Hosts static methods for unmarshalling a package from a |PackageReader| - instance. - """ - - @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*. - """ - parts = Unmarshaller._unmarshal_parts(pkg_reader, package, part_factory) - Unmarshaller._unmarshal_relationships(pkg_reader, package, parts) - for part in parts.values(): - part.after_unmarshal() - package.after_unmarshal() - - @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*. - """ - parts = {} - for partname, content_type, blob in pkg_reader.iter_sparts(): - parts[partname] = part_factory(partname, content_type, blob, package) - return parts - - @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*. - """ - 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.load_rel(srel.reltype, target, srel.rId, srel.is_external) - - -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 - self._reltype = reltype - self._target = target - self._baseURI = baseURI - self._is_external = bool(external) - - @property - def is_external(self): - return self._is_external - - @property - def reltype(self): - return self._reltype - - @property - def rId(self): - return self._rId - - @property - def target_part(self): - if self._is_external: - raise ValueError( - "target_part property on _Relationship is undef" - "ined when target mode is External" - ) - return self._target - - @property - def target_ref(self): - if self._is_external: - return self._target - else: - return self._target.partname.relative_ref(self._baseURI) diff --git a/pptx/opc/packuri.py b/pptx/opc/packuri.py deleted file mode 100644 index f9408e0c3..000000000 --- a/pptx/opc/packuri.py +++ /dev/null @@ -1,118 +0,0 @@ -# encoding: utf-8 - -""" -Provides the PackURI value type along with some useful known pack URI strings -such as PACKAGE_URI. -""" - -import posixpath -import re - - -class PackURI(str): - """ - Provides access to pack URI components such as the baseURI and the - filename slice. Behaves as |str| otherwise. - """ - - _filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?") - - def __new__(cls, pack_uri_str): - if not pack_uri_str[0] == "/": - tmpl = "PackURI must begin with slash, got '%s'" - raise ValueError(tmpl % pack_uri_str) - return str.__new__(cls, pack_uri_str) - - @staticmethod - def from_rel_ref(baseURI, relative_ref): - """ - Return a |PackURI| instance containing the absolute pack URI formed by - translating *relative_ref* onto *baseURI*. - """ - joined_uri = posixpath.join(baseURI, relative_ref) - abs_uri = posixpath.abspath(joined_uri) - return PackURI(abs_uri) - - @property - def baseURI(self): - """ - The base URI of this pack URI, the directory portion, roughly - speaking. E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. - For the package pseudo-partname '/', baseURI is '/'. - """ - return posixpath.split(self)[0] - - @property - def ext(self): - """ - The extension portion of this pack URI, e.g. ``'xml'`` for - ``'/ppt/slides/slide1.xml'``. Note that the period is not included. - """ - # raw_ext is either empty string or starts with period, e.g. '.xml' - raw_ext = posixpath.splitext(self)[1] - return raw_ext[1:] if raw_ext.startswith(".") else raw_ext - - @property - def filename(self): - """ - The "filename" portion of this pack URI, e.g. ``'slide1.xml'`` for - ``'/ppt/slides/slide1.xml'``. For the package pseudo-partname '/', - filename is ''. - """ - return posixpath.split(self)[1] - - @property - def idx(self): - """ - Return partname index as integer for tuple partname or None for - singleton partname, e.g. ``21`` for ``'/ppt/slides/slide21.xml'`` and - |None| for ``'/ppt/presentation.xml'``. - """ - filename = self.filename - if not filename: - return None - name_part = posixpath.splitext(filename)[0] # filename w/ext removed - match = self._filename_re.match(name_part) - if match is None: - return None - if match.group(2): - return int(match.group(2)) - return None - - @property - def membername(self): - """ - The pack URI with the leading slash stripped off, the form used as - the Zip file membername for the package item. Returns '' for the - package pseudo-partname '/'. - """ - return self[1:] - - def relative_ref(self, baseURI): - """ - Return string containing relative reference to package item from - *baseURI*. E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would - return '../slideLayouts/slideLayout1.xml' for baseURI '/ppt/slides'. - """ - # workaround for posixpath bug in 2.6, doesn't generate correct - # relative path when *start* (second) parameter is root ('/') - if baseURI == "/": - relpath = self[1:] - else: - relpath = posixpath.relpath(self, baseURI) - return relpath - - @property - def rels_uri(self): - """ - The pack URI of the .rels part corresponding to the current pack URI. - Only produces sensible output if the pack URI is a partname or the - package pseudo-partname '/'. - """ - rels_filename = "%s.rels" % self.filename - rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename) - return PackURI(rels_uri_str) - - -PACKAGE_URI = PackURI("/") -CONTENT_TYPES_URI = PackURI("/[Content_Types].xml") diff --git a/pptx/opc/phys_pkg.py b/pptx/opc/phys_pkg.py deleted file mode 100644 index 90ca398cf..000000000 --- a/pptx/opc/phys_pkg.py +++ /dev/null @@ -1,159 +0,0 @@ -# encoding: utf-8 - -""" -Provides a general interface to a *physical* OPC package, such as a zip file. -""" - -from __future__ import absolute_import - -import os - -from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED - -from ..compat import is_string -from ..exceptions import PackageNotFoundError - -from .packuri import CONTENT_TYPES_URI - - -class PhysPkgReader(object): - """ - Factory for physical package reader objects. - """ - - def __new__(cls, pkg_file): - # if *pkg_file* is a string, treat it as a path - if is_string(pkg_file): - if os.path.isdir(pkg_file): - reader_cls = _DirPkgReader - elif is_zipfile(pkg_file): - reader_cls = _ZipPkgReader - else: - raise PackageNotFoundError("Package not found at '%s'" % pkg_file) - else: # assume it's a stream and pass it to Zip reader to sort out - reader_cls = _ZipPkgReader - - return super(PhysPkgReader, cls).__new__(reader_cls) - - -class PhysPkgWriter(object): - """ - Factory for physical package writer objects. - """ - - def __new__(cls, pkg_file): - return super(PhysPkgWriter, cls).__new__(_ZipPkgWriter) - - -class _DirPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for an OPC package extracted into a - directory. - """ - - def __init__(self, path): - """ - *path* is the path to a directory containing an expanded package. - """ - super(_DirPkgReader, self).__init__() - self._path = os.path.abspath(path) - - def blob_for(self, pack_uri): - """ - Return contents of file corresponding to *pack_uri* in package - directory. - """ - path = os.path.join(self._path, pack_uri.membername) - with open(path, "rb") as f: - blob = f.read() - return blob - - def close(self): - """ - Provides interface consistency with |ZipFileSystem|, but does - nothing, a directory file system doesn't need closing. - """ - pass - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri*, or None if the - item has no rels item. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except IOError: - rels_xml = None - return rels_xml - - -class _ZipPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for a zip file OPC package. - """ - - def __init__(self, pkg_file): - super(_ZipPkgReader, self).__init__() - self._zipf = ZipFile(pkg_file, "r") - - def blob_for(self, pack_uri): - """ - Return blob corresponding to *pack_uri*. Raises |ValueError| if no - matching member is present in zip archive. - """ - return self._zipf.read(pack_uri.membername) - - def close(self): - """ - Close the zip archive, releasing any resources it is using. - """ - self._zipf.close() - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the zip package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri* or None if no rels - item is present. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except KeyError: - rels_xml = None - return rels_xml - - -class _ZipPkgWriter(PhysPkgWriter): - """ - Implements |PhysPkgWriter| interface for a zip file OPC package. - """ - - def __init__(self, pkg_file): - super(_ZipPkgWriter, self).__init__() - self._zipf = ZipFile(pkg_file, "w", compression=ZIP_DEFLATED) - - def close(self): - """ - Close the zip archive, flushing any pending physical writes and - releasing any resources it's using. - """ - self._zipf.close() - - def write(self, pack_uri, blob): - """ - Write *blob* to this zip package with the membername corresponding to - *pack_uri*. - """ - self._zipf.writestr(pack_uri.membername, blob) diff --git a/pptx/opc/pkgreader.py b/pptx/opc/pkgreader.py deleted file mode 100644 index 59c74da53..000000000 --- a/pptx/opc/pkgreader.py +++ /dev/null @@ -1,293 +0,0 @@ -# encoding: utf-8 - -""" -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 - - -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 - self._sparts = sparts - - @staticmethod - def from_file(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) - sparts = PackageReader._load_serialized_parts( - phys_reader, pkg_srels, content_types - ) - phys_reader.close() - return PackageReader(content_types, pkg_srels, sparts) - - def iter_sparts(self): - """ - Generate a 3-tuple `(partname, content_type, blob)` for each of the - serialized parts in the package. - """ - for spart in self._sparts: - yield (spart.partname, spart.content_type, spart.blob) - - def iter_srels(self): - """ - 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: - for srel in spart.srels: - yield (spart.partname, srel) - - @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*. - """ - sparts = [] - part_walker = PackageReader._walk_phys_parts(phys_reader, pkg_srels) - for partname, blob, srels in part_walker: - content_type = content_types[partname] - spart = _SerializedPart(partname, content_type, blob, srels) - sparts.append(spart) - return tuple(sparts) - - @staticmethod - def _srels_for(phys_reader, source_uri): - """ - Return |_SerializedRelationshipCollection| instance populated with - relationships for source identified by *source_uri*. - """ - rels_xml = phys_reader.rels_xml_for(source_uri) - return _SerializedRelationshipCollection.load_from_xml( - source_uri.baseURI, rels_xml - ) - - @staticmethod - def _walk_phys_parts(phys_reader, srels, visited_partnames=None): - """ - Generate a 3-tuple `(partname, blob, 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: - if srel.is_external: - continue - partname = srel.target_partname - if partname in visited_partnames: - continue - visited_partnames.append(partname) - part_srels = PackageReader._srels_for(phys_reader, partname) - blob = phys_reader.blob_for(partname) - yield (partname, blob, part_srels) - for partname, blob, srels in PackageReader._walk_phys_parts( - phys_reader, part_srels, visited_partnames - ): - yield (partname, blob, srels) - - -class _ContentTypeMap(object): - """ - Value type providing dictionary semantics for looking up content type by - part name, e.g. ``content_type = cti['/ppt/presentation.xml']``. - """ - - def __init__(self): - super(_ContentTypeMap, self).__init__() - self._overrides = CaseInsensitiveDict() - self._defaults = CaseInsensitiveDict() - - def __getitem__(self, partname): - """ - Return content type for part identified by *partname*. - """ - if not isinstance(partname, PackURI): - tmpl = "_ContentTypeMap key must be , got %s" - raise KeyError(tmpl % type(partname)) - if partname in self._overrides: - return self._overrides[partname] - if partname.ext in self._defaults: - return self._defaults[partname.ext] - tmpl = "no content type for partname '%s' in [Content_Types].xml" - raise KeyError(tmpl % partname) - - @staticmethod - def from_xml(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.override_lst: - ct_map._add_override(o.partName, o.contentType) - for d in types_elm.default_lst: - ct_map._add_default(d.extension, d.contentType) - return ct_map - - def _add_default(self, extension, content_type): - """ - Add the default mapping of *extension* to *content_type* to this - content type mapping. *extension* does not include the leading - period. - """ - 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. - """ - 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. - """ - - def __init__(self, partname, content_type, blob, srels): - super(_SerializedPart, self).__init__() - self._partname = partname - self._content_type = content_type - self._blob = blob - self._srels = srels - - @property - def partname(self): - return self._partname - - @property - def content_type(self): - return self._content_type - - @property - def blob(self): - return self._blob - - @property - def srels(self): - return self._srels - - -class _SerializedRelationship(object): - """ - Value object representing a serialized relationship in an OPC package. - Serialized, in this case, means any target part is referred to via its - partname rather than a direct link to an in-memory |Part| object. - """ - - def __init__(self, baseURI, rel_elm): - super(_SerializedRelationship, self).__init__() - self._baseURI = baseURI - self._rId = rel_elm.rId - self._reltype = rel_elm.reltype - self._target_mode = rel_elm.targetMode - self._target_ref = rel_elm.target_ref - - @property - def is_external(self): - """ - True if target_mode is ``RTM.EXTERNAL`` - """ - return self._target_mode == RTM.EXTERNAL - - @property - def reltype(self): - """Relationship type, like ``RT.OFFICE_DOCUMENT``""" - return self._reltype - - @property - def rId(self): - """ - 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``. - """ - 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. - """ - 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. - """ - if self.is_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) - return self._target_partname - - -class _SerializedRelationshipCollection(object): - """ - Read-only sequence of |_SerializedRelationship| instances corresponding - to the relationships item XML passed to constructor. - """ - - def __init__(self): - super(_SerializedRelationshipCollection, self).__init__() - self._srels = [] - - def __iter__(self): - """Support iteration, e.g. 'for x in srels:'""" - return self._srels.__iter__() - - @staticmethod - def load_from_xml(baseURI, rels_item_xml): - """ - Return |_SerializedRelationshipCollection| instance loaded with the - relationships contained in *rels_item_xml*. Returns an empty - collection if *rels_item_xml* is |None|. - """ - srels = _SerializedRelationshipCollection() - if rels_item_xml is not None: - rels_elm = parse_xml(rels_item_xml) - for rel_elm in rels_elm.relationship_lst: - srels._srels.append(_SerializedRelationship(baseURI, rel_elm)) - return srels diff --git a/pptx/opc/pkgwriter.py b/pptx/opc/pkgwriter.py deleted file mode 100644 index 688c21311..000000000 --- a/pptx/opc/pkgwriter.py +++ /dev/null @@ -1,119 +0,0 @@ -# encoding: utf-8 - -""" -Provides a low-level, write-only API to a serialized Open Packaging -Convention (OPC) package, essentially an implementation of OpcPackage.save() -""" - -from __future__ import absolute_import - -from .constants import CONTENT_TYPE as CT -from .oxml import CT_Types, serialize_part_xml -from .packuri import CONTENT_TYPES_URI, PACKAGE_URI -from .phys_pkg import PhysPkgWriter -from .shared import CaseInsensitiveDict -from .spec import default_content_types - - -class PackageWriter(object): - """ - Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be - either a path to a zip file (a string) or a file-like object. Its single - API method, :meth:`write`, is static, so this class is not intended to - be instantiated. - """ - - @staticmethod - def write(pkg_file, pkg_rels, parts): - """ - Write a physical package (.pptx file) to *pkg_file* containing - *pkg_rels* and *parts* and a content types stream based on the - content types of the parts. - """ - phys_writer = PhysPkgWriter(pkg_file) - PackageWriter._write_content_types_stream(phys_writer, parts) - PackageWriter._write_pkg_rels(phys_writer, pkg_rels) - PackageWriter._write_parts(phys_writer, parts) - phys_writer.close() - - @staticmethod - def _write_content_types_stream(phys_writer, parts): - """ - Write ``[Content_Types].xml`` part to the physical package with an - appropriate content type lookup target for each part in *parts*. - """ - content_types_blob = serialize_part_xml(_ContentTypesItem.xml_for(parts)) - phys_writer.write(CONTENT_TYPES_URI, content_types_blob) - - @staticmethod - def _write_parts(phys_writer, parts): - """ - Write the blob of each part in *parts* to the package, along with a - rels item for its relationships if and only if it has any. - """ - for part in parts: - phys_writer.write(part.partname, part.blob) - if len(part._rels): - phys_writer.write(part.partname.rels_uri, part._rels.xml) - - @staticmethod - def _write_pkg_rels(phys_writer, pkg_rels): - """ - Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the - package. - """ - phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) - - -class _ContentTypesItem(object): - """ - Service class that composes a content types item ([Content_Types].xml) - based on a list of parts. Not meant to be instantiated directly, its - single interface method is xml_for(), e.g. - ``_ContentTypesItem.xml_for(parts)``. - """ - - def __init__(self): - self._defaults = CaseInsensitiveDict() - self._overrides = dict() - - @classmethod - def xml_for(cls, parts): - """ - Return content types XML mapping each part in *parts* to the - appropriate content type and suitable for storage as - ``[Content_Types].xml`` in an OPC package. - """ - cti = cls() - cti._defaults["rels"] = CT.OPC_RELATIONSHIPS - cti._defaults["xml"] = CT.XML - for part in parts: - cti._add_content_type(part.partname, part.content_type) - return cti._xml() - - def _add_content_type(self, partname, content_type): - """ - Add a content type for the part with *partname* and *content_type*, - using a default or override as appropriate. - """ - ext = partname.ext - if (ext.lower(), content_type) in default_content_types: - self._defaults[ext] = content_type - else: - self._overrides[partname] = content_type - - def _xml(self): - """ - Return etree element containing the XML representation of this content - types item, suitable for serialization to the ``[Content_Types].xml`` - item for an OPC package. Although the sequence of elements is not - strictly significant, as an aid to testing and readability Default - elements are sorted by extension and Override elements are sorted by - partname. - """ - _types_elm = CT_Types.new() - for ext in sorted(self._defaults.keys()): - _types_elm.add_default(ext, self._defaults[ext]) - for partname in sorted(self._overrides.keys()): - _types_elm.add_override(partname, self._overrides[partname]) - return _types_elm diff --git a/pptx/opc/shared.py b/pptx/opc/shared.py deleted file mode 100644 index 08cc0c2a9..000000000 --- a/pptx/opc/shared.py +++ /dev/null @@ -1,25 +0,0 @@ -# encoding: utf-8 - -""" -Objects shared by modules in the pptx.opc sub-package -""" - -from __future__ import absolute_import, print_function, unicode_literals - - -class CaseInsensitiveDict(dict): - """ - Mapping type that behaves like dict except that it matches without respect - to the case of the key. E.g. cid['A'] == cid['a']. Note this is not - general-purpose, just complete enough to satisfy opc package needs. It - assumes str keys for example. - """ - - def __contains__(self, key): - return super(CaseInsensitiveDict, self).__contains__(key.lower()) - - def __getitem__(self, key): - return super(CaseInsensitiveDict, self).__getitem__(key.lower()) - - def __setitem__(self, key, value): - return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) diff --git a/pptx/oxml/action.py b/pptx/oxml/action.py deleted file mode 100644 index baaeb923d..000000000 --- a/pptx/oxml/action.py +++ /dev/null @@ -1,59 +0,0 @@ -# encoding: utf-8 - -""" -lxml custom element classes for text-related XML elements. -""" - -from __future__ import absolute_import - -from .simpletypes import XsdString -from .xmlchemy import BaseOxmlElement, OptionalAttribute - - -class CT_Hyperlink(BaseOxmlElement): - """ - Custom element class for elements. - """ - - rId = OptionalAttribute("r:id", XsdString) - action = OptionalAttribute("action", XsdString) - - @property - def action_fields(self): - """ - A dictionary containing any key-value pairs present in the query - portion of the `ppaction://` URL in the action attribute. For example - `{'id':'0', 'return':'true'}` in - 'ppaction://customshow?id=0&return=true'. Returns an empty dictionary - if the URL contains no query string or if no action attribute is - present. - """ - url = self.action - - if url is None: - return {} - - halves = url.split("?") - if len(halves) == 1: - return {} - - key_value_pairs = halves[1].split("&") - return dict([pair.split("=") for pair in key_value_pairs]) - - @property - def action_verb(self): - """ - The host portion of the `ppaction://` URL contained in the action - attribute. For example 'customshow' in - 'ppaction://customshow?id=0&return=true'. Returns |None| if no action - attribute is present. - """ - url = self.action - - if url is None: - return None - - protocol_and_host = url.split("?")[0] - host = protocol_and_host[11:] - - return host diff --git a/pptx/oxml/ns.py b/pptx/oxml/ns.py deleted file mode 100644 index f83c1cd0b..000000000 --- a/pptx/oxml/ns.py +++ /dev/null @@ -1,139 +0,0 @@ -# encoding: utf-8 - -""" -Namespace related objects. -""" - -from __future__ import absolute_import - - -#: Maps namespace prefix to namespace name for all known PowerPoint XML -#: namespaces. -_nsmap = { - "a": ("http://schemas.openxmlformats.org/drawingml/2006/main"), - "c": ("http://schemas.openxmlformats.org/drawingml/2006/chart"), - "cp": ( - "http://schemas.openxmlformats.org/package/2006/metadata/core-pro" "perties" - ), - "ct": ("http://schemas.openxmlformats.org/package/2006/content-types"), - "dc": ("http://purl.org/dc/elements/1.1/"), - "dcmitype": ("http://purl.org/dc/dcmitype/"), - "dcterms": ("http://purl.org/dc/terms/"), - "ep": ( - "http://schemas.openxmlformats.org/officeDocument/2006/extended-p" "roperties" - ), - "i": ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationsh" "ips/image" - ), - "m": ("http://schemas.openxmlformats.org/officeDocument/2006/math"), - "mo": ("http://schemas.microsoft.com/office/mac/office/2008/main"), - "mv": ("urn:schemas-microsoft-com:mac:vml"), - "o": ("urn:schemas-microsoft-com:office:office"), - "p": ("http://schemas.openxmlformats.org/presentationml/2006/main"), - "pd": ("http://schemas.openxmlformats.org/drawingml/2006/presentationDra" "wing"), - "pic": ("http://schemas.openxmlformats.org/drawingml/2006/picture"), - "pr": ("http://schemas.openxmlformats.org/package/2006/relationships"), - "r": ("http://schemas.openxmlformats.org/officeDocument/2006/relationsh" "ips"), - "sl": ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationsh" - "ips/slideLayout" - ), - "v": ("urn:schemas-microsoft-com:vml"), - "ve": ("http://schemas.openxmlformats.org/markup-compatibility/2006"), - "w": ("http://schemas.openxmlformats.org/wordprocessingml/2006/main"), - "w10": ("urn:schemas-microsoft-com:office:word"), - "wne": ("http://schemas.microsoft.com/office/word/2006/wordml"), - "wp": ("http://schemas.openxmlformats.org/drawingml/2006/wordprocessingD" "rawing"), - "xsi": ("http://www.w3.org/2001/XMLSchema-instance"), -} - - -class NamespacePrefixedTag(str): - """ - Value object that knows the semantics of an XML tag having a namespace - prefix. - """ - - def __new__(cls, nstag, *args): - return super(NamespacePrefixedTag, cls).__new__(cls, nstag) - - def __init__(self, nstag): - self._pfx, self._local_part = nstag.split(":") - self._ns_uri = _nsmap[self._pfx] - - @property - def clark_name(self): - return "{%s}%s" % (self._ns_uri, self._local_part) - - @property - def local_part(self): - """ - Return the local part of the tag as a string. E.g. 'foobar' is - returned for tag 'f:foobar'. - """ - return self._local_part - - @property - def nsmap(self): - """ - Return a dict having a single member, mapping the namespace prefix of - this tag to it's namespace name (e.g. {'f': 'http://foo/bar'}). This - is handy for passing to xpath calls and other uses. - """ - return {self._pfx: self._ns_uri} - - @property - def nspfx(self): - """ - Return the string namespace prefix for the tag, e.g. 'f' is returned - for tag 'f:foobar'. - """ - return self._pfx - - @property - def nsuri(self): - """ - Return the namespace URI for the tag, e.g. 'http://foo/bar' would be - returned for tag 'f:foobar' if the 'f' prefix maps to - 'http://foo/bar' in _nsmap. - """ - return self._ns_uri - - -def namespaces(*prefixes): - """ - Return a dict containing the subset namespace prefix mappings specified by - *prefixes*. Any number of namespace prefixes can be supplied, e.g. - namespaces('a', 'r', 'p'). - """ - namespaces = {} - for prefix in prefixes: - namespaces[prefix] = _nsmap[prefix] - return namespaces - - -nsmap = namespaces # alias for more compact use with Element() - - -def nsdecls(*prefixes): - return " ".join(['xmlns:%s="%s"' % (pfx, _nsmap[pfx]) for pfx in prefixes]) - - -def nsuri(nspfx): - """ - Return the namespace URI corresponding to *nspfx*. For example, it would - return 'http://foo/bar' for an *nspfx* of 'f' if the 'f' prefix maps to - 'http://foo/bar' in _nsmap. - """ - return _nsmap[nspfx] - - -def qn(namespace_prefixed_tag): - """ - Return a Clark-notation qualified tag name corresponding to - *namespace_prefixed_tag*, a string like 'p:body'. 'qn' stands for - *qualified name*. As an example, ``qn('p:cSld')`` returns - ``'{http://schemas.../main}cSld'``. - """ - nsptag = NamespacePrefixedTag(namespace_prefixed_tag) - return nsptag.clark_name diff --git a/pptx/oxml/presentation.py b/pptx/oxml/presentation.py deleted file mode 100644 index 17616cb4f..000000000 --- a/pptx/oxml/presentation.py +++ /dev/null @@ -1,95 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes for presentation-related XML elements. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from .simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString -from .xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne, ZeroOrMore - - -class CT_Presentation(BaseOxmlElement): - """ - ```` element, root of the Presentation part stored as - ``/ppt/presentation.xml``. - """ - - sldMasterIdLst = ZeroOrOne( - "p:sldMasterIdLst", - successors=( - "p:notesMasterIdLst", - "p:handoutMasterIdLst", - "p:sldIdLst", - "p:sldSz", - "p:notesSz", - ), - ) - sldIdLst = ZeroOrOne("p:sldIdLst", successors=("p:sldSz", "p:notesSz")) - sldSz = ZeroOrOne("p:sldSz", successors=("p:notesSz",)) - - -class CT_SlideId(BaseOxmlElement): - """ - ```` element, direct child of that contains an rId - reference to a slide in the presentation. - """ - - id = RequiredAttribute("id", ST_SlideId) - rId = RequiredAttribute("r:id", XsdString) - - -class CT_SlideIdList(BaseOxmlElement): - """ - ```` element, direct child of that contains - a list of the slide parts in the presentation. - """ - - sldId = ZeroOrMore("p:sldId") - - def add_sldId(self, rId): - """ - Return a reference to a newly created child element having - its r:id attribute set to *rId*. - """ - return self._add_sldId(id=self._next_id, rId=rId) - - @property - def _next_id(self): - """ - Return the next available slide ID as an int. Valid slide IDs start - at 256. The next integer value greater than the max value in use is - chosen, which minimizes that chance of reusing the id of a deleted - slide. - """ - id_str_lst = self.xpath("./p:sldId/@id") - return max([255] + [int(id_str) for id_str in id_str_lst]) + 1 - - -class CT_SlideMasterIdList(BaseOxmlElement): - """ - ```` element, child of ```` containing - references to the slide masters that belong to the presentation. - """ - - sldMasterId = ZeroOrMore("p:sldMasterId") - - -class CT_SlideMasterIdListEntry(BaseOxmlElement): - """ - ```` element, child of ```` containing - a reference to a slide master. - """ - - rId = RequiredAttribute("r:id", XsdString) - - -class CT_SlideSize(BaseOxmlElement): - """ - ```` element, direct child of that contains the - width and height of slides in the presentation. - """ - - cx = RequiredAttribute("cx", ST_SlideSizeCoordinate) - cy = RequiredAttribute("cy", ST_SlideSizeCoordinate) diff --git a/pptx/oxml/shapes/connector.py b/pptx/oxml/shapes/connector.py deleted file mode 100644 index ebbe1045f..000000000 --- a/pptx/oxml/shapes/connector.py +++ /dev/null @@ -1,115 +0,0 @@ -# encoding: utf-8 - -""" -lxml custom element classes for shape-related XML elements. -""" - -from __future__ import absolute_import - -from .. import parse_xml -from ..ns import nsdecls -from .shared import BaseShapeElement -from ..simpletypes import ST_DrawingElementId, XsdUnsignedInt -from ..xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne - - -class CT_Connection(BaseShapeElement): - """ - A `a:stCxn` or `a:endCxn` element specifying a connection between - an end-point of a connector and a shape connection point. - """ - - id = RequiredAttribute("id", ST_DrawingElementId) - idx = RequiredAttribute("idx", XsdUnsignedInt) - - -class CT_Connector(BaseShapeElement): - """ - A line/connector shape ```` element - """ - - _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst") - nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr") - spPr = OneAndOnlyOne("p:spPr") - del _tag_seq - - @classmethod - def new_cxnSp(cls, id_, name, prst, x, y, cx, cy, flipH, flipV): - """ - Return a new ```` element tree configured as a base - connector. - """ - tmpl = cls._cxnSp_tmpl() - flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "") - xml = tmpl.format( - **{ - "nsdecls": nsdecls("a", "p"), - "id": id_, - "name": name, - "x": x, - "y": y, - "cx": cx, - "cy": cy, - "prst": prst, - "flip": flip, - } - ) - return parse_xml(xml) - - @staticmethod - def _cxnSp_tmpl(): - 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" - "" - ) - - -class CT_ConnectorNonVisual(BaseOxmlElement): - """ - ```` element, container for the non-visual properties of - a connector, such as name, id, etc. - """ - - cNvPr = OneAndOnlyOne("p:cNvPr") - cNvCxnSpPr = OneAndOnlyOne("p:cNvCxnSpPr") - nvPr = OneAndOnlyOne("p:nvPr") - - -class CT_NonVisualConnectorProperties(BaseOxmlElement): - """ - `p:cNvCxnSpPr` element, container for the non-visual properties specific - to a connector shape, such as connections and connector locking. - """ - - _tag_seq = ("a:cxnSpLocks", "a:stCxn", "a:endCxn", "a:extLst") - stCxn = ZeroOrOne("a:stCxn", successors=_tag_seq[2:]) - endCxn = ZeroOrOne("a:endCxn", successors=_tag_seq[3:]) - del _tag_seq diff --git a/pptx/oxml/shapes/graphfrm.py b/pptx/oxml/shapes/graphfrm.py deleted file mode 100644 index 4c4215207..000000000 --- a/pptx/oxml/shapes/graphfrm.py +++ /dev/null @@ -1,338 +0,0 @@ -# encoding: utf-8 - -"""lxml custom element class for CT_GraphicalObjectFrame XML element.""" - -from pptx.oxml import parse_xml -from pptx.oxml.chart.chart import CT_Chart -from pptx.oxml.ns import nsdecls -from pptx.oxml.shapes.shared import BaseShapeElement -from pptx.oxml.simpletypes import XsdBoolean, XsdString -from pptx.oxml.table import CT_Table -from pptx.oxml.xmlchemy import ( - BaseOxmlElement, - OneAndOnlyOne, - OptionalAttribute, - RequiredAttribute, - ZeroOrOne, -) -from pptx.spec import ( - GRAPHIC_DATA_URI_CHART, - GRAPHIC_DATA_URI_OLEOBJ, - GRAPHIC_DATA_URI_TABLE, -) - - -class CT_GraphicalObject(BaseOxmlElement): - """ - ```` element, which is the container for the reference to or - definition of the framed graphical object (table, chart, etc.). - """ - - graphicData = OneAndOnlyOne("a:graphicData") - - @property - def chart(self): - """ - The ```` grandchild element, or |None| if not present. - """ - return self.graphicData.chart - - -class CT_GraphicalObjectData(BaseShapeElement): - """ - ```` element, the direct container for a table, a chart, - or another graphical object. - """ - - chart = ZeroOrOne("c:chart") - tbl = ZeroOrOne("a:tbl") - uri = RequiredAttribute("uri", XsdString) - - @property - def blob_rId(self): - """Optional "r:id" attribute value of `` descendent element. - - This value is `None` when this `p:graphicData` element does not enclose an OLE - object. This value could also be `None` if an enclosed OLE object does not - specify this attribute (it is specified optional in the schema) but so far, all - OLE objects we've encountered specify this value. - """ - return None if self._oleObj is None else self._oleObj.rId - - @property - def is_embedded_ole_obj(self): - """Optional boolean indicating an embedded OLE object. - - Returns `None` when this `p:graphicData` element does not enclose an OLE object. - `True` indicates an embedded OLE object and `False` indicates a linked OLE - object. - """ - return None if self._oleObj is None else self._oleObj.is_embedded - - @property - def progId(self): - """Optional str value of "progId" attribute of `` descendent. - - This value identifies the "type" of the embedded object in terms of the - application used to open it. - - This value is `None` when this `p:graphicData` element does not enclose an OLE - object. This could also be `None` if an enclosed OLE object does not specify - this attribute (it is specified optional in the schema) but so far, all OLE - objects we've encountered specify this value. - """ - return None if self._oleObj is None else self._oleObj.progId - - @property - def showAsIcon(self): - """Optional value of "showAsIcon" attribute value of `p:oleObj` descendent. - - This value is `None` when this `p:graphicData` element does not enclose an OLE - object. It is False when the `showAsIcon` attribute is omitted on the `p:oleObj` - element. - """ - return None if self._oleObj is None else self._oleObj.showAsIcon - - @property - def _oleObj(self): - """Optional `` element contained in this `p:graphicData' element. - - Returns `None` when this graphic-data element does not enclose an OLE object. - Note that this returns the last `p:oleObj` element found. There can be more - than one `p:oleObj` element because an `` element may - appear as the child of `p:graphicData` and that alternate-content subtree can - contain multiple compatibility choices. The last one should suit best for - reading purposes because it contains the lowest common denominator. - """ - oleObjs = self.xpath(".//p:oleObj") - return oleObjs[-1] if oleObjs else None - - -class CT_GraphicalObjectFrame(BaseShapeElement): - """ - ```` element, which is a container for a table, a chart, - or another graphical object. - """ - - nvGraphicFramePr = OneAndOnlyOne("p:nvGraphicFramePr") - xfrm = OneAndOnlyOne("p:xfrm") - graphic = OneAndOnlyOne("a:graphic") - - @property - def chart(self): - """ - The ```` great-grandchild element, or |None| if not present. - """ - return self.graphic.chart - - @property - def chart_rId(self): - """ - The ``rId`` attribute of the ```` great-grandchild element, - or |None| if not present. - """ - chart = self.chart - if chart is None: - return None - return chart.rId - - def get_or_add_xfrm(self): - """ - Return the required ```` child element. Overrides version on - BaseShapeElement. - """ - return self.xfrm - - @property - def graphicData(self): - """` grandchild of this graphic-frame element.""" - return self.graphic.graphicData - - @property - def graphicData_uri(self): - """str value of `uri` attribute of ` grandchild.""" - return self.graphic.graphicData.uri - - @property - def has_oleobj(self): - """True for graphicFrame containing an OLE object, False otherwise.""" - return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ - - @property - def is_embedded_ole_obj(self): - """Optional boolean indicating an embedded OLE object. - - Returns `None` when this `p:graphicFrame` element does not enclose an OLE - object. `True` indicates an embedded OLE object and `False` indicates a linked - OLE object. - """ - return self.graphicData.is_embedded_ole_obj - - @classmethod - def new_chart_graphicFrame(cls, id_, name, rId, x, y, cx, cy): - """ - Return a ```` element tree populated with a chart - element. - """ - graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy) - graphicData = graphicFrame.graphic.graphicData - graphicData.uri = GRAPHIC_DATA_URI_CHART - graphicData.append(CT_Chart.new_chart(rId)) - return graphicFrame - - @classmethod - def new_graphicFrame(cls, id_, name, x, y, cx, cy): - """ - Return a new ```` element tree suitable for - containing a table or chart. Note that a graphicFrame element is not - a valid shape until it contains a graphical object such as a table. - """ - xml = cls._graphicFrame_tmpl() % (id_, name, x, y, cx, cy) - graphicFrame = parse_xml(xml) - return graphicFrame - - @classmethod - def new_ole_object_graphicFrame( - cls, id_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy - ): - """Return newly-created `` for embedded OLE-object. - - `ole_object_rId` identifies the relationship to the OLE-object part. - - `progId` is a str identifying the object-type in terms of the application - (program) used to open it. This becomes an attribute of the same name in the - `p:oleObj` element. - - `icon_rId` identifies the relationship to an image part used to display the - OLE-object as an icon (vs. a preview). - """ - return parse_xml( - cls._graphicFrame_xml_for_ole_object( - id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId - ) - ) - - @classmethod - def new_table_graphicFrame(cls, id_, name, rows, cols, x, y, cx, cy): - """ - Return a ```` element tree populated with a table - element. - """ - graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy) - graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE - graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy)) - return graphicFrame - - @classmethod - def _graphicFrame_tmpl(cls): - return ( - "\n" - " \n" - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - ' \n' - ' \n' - " \n" - " \n" - " \n" - " \n" - "" - % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d") - ) - - @classmethod - def _graphicFrame_xml_for_ole_object( - cls, id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId - ): - """str XML for element of an embedded OLE-object shape.""" - 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' - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "" - ).format( - nsdecls=nsdecls("a", "p", "r"), - id_=id_, - name=name, - x=x, - y=y, - cx=cx, - cy=cy, - ole_object_rId=ole_object_rId, - progId=progId, - icon_rId=icon_rId, - ) - - -class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement): - """`` element. - - This contains the non-visual properties of a graphic frame, such as name, id, etc. - """ - - cNvPr = OneAndOnlyOne("p:cNvPr") - nvPr = OneAndOnlyOne("p:nvPr") - - -class CT_OleObject(BaseOxmlElement): - """`` element, container for an OLE object (e.g. Excel file). - - An OLE object can be either linked or embedded (hence the name). - """ - - progId = OptionalAttribute("progId", XsdString) - rId = OptionalAttribute("r:id", XsdString) - showAsIcon = OptionalAttribute("showAsIcon", XsdBoolean, default=False) - - @property - def is_embedded(self): - """True when this OLE object is embedded, False when it is linked.""" - return True if len(self.xpath("./p:embed")) > 0 else False diff --git a/pptx/oxml/text.py b/pptx/oxml/text.py deleted file mode 100644 index cf99dd0da..000000000 --- a/pptx/oxml/text.py +++ /dev/null @@ -1,577 +0,0 @@ -# encoding: utf-8 - -"""Custom element classes for text-related XML elements""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import re - -from pptx.compat import to_unicode -from pptx.enum.lang import MSO_LANGUAGE_ID -from pptx.enum.text import ( - MSO_AUTO_SIZE, - MSO_TEXT_UNDERLINE_TYPE, - MSO_VERTICAL_ANCHOR, - PP_PARAGRAPH_ALIGNMENT, -) -from pptx.exc import InvalidXmlError -from pptx.oxml import parse_xml -from pptx.oxml.dml.fill import CT_GradientFillProperties -from pptx.oxml.ns import nsdecls -from pptx.oxml.simpletypes import ( - ST_Coordinate32, - ST_TextFontScalePercentOrPercentString, - ST_TextFontSize, - ST_TextIndentLevelType, - ST_TextSpacingPercentOrPercentString, - ST_TextSpacingPoint, - ST_TextTypeface, - ST_TextWrappingType, - XsdBoolean, -) -from pptx.oxml.xmlchemy import ( - BaseOxmlElement, - Choice, - OneAndOnlyOne, - OneOrMore, - OptionalAttribute, - RequiredAttribute, - ZeroOrMore, - ZeroOrOne, - ZeroOrOneChoice, -) -from pptx.util import Emu, Length - - -class CT_RegularTextRun(BaseOxmlElement): - """`a:r` custom element class""" - - rPr = ZeroOrOne("a:rPr", successors=("a:t",)) - t = OneAndOnlyOne("a:t") - - @property - def text(self): - """(unicode) str containing text of (required) `a:t` child""" - text = self.t.text - # t.text is None when t element is empty, e.g. '' - return to_unicode(text) if text is not None else "" - - @text.setter - def text(self, str): - """*str* is unicode value to replace run text.""" - self.t.text = self._escape_ctrl_chars(str) - - @staticmethod - def _escape_ctrl_chars(s): - """Return str after replacing each control character with a plain-text escape. - - For example, a BEL character (x07) would appear as "_x0007_". Horizontal-tab - (x09) and line-feed (x0A) are not escaped. All other characters in the range - x00-x1F are escaped. - """ - return re.sub( - r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s - ) - - -class CT_TextBody(BaseOxmlElement): - """`p:txBody` custom element class. - - Also used for `c:txPr` in charts and perhaps other elements. - """ - - bodyPr = OneAndOnlyOne("a:bodyPr") - p = OneOrMore("a:p") - - def clear_content(self): - """Remove all `a:p` children, but leave any others. - - cf. lxml `_Element.clear()` method which removes all children. - """ - for p in self.p_lst: - self.remove(p) - - @property - def defRPr(self): - """ - ```` element of required first ``p`` child, added with its - ancestors if not present. Used when element is a ```` in - a chart and the ``p`` element is used only to specify formatting, not - content. - """ - p = self.p_lst[0] - pPr = p.get_or_add_pPr() - defRPr = pPr.get_or_add_defRPr() - return defRPr - - @property - def is_empty(self): - """True if only a single empty `a:p` element is present.""" - ps = self.p_lst - if len(ps) > 1: - return False - - if not ps: - raise InvalidXmlError("p:txBody must have at least one a:p") - - if ps[0].text != "": - return False - return True - - @classmethod - def new(cls): - """ - Return a new ```` element tree - """ - xml = cls._txBody_tmpl() - txBody = parse_xml(xml) - return txBody - - @classmethod - def new_a_txBody(cls): - """ - Return a new ```` element tree, suitable for use in a table - cell and possibly other situations. - """ - xml = cls._a_txBody_tmpl() - txBody = parse_xml(xml) - return txBody - - @classmethod - def new_p_txBody(cls): - """ - Return a new ```` element tree, suitable for use in an - ```` element. - """ - xml = cls._p_txBody_tmpl() - return parse_xml(xml) - - @classmethod - def new_txPr(cls): - """ - Return a ```` element tree suitable for use in a chart object - like data labels or tick labels. - """ - xml = ( - "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "\n" - ) % nsdecls("c", "a") - txPr = parse_xml(xml) - return txPr - - def unclear_content(self): - """Ensure p:txBody has at least one a:p child. - - Intuitively, reverse a ".clear_content()" operation to minimum - conformance with spec (single empty paragraph). - """ - if len(self.p_lst) > 0: - return - self.add_p() - - @classmethod - def _a_txBody_tmpl(cls): - return ( - "\n" - " \n" - " \n" - "\n" % (nsdecls("a")) - ) - - @classmethod - def _p_txBody_tmpl(cls): - return ( - "\n" - " \n" - " \n" - "\n" % (nsdecls("p", "a")) - ) - - @classmethod - def _txBody_tmpl(cls): - return ( - "\n" - " \n" - " \n" - " \n" - "\n" % (nsdecls("a", "p")) - ) - - -class CT_TextBodyProperties(BaseOxmlElement): - """ - custom element class - """ - - eg_textAutoFit = ZeroOrOneChoice( - (Choice("a:noAutofit"), Choice("a:normAutofit"), Choice("a:spAutoFit")), - successors=("a:scene3d", "a:sp3d", "a:flatTx", "a:extLst"), - ) - lIns = OptionalAttribute("lIns", ST_Coordinate32, default=Emu(91440)) - tIns = OptionalAttribute("tIns", ST_Coordinate32, default=Emu(45720)) - rIns = OptionalAttribute("rIns", ST_Coordinate32, default=Emu(91440)) - bIns = OptionalAttribute("bIns", ST_Coordinate32, default=Emu(45720)) - anchor = OptionalAttribute("anchor", MSO_VERTICAL_ANCHOR) - wrap = OptionalAttribute("wrap", ST_TextWrappingType) - - @property - def autofit(self): - """ - The autofit setting for the text frame, a member of the - ``MSO_AUTO_SIZE`` enumeration. - """ - if self.noAutofit is not None: - return MSO_AUTO_SIZE.NONE - if self.normAutofit is not None: - return MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE - if self.spAutoFit is not None: - return MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT - return None - - @autofit.setter - def autofit(self, value): - if value is not None and value not in MSO_AUTO_SIZE._valid_settings: - raise ValueError( - "only None or a member of the MSO_AUTO_SIZE enumeration can " - "be assigned to CT_TextBodyProperties.autofit, got %s" % value - ) - self._remove_eg_textAutoFit() - if value == MSO_AUTO_SIZE.NONE: - self._add_noAutofit() - elif value == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE: - self._add_normAutofit() - elif value == MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT: - self._add_spAutoFit() - - -class CT_TextCharacterProperties(BaseOxmlElement): - """`a:rPr, a:defRPr, and `a:endParaRPr` custom element class. - - 'rPr' is short for 'run properties', and it corresponds to the |Font| - proxy class. - """ - - eg_fillProperties = ZeroOrOneChoice( - ( - Choice("a:noFill"), - Choice("a:solidFill"), - Choice("a:gradFill"), - Choice("a:blipFill"), - Choice("a:pattFill"), - Choice("a:grpFill"), - ), - successors=( - "a:effectLst", - "a:effectDag", - "a:highlight", - "a:uLnTx", - "a:uLn", - "a:uFillTx", - "a:uFill", - "a:latin", - "a:ea", - "a:cs", - "a:sym", - "a:hlinkClick", - "a:hlinkMouseOver", - "a:rtl", - "a:extLst", - ), - ) - latin = ZeroOrOne( - "a:latin", - successors=( - "a:ea", - "a:cs", - "a:sym", - "a:hlinkClick", - "a:hlinkMouseOver", - "a:rtl", - "a:extLst", - ), - ) - hlinkClick = ZeroOrOne( - "a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst") - ) - - lang = OptionalAttribute("lang", MSO_LANGUAGE_ID) - sz = OptionalAttribute("sz", ST_TextFontSize) - b = OptionalAttribute("b", XsdBoolean) - i = OptionalAttribute("i", XsdBoolean) - u = OptionalAttribute("u", MSO_TEXT_UNDERLINE_TYPE) - - def _new_gradFill(self): - return CT_GradientFillProperties.new_gradFill() - - def add_hlinkClick(self, rId): - """ - Add an child element with r:id attribute set to *rId*. - """ - hlinkClick = self.get_or_add_hlinkClick() - hlinkClick.rId = rId - return hlinkClick - - -class CT_TextField(BaseOxmlElement): - """ - field element, for either a slide number or date field - """ - - rPr = ZeroOrOne("a:rPr", successors=("a:pPr", "a:t")) - t = ZeroOrOne("a:t", successors=()) - - @property - def text(self): - """ - The text of the ```` child element. - """ - t = self.t - if t is None: - return "" - text = t.text - return to_unicode(text) if text is not None else "" - - -class CT_TextFont(BaseOxmlElement): - """ - Custom element class for , , , and child - elements of CT_TextCharacterProperties, e.g. . - """ - - typeface = RequiredAttribute("typeface", ST_TextTypeface) - - -class CT_TextLineBreak(BaseOxmlElement): - """`a:br` line break element""" - - rPr = ZeroOrOne("a:rPr", successors=()) - - @property - def text(self): - """Unconditionally a single vertical-tab character. - - A line break element can contain no text other than the implicit line feed it - represents. - """ - return "\v" - - -class CT_TextNormalAutofit(BaseOxmlElement): - """ - element specifying fit text to shape font reduction, etc. - """ - - fontScale = OptionalAttribute( - "fontScale", ST_TextFontScalePercentOrPercentString, default=100.0 - ) - - -class CT_TextParagraph(BaseOxmlElement): - """`a:p` custom element class""" - - pPr = ZeroOrOne("a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr")) - r = ZeroOrMore("a:r", successors=("a:endParaRPr",)) - br = ZeroOrMore("a:br", successors=("a:endParaRPr",)) - endParaRPr = ZeroOrOne("a:endParaRPr", successors=()) - - def add_br(self): - """ - Return a newly appended element. - """ - return self._add_br() - - def add_r(self, text=None): - """ - Return a newly appended element. - """ - r = self._add_r() - if text: - r.text = text - return r - - def append_text(self, text): - """Append `a:r` and `a:br` elements to *p* based on *text*. - - Any `\n` or `\v` (vertical-tab) characters in *text* delimit `a:r` (run) - elements and themselves are translated to `a:br` (line-break) elements. The - vertical-tab character appears in clipboard text from PowerPoint at "soft" - line-breaks (new-line, but not new paragraph). - """ - for idx, r_str in enumerate(re.split("\n|\v", text)): - # ---breaks are only added *between* items, not at start--- - if idx > 0: - self.add_br() - # ---runs that would be empty are not added--- - if r_str: - self.add_r(r_str) - - @property - def content_children(self): - """Sequence containing text-container child elements of this `a:p` element. - - These include `a:r`, `a:br`, and `a:fld`. - """ - text_types = {CT_RegularTextRun, CT_TextLineBreak, CT_TextField} - return tuple(elm for elm in self if type(elm) in text_types) - - @property - def text(self): - """str text contained in this paragraph.""" - # ---note this shadows the lxml _Element.text--- - return "".join([child.text for child in self.content_children]) - - def _new_r(self): - r_xml = "" % nsdecls("a") - return parse_xml(r_xml) - - -class CT_TextParagraphProperties(BaseOxmlElement): - """ - custom element class - """ - - _tag_seq = ( - "a:lnSpc", - "a:spcBef", - "a:spcAft", - "a:buClrTx", - "a:buClr", - "a:buSzTx", - "a:buSzPct", - "a:buSzPts", - "a:buFontTx", - "a:buFont", - "a:buNone", - "a:buAutoNum", - "a:buChar", - "a:buBlip", - "a:tabLst", - "a:defRPr", - "a:extLst", - ) - lnSpc = ZeroOrOne("a:lnSpc", successors=_tag_seq[1:]) - spcBef = ZeroOrOne("a:spcBef", successors=_tag_seq[2:]) - spcAft = ZeroOrOne("a:spcAft", successors=_tag_seq[3:]) - defRPr = ZeroOrOne("a:defRPr", successors=_tag_seq[16:]) - lvl = OptionalAttribute("lvl", ST_TextIndentLevelType, default=0) - algn = OptionalAttribute("algn", PP_PARAGRAPH_ALIGNMENT) - del _tag_seq - - @property - def line_spacing(self): - """ - The spacing between baselines of successive lines in this paragraph. - A float value indicates a number of lines. A |Length| value indicates - a fixed spacing. Value is contained in `./a:lnSpc/a:spcPts/@val` or - `./a:lnSpc/a:spcPct/@val`. Value is |None| if no element is present. - """ - lnSpc = self.lnSpc - if lnSpc is None: - return None - if lnSpc.spcPts is not None: - return lnSpc.spcPts.val - return lnSpc.spcPct.val - - @line_spacing.setter - def line_spacing(self, value): - self._remove_lnSpc() - if value is None: - return - if isinstance(value, Length): - self._add_lnSpc().set_spcPts(value) - else: - self._add_lnSpc().set_spcPct(value) - - @property - def space_after(self): - """ - The EMU equivalent of the centipoints value in - `./a:spcAft/a:spcPts/@val`. - """ - spcAft = self.spcAft - if spcAft is None: - return None - spcPts = spcAft.spcPts - if spcPts is None: - return None - return spcPts.val - - @space_after.setter - def space_after(self, value): - self._remove_spcAft() - if value is not None: - self._add_spcAft().set_spcPts(value) - - @property - def space_before(self): - """ - The EMU equivalent of the centipoints value in - `./a:spcBef/a:spcPts/@val`. - """ - spcBef = self.spcBef - if spcBef is None: - return None - spcPts = spcBef.spcPts - if spcPts is None: - return None - return spcPts.val - - @space_before.setter - def space_before(self, value): - self._remove_spcBef() - if value is not None: - self._add_spcBef().set_spcPts(value) - - -class CT_TextSpacing(BaseOxmlElement): - """ - Used for , , and elements. - """ - - # this should actually be a OneAndOnlyOneChoice, but that's not - # implemented yet. - spcPct = ZeroOrOne("a:spcPct") - spcPts = ZeroOrOne("a:spcPts") - - def set_spcPct(self, value): - """ - Set spacing to *value* lines, e.g. 1.75 lines. A ./a:spcPts child is - removed if present. - """ - self._remove_spcPts() - spcPct = self.get_or_add_spcPct() - spcPct.val = value - - def set_spcPts(self, value): - """ - Set spacing to *value* points. A ./a:spcPct child is removed if - present. - """ - self._remove_spcPct() - spcPts = self.get_or_add_spcPts() - spcPts.val = value - - -class CT_TextSpacingPercent(BaseOxmlElement): - """ - element, specifying spacing in thousandths of a percent in its - `val` attribute. - """ - - val = RequiredAttribute("val", ST_TextSpacingPercentOrPercentString) - - -class CT_TextSpacingPoint(BaseOxmlElement): - """ - element, specifying spacing in centipoints in its `val` - attribute. - """ - - val = RequiredAttribute("val", ST_TextSpacingPoint) diff --git a/pptx/parts/chart.py b/pptx/parts/chart.py deleted file mode 100644 index 4ca859e22..000000000 --- a/pptx/parts/chart.py +++ /dev/null @@ -1,101 +0,0 @@ -# encoding: utf-8 - -""" -Chart part objects, including Chart and Charts -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from ..chart.chart import Chart -from .embeddedpackage import EmbeddedXlsxPart -from ..opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT -from ..opc.package import XmlPart -from ..util import lazyproperty - - -class ChartPart(XmlPart): - """ - A chart part; corresponds to parts having partnames matching - ppt/charts/chart[1-9][0-9]*.xml - """ - - partname_template = "/ppt/charts/chart%d.xml" - - @classmethod - def new(cls, chart_type, chart_data, package): - """ - Return a new |ChartPart| instance added to *package* containing - a chart of *chart_type* and depicting *chart_data*. - """ - chart_blob = chart_data.xml_bytes(chart_type) - partname = package.next_partname(cls.partname_template) - content_type = CT.DML_CHART - chart_part = cls.load(partname, content_type, chart_blob, package) - xlsx_blob = chart_data.xlsx_blob - chart_part.chart_workbook.update_from_xlsx_blob(xlsx_blob) - return chart_part - - @lazyproperty - def chart(self): - """ - The |Chart| object representing the chart in this part. - """ - return Chart(self._element, self) - - @lazyproperty - def chart_workbook(self): - """ - The |ChartWorkbook| object providing access to the external chart - data in a linked or embedded Excel workbook. - """ - return ChartWorkbook(self._element, self) - - -class ChartWorkbook(object): - """ - Provides access to the external chart data in a linked or embedded Excel - workbook. - """ - - def __init__(self, chartSpace, chart_part): - super(ChartWorkbook, self).__init__() - self._chartSpace = chartSpace - self._chart_part = chart_part - - def update_from_xlsx_blob(self, xlsx_blob): - """ - Replace the Excel spreadsheet in the related |EmbeddedXlsxPart| with - the Excel binary in *xlsx_blob*, adding a new |EmbeddedXlsxPart| if - there isn't one. - """ - xlsx_part = self.xlsx_part - if xlsx_part is None: - self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._package) - return - xlsx_part.blob = xlsx_blob - - @property - def xlsx_part(self): - """ - Return the related |EmbeddedXlsxPart| object having its rId at - `c:chartSpace/c:externalData/@rId` or |None| if there is no - `` element. - """ - xlsx_part_rId = self._chartSpace.xlsx_part_rId - if xlsx_part_rId is None: - return None - return self._chart_part.related_parts[xlsx_part_rId] - - @xlsx_part.setter - def xlsx_part(self, xlsx_part): - """ - Set the related |EmbeddedXlsxPart| to *xlsx_part*. Assume one does - not already exist. - """ - rId = self._chart_part.relate_to(xlsx_part, RT.PACKAGE) - externalData = self._chartSpace.get_or_add_externalData() - externalData.rId = rId - - @property - def _package(self): - return self._chart_part.package diff --git a/pptx/parts/image.py b/pptx/parts/image.py deleted file mode 100644 index cb4c74489..000000000 --- a/pptx/parts/image.py +++ /dev/null @@ -1,291 +0,0 @@ -# encoding: utf-8 - -"""ImagePart and related objects.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import hashlib -import os - -try: - from PIL import Image as PIL_Image -except ImportError: - import Image as PIL_Image - -from ..compat import BytesIO, is_string -from ..opc.package import Part -from ..opc.spec import image_content_types -from ..util import lazyproperty - - -class ImagePart(Part): - """ - An image part, generally having a partname matching the regex - ``ppt/media/image[1-9][0-9]*.*``. - """ - - def __init__(self, partname, content_type, blob, package, filename=None): - super(ImagePart, self).__init__(partname, content_type, blob, package) - self._filename = filename - - @classmethod - def load(cls, partname, content_type, blob, package): - return cls(partname, content_type, blob, package) - - @classmethod - def new(cls, package, image): - """ - Return a new |ImagePart| instance containing *image*, which is an - |Image| object. - """ - partname = package.next_image_partname(image.ext) - return cls(partname, image.content_type, image.blob, package, image.filename) - - @property - def desc(self): - """ - The filename associated with this image, either the filename of - the original image or a generic name of the form ``image.ext`` - where ``ext`` is appropriate to the image file format, e.g. - ``'jpg'``. An image created using a path will have that filename; one - created with a file-like object will have a generic name. - """ - # return generic filename if original filename is unknown - if self._filename is None: - return "image.%s" % self.ext - return self._filename - - @property - def ext(self): - """ - Return file extension for this image e.g. ``'png'``. - """ - return self.partname.ext - - @property - def image(self): - """ - An |Image| object containing the image in this image part. - """ - return Image(self.blob, self.desc) - - def scale(self, scaled_cx, scaled_cy): - """ - Return scaled image dimensions in EMU based on the combination of - parameters supplied. If *scaled_cx* and *scaled_cy* are both |None|, - the native image size is returned. If neither *scaled_cx* nor - *scaled_cy* is |None|, their values are returned unchanged. If - a value is provided for either *scaled_cx* or *scaled_cy* and the - other is |None|, the missing value is calculated such that the - image's aspect ratio is preserved. - """ - image_cx, image_cy = self._native_size - - if scaled_cx is None and scaled_cy is None: - scaled_cx = image_cx - scaled_cy = image_cy - elif scaled_cx is None: - scaling_factor = float(scaled_cy) / float(image_cy) - scaled_cx = int(round(image_cx * scaling_factor)) - elif scaled_cy is None: - scaling_factor = float(scaled_cx) / float(image_cx) - scaled_cy = int(round(image_cy * scaling_factor)) - - return scaled_cx, scaled_cy - - @lazyproperty - def sha1(self): - """ - The SHA1 hash digest for the image binary of this image part, like: - ``'1be010ea47803b00e140b852765cdf84f491da47'``. - """ - return hashlib.sha1(self._blob).hexdigest() - - @property - def _dpi(self): - """ - A (horz_dpi, vert_dpi) 2-tuple (ints) representing the dots-per-inch - property of this image. - """ - image = Image.from_blob(self.blob) - return image.dpi - - @property - def _native_size(self): - """ - A (width, height) 2-tuple representing the native dimensions of the - image in EMU, calculated based on the image DPI value, if present, - assuming 72 dpi as a default. - """ - EMU_PER_INCH = 914400 - horz_dpi, vert_dpi = self._dpi - width_px, height_px = self._px_size - - width = EMU_PER_INCH * width_px / horz_dpi - height = EMU_PER_INCH * height_px / vert_dpi - - return width, height - - @property - def _px_size(self): - """ - A (width, height) 2-tuple representing the dimensions of this image - in pixels. - """ - image = Image.from_blob(self.blob) - return image.size - - -class Image(object): - """ - Immutable value object representing an image such as a JPEG, PNG, or GIF. - """ - - def __init__(self, blob, filename): - super(Image, self).__init__() - self._blob = blob - self._filename = filename - - @classmethod - def from_blob(cls, blob, filename=None): - """ - Return a new |Image| object loaded from the image binary in *blob*. - """ - return cls(blob, filename) - - @classmethod - def from_file(cls, image_file): - """ - Return a new |Image| object loaded from *image_file*, which can be - either a path (string) or a file-like object. - """ - if is_string(image_file): - # treat image_file as a path - with open(image_file, "rb") as f: - blob = f.read() - filename = os.path.basename(image_file) - else: - # assume image_file is a file-like object - # ---reposition file cursor if it has one--- - if callable(getattr(image_file, "seek")): - image_file.seek(0) - blob = image_file.read() - filename = None - - return cls.from_blob(blob, filename) - - @property - def blob(self): - """ - The binary image bytestream of this image. - """ - return self._blob - - @lazyproperty - def content_type(self): - """ - MIME-type of this image, e.g. ``'image/jpeg'``. - """ - return image_content_types[self.ext] - - @lazyproperty - def dpi(self): - """ - A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch - resolution of this image. A default value of (72, 72) is used if the - dpi is not specified in the image file. - """ - - def int_dpi(dpi): - """ - Return an integer dots-per-inch value corresponding to *dpi*. If - *dpi* is |None|, a non-numeric type, less than 1 or greater than - 2048, 72 is returned. - """ - try: - int_dpi = int(round(float(dpi))) - if int_dpi < 1 or int_dpi > 2048: - int_dpi = 72 - except (TypeError, ValueError): - int_dpi = 72 - return int_dpi - - def normalize_pil_dpi(pil_dpi): - """ - Return a (horz_dpi, vert_dpi) 2-tuple corresponding to *pil_dpi*, - the value for the 'dpi' key in the ``info`` dict of a PIL image. - If the 'dpi' key is not present or contains an invalid value, - ``(72, 72)`` is returned. - """ - if isinstance(pil_dpi, tuple): - return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1])) - return (72, 72) - - return normalize_pil_dpi(self._pil_props[2]) - - @lazyproperty - def ext(self): - """ - Canonical file extension for this image e.g. ``'png'``. The returned - extension is all lowercase and is the canonical extension for the - content type of this image, regardless of what extension may have - been used in its filename, if any. - """ - ext_map = { - "BMP": "bmp", - "GIF": "gif", - "JPEG": "jpg", - "PNG": "png", - "TIFF": "tiff", - "WMF": "wmf", - } - format = self._format - if format not in ext_map: - tmpl = "unsupported image format, expected one of: %s, got '%s'" - raise ValueError(tmpl % (ext_map.keys(), format)) - return ext_map[format] - - @property - def filename(self): - """ - The filename from the path from which this image was loaded, if - loaded from the filesystem. |None| if no filename was used in - loading, such as when loaded from an in-memory stream. - """ - return self._filename - - @lazyproperty - def sha1(self): - """ - SHA1 hash digest of the image blob - """ - return hashlib.sha1(self._blob).hexdigest() - - @lazyproperty - def size(self): - """ - A (width, height) 2-tuple specifying the dimensions of this image in - pixels. - """ - return self._pil_props[1] - - @property - def _format(self): - """ - The PIL Image format of this image, e.g. 'PNG'. - """ - return self._pil_props[0] - - @lazyproperty - def _pil_props(self): - """ - A tuple containing useful image properties extracted from this image - using Pillow (Python Imaging Library, or 'PIL'). - """ - stream = BytesIO(self._blob) - pil_image = PIL_Image.open(stream) - format = pil_image.format - width_px, height_px = pil_image.size - dpi = pil_image.info.get("dpi") - stream.close() - return (format, (width_px, height_px), dpi) diff --git a/pptx/parts/presentation.py b/pptx/parts/presentation.py deleted file mode 100644 index fed4b0f64..000000000 --- a/pptx/parts/presentation.py +++ /dev/null @@ -1,138 +0,0 @@ -# encoding: utf-8 - -""" -Presentation part, the main part in a .pptx package. -""" - -from __future__ import absolute_import - -from ..opc.constants import RELATIONSHIP_TYPE as RT -from ..opc.package import XmlPart -from ..opc.packuri import PackURI -from ..presentation import Presentation -from .slide import NotesMasterPart, SlidePart -from ..util import lazyproperty - - -class PresentationPart(XmlPart): - """ - Top level class in object model, represents the contents of the /ppt - directory of a .pptx file. - """ - - def add_slide(self, slide_layout): - """ - Return an (rId, slide) pair of a newly created blank slide that - inherits appearance from *slide_layout*. - """ - partname = self._next_slide_partname - slide_layout_part = slide_layout.part - slide_part = SlidePart.new(partname, self.package, slide_layout_part) - rId = self.relate_to(slide_part, RT.SLIDE) - return rId, slide_part.slide - - @property - def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties of this presentation. - """ - return self.package.core_properties - - def get_slide(self, slide_id): - """ - Return the |Slide| object identified by *slide_id* (in this - presentation), or |None| if not found. - """ - for sldId in self._element.sldIdLst: - if sldId.id == slide_id: - return self.related_parts[sldId.rId].slide - return None - - @lazyproperty - def notes_master(self): - """ - Return the |NotesMaster| object for this presentation. If the - presentation does not have a notes master, one is created from - a default template. The same single instance is returned on each - call. - """ - return self.notes_master_part.notes_master - - @lazyproperty - def notes_master_part(self): - """ - Return the |NotesMasterPart| object for this presentation. If the - presentation does not have a notes master, one is created from - a default template. The same single instance is returned on each - call. - """ - try: - return self.part_related_by(RT.NOTES_MASTER) - except KeyError: - notes_master_part = NotesMasterPart.create_default(self.package) - self.relate_to(notes_master_part, RT.NOTES_MASTER) - return notes_master_part - - @lazyproperty - def presentation(self): - """ - A |Presentation| object providing access to the content of this - presentation. - """ - return Presentation(self._element, self) - - def related_slide(self, rId): - """ - Return the |Slide| object for the related |SlidePart| corresponding - to relationship key *rId*. - """ - return self.related_parts[rId].slide - - def related_slide_master(self, rId): - """ - Return the |SlideMaster| object for the related |SlideMasterPart| - corresponding to relationship key *rId*. - """ - return self.related_parts[rId].slide_master - - def rename_slide_parts(self, rIds): - """ - Assign incrementing partnames like ``/ppt/slides/slide9.xml`` to the - slide parts identified by *rIds*, in the order their id appears in - that sequence. The name portion is always ``slide``. The number part - forms a continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). - The extension is always ``.xml``. - """ - for idx, rId in enumerate(rIds): - slide_part = self.related_parts[rId] - slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1)) - - def save(self, path_or_stream): - """ - Save this presentation package 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) - - def slide_id(self, slide_part): - """ - Return the slide identifier associated with *slide_part* in this - presentation. - """ - for sldId in self._element.sldIdLst: - if self.related_parts[sldId.rId] is slide_part: - return sldId.id - raise ValueError("matching slide_part not found") - - @property - def _next_slide_partname(self): - """ - Return |PackURI| instance containing the partname for a slide to be - appended to this slide collection, e.g. ``/ppt/slides/slide9.xml`` - for a slide collection containing 8 slides. - """ - sldIdLst = self._element.get_or_add_sldIdLst() - partname_str = "/ppt/slides/slide%d.xml" % (len(sldIdLst) + 1) - return PackURI(partname_str) diff --git a/pptx/presentation.py b/pptx/presentation.py deleted file mode 100644 index 4f01e71bc..000000000 --- a/pptx/presentation.py +++ /dev/null @@ -1,112 +0,0 @@ -# encoding: utf-8 - -""" -Main presentation object. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from .shared import PartElementProxy -from .slide import SlideMasters, Slides -from .util import lazyproperty - - -class Presentation(PartElementProxy): - """ - PresentationML (PML) presentation. Not intended to be constructed - directly. Use :func:`pptx.Presentation` to open or create a presentation. - """ - - __slots__ = ("_slide_masters", "_slides") - - @property - def core_properties(self): - """ - Instance of |CoreProperties| holding the read/write Dublin Core - document properties for this presentation. - """ - return self.part.core_properties - - @property - def notes_master(self): - """ - Instance of |NotesMaster| for this presentation. If the presentation - does not have a notes master, one is created from a default template - and returned. The same single instance is returned on each call. - """ - return self.part.notes_master - - def save(self, file): - """ - Save this presentation to *file*, where *file* can be either a path - to a file (a string) or a file-like object. - """ - self.part.save(file) - - @property - def slide_height(self): - """ - Height of slides in this presentation, in English Metric Units (EMU). - Returns |None| if no slide width is defined. Read/write. - """ - sldSz = self._element.sldSz - if sldSz is None: - return None - return sldSz.cy - - @slide_height.setter - def slide_height(self, height): - sldSz = self._element.get_or_add_sldSz() - sldSz.cy = height - - @property - def slide_layouts(self): - """ - Sequence of |SlideLayout| instances belonging to the first - |SlideMaster| of this presentation. A presentation can have more than - one slide master and each master will have its own set of layouts. - This property is a convenience for the common case where the - presentation has only a single slide master. - """ - return self.slide_masters[0].slide_layouts - - @property - def slide_master(self): - """ - First |SlideMaster| object belonging to this presentation. Typically, - presentations have only a single slide master. This property provides - simpler access in that common case. - """ - return self.slide_masters[0] - - @lazyproperty - def slide_masters(self): - """ - Sequence of |SlideMaster| objects belonging to this presentation - """ - return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self) - - @property - def slide_width(self): - """ - Width of slides in this presentation, in English Metric Units (EMU). - Returns |None| if no slide width is defined. Read/write. - """ - sldSz = self._element.sldSz - if sldSz is None: - return None - return sldSz.cx - - @slide_width.setter - def slide_width(self, width): - sldSz = self._element.get_or_add_sldSz() - sldSz.cx = width - - @lazyproperty - def slides(self): - """ - |Slides| object containing the slides in this presentation. - """ - sldIdLst = self._element.get_or_add_sldIdLst() - self.part.rename_slide_parts([sldId.rId for sldId in sldIdLst]) - return Slides(sldIdLst, self) diff --git a/pptx/shapes/__init__.py b/pptx/shapes/__init__.py deleted file mode 100644 index c8e1f24d9..000000000 --- a/pptx/shapes/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# encoding: utf-8 - -""" -Objects used across sub-package -""" - - -class Subshape(object): - """ - Provides common services for drawing elements that occur below a shape - but may occasionally require an ancestor object to provide a service, - such as add or drop a relationship. Provides ``self._parent`` attribute - to subclasses. - """ - - def __init__(self, parent): - super(Subshape, self).__init__() - self._parent = parent - - @property - def part(self): - """ - The package part containing this object - """ - return self._parent.part diff --git a/pptx/shapes/autoshape.py b/pptx/shapes/autoshape.py deleted file mode 100644 index 9c176c2bd..000000000 --- a/pptx/shapes/autoshape.py +++ /dev/null @@ -1,393 +0,0 @@ -# encoding: utf-8 - -"""Autoshape-related objects such as Shape and Adjustment.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from numbers import Number - -from pptx.dml.fill import FillFormat -from pptx.dml.line import LineFormat -from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE -from pptx.shapes.base import BaseShape -from pptx.spec import autoshape_types -from pptx.text.text import TextFrame -from pptx.util import lazyproperty - - -class Adjustment(object): - """ - An adjustment value for an autoshape. - - An adjustment value corresponds to the position of an adjustment handle on - an auto shape. Adjustment handles are the small yellow diamond-shaped - handles that appear on certain auto shapes and allow the outline of the - shape to be adjusted. For example, a rounded rectangle has an adjustment - handle that allows the radius of its corner rounding to be adjusted. - - Values are |float| and generally range from 0.0 to 1.0, although the value - can be negative or greater than 1.0 in certain circumstances. - """ - - def __init__(self, name, def_val, actual=None): - super(Adjustment, self).__init__() - self.name = name - self.def_val = def_val - self.actual = actual - - @property - def effective_value(self): - """ - Read/write |float| representing normalized adjustment value for this - adjustment. Actual values are a large-ish integer expressed in shape - coordinates, nominally between 0 and 100,000. The effective value is - normalized to a corresponding value nominally between 0.0 and 1.0. - Intuitively this represents the proportion of the width or height of - the shape at which the adjustment value is located from its starting - point. For simple shapes such as a rounded rectangle, this intuitive - correspondence holds. For more complicated shapes and at more extreme - shape proportions (e.g. width is much greater than height), the value - can become negative or greater than 1.0. - """ - raw_value = self.actual - if raw_value is None: - raw_value = self.def_val - return self._normalize(raw_value) - - @effective_value.setter - def effective_value(self, value): - if not isinstance(value, Number): - tmpl = "adjustment value must be numeric, got '%s'" - raise ValueError(tmpl % value) - self.actual = self._denormalize(value) - - @staticmethod - def _denormalize(value): - """ - Return integer corresponding to normalized *raw_value* on unit basis - of 100,000. See Adjustment.normalize for additional details. - """ - return int(value * 100000.0) - - @staticmethod - def _normalize(raw_value): - """ - Return normalized value for *raw_value*. A normalized value is a - |float| between 0.0 and 1.0 for nominal raw values between 0 and - 100,000. Raw values less than 0 and greater than 100,000 are valid - and return values calculated on the same unit basis of 100,000. - """ - return raw_value / 100000.0 - - @property - def val(self): - """ - Denormalized effective value (expressed in shape coordinates), - suitable for using in the XML. - """ - return self.actual if self.actual is not None else self.def_val - - -class AdjustmentCollection(object): - """ - Sequence of |Adjustment| instances for an auto shape, each representing - an available adjustment for a shape of its type. Supports ``len()`` and - indexed access, e.g. ``shape.adjustments[1] = 0.15``. - """ - - def __init__(self, prstGeom): - super(AdjustmentCollection, self).__init__() - self._adjustments_ = self._initialized_adjustments(prstGeom) - self._prstGeom = prstGeom - - def __getitem__(self, key): - """Provides indexed access, (e.g. 'adjustments[9]').""" - return self._adjustments_[key].effective_value - - def __setitem__(self, key, value): - """ - Provides item assignment via an indexed expression, e.g. - ``adjustments[9] = 999.9``. Causes all adjustment values in - collection to be written to the XML. - """ - self._adjustments_[key].effective_value = value - self._rewrite_guides() - - def _initialized_adjustments(self, prstGeom): - """ - Return an initialized list of adjustment values based on the contents - of *prstGeom* - """ - if prstGeom is None: - return [] - davs = AutoShapeType.default_adjustment_values(prstGeom.prst) - adjustments = [Adjustment(name, def_val) for name, def_val in davs] - self._update_adjustments_with_actuals(adjustments, prstGeom.gd_lst) - return adjustments - - def _rewrite_guides(self): - """ - Write ```` elements to the XML, one for each adjustment value. - Any existing guide elements are overwritten. - """ - guides = [(adj.name, adj.val) for adj in self._adjustments_] - self._prstGeom.rewrite_guides(guides) - - @staticmethod - def _update_adjustments_with_actuals(adjustments, guides): - """ - Update |Adjustment| instances in *adjustments* with actual values - held in *guides*, a list of ```` elements. Guides with a name - that does not match an adjustment object are skipped. - """ - adjustments_by_name = dict((adj.name, adj) for adj in adjustments) - for gd in guides: - name = gd.name - actual = int(gd.fmla[4:]) - try: - adjustment = adjustments_by_name[name] - except KeyError: - continue - adjustment.actual = actual - return - - @property - def _adjustments(self): - """ - Sequence containing direct references to the |Adjustment| objects - contained in collection. - """ - return tuple(self._adjustments_) - - def __len__(self): - """Implement built-in function len()""" - return len(self._adjustments_) - - -class AutoShapeType(object): - """ - Return an instance of |AutoShapeType| containing metadata for an auto - shape of type identified by *autoshape_type_id*. Instances are cached, so - no more than one instance for a particular auto shape type is in memory. - - Instances provide the following attributes: - - .. attribute:: autoshape_type_id - - Integer uniquely identifying this auto shape type. Corresponds to a - value in ``pptx.constants.MSO`` like ``MSO_SHAPE.ROUNDED_RECTANGLE``. - - .. attribute:: basename - - Base part of shape name for auto shapes of this type, e.g. ``Rounded - Rectangle`` becomes ``Rounded Rectangle 99`` when the distinguishing - integer is added to the shape name. - - .. attribute:: prst - - String identifier for this auto shape type used in the ```` - element. - - .. attribute:: desc - - Informal string description of auto shape. - - """ - - _instances = {} - - def __new__(cls, autoshape_type_id): - """ - Only create new instance on first call for content_type. After that, - use cached instance. - """ - # if there's not a matching instance in the cache, create one - if autoshape_type_id not in cls._instances: - inst = super(AutoShapeType, cls).__new__(cls) - cls._instances[autoshape_type_id] = inst - # return the instance; note that __init__() gets called either way - return cls._instances[autoshape_type_id] - - def __init__(self, autoshape_type_id): - """Initialize attributes from constant values in pptx.spec""" - # skip loading if this instance is from the cache - if hasattr(self, "_loaded"): - return - # raise on bad autoshape_type_id - if autoshape_type_id not in autoshape_types: - raise KeyError( - "no autoshape type with id '%s' in pptx.spec.autoshape_types" - % autoshape_type_id - ) - # otherwise initialize new instance - autoshape_type = autoshape_types[autoshape_type_id] - self._autoshape_type_id = autoshape_type_id - self._basename = autoshape_type["basename"] - self._loaded = True - - @property - def autoshape_type_id(self): - """ - MSO_AUTO_SHAPE_TYPE enumeration value for this auto shape type - """ - return self._autoshape_type_id - - @property - def basename(self): - """ - Base of shape name (less the distinguishing integer) for this auto - shape type - """ - return self._basename - - @classmethod - def default_adjustment_values(cls, prst): - """ - Return sequence of name, value tuples representing the adjustment - value defaults for the auto shape type identified by *prst*. - """ - return autoshape_types[prst]["avLst"] - - @property - def desc(self): - """Informal description of this auto shape type""" - return self._desc - - @classmethod - def id_from_prst(cls, prst): - """ - Return auto shape id (e.g. ``MSO_SHAPE.RECTANGLE``) corresponding to - preset geometry keyword *prst*. - """ - return MSO_AUTO_SHAPE_TYPE.from_xml(prst) - - @property - def prst(self): - """ - Preset geometry identifier string for this auto shape. Used in the - ``prst`` attribute of ```` element to specify the geometry - to be used in rendering the shape, for example ``'roundRect'``. - """ - return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id) - - -class Shape(BaseShape): - """A shape that can appear on a slide. - - Corresponds to the ```` element that can appear in any of the slide-type parts - (slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster). - """ - - def __init__(self, sp, parent): - super(Shape, self).__init__(sp, parent) - self._sp = sp - - @lazyproperty - def adjustments(self): - """ - Read-only reference to |AdjustmentCollection| instance for this - shape - """ - return AdjustmentCollection(self._sp.prstGeom) - - @property - def auto_shape_type(self): - """ - Enumeration value identifying the type of this auto shape, like - ``MSO_SHAPE.ROUNDED_RECTANGLE``. Raises |ValueError| if this shape is - not an auto shape. - """ - if not self._sp.is_autoshape: - raise ValueError("shape is not an auto shape") - return self._sp.prst - - @lazyproperty - def fill(self): - """ - |FillFormat| instance for this shape, providing access to fill - properties such as fill color. - """ - return FillFormat.from_fill_parent(self._sp.spPr) - - def get_or_add_ln(self): - """ - Return the ```` element containing the line format properties - XML for this shape. - """ - return self._sp.get_or_add_ln() - - @property - def has_text_frame(self): - """ - |True| if this shape can contain text. Always |True| for an - AutoShape. - """ - return True - - @lazyproperty - def line(self): - """ - |LineFormat| instance for this shape, providing access to line - properties such as line color. - """ - return LineFormat(self) - - @property - def ln(self): - """ - The ```` element containing the line format properties such as - line color and width. |None| if no ```` element is present. - """ - return self._sp.ln - - @property - def shape_type(self): - """ - Unique integer identifying the type of this shape, like - ``MSO_SHAPE_TYPE.TEXT_BOX``. - """ - if self.is_placeholder: - return MSO_SHAPE_TYPE.PLACEHOLDER - if self._sp.has_custom_geometry: - return MSO_SHAPE_TYPE.FREEFORM - if self._sp.is_autoshape: - return MSO_SHAPE_TYPE.AUTO_SHAPE - if self._sp.is_textbox: - return MSO_SHAPE_TYPE.TEXT_BOX - msg = "Shape instance of unrecognized shape type" - raise NotImplementedError(msg) - - @property - def text(self): - """Read/write. Unicode (str in Python 3) representation of shape text. - - The returned string will contain a newline character (``"\\n"``) separating each - paragraph and a vertical-tab (``"\\v"``) character for each line break (soft - carriage return) in the shape's text. - - Assignment to *text* replaces all text previously contained in the shape, along - with any paragraph or font formatting applied to it. A newline character - (``"\\n"``) in the assigned text causes a new paragraph to be started. - A vertical-tab (``"\\v"``) character in the assigned text causes a line-break - (soft carriage-return) to be inserted. (The vertical-tab character appears in - clipboard text copied from PowerPoint as its encoding of line-breaks.) - - Either bytes (Python 2 str) or unicode (Python 3 str) can be assigned. Bytes can - be 7-bit ASCII or UTF-8 encoded 8-bit bytes. Bytes values are converted to - unicode assuming UTF-8 encoding (which also works for ASCII). - """ - return self.text_frame.text - - @text.setter - def text(self, text): - self.text_frame.text = text - - @property - def text_frame(self): - """|TextFrame| instance for this shape. - - Contains the text of the shape and provides access to text formatting - properties. - """ - txBody = self._element.get_or_add_txBody() - return TextFrame(txBody, self) diff --git a/pptx/shapes/base.py b/pptx/shapes/base.py deleted file mode 100644 index c9472434d..000000000 --- a/pptx/shapes/base.py +++ /dev/null @@ -1,252 +0,0 @@ -# encoding: utf-8 - -"""Base shape-related objects such as BaseShape.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from pptx.action import ActionSetting -from pptx.dml.effect import ShadowFormat -from pptx.shared import ElementProxy -from pptx.util import lazyproperty - - -class BaseShape(object): - """Base class for shape objects. - - Subclasses include |Shape|, |Picture|, and |GraphicFrame|. - """ - - def __init__(self, shape_elm, parent): - super(BaseShape, self).__init__() - self._element = shape_elm - self._parent = parent - - def __eq__(self, other): - """|True| if this shape object proxies the same element as *other*. - - 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, BaseShape): - return False - return self._element is other._element - - def __ne__(self, other): - if not isinstance(other, BaseShape): - return True - return self._element is not other._element - - @lazyproperty - def click_action(self): - """|ActionSetting| instance providing access to click behaviors. - - Click behaviors are hyperlink-like behaviors including jumping to - a hyperlink (web page) or to another slide in the presentation. The - click action is that defined on the overall shape, not a run of text - within the shape. An |ActionSetting| object is always returned, even - when no click behavior is defined on the shape. - """ - cNvPr = self._element._nvXxPr.cNvPr - return ActionSetting(cNvPr, self) - - @property - def element(self): - """`lxml` element for this shape, e.g. a CT_Shape instance. - - Note that manipulating this element improperly can produce an invalid - presentation file. Make sure you know what you're doing if you use - this to change the underlying XML. - """ - return self._element - - @property - def has_chart(self): - """ - |True| if this shape is a graphic frame containing a chart object. - |False| otherwise. When |True|, the chart object can be accessed - using the ``.chart`` property. - """ - # This implementation is unconditionally False, the True version is - # on GraphicFrame subclass. - return False - - @property - def has_table(self): - """ - |True| if this shape is a graphic frame containing a table object. - |False| otherwise. When |True|, the table object can be accessed - using the ``.table`` property. - """ - # This implementation is unconditionally False, the True version is - # on GraphicFrame subclass. - return False - - @property - def has_text_frame(self): - """ - |True| if this shape can contain text. - """ - # overridden on Shape to return True. Only has text frame - return False - - @property - def height(self): - """ - Read/write. Integer distance between top and bottom extents of shape - in EMUs - """ - return self._element.cy - - @height.setter - def height(self, value): - self._element.cy = value - - @property - def is_placeholder(self): - """ - True if this shape is a placeholder. A shape is a placeholder if it - has a element. - """ - return self._element.has_ph_elm - - @property - def left(self): - """ - Read/write. Integer distance of the left edge of this shape from the - left edge of the slide, in English Metric Units (EMU) - """ - return self._element.x - - @left.setter - def left(self, value): - self._element.x = value - - @property - def name(self): - """ - Name of this shape, e.g. 'Picture 7' - """ - return self._element.shape_name - - @name.setter - def name(self, value): - self._element._nvXxPr.cNvPr.name = value - - @property - def part(self): - """The package part containing this shape. - - A |BaseSlidePart| subclass in this case. Access to a slide part - should only be required if you are extending the behavior of |pp| API - objects. - """ - return self._parent.part - - @property - def placeholder_format(self): - """ - A |_PlaceholderFormat| object providing access to - placeholder-specific properties such as placeholder type. Raises - |ValueError| on access if the shape is not a placeholder. - """ - if not self.is_placeholder: - raise ValueError("shape is not a placeholder") - return _PlaceholderFormat(self._element.ph) - - @property - def rotation(self): - """ - Read/write float. Degrees of clockwise rotation. Negative values can - be assigned to indicate counter-clockwise rotation, e.g. assigning - -45.0 will change setting to 315.0. - """ - return self._element.rot - - @rotation.setter - def rotation(self, value): - self._element.rot = value - - @lazyproperty - def shadow(self): - """|ShadowFormat| object providing access to shadow for this shape. - - A |ShadowFormat| object is always returned, even when no shadow is - explicitly defined on this shape (i.e. it inherits its shadow - behavior). - """ - return ShadowFormat(self._element.spPr) - - @property - def shape_id(self): - """Read-only positive integer identifying this shape. - - The id of a shape is unique among all shapes on a slide. - """ - return self._element.shape_id - - @property - def shape_type(self): - """ - Unique integer identifying the type of this shape, like - ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses. - """ - # # This one returns |None| unconditionally to account for shapes - # # that haven't been implemented yet, like group shape and chart. - # # Once those are done this should raise |NotImplementedError|. - # msg = 'shape_type property must be implemented by subclasses' - # raise NotImplementedError(msg) - return None - - @property - def top(self): - """ - Read/write. Integer distance of the top edge of this shape from the - top edge of the slide, in English Metric Units (EMU) - """ - return self._element.y - - @top.setter - def top(self, value): - self._element.y = value - - @property - def width(self): - """ - Read/write. Integer distance between left and right extents of shape - in EMUs - """ - return self._element.cx - - @width.setter - def width(self, value): - self._element.cx = value - - -class _PlaceholderFormat(ElementProxy): - """ - Accessed via the :attr:`~.BaseShape.placeholder_format` property of - a placeholder shape, provides properties specific to placeholders, such - as the placeholder type. - """ - - @property - def element(self): - """ - The `p:ph` element proxied by this object. - """ - return super(_PlaceholderFormat, self).element - - @property - def idx(self): - """ - Integer placeholder 'idx' attribute. - """ - return self._element.idx - - @property - def type(self): - """ - Placeholder type, a member of the :ref:`PpPlaceholderType` - enumeration, e.g. PP_PLACEHOLDER.CHART - """ - return self._element.type diff --git a/pptx/shapes/freeform.py b/pptx/shapes/freeform.py deleted file mode 100644 index 0168b2baf..000000000 --- a/pptx/shapes/freeform.py +++ /dev/null @@ -1,311 +0,0 @@ -# encoding: utf-8 - -"""Objects related to construction of freeform shapes.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from pptx.compat import Sequence -from pptx.util import lazyproperty - - -class FreeformBuilder(Sequence): - """Allows a freeform shape to be specified and created. - - The initial pen position is provided on construction. From there, drawing - proceeds using successive calls to draw line segments. The freeform shape - may be closed by calling the :meth:`close` method. - - A shape may have more than one contour, in which case overlapping areas - are "subtracted". A contour is a sequence of line segments beginning with - a "move-to" operation. A move-to operation is automatically inserted in - each new freeform; additional move-to ops can be inserted with the - `.move_to()` method. - """ - - def __init__(self, shapes, start_x, start_y, x_scale, y_scale): - super(FreeformBuilder, self).__init__() - self._shapes = shapes - self._start_x = start_x - self._start_y = start_y - self._x_scale = x_scale - self._y_scale = y_scale - - def __getitem__(self, idx): - return self._drawing_operations.__getitem__(idx) - - def __iter__(self): - return self._drawing_operations.__iter__() - - def __len__(self): - return self._drawing_operations.__len__() - - @classmethod - def new(cls, shapes, start_x, start_y, x_scale, y_scale): - """Return a new |FreeformBuilder| object. - - The initial pen location is specified (in local coordinates) by - (*start_x*, *start_y*). - """ - return cls(shapes, int(round(start_x)), int(round(start_y)), x_scale, y_scale) - - def add_line_segments(self, vertices, close=True): - """Add a straight line segment to each point in *vertices*. - - *vertices* must be an iterable of (x, y) pairs (2-tuples). Each x and - y value is rounded to the nearest integer before use. The optional - *close* parameter determines whether the resulting contour is - *closed* or left *open*. - - Returns this |FreeformBuilder| object so it can be used in chained - calls. - """ - for x, y in vertices: - self._add_line_segment(x, y) - if close: - self._add_close() - return self - - def convert_to_shape(self, origin_x=0, origin_y=0): - """Return new freeform shape positioned relative to specified offset. - - *origin_x* and *origin_y* locate the origin of the local coordinate - system in slide coordinates (EMU), perhaps most conveniently by use - of a |Length| object. - - Note that this method may be called more than once to add multiple - shapes of the same geometry in different locations on the slide. - """ - sp = self._add_freeform_sp(origin_x, origin_y) - path = self._start_path(sp) - for drawing_operation in self: - drawing_operation.apply_operation_to(path) - return self._shapes._shape_factory(sp) - - def move_to(self, x, y): - """Move pen to (x, y) (local coordinates) without drawing line. - - Returns this |FreeformBuilder| object so it can be used in chained - calls. - """ - self._drawing_operations.append(_MoveTo.new(self, x, y)) - return self - - @property - def shape_offset_x(self): - """Return x distance of shape origin from local coordinate origin. - - The returned integer represents the leftmost extent of the freeform - shape, in local coordinates. Note that the bounding box of the shape - need not start at the local origin. - """ - min_x = self._start_x - for drawing_operation in self: - if hasattr(drawing_operation, "x"): - min_x = min(min_x, drawing_operation.x) - return min_x - - @property - def shape_offset_y(self): - """Return y distance of shape origin from local coordinate origin. - - The returned integer represents the topmost extent of the freeform - shape, in local coordinates. Note that the bounding box of the shape - need not start at the local origin. - """ - min_y = self._start_y - for drawing_operation in self: - if hasattr(drawing_operation, "y"): - min_y = min(min_y, drawing_operation.y) - return min_y - - def _add_close(self): - """Add a close |_Close| operation to the drawing sequence.""" - self._drawing_operations.append(_Close.new()) - - def _add_freeform_sp(self, origin_x, origin_y): - """Add a freeform `p:sp` element having no drawing elements. - - *origin_x* and *origin_y* are specified in slide coordinates, and - represent the location of the local coordinates origin on the slide. - """ - spTree = self._shapes._spTree - return spTree.add_freeform_sp( - origin_x + self._left, origin_y + self._top, self._width, self._height - ) - - def _add_line_segment(self, x, y): - """Add a |_LineSegment| operation to the drawing sequence.""" - self._drawing_operations.append(_LineSegment.new(self, x, y)) - - @lazyproperty - def _drawing_operations(self): - """Return the sequence of drawing operation objects for freeform.""" - return [] - - @property - def _dx(self): - """Return integer width of this shape's path in local units.""" - min_x = max_x = self._start_x - for drawing_operation in self: - if hasattr(drawing_operation, "x"): - min_x = min(min_x, drawing_operation.x) - max_x = max(max_x, drawing_operation.x) - return max_x - min_x - - @property - def _dy(self): - """Return integer height of this shape's path in local units.""" - min_y = max_y = self._start_y - for drawing_operation in self: - if hasattr(drawing_operation, "y"): - min_y = min(min_y, drawing_operation.y) - max_y = max(max_y, drawing_operation.y) - return max_y - min_y - - @property - def _height(self): - """Return vertical size of this shape's path in slide coordinates. - - This value is based on the actual extents of the shape and does not - include any positioning offset. - """ - return int(round(self._dy * self._y_scale)) - - @property - def _left(self): - """Return leftmost extent of this shape's path in slide coordinates. - - Note that this value does not include any positioning offset; it - assumes the drawing (local) coordinate origin is at (0, 0) on the - slide. - """ - return int(round(self.shape_offset_x * self._x_scale)) - - def _local_to_shape(self, local_x, local_y): - """Translate local coordinates point to shape coordinates. - - Shape coordinates have the same unit as local coordinates, but are - offset such that the origin of the shape coordinate system (0, 0) is - located at the top-left corner of the shape bounding box. - """ - return (local_x - self.shape_offset_x, local_y - self.shape_offset_y) - - def _start_path(self, sp): - """Return a newly created `a:path` element added to *sp*. - - The returned `a:path` element has an `a:moveTo` element representing - the shape starting point as its only child. - """ - path = sp.add_path(w=self._dx, h=self._dy) - path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y)) - return path - - @property - def _top(self): - """Return topmost extent of this shape's path in slide coordinates. - - Note that this value does not include any positioning offset; it - assumes the drawing (local) coordinate origin is located at slide - coordinates (0, 0) (top-left corner of slide). - """ - return int(round(self.shape_offset_y * self._y_scale)) - - @property - def _width(self): - """Return width of this shape's path in slide coordinates. - - This value is based on the actual extents of the shape path and does - not include any positioning offset. - """ - return int(round(self._dx * self._x_scale)) - - -class _BaseDrawingOperation(object): - """Base class for freeform drawing operations. - - A drawing operation has at least one location (x, y) in local - coordinates. - """ - - def __init__(self, freeform_builder, x, y): - super(_BaseDrawingOperation, self).__init__() - self._freeform_builder = freeform_builder - self._x = x - self._y = y - - def apply_operation_to(self, path): - """Add the XML element(s) implementing this operation to *path*. - - Must be implemented by each subclass. - """ - raise NotImplementedError("must be implemented by each subclass") - - @property - def x(self): - """Return the horizontal (x) target location of this operation. - - The returned value is an integer in local coordinates. - """ - return self._x - - @property - def y(self): - """Return the vertical (y) target location of this operation. - - The returned value is an integer in local coordinates. - """ - return self._y - - -class _Close(object): - """Specifies adding a `` element to the current contour.""" - - @classmethod - def new(cls): - """Return a new _Close object.""" - return cls() - - def apply_operation_to(self, path): - """Add `a:close` element to *path*.""" - return path.add_close() - - -class _LineSegment(_BaseDrawingOperation): - """Specifies a straight line segment ending at the specified point.""" - - @classmethod - def new(cls, freeform_builder, x, y): - """Return a new _LineSegment object ending at point *(x, y)*. - - Both *x* and *y* are rounded to the nearest integer before use. - """ - return cls(freeform_builder, int(round(x)), int(round(y))) - - def apply_operation_to(self, path): - """Add `a:lnTo` element to *path* for this line segment. - - Returns the `a:lnTo` element newly added to the path. - """ - return path.add_lnTo( - self._x - self._freeform_builder.shape_offset_x, - self._y - self._freeform_builder.shape_offset_y, - ) - - -class _MoveTo(_BaseDrawingOperation): - """Specifies a new pen position.""" - - @classmethod - def new(cls, freeform_builder, x, y): - """Return a new _MoveTo object for move to point *(x, y)*. - - Both *x* and *y* are rounded to the nearest integer before use. - """ - return cls(freeform_builder, int(round(x)), int(round(y))) - - def apply_operation_to(self, path): - """Add `a:moveTo` element to *path* for this line segment.""" - return path.add_moveTo( - self._x - self._freeform_builder.shape_offset_x, - self._y - self._freeform_builder.shape_offset_y, - ) diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py deleted file mode 100644 index 5837aa952..000000000 --- a/pptx/shapes/shapetree.py +++ /dev/null @@ -1,1135 +0,0 @@ -# encoding: utf-8 - -"""The shape tree, the structure that holds a slide's shapes.""" - -import os - -from pptx.compat import BytesIO -from pptx.enum.shapes import PP_PLACEHOLDER, PROG_ID -from pptx.media import SPEAKER_IMAGE_BYTES, Video -from pptx.opc.constants import CONTENT_TYPE as CT -from pptx.oxml.ns import qn -from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame -from pptx.oxml.shapes.picture import CT_Picture -from pptx.oxml.simpletypes import ST_Direction -from pptx.shapes.autoshape import AutoShapeType, Shape -from pptx.shapes.base import BaseShape -from pptx.shapes.connector import Connector -from pptx.shapes.freeform import FreeformBuilder -from pptx.shapes.graphfrm import GraphicFrame -from pptx.shapes.group import GroupShape -from pptx.shapes.picture import Movie, Picture -from pptx.shapes.placeholder import ( - ChartPlaceholder, - LayoutPlaceholder, - MasterPlaceholder, - NotesSlidePlaceholder, - PicturePlaceholder, - PlaceholderGraphicFrame, - PlaceholderPicture, - SlidePlaceholder, - TablePlaceholder, -) -from pptx.shared import ParentedElementProxy -from pptx.util import Emu, lazyproperty - -# +-- _BaseShapes -# | | -# | +-- _BaseGroupShapes -# | | | -# | | +-- GroupShapes -# | | | -# | | +-- SlideShapes -# | | -# | +-- LayoutShapes -# | | -# | +-- MasterShapes -# | | -# | +-- NotesSlideShapes -# | | -# | +-- BasePlaceholders -# | | -# | +-- LayoutPlaceholders -# | | -# | +-- MasterPlaceholders -# | | -# | +-- NotesSlidePlaceholders -# | -# +-- SlidePlaceholders - - -class _BaseShapes(ParentedElementProxy): - """ - Base class for a shape collection appearing in a slide-type object, - include Slide, SlideLayout, and SlideMaster, providing common methods. - """ - - def __init__(self, spTree, parent): - super(_BaseShapes, self).__init__(spTree, parent) - self._spTree = spTree - self._cached_max_shape_id = None - - def __getitem__(self, idx): - """ - Return shape at *idx* in sequence, e.g. ``shapes[2]``. - """ - shape_elms = list(self._iter_member_elms()) - try: - shape_elm = shape_elms[idx] - except IndexError: - raise IndexError("shape index out of range") - return self._shape_factory(shape_elm) - - def __iter__(self): - """ - Generate a reference to each shape in the collection, in sequence. - """ - for shape_elm in self._iter_member_elms(): - yield self._shape_factory(shape_elm) - - def __len__(self): - """ - Return count of shapes in this shape tree. A group shape contributes - 1 to the total, without regard to the number of shapes contained in - the group. - """ - shape_elms = list(self._iter_member_elms()) - return len(shape_elms) - - def clone_placeholder(self, placeholder): - """ - Add a new placeholder shape based on *placeholder*. - """ - sp = placeholder.element - ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx) - id_ = self._next_shape_id - name = self._next_ph_name(ph_type, id_, orient) - self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx) - - def ph_basename(self, ph_type): - """ - Return the base name for a placeholder of *ph_type* in this shape - collection. There is some variance between slide types, for example - a notes slide uses a different name for the body placeholder, so this - method can be overriden by subclasses. - """ - return { - PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder", - PP_PLACEHOLDER.BODY: "Text Placeholder", - PP_PLACEHOLDER.CENTER_TITLE: "Title", - PP_PLACEHOLDER.CHART: "Chart Placeholder", - PP_PLACEHOLDER.DATE: "Date Placeholder", - PP_PLACEHOLDER.FOOTER: "Footer Placeholder", - PP_PLACEHOLDER.HEADER: "Header Placeholder", - PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder", - PP_PLACEHOLDER.OBJECT: "Content Placeholder", - PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder", - PP_PLACEHOLDER.PICTURE: "Picture Placeholder", - PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder", - PP_PLACEHOLDER.SUBTITLE: "Subtitle", - PP_PLACEHOLDER.TABLE: "Table Placeholder", - PP_PLACEHOLDER.TITLE: "Title", - }[ph_type] - - @property - def turbo_add_enabled(self): - """True if "turbo-add" mode is enabled. Read/Write. - - EXPERIMENTAL: This feature can radically improve performance when - adding large numbers (hundreds of shapes) to a slide. It works by - caching the last shape ID used and incrementing that value to assign - the next shape id. This avoids repeatedly searching all shape ids in - the slide each time a new ID is required. - - Performance is not noticeably improved for a slide with a relatively - small number of shapes, but because the search time rises with the - square of the shape count, this option can be useful for optimizing - generation of a slide composed of many shapes. - - Shape-id collisions can occur (causing a repair error on load) if - more than one |Slide| object is used to interact with the same slide - in the presentation. Note that the |Slides| collection creates a new - |Slide| object each time a slide is accessed - (e.g. `slide = prs.slides[0]`, so you must be careful to limit use to - a single |Slide| object. - """ - return self._cached_max_shape_id is not None - - @turbo_add_enabled.setter - def turbo_add_enabled(self, value): - enable = bool(value) - self._cached_max_shape_id = self._spTree.max_shape_id if enable else None - - @staticmethod - def _is_member_elm(shape_elm): - """ - Return true if *shape_elm* represents a member of this collection, - False otherwise. - """ - return True - - def _iter_member_elms(self): - """ - Generate each child of the ```` element that corresponds to - a shape, in the sequence they appear in the XML. - """ - for shape_elm in self._spTree.iter_shape_elms(): - if self._is_member_elm(shape_elm): - yield shape_elm - - def _next_ph_name(self, ph_type, id, orient): - """ - Next unique placeholder name for placeholder shape of type *ph_type*, - with id number *id* and orientation *orient*. Usually will be standard - placeholder root name suffixed with id-1, e.g. - _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> - 'Table Placeholder 3'. The number is incremented as necessary to make - the name unique within the collection. If *orient* is ``'vert'``, the - placeholder name is prefixed with ``'Vertical '``. - """ - basename = self.ph_basename(ph_type) - - # prefix rootname with 'Vertical ' if orient is 'vert' - if orient == ST_Direction.VERT: - basename = "Vertical %s" % basename - - # increment numpart as necessary to make name unique - numpart = id - 1 - names = self._spTree.xpath("//p:cNvPr/@name") - while True: - name = "%s %d" % (basename, numpart) - if name not in names: - break - numpart += 1 - - return name - - @property - def _next_shape_id(self): - """Return a unique shape id suitable for use with a new shape. - - The returned id is 1 greater than the maximum shape id used so far. - In practice, the minimum id is 2 because the spTree element is always - assigned id="1". - """ - # ---presence of cached-max-shape-id indicates turbo mode is on--- - if self._cached_max_shape_id is not None: - self._cached_max_shape_id += 1 - return self._cached_max_shape_id - - return self._spTree.max_shape_id + 1 - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return BaseShapeFactory(shape_elm, self) - - -class _BaseGroupShapes(_BaseShapes): - """Base class for shape-trees that can add shapes.""" - - def __init__(self, grpSp, parent): - super(_BaseGroupShapes, self).__init__(grpSp, parent) - self._grpSp = grpSp - - def add_chart(self, chart_type, x, y, cx, cy, chart_data): - """Add a new chart of *chart_type* to the slide. - - The chart is positioned at (*x*, *y*), has size (*cx*, *cy*), and - depicts *chart_data*. *chart_type* is one of the :ref:`XlChartType` - enumeration values. *chart_data* is a |ChartData| object populated - with the categories and series values for the chart. - - Note that a |GraphicFrame| shape object is returned, not the |Chart| - object contained in that graphic frame shape. The chart object may be - accessed using the :attr:`chart` property of the returned - |GraphicFrame| object. - """ - rId = self.part.add_chart_part(chart_type, chart_data) - graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy) - self._recalculate_extents() - return self._shape_factory(graphicFrame) - - def add_connector(self, connector_type, begin_x, begin_y, end_x, end_y): - """Add a newly created connector shape to the end of this shape tree. - - *connector_type* is a member of the :ref:`MsoConnectorType` - enumeration and the end-point values are specified as EMU values. The - returned connector is of type *connector_type* and has begin and end - points as specified. - """ - cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y) - self._recalculate_extents() - return self._shape_factory(cxnSp) - - def add_group_shape(self, shapes=[]): - """Return a |GroupShape| object newly appended to this shape tree. - - The group shape is empty and must be populated with shapes using - methods on its shape tree, available on its `.shapes` property. The - position and extents of the group shape are determined by the shapes - it contains; its position and extents are recalculated each time - a shape is added to it. - """ - grpSp = self._element.add_grpSp() - for shape in shapes: - grpSp.insert_element_before(shape._element, "p:extLst") - if shapes: - grpSp.recalculate_extents() - return self._shape_factory(grpSp) - - def add_ole_object( - self, object_file, prog_id, left, top, width=None, height=None, icon_file=None - ): - """Return newly-created GraphicFrame shape embedding `object_file`. - - The returned graphic-frame shape contains `object_file` as an embedded OLE - object. It is displayed as an icon at `left`, `top` with size `width`, `height`. - `width` and `height` may be omitted when `prog_id` is a member of `PROG_ID`, in - which case the default icon size is used. This is advised for best appearance - where applicable because it avoids an icon with a "stretched" appearance. - - `object_file` may either be a str path to the file or a file-like - object (such as `io.BytesIO`) containing the bytes of the object file. - - `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value - like `"Adobe.Exchange.7"` determined by inspecting the XML generated by - PowerPoint for an object of the desired type. - - `icon_file` may either be a str path to an image file or a file-like object - containing the image. The image provided will be displayed in lieu of the OLE - object; double-clicking on the image opens the object (subject to - operating-system limitations). The image file can be any supported image file. - Those produced by PowerPoint itself are generally EMF and can be harvested from - a PPTX package that embeds such an object. PNG and JPG also work fine. - """ - graphicFrame = _OleObjectElementCreator.graphicFrame( - self, - self._next_shape_id, - object_file, - prog_id, - left, - top, - width, - height, - icon_file, - ) - self._spTree.append(graphicFrame) - self._recalculate_extents() - return self._shape_factory(graphicFrame) - - def add_picture(self, image_file, left, top, width=None, height=None): - """Add picture shape displaying image in *image_file*. - - *image_file* can be either a path to a file (a string) or a file-like - object. The picture is positioned with its top-left corner at (*top*, - *left*). If *width* and *height* are both |None|, the native size of - the image is used. If only one of *width* or *height* is used, the - unspecified dimension is calculated to preserve the aspect ratio of - the image. If both are specified, the picture is stretched to fit, - without regard to its native aspect ratio. - """ - image_part, rId = self.part.get_or_add_image_part(image_file) - pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height) - self._recalculate_extents() - return self._shape_factory(pic) - - def add_shape(self, autoshape_type_id, left, top, width, height): - """Return new |Shape| object appended to this shape tree. - - *autoshape_type_id* is a member of :ref:`MsoAutoShapeType` e.g. - ``MSO_SHAPE.RECTANGLE`` specifying the type of shape to be added. The - remaining arguments specify the new shape's position and size. - """ - autoshape_type = AutoShapeType(autoshape_type_id) - sp = self._add_sp(autoshape_type, left, top, width, height) - self._recalculate_extents() - return self._shape_factory(sp) - - def add_textbox(self, left, top, width, height): - """Return newly added text box shape appended to this shape tree. - - The text box is of the specified size, located at the specified - position on the slide. - """ - sp = self._add_textbox_sp(left, top, width, height) - self._recalculate_extents() - return self._shape_factory(sp) - - def build_freeform(self, start_x=0, start_y=0, scale=1.0): - """Return |FreeformBuilder| object to specify a freeform shape. - - The optional *start_x* and *start_y* arguments specify the starting - pen position in local coordinates. They will be rounded to the - nearest integer before use and each default to zero. - - The optional *scale* argument specifies the size of local coordinates - proportional to slide coordinates (EMU). If the vertical scale is - different than the horizontal scale (local coordinate units are - "rectangular"), a pair of numeric values can be provided as the - *scale* argument, e.g. `scale=(1.0, 2.0)`. In this case the first - number is interpreted as the horizontal (X) scale and the second as - the vertical (Y) scale. - - A convenient method for calculating scale is to divide a |Length| - object by an equivalent count of local coordinate units, e.g. - `scale = Inches(1)/1000` for 1000 local units per inch. - """ - try: - x_scale, y_scale = scale - except TypeError: - x_scale = y_scale = scale - - return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale) - - def index(self, shape): - """Return the index of *shape* in this sequence. - - Raises |ValueError| if *shape* is not in the collection. - """ - shape_elms = list(self._element.iter_shape_elms()) - return shape_elms.index(shape.element) - - def _add_chart_graphicFrame(self, rId, x, y, cx, cy): - """Return new `p:graphicFrame` element appended to this shape tree. - - The `p:graphicFrame` element has the specified position and size and - refers to the chart part identified by *rId*. - """ - shape_id = self._next_shape_id - name = "Chart %d" % (shape_id - 1) - graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame( - shape_id, name, rId, x, y, cx, cy - ) - self._spTree.append(graphicFrame) - return graphicFrame - - def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, end_y): - """Return a newly-added `p:cxnSp` element as specified. - - The `p:cxnSp` element is for a connector of *connector_type* - beginning at (*begin_x*, *begin_y*) and extending to - (*end_x*, *end_y*). - """ - id_ = self._next_shape_id - name = "Connector %d" % (id_ - 1) - - flipH, flipV = begin_x > end_x, begin_y > end_y - x, y = min(begin_x, end_x), min(begin_y, end_y) - cx, cy = abs(end_x - begin_x), abs(end_y - begin_y) - - return self._element.add_cxnSp( - id_, name, connector_type, x, y, cx, cy, flipH, flipV - ) - - def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy): - """Return a newly appended `p:pic` element as specified. - - The `p:pic` element displays the image in *image_part* with size and - position specified by *x*, *y*, *cx*, and *cy*. The element is - appended to the shape tree, causing it to be displayed first in - z-order on the slide. - """ - id_ = self._next_shape_id - scaled_cx, scaled_cy = image_part.scale(cx, cy) - name = "Picture %d" % (id_ - 1) - desc = image_part.desc - pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy) - return pic - - def _add_sp(self, autoshape_type, x, y, cx, cy): - """Return newly-added `p:sp` element as specified. - - `p:sp` element is of *autoshape_type* at position (*x*, *y*) and of - size (*cx*, *cy*). - """ - id_ = self._next_shape_id - name = "%s %d" % (autoshape_type.basename, id_ - 1) - sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy) - return sp - - def _add_textbox_sp(self, x, y, cx, cy): - """Return newly-appended textbox `p:sp` element. - - Element has position (*x*, *y*) and size (*cx*, *cy*). - """ - id_ = self._next_shape_id - name = "TextBox %d" % (id_ - 1) - sp = self._spTree.add_textbox(id_, name, x, y, cx, cy) - return sp - - def _recalculate_extents(self): - """Adjust position and size to incorporate all contained shapes. - - This would typically be called when a contained shape is added, - removed, or its position or size updated. - """ - # ---default behavior is to do nothing, GroupShapes overrides to - # produce the distinctive behavior of groups and subgroups.--- - pass - - -class GroupShapes(_BaseGroupShapes): - """The sequence of child shapes belonging to a group shape. - - Note that this collection can itself contain a group shape, making this - part of a recursive, tree data structure (acyclic graph). - """ - - def _recalculate_extents(self): - """Adjust position and size to incorporate all contained shapes. - - This would typically be called when a contained shape is added, - removed, or its position or size updated. - """ - self._grpSp.recalculate_extents() - - -class SlideShapes(_BaseGroupShapes): - """Sequence of shapes appearing on a slide. - - The first shape in the sequence is the backmost in z-order and the last - shape is topmost. Supports indexed access, len(), index(), and iteration. - """ - - def add_movie( - self, - movie_file, - left, - top, - width, - height, - poster_frame_image=None, - mime_type=CT.VIDEO, - ): - """Return newly added movie shape displaying video in *movie_file*. - - **EXPERIMENTAL.** This method has important limitations: - - * The size must be specified; no auto-scaling such as that provided - by :meth:`add_picture` is performed. - * The MIME type of the video file should be specified, e.g. - 'video/mp4'. The provided video file is not interrogated for its - type. The MIME type `video/unknown` is used by default (and works - fine in tests as of this writing). - * A poster frame image must be provided, it cannot be automatically - extracted from the video file. If no poster frame is provided, the - default "media loudspeaker" image will be used. - - Return a newly added movie shape to the slide, positioned at (*left*, - *top*), having size (*width*, *height*), and containing *movie_file*. - Before the video is started, *poster_frame_image* is displayed as - a placeholder for the video. - """ - movie_pic = _MoviePicElementCreator.new_movie_pic( - self, - self._next_shape_id, - movie_file, - left, - top, - width, - height, - poster_frame_image, - mime_type, - ) - self._spTree.append(movie_pic) - self._add_video_timing(movie_pic) - return self._shape_factory(movie_pic) - - def add_table(self, rows, cols, left, top, width, height): - """ - Add a |GraphicFrame| object containing a table with the specified - number of *rows* and *cols* and the specified position and size. - *width* is evenly distributed between the columns of the new table. - Likewise, *height* is evenly distributed between the rows. Note that - the ``.table`` property on the returned |GraphicFrame| shape must be - used to access the enclosed |Table| object. - """ - graphicFrame = self._add_graphicFrame_containing_table( - rows, cols, left, top, width, height - ) - graphic_frame = self._shape_factory(graphicFrame) - return graphic_frame - - def clone_layout_placeholders(self, slide_layout): - """ - Add placeholder shapes based on those in *slide_layout*. Z-order of - placeholders is preserved. Latent placeholders (date, slide number, - and footer) are not cloned. - """ - for placeholder in slide_layout.iter_cloneable_placeholders(): - self.clone_placeholder(placeholder) - - @property - def placeholders(self): - """ - Instance of |SlidePlaceholders| containing sequence of placeholder - shapes in this slide. - """ - return self.parent.placeholders - - @property - def title(self): - """ - The title placeholder shape on the slide or |None| if the slide has - no title placeholder. - """ - for elm in self._spTree.iter_ph_elms(): - if elm.ph_idx == 0: - return self._shape_factory(elm) - return None - - def _add_graphicFrame_containing_table(self, rows, cols, x, y, cx, cy): - """ - Return a newly added ```` element containing a table - as specified by the parameters. - """ - _id = self._next_shape_id - name = "Table %d" % (_id - 1) - graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy) - return graphicFrame - - def _add_video_timing(self, pic): - """Add a `p:video` element under `p:sld/p:timing`. - - The element will refer to the specified *pic* element by its shape - id, and cause the video play controls to appear for that video. - """ - sld = self._spTree.xpath("/p:sld")[0] - childTnLst = sld.get_or_add_childTnLst() - childTnLst.add_video(pic.shape_id) - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return SlideShapeFactory(shape_elm, self) - - -class LayoutShapes(_BaseShapes): - """ - Sequence of shapes appearing on a slide layout. The first shape in the - sequence is the backmost in z-order and the last shape is topmost. - Supports indexed access, len(), index(), and iteration. - """ - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return _LayoutShapeFactory(shape_elm, self) - - -class MasterShapes(_BaseShapes): - """ - Sequence of shapes appearing on a slide master. The first shape in the - sequence is the backmost in z-order and the last shape is topmost. - Supports indexed access, len(), and iteration. - """ - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return _MasterShapeFactory(shape_elm, self) - - -class NotesSlideShapes(_BaseShapes): - """ - Sequence of shapes appearing on a notes slide. The first shape in the - sequence is the backmost in z-order and the last shape is topmost. - Supports indexed access, len(), index(), and iteration. - """ - - def ph_basename(self, ph_type): - """ - Return the base name for a placeholder of *ph_type* in this shape - collection. A notes slide uses a different name for the body - placeholder and has some unique placeholder types, so this - method overrides the default in the base class. - """ - return { - PP_PLACEHOLDER.BODY: "Notes Placeholder", - PP_PLACEHOLDER.DATE: "Date Placeholder", - PP_PLACEHOLDER.FOOTER: "Footer Placeholder", - PP_PLACEHOLDER.HEADER: "Header Placeholder", - PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder", - PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder", - }[ph_type] - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm* appearing on a notes slide. - """ - return _NotesSlideShapeFactory(shape_elm, self) - - -class BasePlaceholders(_BaseShapes): - """ - Base class for placeholder collections that differentiate behaviors for - a master, layout, and slide. By default, placeholder shapes are - constructed using |BaseShapeFactory|. Subclasses should override - :method:`_shape_factory` to use custom placeholder classes. - """ - - @staticmethod - def _is_member_elm(shape_elm): - """ - True if *shape_elm* is a placeholder shape, False otherwise. - """ - return shape_elm.has_ph_elm - - -class LayoutPlaceholders(BasePlaceholders): - """ - Sequence of |LayoutPlaceholder| instances representing the placeholder - shapes on a slide layout. - """ - - def get(self, idx, default=None): - """ - Return the first placeholder shape with matching *idx* value, or - *default* if not found. - """ - for placeholder in self: - if placeholder.element.ph_idx == idx: - return placeholder - return default - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return _LayoutShapeFactory(shape_elm, self) - - -class MasterPlaceholders(BasePlaceholders): - """ - Sequence of _MasterPlaceholder instances representing the placeholder - shapes on a slide master. - """ - - def get(self, ph_type, default=None): - """ - Return the first placeholder shape with type *ph_type* (e.g. 'body'), - or *default* if no such placeholder shape is present in the - collection. - """ - for placeholder in self: - if placeholder.ph_type == ph_type: - return placeholder - return default - - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return _MasterShapeFactory(shape_elm, self) - - -class NotesSlidePlaceholders(MasterPlaceholders): - """ - Sequence of placeholder shapes on a notes slide. - """ - - def _shape_factory(self, placeholder_elm): - """ - Return an instance of the appropriate placeholder proxy class for - *placeholder_elm*. - """ - return _NotesSlideShapeFactory(placeholder_elm, self) - - -class SlidePlaceholders(ParentedElementProxy): - """ - Collection of placeholder shapes on a slide. Supports iteration, - :func:`len`, and dictionary-style lookup on the `idx` value of the - placeholders it contains. - """ - - __slots__ = () - - def __getitem__(self, idx): - """ - Access placeholder shape having *idx*. Note that while this looks - like list access, idx is actually a dictionary key and will raise - |KeyError| if no placeholder with that idx value is in the - collection. - """ - for e in self._element.iter_ph_elms(): - if e.ph_idx == idx: - return SlideShapeFactory(e, self) - raise KeyError("no placeholder on this slide with idx == %d" % idx) - - def __iter__(self): - """ - Generate placeholder shapes in `idx` order. - """ - ph_elms = sorted( - [e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx - ) - return (SlideShapeFactory(e, self) for e in ph_elms) - - def __len__(self): - """ - Return count of placeholder shapes. - """ - return len(list(self._element.iter_ph_elms())) - - -def BaseShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm*. - """ - tag = shape_elm.tag - - if tag == qn("p:pic"): - videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile") - if videoFiles: - return Movie(shape_elm, parent) - return Picture(shape_elm, parent) - - shape_cls = { - qn("p:cxnSp"): Connector, - qn("p:grpSp"): GroupShape, - qn("p:sp"): Shape, - qn("p:graphicFrame"): GraphicFrame, - }.get(tag, BaseShape) - - return shape_cls(shape_elm, parent) - - -def _LayoutShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide layout. - """ - tag_name = shape_elm.tag - if tag_name == qn("p:sp") and shape_elm.has_ph_elm: - return LayoutPlaceholder(shape_elm, parent) - return BaseShapeFactory(shape_elm, parent) - - -def _MasterShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide master. - """ - tag_name = shape_elm.tag - if tag_name == qn("p:sp") and shape_elm.has_ph_elm: - return MasterPlaceholder(shape_elm, parent) - return BaseShapeFactory(shape_elm, parent) - - -def _NotesSlideShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a notes slide. - """ - tag_name = shape_elm.tag - if tag_name == qn("p:sp") and shape_elm.has_ph_elm: - return NotesSlidePlaceholder(shape_elm, parent) - return BaseShapeFactory(shape_elm, parent) - - -def _SlidePlaceholderFactory(shape_elm, parent): - """ - Return a placeholder shape of the appropriate type for *shape_elm*. - """ - tag = shape_elm.tag - if tag == qn("p:sp"): - Constructor = { - PP_PLACEHOLDER.BITMAP: PicturePlaceholder, - PP_PLACEHOLDER.CHART: ChartPlaceholder, - PP_PLACEHOLDER.PICTURE: PicturePlaceholder, - PP_PLACEHOLDER.TABLE: TablePlaceholder, - }.get(shape_elm.ph_type, SlidePlaceholder) - elif tag == qn("p:graphicFrame"): - Constructor = PlaceholderGraphicFrame - elif tag == qn("p:pic"): - Constructor = PlaceholderPicture - else: - Constructor = BaseShapeFactory - return Constructor(shape_elm, parent) - - -def SlideShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide. - """ - if shape_elm.has_ph_elm: - return _SlidePlaceholderFactory(shape_elm, parent) - return BaseShapeFactory(shape_elm, parent) - - -class _MoviePicElementCreator(object): - """Functional service object for creating a new movie p:pic element. - - It's entire external interface is its :meth:`new_movie_pic` class method - that returns a new `p:pic` element containing the specified video. This - class is not intended to be constructed or an instance of it retained by - the caller; it is a "one-shot" object, really a function wrapped in - a object such that its helper methods can be organized here. - """ - - def __init__( - self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file, mime_type - ): - super(_MoviePicElementCreator, self).__init__() - self._shapes = shapes - self._shape_id = shape_id - self._movie_file = movie_file - self._x, self._y, self._cx, self._cy = x, y, cx, cy - self._poster_frame_file = poster_frame_file - self._mime_type = mime_type - - @classmethod - def new_movie_pic( - cls, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type - ): - """Return a new `p:pic` element containing video in *movie_file*. - - If *mime_type* is None, 'video/unknown' is used. If - *poster_frame_file* is None, the default "media loudspeaker" image is - used. - """ - return cls( - shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type - )._pic - return - - @property - def _media_rId(self): - """Return the rId of RT.MEDIA relationship to video part. - - For historical reasons, there are two relationships to the same part; - one is the video rId and the other is the media rId. - """ - return self._video_part_rIds[0] - - @lazyproperty - def _pic(self): - """Return the new `p:pic` element referencing the video.""" - return CT_Picture.new_video_pic( - self._shape_id, - self._shape_name, - self._video_rId, - self._media_rId, - self._poster_frame_rId, - self._x, - self._y, - self._cx, - self._cy, - ) - - @lazyproperty - def _poster_frame_image_file(self): - """Return the image file for video placeholder image. - - If no poster frame file is provided, the default "media loudspeaker" - image is used. - """ - poster_frame_file = self._poster_frame_file - if poster_frame_file is None: - return BytesIO(SPEAKER_IMAGE_BYTES) - return poster_frame_file - - @lazyproperty - def _poster_frame_rId(self): - """Return the rId of relationship to poster frame image. - - The poster frame is the image used to represent the video before it's - played. - """ - _, poster_frame_rId = self._slide_part.get_or_add_image_part( - self._poster_frame_image_file - ) - return poster_frame_rId - - @property - def _shape_name(self): - """Return the appropriate shape name for the p:pic shape. - - A movie shape is named with the base filename of the video. - """ - return self._video.filename - - @property - def _slide_part(self): - """Return SlidePart object for slide containing this movie.""" - return self._shapes.part - - @lazyproperty - def _video(self): - """Return a |Video| object containing the movie file.""" - return Video.from_path_or_file_like(self._movie_file, self._mime_type) - - @lazyproperty - def _video_part_rIds(self): - """Return the rIds for relationships to media part for video. - - This is where the media part and its relationships to the slide are - actually created. - """ - media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video) - return media_rId, video_rId - - @property - def _video_rId(self): - """Return the rId of RT.VIDEO relationship to video part. - - For historical reasons, there are two relationships to the same part; - one is the video rId and the other is the media rId. - """ - return self._video_part_rIds[1] - - -class _OleObjectElementCreator(object): - """Functional service object for creating a new OLE-object p:graphicFrame element. - - It's entire external interface is its :meth:`graphicFrame` class method that returns - a new `p:graphicFrame` element containing the specified embedded OLE-object shape. - This class is not intended to be constructed or an instance of it retained by the - caller; it is a "one-shot" object, really a function wrapped in a object such that - its helper methods can be organized here. - """ - - def __init__( - self, shapes, shape_id, ole_object_file, prog_id, x, y, cx, cy, icon_file - ): - self._shapes = shapes - self._shape_id = shape_id - self._ole_object_file = ole_object_file - self._prog_id_arg = prog_id - self._x = x - self._y = y - self._cx_arg = cx - self._cy_arg = cy - self._icon_file_arg = icon_file - - @classmethod - def graphicFrame( - cls, shapes, shape_id, ole_object_file, prog_id, x, y, cx, cy, icon_file - ): - """Return new `p:graphicFrame` element containing embedded `ole_object_file`.""" - return cls( - shapes, shape_id, ole_object_file, prog_id, x, y, cx, cy, icon_file - )._graphicFrame - - @lazyproperty - def _graphicFrame(self): - """Newly-created `p:graphicFrame` element referencing embedded OLE-object.""" - return CT_GraphicalObjectFrame.new_ole_object_graphicFrame( - self._shape_id, - self._shape_name, - self._ole_object_rId, - self._progId, - self._icon_rId, - self._x, - self._y, - self._cx, - self._cy, - ) - - @lazyproperty - def _cx(self): - """Emu object specifying width of "show-as-icon" image for OLE shape.""" - # --- a user-specified width overrides any default --- - if self._cx_arg is not None: - return self._cx_arg - - # --- the default width is specified by the PROG_ID member if prog_id is one, - # --- otherwise it gets the default icon width. - return ( - Emu(self._prog_id_arg.width) - if self._prog_id_arg in PROG_ID - else Emu(965200) - ) - - @lazyproperty - def _cy(self): - """Emu object specifying height of "show-as-icon" image for OLE shape.""" - # --- a user-specified width overrides any default --- - if self._cy_arg is not None: - return self._cy_arg - - # --- the default height is specified by the PROG_ID member if prog_id is one, - # --- otherwise it gets the default icon height. - return ( - Emu(self._prog_id_arg.height) - if self._prog_id_arg in PROG_ID - else Emu(609600) - ) - - @lazyproperty - def _icon_image_file(self): - """Reference to image file containing icon to show in lieu of this object. - - This can be either a str path or a file-like object (io.BytesIO typically). - """ - # --- a user-specified icon overrides any default --- - if self._icon_file_arg is not None: - return self._icon_file_arg - - # --- A prog_id belonging to PROG_ID gets its icon filename from there. A - # --- user-specified (str) prog_id gets the default icon. - icon_filename = ( - self._prog_id_arg.icon_filename - if self._prog_id_arg in PROG_ID - else "generic-icon.emf" - ) - - _thisdir = os.path.split(__file__)[0] - return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename)) - - @lazyproperty - def _icon_rId(self): - """str rId like "rId7" of rel to icon (image) representing OLE-object part.""" - _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file) - return rId - - @lazyproperty - def _ole_object_rId(self): - """str rId like "rId6" of relationship to embedded ole_object part. - - This is where the ole_object part and its relationship to the slide are actually - created. - """ - return self._slide_part.add_embedded_ole_object_part( - self._prog_id_arg, self._ole_object_file - ) - - @lazyproperty - def _progId(self): - """str like "Excel.Sheet.12" identifying program used to open object. - - This value appears in the `progId` attribute of the `p:oleObj` element for the - object. - """ - prog_id_arg = self._prog_id_arg - - # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller - # --- has specified it explicitly (as str) - return prog_id_arg.progId if prog_id_arg in PROG_ID else prog_id_arg - - @lazyproperty - def _shape_name(self): - """str name like "Object 1" for the embedded ole_object shape. - - The name is formed from the prefix "Object " and the shape-id decremented by 1. - """ - return "Object %d" % (self._shape_id - 1) - - @lazyproperty - def _slide_part(self): - """SlidePart object for this slide.""" - return self._shapes.part diff --git a/pptx/shared.py b/pptx/shared.py deleted file mode 100644 index 27dbba5de..000000000 --- a/pptx/shared.py +++ /dev/null @@ -1,95 +0,0 @@ -# encoding: utf-8 - -""" -Objects shared by pptx modules. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - - -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-pptx other than custom element (oxml) classes. - """ - - __slots__ = ("_element",) - - def __init__(self, element): - self._element = element - - def __eq__(self, other): - """ - Return |True| if this proxy object refers to the same oxml element as - does *other*. ElementProxy objects are value objects and should - maintain no mutable local state. Equality for proxy objects is - defined as referring to the same XML element, whether or not they are - the same proxy object instance. - """ - if not isinstance(other, ElementProxy): - return False - return self._element is other._element - - def __ne__(self, other): - if not isinstance(other, ElementProxy): - return True - return self._element is not other._element - - @property - def element(self): - """ - The lxml element proxied by this object. - """ - return self._element - - -class ParentedElementProxy(ElementProxy): - """ - 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 the :attr:`_parent` - attribute to subclasses and the public :attr:`parent` read-only property. - """ - - __slots__ = ("_parent",) - - def __init__(self, element, parent): - super(ParentedElementProxy, self).__init__(element) - self._parent = parent - - @property - def parent(self): - """ - The ancestor proxy object to this one. For example, the parent of - a shape is generally the |SlideShapes| object that contains it. - """ - return self._parent - - @property - def part(self): - """ - The package part containing this object - """ - return self._parent.part - - -class PartElementProxy(ElementProxy): - """ - Provides common members for proxy objects that wrap the root element of - a part such as `p:sld`. - """ - - __slots__ = ("_part",) - - def __init__(self, element, part): - super(PartElementProxy, self).__init__(element) - self._part = part - - @property - def part(self): - """ - The package part containing this object - """ - return self._part diff --git a/pptx/table.py b/pptx/table.py deleted file mode 100644 index 2de17175f..000000000 --- a/pptx/table.py +++ /dev/null @@ -1,523 +0,0 @@ -# encoding: utf-8 - -"""Table-related objects such as Table and Cell.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from pptx.compat import is_integer -from pptx.dml.fill import FillFormat -from pptx.oxml.table import TcRange -from pptx.shapes import Subshape -from pptx.text.text import TextFrame -from pptx.util import lazyproperty - - -class Table(object): - """A DrawingML table object. - - Not intended to be constructed directly, use - :meth:`.Slide.shapes.add_table` to add a table to a slide. - """ - - def __init__(self, tbl, graphic_frame): - super(Table, self).__init__() - self._tbl = tbl - self._graphic_frame = graphic_frame - - def cell(self, row_idx, col_idx): - """Return cell at *row_idx*, *col_idx*. - - Return value is an instance of |_Cell|. *row_idx* and *col_idx* are - zero-based, e.g. cell(0, 0) is the top, left cell in the table. - """ - return _Cell(self._tbl.tc(row_idx, col_idx), self) - - @lazyproperty - def columns(self): - """ - Read-only reference to collection of |_Column| objects representing - the table's columns. |_Column| objects are accessed using list - notation, e.g. ``col = tbl.columns[0]``. - """ - return _ColumnCollection(self._tbl, self) - - @property - def first_col(self): - """ - Read/write boolean property which, when true, indicates the first - column should be formatted differently, as for a side-heading column - at the far left of the table. - """ - return self._tbl.firstCol - - @first_col.setter - def first_col(self, value): - self._tbl.firstCol = value - - @property - def first_row(self): - """ - Read/write boolean property which, when true, indicates the first - row should be formatted differently, e.g. for column headings. - """ - return self._tbl.firstRow - - @first_row.setter - def first_row(self, value): - self._tbl.firstRow = value - - @property - def horz_banding(self): - """ - Read/write boolean property which, when true, indicates the rows of - the table should appear with alternating shading. - """ - return self._tbl.bandRow - - @horz_banding.setter - def horz_banding(self, value): - self._tbl.bandRow = value - - def iter_cells(self): - """Generate _Cell object for each cell in this table. - - Each grid cell is generated in left-to-right, top-to-bottom order. - """ - return (_Cell(tc, self) for tc in self._tbl.iter_tcs()) - - @property - def last_col(self): - """ - Read/write boolean property which, when true, indicates the last - column should be formatted differently, as for a row totals column at - the far right of the table. - """ - return self._tbl.lastCol - - @last_col.setter - def last_col(self, value): - self._tbl.lastCol = value - - @property - def last_row(self): - """ - Read/write boolean property which, when true, indicates the last - row should be formatted differently, as for a totals row at the - bottom of the table. - """ - return self._tbl.lastRow - - @last_row.setter - def last_row(self, value): - self._tbl.lastRow = value - - def notify_height_changed(self): - """ - Called by a row when its height changes, triggering the graphic frame - to recalculate its total height (as the sum of the row heights). - """ - new_table_height = sum([row.height for row in self.rows]) - self._graphic_frame.height = new_table_height - - def notify_width_changed(self): - """ - Called by a column when its width changes, triggering the graphic - frame to recalculate its total width (as the sum of the column - widths). - """ - new_table_width = sum([col.width for col in self.columns]) - self._graphic_frame.width = new_table_width - - @property - def part(self): - """ - The package part containing this table. - """ - return self._graphic_frame.part - - @lazyproperty - def rows(self): - """ - Read-only reference to collection of |_Row| objects representing the - table's rows. |_Row| objects are accessed using list notation, e.g. - ``col = tbl.rows[0]``. - """ - return _RowCollection(self._tbl, self) - - @property - def vert_banding(self): - """ - Read/write boolean property which, when true, indicates the columns - of the table should appear with alternating shading. - """ - return self._tbl.bandCol - - @vert_banding.setter - def vert_banding(self, value): - self._tbl.bandCol = value - - -class _Cell(Subshape): - """Table cell""" - - def __init__(self, tc, parent): - super(_Cell, self).__init__(parent) - self._tc = tc - - def __eq__(self, other): - """|True| if this object proxies the same element as *other*. - - 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, type(self)): - return False - return self._tc is other._tc - - def __ne__(self, other): - if not isinstance(other, type(self)): - return True - return self._tc is not other._tc - - @lazyproperty - def fill(self): - """ - |FillFormat| instance for this cell, providing access to fill - properties such as foreground color. - """ - tcPr = self._tc.get_or_add_tcPr() - return FillFormat.from_fill_parent(tcPr) - - @property - def is_merge_origin(self): - """True if this cell is the top-left grid cell in a merged cell.""" - return self._tc.is_merge_origin - - @property - def is_spanned(self): - """True if this cell is spanned by a merge-origin cell. - - A merge-origin cell "spans" the other grid cells in its merge range, - consuming their area and "shadowing" the spanned grid cells. - - Note this value is |False| for a merge-origin cell. A merge-origin - cell spans other grid cells, but is not itself a spanned cell. - """ - return self._tc.is_spanned - - @property - def margin_left(self): - """ - Read/write integer value of left margin of cell as a |Length| value - object. If assigned |None|, the default value is used, 0.1 inches for - left and right margins and 0.05 inches for top and bottom. - """ - return self._tc.marL - - @margin_left.setter - def margin_left(self, margin_left): - self._validate_margin_value(margin_left) - self._tc.marL = margin_left - - @property - def margin_right(self): - """ - Right margin of cell. - """ - return self._tc.marR - - @margin_right.setter - def margin_right(self, margin_right): - self._validate_margin_value(margin_right) - self._tc.marR = margin_right - - @property - def margin_top(self): - """ - Top margin of cell. - """ - return self._tc.marT - - @margin_top.setter - def margin_top(self, margin_top): - self._validate_margin_value(margin_top) - self._tc.marT = margin_top - - @property - def margin_bottom(self): - """ - Bottom margin of cell. - """ - return self._tc.marB - - @margin_bottom.setter - def margin_bottom(self, margin_bottom): - self._validate_margin_value(margin_bottom) - self._tc.marB = margin_bottom - - def merge(self, other_cell): - """Create merged cell from this cell to *other_cell*. - - This cell and *other_cell* specify opposite corners of the merged - cell range. Either diagonal of the cell region may be specified in - either order, e.g. self=bottom-right, other_cell=top-left, etc. - - Raises |ValueError| if the specified range already contains merged - cells anywhere within its extents or if *other_cell* is not in the - same table as *self*. - """ - tc_range = TcRange(self._tc, other_cell._tc) - - if not tc_range.in_same_table: - raise ValueError("other_cell from different table") - if tc_range.contains_merged_cell: - raise ValueError("range contains one or more merged cells") - - tc_range.move_content_to_origin() - - row_count, col_count = tc_range.dimensions - - for tc in tc_range.iter_top_row_tcs(): - tc.rowSpan = row_count - for tc in tc_range.iter_left_col_tcs(): - tc.gridSpan = col_count - for tc in tc_range.iter_except_left_col_tcs(): - tc.hMerge = True - for tc in tc_range.iter_except_top_row_tcs(): - tc.vMerge = True - - @property - def span_height(self): - """int count of rows spanned by this cell. - - The value of this property may be misleading (often 1) on cells where - `.is_merge_origin` is not |True|, since only a merge-origin cell - contains complete span information. This property is only intended - for use on cells known to be a merge origin by testing - `.is_merge_origin`. - """ - return self._tc.rowSpan - - @property - def span_width(self): - """int count of columns spanned by this cell. - - The value of this property may be misleading (often 1) on cells where - `.is_merge_origin` is not |True|, since only a merge-origin cell - contains complete span information. This property is only intended - for use on cells known to be a merge origin by testing - `.is_merge_origin`. - """ - return self._tc.gridSpan - - def split(self): - """Remove merge from this (merge-origin) cell. - - The merged cell represented by this object will be "unmerged", - yielding a separate unmerged cell for each grid cell previously - spanned by this merge. - - Raises |ValueError| when this cell is not a merge-origin cell. Test - with `.is_merge_origin` before calling. - """ - if not self.is_merge_origin: - raise ValueError( - "not a merge-origin cell; only a merge-origin cell can be sp" "lit" - ) - - tc_range = TcRange.from_merge_origin(self._tc) - - for tc in tc_range.iter_tcs(): - tc.rowSpan = tc.gridSpan = 1 - tc.hMerge = tc.vMerge = False - - @property - def text(self): - """Unicode (str in Python 3) representation of cell contents. - - The returned string will contain a newline character (``"\\n"``) separating each - paragraph and a vertical-tab (``"\\v"``) character for each line break (soft - carriage return) in the cell's text. - - Assignment to *text* replaces all text currently contained in the cell. A - newline character (``"\\n"``) in the assigned text causes a new paragraph to be - started. A vertical-tab (``"\\v"``) character in the assigned text causes - a line-break (soft carriage-return) to be inserted. (The vertical-tab character - appears in clipboard text copied from PowerPoint as its encoding of - line-breaks.) - - Either bytes (Python 2 str) or unicode (Python 3 str) can be assigned. Bytes can - be 7-bit ASCII or UTF-8 encoded 8-bit bytes. Bytes values are converted to - unicode assuming UTF-8 encoding (which correctly decodes ASCII). - """ - return self.text_frame.text - - @text.setter - def text(self, text): - self.text_frame.text = text - - @property - def text_frame(self): - """ - |TextFrame| instance containing the text that appears in the cell. - """ - txBody = self._tc.get_or_add_txBody() - return TextFrame(txBody, self) - - @property - def vertical_anchor(self): - """Vertical alignment of this cell. - - This value is a member of the :ref:`MsoVerticalAnchor` enumeration or - |None|. A value of |None| indicates the cell has no explicitly - applied vertical anchor setting and its effective value is inherited - from its style-hierarchy ancestors. - - Assigning |None| to this property causes any explicitly applied - vertical anchor setting to be cleared and inheritance of its - effective value to be restored. - """ - return self._tc.anchor - - @vertical_anchor.setter - def vertical_anchor(self, mso_anchor_idx): - self._tc.anchor = mso_anchor_idx - - @staticmethod - def _validate_margin_value(margin_value): - """ - Raise ValueError if *margin_value* is not a positive integer value or - |None|. - """ - if not is_integer(margin_value) and margin_value is not None: - tmpl = "margin value must be integer or None, got '%s'" - raise TypeError(tmpl % margin_value) - - -class _Column(Subshape): - """Table column""" - - def __init__(self, gridCol, parent): - super(_Column, self).__init__(parent) - self._gridCol = gridCol - - @property - def width(self): - """ - Width of column in EMU. - """ - return self._gridCol.w - - @width.setter - def width(self, width): - self._gridCol.w = width - self._parent.notify_width_changed() - - -class _Row(Subshape): - """Table row""" - - def __init__(self, tr, parent): - super(_Row, self).__init__(parent) - self._tr = tr - - @property - def cells(self): - """ - Read-only reference to collection of cells in row. An individual cell - is referenced using list notation, e.g. ``cell = row.cells[0]``. - """ - return _CellCollection(self._tr, self) - - @property - def height(self): - """ - Height of row in EMU. - """ - return self._tr.h - - @height.setter - def height(self, height): - self._tr.h = height - self._parent.notify_height_changed() - - -class _CellCollection(Subshape): - """Horizontal sequence of row cells""" - - def __init__(self, tr, parent): - super(_CellCollection, self).__init__(parent) - self._tr = tr - - def __getitem__(self, idx): - """Provides indexed access, (e.g. 'cells[0]').""" - if idx < 0 or idx >= len(self._tr.tc_lst): - msg = "cell index [%d] out of range" % idx - raise IndexError(msg) - return _Cell(self._tr.tc_lst[idx], self) - - def __iter__(self): - """Provides iterability.""" - return (_Cell(tc, self) for tc in self._tr.tc_lst) - - def __len__(self): - """Supports len() function (e.g. 'len(cells) == 1').""" - return len(self._tr.tc_lst) - - -class _ColumnCollection(Subshape): - """Sequence of table columns.""" - - def __init__(self, tbl, parent): - super(_ColumnCollection, self).__init__(parent) - self._tbl = tbl - - def __getitem__(self, idx): - """ - Provides indexed access, (e.g. 'columns[0]'). - """ - if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst): - msg = "column index [%d] out of range" % idx - raise IndexError(msg) - return _Column(self._tbl.tblGrid.gridCol_lst[idx], self) - - def __len__(self): - """ - Supports len() function (e.g. 'len(columns) == 1'). - """ - return len(self._tbl.tblGrid.gridCol_lst) - - def notify_width_changed(self): - """ - Called by a column when its width changes. Pass along to parent. - """ - self._parent.notify_width_changed() - - -class _RowCollection(Subshape): - """Sequence of table rows""" - - def __init__(self, tbl, parent): - super(_RowCollection, self).__init__(parent) - self._tbl = tbl - - def __getitem__(self, idx): - """ - Provides indexed access, (e.g. 'rows[0]'). - """ - if idx < 0 or idx >= len(self): - msg = "row index [%d] out of range" % idx - raise IndexError(msg) - return _Row(self._tbl.tr_lst[idx], self) - - def __len__(self): - """ - Supports len() function (e.g. 'len(rows) == 1'). - """ - return len(self._tbl.tr_lst) - - def notify_height_changed(self): - """ - Called by a row when its height changes. Pass along to parent. - """ - self._parent.notify_height_changed() diff --git a/pptx/text/text.py b/pptx/text/text.py deleted file mode 100644 index b880cf4ec..000000000 --- a/pptx/text/text.py +++ /dev/null @@ -1,719 +0,0 @@ -# encoding: utf-8 - -"""Text-related objects such as TextFrame and Paragraph.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from pptx.compat import to_unicode -from pptx.dml.fill import FillFormat -from pptx.enum.dml import MSO_FILL -from pptx.enum.lang import MSO_LANGUAGE_ID -from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE -from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.oxml.simpletypes import ST_TextWrappingType -from pptx.shapes import Subshape -from pptx.text.fonts import FontFiles -from pptx.text.layout import TextFitter -from pptx.util import Centipoints, Emu, lazyproperty, Pt - - -class TextFrame(Subshape): - """ - The part of a shape that contains its text. Not all shapes have a text - frame. Corresponds to the ```` element that can appear as a - child element of ````. Not intended to be constructed directly. - """ - - def __init__(self, txBody, parent): - super(TextFrame, self).__init__(parent) - self._element = self._txBody = txBody - - def add_paragraph(self): - """ - Return new |_Paragraph| instance appended to the sequence of - paragraphs contained in this text frame. - """ - p = self._txBody.add_p() - return _Paragraph(p, self) - - @property - def auto_size(self): - """ - The type of automatic resizing that should be used to fit the text of - this shape within its bounding box when the text would otherwise - extend beyond the shape boundaries. May be |None|, - ``MSO_AUTO_SIZE.NONE``, ``MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT``, or - ``MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE``. - """ - return self._bodyPr.autofit - - @auto_size.setter - def auto_size(self, value): - self._bodyPr.autofit = value - - def clear(self): - """ - Remove all paragraphs except one empty one. - """ - for p in self._txBody.p_lst[1:]: - self._txBody.remove(p) - p = self.paragraphs[0] - p.clear() - - def fit_text( - self, - font_family="Calibri", - max_size=18, - bold=False, - italic=False, - font_file=None, - ): - """Fit text-frame text entirely within bounds of its shape. - - Make the text in this text frame fit entirely within the bounds of - its shape by setting word wrap on and applying the "best-fit" font - size to all the text it contains. :attr:`TextFrame.auto_size` is set - to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not be set larger - than *max_size* points. If the path to a matching TrueType font is - provided as *font_file*, that font file will be used for the font - metrics. If *font_file* is |None|, best efforts are made to locate - a font file with matchhing *font_family*, *bold*, and *italic* - installed on the current system (usually succeeds if the font is - installed). - """ - # ---no-op when empty as fit behavior not defined for that case--- - if self.text == "": - return - - font_size = self._best_fit_font_size( - font_family, max_size, bold, italic, font_file - ) - self._apply_fit(font_family, font_size, bold, italic) - - @property - def margin_bottom(self): - """ - |Length| value representing the inset of text from the bottom text - frame border. :meth:`pptx.util.Inches` provides a convenient way of - setting the value, e.g. ``text_frame.margin_bottom = Inches(0.05)``. - """ - return self._bodyPr.bIns - - @margin_bottom.setter - def margin_bottom(self, emu): - self._bodyPr.bIns = emu - - @property - def margin_left(self): - """ - Inset of text from left text frame border as |Length| value. - """ - return self._bodyPr.lIns - - @margin_left.setter - def margin_left(self, emu): - self._bodyPr.lIns = emu - - @property - def margin_right(self): - """ - Inset of text from right text frame border as |Length| value. - """ - return self._bodyPr.rIns - - @margin_right.setter - def margin_right(self, emu): - self._bodyPr.rIns = emu - - @property - def margin_top(self): - """ - Inset of text from top text frame border as |Length| value. - """ - return self._bodyPr.tIns - - @margin_top.setter - def margin_top(self, emu): - self._bodyPr.tIns = emu - - @property - def paragraphs(self): - """ - Immutable sequence of |_Paragraph| instances corresponding to the - paragraphs in this text frame. A text frame always contains at least - one paragraph. - """ - return tuple([_Paragraph(p, self) for p in self._txBody.p_lst]) - - @property - def text(self): - """Unicode/str containing all text in this text-frame. - - Read/write. The return value is a str (unicode) containing all text in this - text-frame. A line-feed character (``"\\n"``) separates the text for each - paragraph. A vertical-tab character (``"\\v"``) appears for each line break - (aka. soft carriage-return) encountered. - - The vertical-tab character is how PowerPoint represents a soft carriage return - in clipboard text, which is why that encoding was chosen. - - Assignment replaces all text in the text frame. The assigned value can be - a 7-bit ASCII string, a UTF-8 encoded 8-bit string, or unicode. A bytes value - (such as a Python 2 ``str``) is converted to unicode assuming UTF-8 encoding. - A new paragraph is added for each line-feed character (``"\\n"``) encountered. - A line-break (soft carriage-return) is inserted for each vertical-tab character - (``"\\v"``) encountered. - - Any control character other than newline, tab, or vertical-tab are escaped as - plain-text like "_x001B_" (for ESC (ASCII 32) in this example). - """ - return "\n".join(paragraph.text for paragraph in self.paragraphs) - - @text.setter - def text(self, text): - txBody = self._txBody - txBody.clear_content() - for p_text in to_unicode(text).split("\n"): - p = txBody.add_p() - p.append_text(p_text) - - @property - def vertical_anchor(self): - """ - Read/write member of :ref:`MsoVerticalAnchor` enumeration or |None|, - representing the vertical alignment of text in this text frame. - |None| indicates the effective value should be inherited from this - object's style hierarchy. - """ - return self._txBody.bodyPr.anchor - - @vertical_anchor.setter - def vertical_anchor(self, value): - bodyPr = self._txBody.bodyPr - bodyPr.anchor = value - - @property - def word_wrap(self): - """ - Read-write setting determining whether lines of text in this shape - are wrapped to fit within the shape's width. Valid values are True, - False, or None. True and False turn word wrap on and off, - respectively. Assigning None to word wrap causes any word wrap - setting to be removed from the text frame, causing it to inherit this - setting from its style hierarchy. - """ - return { - ST_TextWrappingType.SQUARE: True, - ST_TextWrappingType.NONE: False, - None: None, - }[self._txBody.bodyPr.wrap] - - @word_wrap.setter - def word_wrap(self, value): - if value not in (True, False, None): - raise ValueError( - "assigned value must be True, False, or None, got %s" % value - ) - self._txBody.bodyPr.wrap = { - True: ST_TextWrappingType.SQUARE, - False: ST_TextWrappingType.NONE, - None: None, - }[value] - - def _apply_fit(self, font_family, font_size, is_bold, is_italic): - """ - Arrange all the text in this text frame to fit inside its extents by - setting auto size off, wrap on, and setting the font of all its text - to *font_family*, *font_size*, *is_bold*, and *is_italic*. - """ - self.auto_size = MSO_AUTO_SIZE.NONE - self.word_wrap = True - self._set_font(font_family, font_size, is_bold, is_italic) - - def _best_fit_font_size(self, family, max_size, bold, italic, font_file): - """ - Return the largest integer point size not greater than *max_size* - that allows all the text in this text frame to fit inside its extents - when rendered using the font described by *family*, *bold*, and - *italic*. If *font_file* is specified, it is used to calculate the - fit, whether or not it matches *family*, *bold*, and *italic*. - """ - if font_file is None: - font_file = FontFiles.find(family, bold, italic) - return TextFitter.best_fit_font_size( - self.text, self._extents, max_size, font_file - ) - - @property - def _bodyPr(self): - return self._txBody.bodyPr - - @property - def _extents(self): - """ - A (cx, cy) 2-tuple representing the effective rendering area for text - within this text frame when margins are taken into account. - """ - return ( - self._parent.width - self.margin_left - self.margin_right, - self._parent.height - self.margin_top - self.margin_bottom, - ) - - def _set_font(self, family, size, bold, italic): - """ - Set the font properties of all the text in this text frame to - *family*, *size*, *bold*, and *italic*. - """ - - def iter_rPrs(txBody): - for p in txBody.p_lst: - for elm in p.content_children: - yield elm.get_or_add_rPr() - # generate a:endParaRPr for each element - yield p.get_or_add_endParaRPr() - - def set_rPr_font(rPr, name, size, bold, italic): - f = Font(rPr) - f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic - - txBody = self._element - for rPr in iter_rPrs(txBody): - set_rPr_font(rPr, family, size, bold, italic) - - -class Font(object): - """ - Character properties object, providing font size, font name, bold, - italic, etc. Corresponds to ```` child element of a run. Also - appears as ```` and ```` in paragraph and - ```` in list style elements. - """ - - def __init__(self, rPr): - super(Font, self).__init__() - self._element = self._rPr = rPr - - @property - def bold(self): - """ - Get or set boolean bold value of |Font|, e.g. - ``paragraph.font.bold = True``. If set to |None|, the bold setting is - cleared and is inherited from an enclosing shape's setting, or a - setting in a style or master. Returns None if no bold attribute is - present, meaning the effective bold value is inherited from a master - or the theme. - """ - return self._rPr.b - - @bold.setter - def bold(self, value): - self._rPr.b = value - - @lazyproperty - def color(self): - """ - The |ColorFormat| instance that provides access to the color settings - for this font. - """ - if self.fill.type != MSO_FILL.SOLID: - self.fill.solid() - return self.fill.fore_color - - @lazyproperty - def fill(self): - """ - |FillFormat| instance for this font, providing access to fill - properties such as fill color. - """ - return FillFormat.from_fill_parent(self._rPr) - - @property - def italic(self): - """ - Get or set boolean italic value of |Font| instance, with the same - behaviors as bold with respect to None values. - """ - return self._rPr.i - - @italic.setter - def italic(self, value): - self._rPr.i = value - - @property - def language_id(self): - """ - Get or set the language id of this |Font| instance. The language id - is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None| - removes any language setting, the same behavior as assigning - `MSO_LANGUAGE_ID.NONE`. - """ - lang = self._rPr.lang - if lang is None: - return MSO_LANGUAGE_ID.NONE - return self._rPr.lang - - @language_id.setter - def language_id(self, value): - if value == MSO_LANGUAGE_ID.NONE: - value = None - self._rPr.lang = 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. Returns |None| if the typeface is currently inherited from the - theme. Setting it to |None| removes any override of the theme - typeface. - """ - latin = self._rPr.latin - if latin is None: - return None - return latin.typeface - - @name.setter - def name(self, value): - if value is None: - self._rPr._remove_latin() - else: - latin = self._rPr.get_or_add_latin() - latin.typeface = 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 its style hierarchy, such as a placeholder or document - defaults (usually 18pt). |Length| is a subclass of |int| having - properties for convenient conversion into points or other length - units. Likewise, the :class:`pptx.util.Pt` class allows convenient - specification of point values:: - - >> font.size = Pt(24) - >> font.size - 304800 - >> font.size.pt - 24.0 - """ - sz = self._rPr.sz - if sz is None: - return None - return Centipoints(sz) - - @size.setter - def size(self, emu): - if emu is None: - self._rPr.sz = None - else: - sz = Emu(emu).centipoints - self._rPr.sz = sz - - @property - def underline(self): - """ - Read/write. |True|, |False|, |None|, or a member of the - :ref:`MsoTextUnderlineType` enumeration indicating the underline - setting for this font. |None| is the default and indicates the - underline setting should be inherited from the style hierarchy, such - as from a placeholder. |True| indicates single underline. |False| - indicates no underline. Other settings such as double and wavy - underlining are indicated with members of the - :ref:`MsoTextUnderlineType` enumeration. - """ - u = self._rPr.u - if u is MSO_UNDERLINE.NONE: - return False - if u is MSO_UNDERLINE.SINGLE_LINE: - return True - return u - - @underline.setter - def underline(self, value): - if value is True: - value = MSO_UNDERLINE.SINGLE_LINE - elif value is False: - value = MSO_UNDERLINE.NONE - self._element.u = value - - -class _Hyperlink(Subshape): - """ - Text run hyperlink object. Corresponds to ```` child - element of the run's properties element (````). - """ - - def __init__(self, rPr, parent): - super(_Hyperlink, self).__init__(parent) - self._rPr = rPr - - @property - def address(self): - """ - Read/write. The URL of the hyperlink. URL can be on http, https, - mailto, or file scheme; others may work. - """ - if self._hlinkClick is None: - return None - return self.part.target_ref(self._hlinkClick.rId) - - @address.setter - def address(self, url): - # implements all three of add, change, and remove hyperlink - if self._hlinkClick is not None: - self._remove_hlinkClick() - if url: - self._add_hlinkClick(url) - - def _add_hlinkClick(self, url): - rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True) - self._rPr.add_hlinkClick(rId) - - @property - def _hlinkClick(self): - return self._rPr.hlinkClick - - def _remove_hlinkClick(self): - assert self._hlinkClick is not None - self.part.drop_rel(self._hlinkClick.rId) - self._rPr._remove_hlinkClick() - - -class _Paragraph(Subshape): - """Paragraph object. Not intended to be constructed directly.""" - - def __init__(self, p, parent): - super(_Paragraph, self).__init__(parent) - self._element = self._p = p - - def add_line_break(self): - """Add line break at end of this paragraph.""" - self._p.add_br() - - def add_run(self): - """ - Return a new run appended to the runs in this paragraph. - """ - r = self._p.add_r() - return _Run(r, self) - - @property - def alignment(self): - """ - Horizontal alignment of this paragraph, represented by either - a member of the enumeration :ref:`PpParagraphAlignment` or |None|. - The value |None| indicates the paragraph should 'inherit' its - effective value from its style hierarchy. Assigning |None| removes - any explicit setting, causing its inherited value to be used. - """ - return self._pPr.algn - - @alignment.setter - def alignment(self, value): - self._pPr.algn = value - - def clear(self): - """ - Remove all content from this paragraph. Paragraph properties are - preserved. Content includes runs, line breaks, and fields. - """ - for elm in self._element.content_children: - self._element.remove(elm) - return self - - @property - def font(self): - """ - |Font| object containing default character properties for the runs in - this paragraph. These character properties override default properties - inherited from parent objects such as the text frame the paragraph is - contained in and they may be overridden by character properties set at - the run level. - """ - return Font(self._defRPr) - - @property - def level(self): - """ - Read-write integer indentation level of this paragraph, having a - range of 0-8 inclusive. 0 represents a top-level paragraph and is the - default value. Indentation level is most commonly encountered in a - bulleted list, as is found on a word bullet slide. - """ - return self._pPr.lvl - - @level.setter - def level(self, level): - self._pPr.lvl = level - - @property - def line_spacing(self): - """ - Numeric or |Length| value specifying the space between baselines in - successive lines of this paragraph. A value of |None| indicates no - explicit value is assigned and its effective value is inherited from - the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`, - 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. - """ - pPr = self._p.pPr - if pPr is None: - return None - return pPr.line_spacing - - @line_spacing.setter - def line_spacing(self, value): - pPr = self._p.get_or_add_pPr() - pPr.line_spacing = value - - @property - def runs(self): - """ - Immutable sequence of |_Run| objects corresponding to the runs in - this paragraph. - """ - return tuple(_Run(r, self) for r in self._element.r_lst) - - @property - def space_after(self): - """ - |Length| value specifying the spacing to appear between this - paragraph and the subsequent paragraph. A value of |None| indicates - no explicit value is assigned and its effective value is inherited - from the paragraph's style hierarchy. |Length| objects provide - convenience properties, such as ``.pt`` and ``.inches``, that allow - easy conversion to various length units. - """ - pPr = self._p.pPr - if pPr is None: - return None - return pPr.space_after - - @space_after.setter - def space_after(self, value): - pPr = self._p.get_or_add_pPr() - pPr.space_after = value - - @property - def space_before(self): - """ - |Length| value specifying the spacing to appear between this - paragraph and the prior paragraph. A value of |None| indicates no - explicit value is assigned and its effective value is inherited from - the paragraph's style hierarchy. |Length| objects provide convenience - properties, such as ``.pt`` and ``.cm``, that allow easy conversion - to various length units. - """ - pPr = self._p.pPr - if pPr is None: - return None - return pPr.space_before - - @space_before.setter - def space_before(self, value): - pPr = self._p.get_or_add_pPr() - pPr.space_before = value - - @property - def text(self): - """str (unicode) representation of paragraph contents. - - Read/write. This value is formed by concatenating the text in each run and field - making up the paragraph, adding a vertical-tab character (``"\\v"``) for each - line-break element (``, soft carriage-return) encountered. - - While the encoding of line-breaks as a vertical tab might be surprising at - first, doing so is consistent with PowerPoint's clipboard copy behavior and - allows a line-break to be distinguished from a paragraph boundary within the str - return value. - - Assignment causes all content in the paragraph to be replaced. Each vertical-tab - character (``"\\v"``) in the assigned str is translated to a line-break, as is - each line-feed character (``"\\n"``). Contrast behavior of line-feed character - in `TextFrame.text` setter. If line-feed characters are intended to produce new - paragraphs, use `TextFrame.text` instead. Any other control characters in the - assigned string are escaped as a hex representation like "_x001B_" (for ESC - (ASCII 27) in this example). - - The assigned value can be a 7-bit ASCII byte string (Python 2 str), a UTF-8 - encoded 8-bit byte string (Python 2 str), or unicode. Bytes values are converted - to unicode assuming UTF-8 encoding. - """ - return "".join(elm.text for elm in self._element.content_children) - - @text.setter - def text(self, text): - self.clear() - self._element.append_text(to_unicode(text)) - - @property - def _defRPr(self): - """ - The |CT_TextCharacterProperties| instance ( element) that - defines the default run properties for runs in this paragraph. Causes - the element to be added if not present. - """ - return self._pPr.get_or_add_defRPr() - - @property - def _pPr(self): - """ - The |CT_TextParagraphProperties| instance for this paragraph, the - element containing its paragraph properties. Causes the - element to be added if not present. - """ - return self._p.get_or_add_pPr() - - -class _Run(Subshape): - """Text run object. Corresponds to ```` child element in a paragraph.""" - - def __init__(self, r, parent): - super(_Run, self).__init__(parent) - self._r = r - - @property - def font(self): - """ - |Font| instance containing run-level character properties for the - text in this run. Character properties can be and perhaps most often - are inherited from parent objects such as the paragraph and slide - layout the run is contained in. Only those specifically overridden at - the run level are contained in the font object. - """ - rPr = self._r.get_or_add_rPr() - return Font(rPr) - - @lazyproperty - def hyperlink(self): - """ - |_Hyperlink| instance acting as proxy for any ```` - element under the run properties element. Created on demand, the - hyperlink object is available whether an ```` element - is present or not, and creates or deletes that element as appropriate - in response to actions on its methods and attributes. - """ - rPr = self._r.get_or_add_rPr() - return _Hyperlink(rPr, self) - - @property - def text(self): - """Read/write. A unicode string containing the text in this run. - - Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII - string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to - unicode assuming UTF-8 encoding. - - Any other control characters in the assigned string other than tab or newline - are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as - "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with - respect to line-feed and vertical-tab characters. - """ - return self._r.text - - @text.setter - def text(self, str): - self._r.text = to_unicode(str) diff --git a/pptx/util.py b/pptx/util.py deleted file mode 100644 index 76e39000a..000000000 --- a/pptx/util.py +++ /dev/null @@ -1,143 +0,0 @@ -# encoding: utf-8 - -"""Utility functions and classes.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - - -class Length(int): - """ - Base class for length classes Inches, Emu, Cm, Mm, Pt, and Px. Provides - properties for converting length values to convenient units. - """ - - _EMUS_PER_INCH = 914400 - _EMUS_PER_CENTIPOINT = 127 - _EMUS_PER_CM = 360000 - _EMUS_PER_MM = 36000 - _EMUS_PER_PT = 12700 - - def __new__(cls, emu): - return int.__new__(cls, emu) - - @property - def inches(self): - """ - Floating point length in inches - """ - return self / float(self._EMUS_PER_INCH) - - @property - def centipoints(self): - """ - Integer length in hundredths of a point (1/7200 inch). Used - internally because PowerPoint stores font size in centipoints. - """ - return self // self._EMUS_PER_CENTIPOINT - - @property - def cm(self): - """ - Floating point length in centimeters - """ - return self / float(self._EMUS_PER_CM) - - @property - def emu(self): - """ - Integer length in English Metric Units - """ - return self - - @property - def mm(self): - """ - Floating point length in millimeters - """ - return self / float(self._EMUS_PER_MM) - - @property - def pt(self): - """ - Floating point length in points - """ - return self / float(self._EMUS_PER_PT) - - -class Inches(Length): - """ - Convenience constructor for length in inches - """ - - def __new__(cls, inches): - emu = int(inches * Length._EMUS_PER_INCH) - return Length.__new__(cls, emu) - - -class Centipoints(Length): - """ - Convenience constructor for length in hundredths of a point - """ - - def __new__(cls, centipoints): - emu = int(centipoints * Length._EMUS_PER_CENTIPOINT) - return Length.__new__(cls, emu) - - -class Cm(Length): - """ - Convenience constructor for length in centimeters - """ - - def __new__(cls, cm): - emu = int(cm * Length._EMUS_PER_CM) - return Length.__new__(cls, emu) - - -class Emu(Length): - """ - Convenience constructor for length in english metric units - """ - - def __new__(cls, emu): - return Length.__new__(cls, int(emu)) - - -class Mm(Length): - """ - Convenience constructor for length in millimeters - """ - - def __new__(cls, mm): - emu = int(mm * Length._EMUS_PER_MM) - return Length.__new__(cls, emu) - - -class Pt(Length): - """ - Convenience value class for specifying a length in points - """ - - def __new__(cls, points): - emu = int(points * Length._EMUS_PER_PT) - return Length.__new__(cls, emu) - - -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/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..400cb6bfd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,145 @@ +[build-system] +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-pptx" +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.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Office/Business :: Office Suites", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "Pillow>=3.3.2", + "XlsxWriter>=0.5.7", + "lxml>=3.1.0", + "typing_extensions>=4.9.0", +] +description = "Create, read, and update PowerPoint 2007+ (.pptx) files." +dynamic = ["version"] +keywords = ["powerpoint", "ppt", "pptx", "openxml", "office"] +license = { text = "MIT" } +readme = "README.rst" +requires-python = ">=3.8" + +[project.urls] +Changelog = "https://github.com/scanny/python-pptx/blob/master/HISTORY.rst" +Documentation = "https://python-pptx.readthedocs.io/en/latest/" +Homepage = "https://github.com/scanny/python-pptx" +Repository = "https://github.com/scanny/python-pptx" + +[tool.black] +line-length = 100 + +[tool.pyright] +exclude = [ + "**/__pycache__", + "**/.*" +] +include = [ + "src/pptx", + "tests/" +] +ignore = [] +pythonPlatform = "All" +pythonVersion = "3.9" +reportImportCycles = false +reportUnnecessaryCast = true +reportUnnecessaryTypeIgnoreComment = true +stubPath = "./typings" +typeCheckingMode = "strict" +verboseOutput = true + +[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 = [ + "docs", + "*.egg-info", + "features", + ".git", + "src", + "spec", + ".tox", +] +python_classes = [ + "Test", + "Describe", +] +python_functions = [ + "test_", + "it_", + "they_", + "but_", + "and_", +] + +[tool.ruff] +line-length = 100 + +# -- don't check these locations -- +exclude = [ + # -- docs/ - documentation Python code is incidental -- + "docs", + # -- lab/ has some experimental code that is not disciplined -- + "lab", + # -- ref/ is not source-code -- + "ref", + # -- spec/ has some ad-hoc discovery code that is not disciplined -- + "spec", +] + +[tool.ruff.lint] +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 -- + "TCH001", # -- detect typing-only imports not under `if TYPE_CHECKING` -- + "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 -- + "W", # -- Warnings, including invalid escape-sequence -- +] +ignore = [ + "COM812", # -- over aggressively insists on trailing commas where not desireable -- + "PT001", # -- wants empty parens on @pytest.fixture where not used (essentially always) -- + "PT005", # -- flags mock fixtures with names intentionally matching private method name -- + "PT011", # -- pytest.raises({exc}) too broad, use match param or more specific exception -- + "PT012", # -- pytest.raises() block should contain a single simple statement -- + "SIM117", # -- merge `with` statements for context managers that have same scope -- +] + +[tool.ruff.lint.isort] +known-first-party = ["pptx"] + +[tool.setuptools.dynamic] +version = {attr = "pptx.__version__"} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..70096eab2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements-test.txt +build +ruff +setuptools>=61.0.0 +twine diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 000000000..90edd8e31 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +Sphinx==1.8.6 +Jinja2==2.11.3 +MarkupSafe==0.23 +alabaster<0.7.14 +-e . diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 000000000..9ddd60fd7 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,8 @@ +-r requirements.txt +behave>=1.2.3 +pyparsing>=2.0.1 +pytest>=2.5 +pytest-coverage +pytest-xdist +ruff +tox diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e69de29bb..000000000 diff --git a/setup.py b/setup.py deleted file mode 100755 index b8dc5272c..000000000 --- a/setup.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python - -import os -import re - -from setuptools import find_packages, setup - - -def ascii_bytes_from(path, *paths): - """ - Return the ASCII characters in the file specified by *path* and *paths*. - The file path is determined by concatenating *path* and any members of - *paths* with a directory separator in between. - """ - file_path = os.path.join(path, *paths) - with open(file_path) as f: - ascii_bytes = f.read() - return ascii_bytes - - -# read required text from files -thisdir = os.path.dirname(__file__) -init_py = ascii_bytes_from(thisdir, "pptx", "__init__.py") -readme = ascii_bytes_from(thisdir, "README.rst") -history = ascii_bytes_from(thisdir, "HISTORY.rst") -license = ascii_bytes_from(thisdir, "LICENSE") - -# Read the version from pptx.__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__ = "([^"]+)"', init_py).group(1) - - -NAME = "python-pptx" -VERSION = version -DESCRIPTION = "Generate and manipulate Open XML PowerPoint (.pptx) files" -KEYWORDS = "powerpoint ppt pptx office open xml" -AUTHOR = "Steve Canny" -AUTHOR_EMAIL = "python-pptx@googlegroups.com" -URL = "http://github.com/scanny/python-pptx" -LICENSE = license -PACKAGES = find_packages(exclude=["tests", "tests.*"]) -PACKAGE_DATA = {"pptx": ["templates/*"]} - -INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2", "XlsxWriter>=0.5.7"] - -TEST_SUITE = "tests" -TESTS_REQUIRE = ["behave", "mock", "pyparsing>=2.0.1", "pytest"] - -CLASSIFIERS = [ - "Development Status :: 4 - Beta", - "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.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Topic :: Office/Business :: Office Suites", - "Topic :: Software Development :: Libraries", -] - -LONG_DESCRIPTION = readme + "\n\n" + history - -ZIP_SAFE = False - -params = { - "name": NAME, - "version": VERSION, - "description": DESCRIPTION, - "keywords": KEYWORDS, - "long_description": LONG_DESCRIPTION, - "author": AUTHOR, - "author_email": AUTHOR_EMAIL, - "url": URL, - "license": LICENSE, - "packages": PACKAGES, - "package_data": PACKAGE_DATA, - "install_requires": INSTALL_REQUIRES, - "tests_require": TESTS_REQUIRE, - "test_suite": TEST_SUITE, - "classifiers": CLASSIFIERS, - "zip_safe": ZIP_SAFE, -} - -setup(**params) diff --git a/pptx/__init__.py b/src/pptx/__init__.py similarity index 66% rename from pptx/__init__.py rename to src/pptx/__init__.py index e843ba8e2..fb5c2d7e4 100644 --- a/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -1,26 +1,20 @@ -# encoding: utf-8 - """Initialization module for python-pptx package.""" -__version__ = "0.6.19" - +from __future__ import annotations -import pptx.exc as exceptions import sys +from typing import TYPE_CHECKING -sys.modules["pptx.exceptions"] = exceptions -del sys - -from pptx.api import Presentation # noqa - -from pptx.opc.constants import CONTENT_TYPE as CT # noqa: E402 -from pptx.opc.package import PartFactory # noqa: E402 -from pptx.parts.chart import ChartPart # noqa: E402 -from pptx.parts.coreprops import CorePropertiesPart # noqa: E402 -from pptx.parts.image import ImagePart # noqa: E402 -from pptx.parts.media import MediaPart # noqa: E402 -from pptx.parts.presentation import PresentationPart # noqa: E402 -from pptx.parts.slide import ( # noqa: E402 +import pptx.exc as exceptions +from pptx.api import Presentation +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.package import PartFactory +from pptx.parts.chart import ChartPart +from pptx.parts.coreprops import CorePropertiesPart +from pptx.parts.image import ImagePart +from pptx.parts.media import MediaPart +from pptx.parts.presentation import PresentationPart +from pptx.parts.slide import ( NotesMasterPart, NotesSlidePart, SlideLayoutPart, @@ -28,7 +22,17 @@ SlidePart, ) -content_type_to_part_class_map = { +if TYPE_CHECKING: + from pptx.opc.package import Part + +__version__ = "1.0.2" + +sys.modules["pptx.exceptions"] = exceptions +del sys + +__all__ = ["Presentation"] + +content_type_to_part_class_map: dict[str, type[Part]] = { CT.PML_PRESENTATION_MAIN: PresentationPart, CT.PML_PRES_MACRO_MAIN: PresentationPart, CT.PML_TEMPLATE_MAIN: PresentationPart, @@ -58,6 +62,8 @@ CT.VIDEO: MediaPart, CT.WMV: MediaPart, CT.X_MS_VIDEO: MediaPart, + # -- accommodate "image/jpg" as an alias for "image/jpeg" -- + "image/jpg": ImagePart, } PartFactory.part_type_for.update(content_type_to_part_class_map) diff --git a/pptx/action.py b/src/pptx/action.py similarity index 65% rename from pptx/action.py rename to src/pptx/action.py index 3ce6778cd..83c6ebf19 100644 --- a/pptx/action.py +++ b/src/pptx/action.py @@ -1,23 +1,35 @@ -# encoding: utf-8 +"""Objects related to mouse click and hover actions on a shape or text.""" -""" -Objects related to mouse click and hover actions on a shape or text. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import TYPE_CHECKING, cast -from .enum.action import PP_ACTION -from .opc.constants import RELATIONSHIP_TYPE as RT -from .shapes import Subshape -from .util import lazyproperty +from pptx.enum.action import PP_ACTION +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.shapes import Subshape +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.shapes.shared import CT_NonVisualDrawingProps + from pptx.oxml.text import CT_TextCharacterProperties + from pptx.parts.slide import SlidePart + from pptx.shapes.base import BaseShape + from pptx.slide import Slide, Slides class ActionSetting(Subshape): """Properties specifying how a shape or run reacts to mouse actions.""" - # Subshape superclass provides access to the Slide Part, which is needed - # to access relationships. - def __init__(self, xPr, parent, hover=False): + # -- The Subshape base class provides access to the Slide Part, which is needed to access + # -- relationships, which is where hyperlinks live. + + def __init__( + self, + xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties, + parent: BaseShape, + hover: bool = False, + ): super(ActionSetting, self).__init__(parent) # xPr is either a cNvPr or rPr element self._element = xPr @@ -26,11 +38,14 @@ def __init__(self, xPr, parent, hover=False): @property def action(self): - """ - A member of the :ref:`PpActionType` enumeration, such as - `PP_ACTION.HYPERLINK`, indicating the type of action that will result - when the specified shape or text is clicked or the mouse pointer is - positioned over the shape during a slide show. + """Member of :ref:`PpActionType` enumeration, such as `PP_ACTION.HYPERLINK`. + + The returned member indicates the type of action that will result when the + specified shape or text is clicked or the mouse pointer is positioned over the + shape during a slide show. + + If there is no click-action or the click-action value is not recognized (is not + one of the official `MsoPpAction` values) then `PP_ACTION.NONE` is returned. """ hlink = self._hlink @@ -59,10 +74,10 @@ def action(self): "ole": PP_ACTION.OLE_VERB, "macro": PP_ACTION.RUN_MACRO, "program": PP_ACTION.RUN_PROGRAM, - }[action_verb] + }.get(action_verb, PP_ACTION.NONE) @lazyproperty - def hyperlink(self): + def hyperlink(self) -> Hyperlink: """ A |Hyperlink| object representing the hyperlink action defined on this click or hover mouse event. A |Hyperlink| object is always @@ -71,7 +86,7 @@ def hyperlink(self): return Hyperlink(self._element, self._parent, self._hover) @property - def target_slide(self): + def target_slide(self) -> Slide | None: """ A reference to the slide in this presentation that is the target of the slide jump action in this shape. Slide jump actions include @@ -117,18 +132,19 @@ def target_slide(self): raise ValueError("no previous slide") return self._slides[prev_slide_idx] elif self.action == PP_ACTION.NAMED_SLIDE: + assert self._hlink is not None rId = self._hlink.rId - return self.part.related_parts[rId].slide + slide_part = cast("SlidePart", self.part.related_part(rId)) + return slide_part.slide @target_slide.setter - def target_slide(self, slide): + def target_slide(self, slide: Slide | None): self._clear_click_action() if slide is None: return hlink = self._element.get_or_add_hlinkClick() hlink.action = "ppaction://hlinksldjump" - this_part, target_part = self.part, slide.part - hlink.rId = this_part.relate_to(target_part, RT.SLIDE) + hlink.rId = self.part.relate_to(slide.part, RT.SLIDE) def _clear_click_action(self): """Remove any existing click action.""" @@ -141,12 +157,13 @@ def _clear_click_action(self): self._element.remove(hlink) @property - def _hlink(self): + def _hlink(self) -> CT_Hyperlink | None: """ - Reference to the `a:hlinkClick` or `h:hlinkHover` element for this + Reference to the `a:hlinkClick` or `a:hlinkHover` element for this click action. Returns |None| if the element is not present. """ if self._hover: + assert isinstance(self._element, CT_NonVisualDrawingProps) return self._element.hlinkHover return self._element.hlinkClick @@ -166,7 +183,7 @@ def _slide_index(self): return self._slides.index(self._slide) @lazyproperty - def _slides(self): + def _slides(self) -> Slides: """ Reference to the slide collection for this presentation. """ @@ -174,11 +191,14 @@ def _slides(self): class Hyperlink(Subshape): - """ - Represents a hyperlink action on a shape or text run. - """ - - def __init__(self, xPr, parent, hover=False): + """Represents a hyperlink action on a shape or text run.""" + + def __init__( + self, + xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties, + parent: BaseShape, + hover: bool = False, + ): super(Hyperlink, self).__init__(parent) # xPr is either a cNvPr or rPr element self._element = xPr @@ -186,14 +206,13 @@ def __init__(self, xPr, parent, hover=False): self._hover = hover @property - def address(self): - """ - Read/write. The URL of the hyperlink. URL can be on http, https, - mailto, or file scheme; others may work. Returns |None| if no - hyperlink is defined, including when another action such as - `RUN_MACRO` is defined on the object. Assigning |None| removes any - action defined on the object, whether it is a hyperlink action or - not. + def address(self) -> str | None: + """Read/write. The URL of the hyperlink. + + URL can be on http, https, mailto, or file scheme; others may work. Returns |None| if no + hyperlink is defined, including when another action such as `RUN_MACRO` is defined on the + object. Assigning |None| removes any action defined on the object, whether it is a hyperlink + action or not. """ hlink = self._hlink @@ -209,7 +228,7 @@ def address(self): return self.part.target_ref(rId) @address.setter - def address(self, url): + def address(self, url: str | None): # implements all three of add, change, and remove hyperlink self._remove_hlink() @@ -218,30 +237,29 @@ def address(self, url): hlink = self._get_or_add_hlink() hlink.rId = rId - def _get_or_add_hlink(self): - """ - Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink - object, depending on the value of `self._hover`. Create one if not - present. + def _get_or_add_hlink(self) -> CT_Hyperlink: + """Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink object. + + The actual element depends on the value of `self._hover`. Create the element if not present. """ if self._hover: - return self._element.get_or_add_hlinkHover() + return cast("CT_NonVisualDrawingProps", self._element).get_or_add_hlinkHover() return self._element.get_or_add_hlinkClick() @property - def _hlink(self): - """ - Reference to the `a:hlinkClick` or `h:hlinkHover` element for this - click action. Returns |None| if the element is not present. + def _hlink(self) -> CT_Hyperlink | None: + """Reference to the `a:hlinkClick` or `h:hlinkHover` element for this click action. + + Returns |None| if the element is not present. """ if self._hover: - return self._element.hlinkHover + return cast("CT_NonVisualDrawingProps", self._element).hlinkHover return self._element.hlinkClick def _remove_hlink(self): - """ - Remove the a:hlinkClick or a:hlinkHover element, including dropping - any relationship it might have. + """Remove the a:hlinkClick or a:hlinkHover element. + + Also drops any relationship it might have. """ hlink = self._hlink if hlink is None: diff --git a/pptx/api.py b/src/pptx/api.py similarity index 54% rename from pptx/api.py rename to src/pptx/api.py index 7670c886f..892f425ab 100644 --- a/pptx/api.py +++ b/src/pptx/api.py @@ -1,21 +1,24 @@ -# encoding: utf-8 +"""Directly exposed API classes, Presentation for now. -""" -Directly exposed API classes, Presentation for now. Provides some syntactic -sugar for interacting with the pptx.presentation.Package graph and also -provides some insulation so not so many classes in the other modules need to -be named as internal (leading underscore). +Provides some syntactic sugar for interacting with the pptx.presentation.Package graph and also +provides some insulation so not so many classes in the other modules need to be named as internal +(leading underscore). """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import os +from typing import IO, TYPE_CHECKING + +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.package import Package -from .opc.constants import CONTENT_TYPE as CT -from .package import Package +if TYPE_CHECKING: + from pptx import presentation + from pptx.parts.presentation import PresentationPart -def Presentation(pptx=None): +def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation: """ Return a |Presentation| object loaded from *pptx*, where *pptx* can be either a path to a ``.pptx`` file (a string) or a file-like object. If @@ -34,18 +37,13 @@ def Presentation(pptx=None): return presentation_part.presentation -def _default_pptx_path(): - """ - Return the path to the built-in default .pptx package. - """ +def _default_pptx_path() -> str: + """Return the path to the built-in default .pptx package.""" _thisdir = os.path.split(__file__)[0] return os.path.join(_thisdir, "templates", "default.pptx") -def _is_pptx_package(prs_part): - """ - Return |True| if *prs_part* is a valid main document part, |False| - otherwise. - """ +def _is_pptx_package(prs_part: PresentationPart): + """Return |True| if *prs_part* is a valid main document part, |False| otherwise.""" valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN) return prs_part.content_type in valid_content_types diff --git a/pptx/chart/__init__.py b/src/pptx/chart/__init__.py similarity index 100% rename from pptx/chart/__init__.py rename to src/pptx/chart/__init__.py diff --git a/pptx/chart/axis.py b/src/pptx/chart/axis.py similarity index 88% rename from pptx/chart/axis.py rename to src/pptx/chart/axis.py index 465676d96..a9b877039 100644 --- a/pptx/chart/axis.py +++ b/src/pptx/chart/axis.py @@ -1,29 +1,23 @@ -# encoding: utf-8 +"""Axis-related chart objects.""" -""" -Axis-related chart objects. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ..dml.chtfmt import ChartFormat -from ..enum.chart import ( +from pptx.dml.chtfmt import ChartFormat +from pptx.enum.chart import ( XL_AXIS_CROSSES, XL_CATEGORY_TYPE, XL_TICK_LABEL_POSITION, XL_TICK_MARK, ) -from ..oxml.ns import qn -from ..shared import ElementProxy -from ..text.text import Font, TextFrame -from ..util import lazyproperty +from pptx.oxml.ns import qn +from pptx.oxml.simpletypes import ST_Orientation +from pptx.shared import ElementProxy +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty class _BaseAxis(object): - """ - Base class for chart axis objects. All axis objects share these - properties. - """ + """Base class for chart axis objects. All axis objects share these properties.""" def __init__(self, xAx): super(_BaseAxis, self).__init__() @@ -182,6 +176,27 @@ def minor_tick_mark(self, value): return self._element._add_minorTickMark(val=value) + @property + def reverse_order(self): + """Read/write bool value specifying whether to reverse plotting order for axis. + + For a category axis, this reverses the order in which the categories are + displayed. This may be desired, for example, on a (horizontal) bar-chart where + by default the first category appears at the bottom. Since we read from + top-to-bottom, many viewers may find it most natural for the first category to + appear on top. + + For a value axis, it reverses the direction of increasing value from + bottom-to-top to top-to-bottom. + """ + return self._element.orientation == ST_Orientation.MAX_MIN + + @reverse_order.setter + def reverse_order(self, value): + self._element.orientation = ( + ST_Orientation.MAX_MIN if bool(value) is True else ST_Orientation.MIN_MAX + ) + @lazyproperty def tick_labels(self): """ @@ -230,8 +245,6 @@ def visible(self, value): class AxisTitle(ElementProxy): """Provides properties for manipulating axis title.""" - __slots__ = ("_title", "_format") - def __init__(self, title): super(AxisTitle, self).__init__(title) self._title = title @@ -279,9 +292,7 @@ def text_frame(self): class CategoryAxis(_BaseAxis): - """ - A category axis of a chart. - """ + """A category axis of a chart.""" @property def category_type(self): @@ -293,10 +304,10 @@ def category_type(self): class DateAxis(_BaseAxis): - """ - A category axis with dates as its category labels and having some special - display behaviors such as making length of equal periods equal and - normalizing month start dates despite unequal month lengths. + """A category axis with dates as its category labels. + + This axis-type has some special display behaviors such as making length of equal + periods equal and normalizing month start dates despite unequal month lengths. """ @property @@ -309,12 +320,7 @@ def category_type(self): class MajorGridlines(ElementProxy): - """ - Provides access to the properties of the major gridlines appearing on an - axis. - """ - - __slots__ = ("_xAx", "_format") + """Provides access to the properties of the major gridlines appearing on an axis.""" def __init__(self, xAx): super(MajorGridlines, self).__init__(xAx) @@ -331,9 +337,7 @@ def format(self): class TickLabels(object): - """ - A service class providing access to formatting of axis tick mark labels. - """ + """A service class providing access to formatting of axis tick mark labels.""" def __init__(self, xAx_elm): super(TickLabels, self).__init__() @@ -415,10 +419,10 @@ def offset(self, value): class ValueAxis(_BaseAxis): - """ - An axis having continuous (as opposed to discrete) values. The vertical - axis is generally a value axis, however both axes of an XY-type chart are - value axes. + """An axis having continuous (as opposed to discrete) values. + + The vertical axis is generally a value axis, however both axes of an XY-type chart + are value axes. """ @property diff --git a/pptx/chart/category.py b/src/pptx/chart/category.py similarity index 98% rename from pptx/chart/category.py rename to src/pptx/chart/category.py index 3d16e6f42..2c28aff5e 100644 --- a/pptx/chart/category.py +++ b/src/pptx/chart/category.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - """Category-related objects. The |category.Categories| object is returned by ``Plot.categories`` and contains zero or @@ -8,9 +6,9 @@ discovery of the depth of that hierarchy and providing means to navigate it. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from pptx.compat import Sequence +from collections.abc import Sequence class Categories(Sequence): diff --git a/pptx/chart/chart.py b/src/pptx/chart/chart.py similarity index 94% rename from pptx/chart/chart.py rename to src/pptx/chart/chart.py index dee9c8d9f..d73aa9338 100644 --- a/pptx/chart/chart.py +++ b/src/pptx/chart/chart.py @@ -1,15 +1,14 @@ -# encoding: utf-8 - """Chart-related objects such as Chart and ChartTitle.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis from pptx.chart.legend import Legend from pptx.chart.plot import PlotFactory, PlotTypeInspector from pptx.chart.series import SeriesCollection from pptx.chart.xmlwriter import SeriesXmlRewriterFactory -from pptx.compat import Sequence from pptx.dml.chtfmt import ChartFormat from pptx.shared import ElementProxy, PartElementProxy from pptx.text.text import Font, TextFrame @@ -79,11 +78,10 @@ def chart_title(self): @property def chart_type(self): - """ - Read-only :ref:`XlChartType` enumeration value specifying the type of - this chart. If the chart has two plots, for example, a line plot - overlayed on a bar plot, the type reported is for the first - (back-most) plot. + """Member of :ref:`XlChartType` enumeration specifying type of this chart. + + If the chart has two plots, for example, a line plot overlayed on a bar plot, + the type reported is for the first (back-most) plot. Read-only. """ first_plot = self.plots[0] return PlotTypeInspector.chart_type(first_plot) @@ -91,12 +89,7 @@ def chart_type(self): @lazyproperty def font(self): """Font object controlling text format defaults for this chart.""" - defRPr = ( - self._chartSpace.get_or_add_txPr() - .p_lst[0] - .get_or_add_pPr() - .get_or_add_defRPr() - ) + defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr() return Font(defRPr) @property @@ -214,8 +207,6 @@ class ChartTitle(ElementProxy): # actually differ in certain fuller behaviors, but at present they're # essentially identical. - __slots__ = ("_title", "_format") - def __init__(self, title): super(ChartTitle, self).__init__(title) self._title = title diff --git a/pptx/chart/data.py b/src/pptx/chart/data.py similarity index 96% rename from pptx/chart/data.py rename to src/pptx/chart/data.py index c95adffbe..ec6a61f31 100644 --- a/pptx/chart/data.py +++ b/src/pptx/chart/data.py @@ -1,12 +1,9 @@ -# encoding: utf-8 +"""ChartData and related objects.""" -""" -ChartData and related objects. -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import datetime +from collections.abc import Sequence from numbers import Number from pptx.chart.xlsx import ( @@ -15,19 +12,17 @@ XyWorkbookWriter, ) from pptx.chart.xmlwriter import ChartXmlWriter -from pptx.compat import Sequence from pptx.util import lazyproperty class _BaseChartData(Sequence): - """ - Base class providing common members for chart data objects. A chart data - object serves as a proxy for the chart data table that will be written to - an Excel worksheet; operating as a sequence of series as well as - providing access to chart-level attributes. A chart data object is used - as a parameter in :meth:`shapes.add_chart` and - :meth:`Chart.replace_data`. The data structure varies between major chart - categories such as category charts and XY charts. + """Base class providing common members for chart data objects. + + A chart data object serves as a proxy for the chart data table that will be written to an + Excel worksheet; operating as a sequence of series as well as providing access to chart-level + attributes. A chart data object is used as a parameter in :meth:`shapes.add_chart` and + :meth:`Chart.replace_data`. The data structure varies between major chart categories such as + category charts and XY charts. """ def __init__(self, number_format="General"): @@ -298,19 +293,20 @@ def add_series(self, name, values=(), number_format=None): series_data.add_data_point(value) return series_data - @lazyproperty + @property def categories(self): - """ - A |data.Categories| object providing access to the hierarchy of - category objects for this chart data. Assigning an iterable of - category labels (strings, numbers, or dates) replaces the - |data.Categories| object with a new one containing a category for - each label in the sequence. + """|data.Categories| object providing access to category-object hierarchy. - Creating a chart from chart data having date categories will cause - the chart to have a |DateAxis| for its category axis. + Assigning an iterable of category labels (strings, numbers, or dates) replaces + the |data.Categories| object with a new one containing a category for each label + in the sequence. + + Creating a chart from chart data having date categories will cause the chart to + have a |DateAxis| for its category axis. """ - return Categories() + if not getattr(self, "_categories", False): + self._categories = Categories() + return self._categories @categories.setter def categories(self, category_labels): diff --git a/pptx/chart/datalabel.py b/src/pptx/chart/datalabel.py similarity index 97% rename from pptx/chart/datalabel.py rename to src/pptx/chart/datalabel.py index ec6f7cba5..af7cdf5c0 100644 --- a/pptx/chart/datalabel.py +++ b/src/pptx/chart/datalabel.py @@ -1,13 +1,9 @@ -# encoding: utf-8 +"""Data label-related objects.""" -""" -Data label-related objects. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals - -from ..text.text import Font, TextFrame -from ..util import lazyproperty +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty class DataLabels(object): diff --git a/pptx/chart/legend.py b/src/pptx/chart/legend.py similarity index 91% rename from pptx/chart/legend.py rename to src/pptx/chart/legend.py index 2926fae23..9bc64dbf8 100644 --- a/pptx/chart/legend.py +++ b/src/pptx/chart/legend.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""Legend of a chart.""" -""" -Legend of a chart. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ..enum.chart import XL_LEGEND_POSITION -from ..text.text import Font -from ..util import lazyproperty +from pptx.enum.chart import XL_LEGEND_POSITION +from pptx.text.text import Font +from pptx.util import lazyproperty class Legend(object): diff --git a/pptx/chart/marker.py b/src/pptx/chart/marker.py similarity index 85% rename from pptx/chart/marker.py rename to src/pptx/chart/marker.py index 738083cf2..cd4b7f024 100644 --- a/pptx/chart/marker.py +++ b/src/pptx/chart/marker.py @@ -1,15 +1,13 @@ -# encoding: utf-8 +"""Marker-related objects. -""" -Marker-related objects. Only the line-type charts Line, XY, and Radar have -markers. +Only the line-type charts Line, XY, and Radar have markers. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from ..dml.chtfmt import ChartFormat -from ..shared import ElementProxy -from ..util import lazyproperty +from pptx.dml.chtfmt import ChartFormat +from pptx.shared import ElementProxy +from pptx.util import lazyproperty class Marker(ElementProxy): @@ -18,8 +16,6 @@ class Marker(ElementProxy): a line-type chart. """ - __slots__ = ("_format",) - @lazyproperty def format(self): """ diff --git a/pptx/chart/plot.py b/src/pptx/chart/plot.py similarity index 95% rename from pptx/chart/plot.py rename to src/pptx/chart/plot.py index ce2d1167e..6e7235855 100644 --- a/pptx/chart/plot.py +++ b/src/pptx/chart/plot.py @@ -1,20 +1,18 @@ -# encoding: utf-8 +"""Plot-related objects. -""" -Plot-related objects. A plot is known as a chart group in the MS API. A chart -can have more than one plot overlayed on each other, such as a line plot -layered over a bar plot. +A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on +each other, such as a line plot layered over a bar plot. """ -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations -from .category import Categories -from .datalabel import DataLabels -from ..enum.chart import XL_CHART_TYPE as XL -from ..oxml.ns import qn -from ..oxml.simpletypes import ST_BarDir, ST_Grouping -from .series import SeriesCollection -from ..util import lazyproperty +from pptx.chart.category import Categories +from pptx.chart.datalabel import DataLabels +from pptx.chart.series import SeriesCollection +from pptx.enum.chart import XL_CHART_TYPE as XL +from pptx.oxml.ns import qn +from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping +from pptx.util import lazyproperty class _BasePlot(object): @@ -58,9 +56,7 @@ def data_labels(self): """ dLbls = self._element.dLbls if dLbls is None: - raise ValueError( - "plot has no data labels, set has_data_labels = True first" - ) + raise ValueError("plot has no data labels, set has_data_labels = True first") return DataLabels(dLbls) @property diff --git a/pptx/chart/point.py b/src/pptx/chart/point.py similarity index 95% rename from pptx/chart/point.py rename to src/pptx/chart/point.py index 258f6ae19..2d42436cb 100644 --- a/pptx/chart/point.py +++ b/src/pptx/chart/point.py @@ -1,12 +1,11 @@ -# encoding: utf-8 - """Data point-related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence from pptx.chart.datalabel import DataLabel from pptx.chart.marker import Marker -from pptx.compat import Sequence from pptx.dml.chtfmt import ChartFormat from pptx.util import lazyproperty diff --git a/pptx/chart/series.py b/src/pptx/chart/series.py similarity index 96% rename from pptx/chart/series.py rename to src/pptx/chart/series.py index 4ae19fbc0..16112eabe 100644 --- a/pptx/chart/series.py +++ b/src/pptx/chart/series.py @@ -1,13 +1,12 @@ -# encoding: utf-8 - """Series-related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence from pptx.chart.datalabel import DataLabels from pptx.chart.marker import Marker from pptx.chart.point import BubblePoints, CategoryPoints, XyPoints -from pptx.compat import Sequence from pptx.dml.chtfmt import ChartFormat from pptx.oxml.ns import qn from pptx.util import lazyproperty @@ -254,8 +253,6 @@ def _SeriesFactory(ser): qn("c:scatterChart"): XySeries, }[xChart_tag] except KeyError: - raise NotImplementedError( - "series class for %s not yet implemented" % xChart_tag - ) + raise NotImplementedError("series class for %s not yet implemented" % xChart_tag) return SeriesCls(ser) diff --git a/pptx/chart/xlsx.py b/src/pptx/chart/xlsx.py similarity index 91% rename from pptx/chart/xlsx.py rename to src/pptx/chart/xlsx.py index 011e5a64d..30b212728 100644 --- a/pptx/chart/xlsx.py +++ b/src/pptx/chart/xlsx.py @@ -1,22 +1,15 @@ -# encoding: utf-8 +"""Chart builder and related objects.""" -""" -Chart builder and related objects. -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations +import io from contextlib import contextmanager from xlsxwriter import Workbook -from ..compat import BytesIO - class _BaseWorkbookWriter(object): - """ - Base class for workbook writers, providing shared members. - """ + """Base class for workbook writers, providing shared members.""" def __init__(self, chart_data): super(_BaseWorkbookWriter, self).__init__() @@ -24,11 +17,8 @@ def __init__(self, chart_data): @property def xlsx_blob(self): - """ - Return the byte stream of an Excel file formatted as chart data for - the category chart specified in the chart data object. - """ - xlsx_file = BytesIO() + """bytes for Excel file containing chart_data.""" + xlsx_file = io.BytesIO() with self._open_worksheet(xlsx_file) as (workbook, worksheet): self._populate_worksheet(workbook, worksheet) return xlsx_file.getvalue() @@ -38,7 +28,7 @@ def _open_worksheet(self, xlsx_file): """ Enable XlsxWriter Worksheet object to be opened, operated on, and then automatically closed within a `with` statement. A filename or - stream object (such as a ``BytesIO`` instance) is expected as + stream object (such as an `io.BytesIO` instance) is expected as *xlsx_file*. """ workbook = Workbook(xlsx_file, {"in_memory": True}) @@ -234,13 +224,9 @@ def _populate_worksheet(self, workbook, worksheet): table, X values in column A and Y values in column B. Place the series label in the first (heading) cell of the column. """ - chart_num_format = workbook.add_format( - {"num_format": self._chart_data.number_format} - ) + chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format}) for series in self._chart_data: - series_num_format = workbook.add_format( - {"num_format": series.number_format} - ) + series_num_format = workbook.add_format({"num_format": series.number_format}) offset = self.series_table_row_offset(series) # write X values worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format) @@ -272,13 +258,9 @@ def _populate_worksheet(self, workbook, worksheet): column C. Place the series label in the first (heading) cell of the values column. """ - chart_num_format = workbook.add_format( - {"num_format": self._chart_data.number_format} - ) + chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format}) for series in self._chart_data: - series_num_format = workbook.add_format( - {"num_format": series.number_format} - ) + series_num_format = workbook.add_format({"num_format": series.number_format}) offset = self.series_table_row_offset(series) # write X values worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format) diff --git a/pptx/chart/xmlwriter.py b/src/pptx/chart/xmlwriter.py similarity index 97% rename from pptx/chart/xmlwriter.py rename to src/pptx/chart/xmlwriter.py index c485a4b88..703c53dd5 100644 --- a/pptx/chart/xmlwriter.py +++ b/src/pptx/chart/xmlwriter.py @@ -1,18 +1,13 @@ -# encoding: utf-8 +"""Composers for default chart XML for various chart types.""" -""" -Composers for default chart XML for various chart types. -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from copy import deepcopy from xml.sax.saxutils import escape -from ..compat import to_unicode -from ..enum.chart import XL_CHART_TYPE -from ..oxml import parse_xml -from ..oxml.ns import nsdecls +from pptx.enum.chart import XL_CHART_TYPE +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls def ChartXmlWriter(chart_type, chart_data): @@ -54,9 +49,7 @@ def ChartXmlWriter(chart_type, chart_data): XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XyChartXmlWriter, }[chart_type] except KeyError: - raise NotImplementedError( - "XML writer for chart type %s not yet implemented" % chart_type - ) + raise NotImplementedError("XML writer for chart type %s not yet implemented" % chart_type) return BuilderCls(chart_type, chart_data) @@ -136,9 +129,7 @@ def numRef_xml(self, wksht_ref, number_format, values): "{pt_xml}" " \n" " \n" - ).format( - **{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml} - ) + ).format(**{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml}) def pt_xml(self, values): """ @@ -149,9 +140,7 @@ def pt_xml(self, values): in the overall data point sequence of the chart and is started at *offset*. """ - xml = (' \n').format( - pt_count=len(values) - ) + xml = (' \n').format(pt_count=len(values)) pt_tmpl = ( ' \n' @@ -289,9 +278,7 @@ def _trim_ser_count_by(self, plotArea, count): for ser in extra_sers: parent = ser.getparent() parent.remove(ser) - extra_xCharts = [ - xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0 - ] + extra_xCharts = [xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0] for xChart in extra_xCharts: parent = xChart.getparent() parent.remove(xChart) @@ -529,9 +516,7 @@ def _barDir_xml(self): return ' \n' elif self._chart_type in col_types: return ' \n' - raise NotImplementedError( - "no _barDir_xml() for chart type %s" % self._chart_type - ) + raise NotImplementedError("no _barDir_xml() for chart type %s" % self._chart_type) @property def _cat_ax_pos(self): @@ -601,9 +586,7 @@ def _grouping_xml(self): return ' \n' elif self._chart_type in percentStacked_types: return ' \n' - raise NotImplementedError( - "no _grouping_xml() for chart type %s" % self._chart_type - ) + raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type) @property def _overlap_xml(self): @@ -870,9 +853,7 @@ def _grouping_xml(self): return ' \n' elif self._chart_type in percentStacked_types: return ' \n' - raise NotImplementedError( - "no _grouping_xml() for chart type %s" % self._chart_type - ) + raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type) @property def _marker_xml(self): @@ -1532,9 +1513,7 @@ def _cat_pt_xml(self): ' \n' " {cat_label}\n" " \n" - ).format( - **{"cat_idx": idx, "cat_label": escape(to_unicode(category.label))} - ) + ).format(**{"cat_idx": idx, "cat_label": escape(str(category.label))}) return xml @property @@ -1573,9 +1552,9 @@ def lvl_pt_xml(level): xml = "" for level in categories.levels: - xml += ( - " \n" "{lvl_pt_xml}" " \n" - ).format(**{"lvl_pt_xml": lvl_pt_xml(level)}) + xml += (" \n" "{lvl_pt_xml}" " \n").format( + **{"lvl_pt_xml": lvl_pt_xml(level)} + ) return xml @property @@ -1793,11 +1772,7 @@ def _bubbleSize_tmpl(self): containing the bubble size values and their spreadsheet range reference. """ - return ( - " \n" - "{numRef_xml}" - " \n" - ) + return " \n" "{numRef_xml}" " \n" class _BubbleSeriesXmlRewriter(_BaseSeriesXmlRewriter): diff --git a/pptx/dml/__init__.py b/src/pptx/dml/__init__.py similarity index 100% rename from pptx/dml/__init__.py rename to src/pptx/dml/__init__.py diff --git a/pptx/dml/chtfmt.py b/src/pptx/dml/chtfmt.py similarity index 69% rename from pptx/dml/chtfmt.py rename to src/pptx/dml/chtfmt.py index ed03ad783..c37e4844d 100644 --- a/pptx/dml/chtfmt.py +++ b/src/pptx/dml/chtfmt.py @@ -1,17 +1,15 @@ -# encoding: utf-8 +"""|ChartFormat| and related objects. -""" -|ChartFormat| and related objects. |ChartFormat| acts as proxy for the `spPr` -element, which provides visual shape properties such as line and fill for -chart elements. +|ChartFormat| acts as proxy for the `spPr` element, which provides visual shape properties such as +line and fill for chart elements. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from .fill import FillFormat -from .line import LineFormat -from ..shared import ElementProxy -from ..util import lazyproperty +from pptx.dml.fill import FillFormat +from pptx.dml.line import LineFormat +from pptx.shared import ElementProxy +from pptx.util import lazyproperty class ChartFormat(ElementProxy): @@ -23,8 +21,6 @@ class ChartFormat(ElementProxy): provided by the :attr:`format` property on the target axis, series, etc. """ - __slots__ = ("_fill", "_line") - @lazyproperty def fill(self): """ diff --git a/pptx/dml/color.py b/src/pptx/dml/color.py similarity index 97% rename from pptx/dml/color.py rename to src/pptx/dml/color.py index 71e619c9b..54155823d 100644 --- a/pptx/dml/color.py +++ b/src/pptx/dml/color.py @@ -1,13 +1,9 @@ -# encoding: utf-8 +"""DrawingML objects related to color, ColorFormat being the most prominent.""" -""" -DrawingML objects related to color, ColorFormat being the most prominent. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ..enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR -from ..oxml.dml.color import ( +from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR +from pptx.oxml.dml.color import ( CT_HslColor, CT_PresetColor, CT_SchemeColor, diff --git a/pptx/dml/effect.py b/src/pptx/dml/effect.py similarity index 93% rename from pptx/dml/effect.py rename to src/pptx/dml/effect.py index 65753014a..9df69ce49 100644 --- a/pptx/dml/effect.py +++ b/src/pptx/dml/effect.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Visual effects on a shape such as shadow, glow, and reflection.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations class ShadowFormat(object): diff --git a/pptx/dml/fill.py b/src/pptx/dml/fill.py similarity index 92% rename from pptx/dml/fill.py rename to src/pptx/dml/fill.py index ddc5ef963..8212af9e8 100644 --- a/pptx/dml/fill.py +++ b/src/pptx/dml/fill.py @@ -1,10 +1,10 @@ -# encoding: utf-8 - """DrawingML objects related to fill.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING -from pptx.compat import Sequence from pptx.dml.color import ColorFormat from pptx.enum.dml import MSO_FILL from pptx.oxml.dml.fill import ( @@ -15,23 +15,28 @@ CT_PatternFillProperties, CT_SolidColorFillProperties, ) +from pptx.oxml.xmlchemy import BaseOxmlElement from pptx.shared import ElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.enum.dml import MSO_FILL_TYPE + from pptx.oxml.xmlchemy import BaseOxmlElement + class FillFormat(object): - """ - Provides access to the current fill properties object and provides - methods to change the fill type. + """Provides access to the current fill properties. + + Also provides methods to change the fill type. """ - def __init__(self, eg_fill_properties_parent, fill_obj): + def __init__(self, eg_fill_properties_parent: BaseOxmlElement, fill_obj: _Fill): super(FillFormat, self).__init__() self._xPr = eg_fill_properties_parent self._fill = fill_obj @classmethod - def from_fill_parent(cls, eg_fillProperties_parent): + def from_fill_parent(cls, eg_fillProperties_parent: BaseOxmlElement) -> FillFormat: """ Return a |FillFormat| instance initialized to the settings contained in *eg_fillProperties_parent*, which must be an element having @@ -151,11 +156,8 @@ def solid(self): self._fill = _SolidFill(solidFill) @property - def type(self): - """ - Return a value from the :ref:`MsoFillType` enumeration corresponding - to the type of this fill. - """ + def type(self) -> MSO_FILL_TYPE: + """The type of this fill, e.g. `MSO_FILL_TYPE.SOLID`.""" return self._fill.type @@ -194,10 +196,7 @@ def back_color(self): @property def fore_color(self): """Raise TypeError for types that do not override this property.""" - tmpl = ( - "fill type %s has no foreground color, call .solid() or .pattern" - "ed() first" - ) + tmpl = "fill type %s has no foreground color, call .solid() or .pattern" "ed() first" raise TypeError(tmpl % self.__class__.__name__) @property @@ -207,9 +206,10 @@ def pattern(self): raise TypeError(tmpl % self.__class__.__name__) @property - def type(self): # pragma: no cover - tmpl = ".type property must be implemented on %s" - raise NotImplementedError(tmpl % self.__class__.__name__) + def type(self) -> MSO_FILL_TYPE: # pragma: no cover + raise NotImplementedError( + f".type property must be implemented on {self.__class__.__name__}" + ) class _BlipFill(_Fill): @@ -251,9 +251,7 @@ def gradient_angle(self): # Since the UI is consistent with trigonometry conventions, we # respect that in the API. clockwise_angle = lin.ang - counter_clockwise_angle = ( - 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle) - ) + counter_clockwise_angle = 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle) return counter_clockwise_angle @gradient_angle.setter @@ -375,8 +373,6 @@ class _GradientStop(ElementProxy): A gradient stop defines a color and a position. """ - __slots__ = ("_gs", "_color") - def __init__(self, gs): super(_GradientStop, self).__init__(gs) self._gs = gs diff --git a/pptx/dml/line.py b/src/pptx/dml/line.py similarity index 93% rename from pptx/dml/line.py rename to src/pptx/dml/line.py index 698c7f633..82be47a40 100644 --- a/pptx/dml/line.py +++ b/src/pptx/dml/line.py @@ -1,12 +1,10 @@ -# encoding: utf-8 - """DrawingML objects related to line formatting.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from ..enum.dml import MSO_FILL -from .fill import FillFormat -from ..util import Emu, lazyproperty +from pptx.dml.fill import FillFormat +from pptx.enum.dml import MSO_FILL +from pptx.util import Emu, lazyproperty class LineFormat(object): diff --git a/pptx/enum/__init__.py b/src/pptx/enum/__init__.py similarity index 100% rename from pptx/enum/__init__.py rename to src/pptx/enum/__init__.py diff --git a/src/pptx/enum/action.py b/src/pptx/enum/action.py new file mode 100644 index 000000000..bc447226f --- /dev/null +++ b/src/pptx/enum/action.py @@ -0,0 +1,71 @@ +"""Enumerations that describe click-action settings.""" + +from __future__ import annotations + +from pptx.enum.base import BaseEnum + + +class PP_ACTION_TYPE(BaseEnum): + """ + Specifies the type of a mouse action (click or hover action). + + Alias: ``PP_ACTION`` + + Example:: + + from pptx.enum.action import PP_ACTION + + assert shape.click_action.action == PP_ACTION.HYPERLINK + + MS API name: `PpActionType` + + https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx + """ + + END_SHOW = (6, "Slide show ends.") + """Slide show ends.""" + + FIRST_SLIDE = (3, "Returns to the first slide.") + """Returns to the first slide.""" + + HYPERLINK = (7, "Hyperlink.") + """Hyperlink.""" + + LAST_SLIDE = (4, "Moves to the last slide.") + """Moves to the last slide.""" + + LAST_SLIDE_VIEWED = (5, "Moves to the last slide viewed.") + """Moves to the last slide viewed.""" + + NAMED_SLIDE = (101, "Moves to slide specified by slide number.") + """Moves to slide specified by slide number.""" + + NAMED_SLIDE_SHOW = (10, "Runs the slideshow.") + """Runs the slideshow.""" + + NEXT_SLIDE = (1, "Moves to the next slide.") + """Moves to the next slide.""" + + NONE = (0, "No action is performed.") + """No action is performed.""" + + OPEN_FILE = (102, "Opens the specified file.") + """Opens the specified file.""" + + OLE_VERB = (11, "OLE Verb.") + """OLE Verb.""" + + PLAY = (12, "Begins the slideshow.") + """Begins the slideshow.""" + + PREVIOUS_SLIDE = (2, "Moves to the previous slide.") + """Moves to the previous slide.""" + + RUN_MACRO = (8, "Runs a macro.") + """Runs a macro.""" + + RUN_PROGRAM = (9, "Runs a program.") + """Runs a program.""" + + +PP_ACTION = PP_ACTION_TYPE diff --git a/src/pptx/enum/base.py b/src/pptx/enum/base.py new file mode 100644 index 000000000..1d49b9c19 --- /dev/null +++ b/src/pptx/enum/base.py @@ -0,0 +1,175 @@ +"""Base classes and other objects used by enumerations.""" + +from __future__ import annotations + +import enum +import textwrap +from typing import TYPE_CHECKING, Any, Type, TypeVar + +if TYPE_CHECKING: + from typing_extensions import Self + +_T = TypeVar("_T", bound="BaseXmlEnum") + + +class BaseEnum(int, enum.Enum): + """Base class for Enums that do not map XML attr values. + + The enum's value will be an integer, corresponding to the integer assigned the + corresponding member in the MS API enum of the same name. + """ + + def __new__(cls, ms_api_value: int, docstr: str): + self = int.__new__(cls, ms_api_value) + self._value_ = ms_api_value + self.__doc__ = docstr.strip() + return self + + def __str__(self): + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" + return f"{self.name} ({self.value})" + + +class BaseXmlEnum(int, enum.Enum): + """Base class for Enums that also map XML attr values. + + The enum's value will be an integer, corresponding to the integer assigned the + corresponding member in the MS API enum of the same name. + """ + + xml_value: str | None + + 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 + 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) -> Self: + """Enumeration member corresponding to XML attribute value `xml_value`. + + Raises `ValueError` if `xml_value` is the empty string ("") or is not an XML attribute + value registered on the enumeration. Note that enum members that do not correspond to one + of the defined values for an XML attribute have `xml_value == ""`. These + "return-value only" members cannot be automatically mapped from an XML attribute value and + must be selected explicitly by code, based on the appropriate conditions. + + Example:: + + >>> WD_PARAGRAPH_ALIGNMENT.from_xml("center") + WD_PARAGRAPH_ALIGNMENT.CENTER + + """ + # -- the empty string never maps to a member -- + member = ( + next((member for member in cls if member.xml_value == xml_value), None) + if xml_value + else None + ) + + if member is None: + raise ValueError(f"{cls.__name__} has no XML mapping for {repr(xml_value)}") + + return member + + @classmethod + def to_xml(cls: Type[_T], value: int | _T) -> str: + """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. + 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 + + @classmethod + def validate(cls: Type[_T], value: _T): + """Raise |ValueError| if `value` is not an assignable value.""" + if value not in cls: + raise ValueError(f"{value} not a member of {cls.__name__} enumeration") + + +class DocsPageFormatter(object): + """Formats a reStructuredText documention page (string) for an enumeration.""" + + def __init__(self, clsname: str, clsdict: dict[str, Any]): + self._clsname = clsname + self._clsdict = clsdict + + @property + def page_str(self): + """ + The RestructuredText documentation page for the enumeration. This is + the only API member for the class. + """ + tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s" + components = ( + self._ms_name, + self._page_title, + self._intro_text, + self._member_defs, + ) + return tmpl % components + + @property + def _intro_text(self): + """ + The docstring of the enumeration, formatted for use at the top of the + documentation page + """ + try: + cls_docstring = self._clsdict["__doc__"] + except KeyError: + cls_docstring = "" + + if cls_docstring is None: + return "" + + return textwrap.dedent(cls_docstring).strip() + + def _member_def(self, member: BaseEnum | BaseXmlEnum): + """Return an individual member definition formatted as an RST glossary entry. + + Output is wrapped to fit within 78 columns. + """ + member_docstring = textwrap.dedent(member.__doc__ or "").strip() + member_docstring = textwrap.fill( + member_docstring, + width=78, + initial_indent=" " * 4, + subsequent_indent=" " * 4, + ) + return "%s\n%s\n" % (member.name, member_docstring) + + @property + def _member_defs(self): + """ + A single string containing the aggregated member definitions section + of the documentation page + """ + members = self._clsdict["__members__"] + member_defs = [self._member_def(member) for member in members if member.name is not None] + return "\n".join(member_defs) + + @property + def _ms_name(self): + """ + The Microsoft API name for this enumeration + """ + return self._clsdict["__ms_name__"] + + @property + def _page_title(self): + """ + The title for the documentation page, formatted as code (surrounded + in double-backtics) and underlined with '=' characters + """ + title_underscore = "=" * (len(self._clsname) + 4) + return "``%s``\n%s" % (self._clsname, title_underscore) diff --git a/src/pptx/enum/chart.py b/src/pptx/enum/chart.py new file mode 100644 index 000000000..2599cf4dd --- /dev/null +++ b/src/pptx/enum/chart.py @@ -0,0 +1,492 @@ +"""Enumerations used by charts and related objects.""" + +from __future__ import annotations + +from pptx.enum.base import BaseEnum, BaseXmlEnum + + +class XL_AXIS_CROSSES(BaseXmlEnum): + """Specifies the point on an axis where the other axis crosses. + + Example:: + + from pptx.enum.chart import XL_AXIS_CROSSES + + value_axis.crosses = XL_AXIS_CROSSES.MAXIMUM + + MS API Name: `XlAxisCrosses` + + https://msdn.microsoft.com/en-us/library/office/ff745402.aspx + """ + + AUTOMATIC = (-4105, "autoZero", "The axis crossing point is set automatically, often at zero.") + """The axis crossing point is set automatically, often at zero.""" + + CUSTOM = (-4114, "", "The .crosses_at property specifies the axis crossing point.") + """The .crosses_at property specifies the axis crossing point.""" + + MAXIMUM = (2, "max", "The axis crosses at the maximum value.") + """The axis crosses at the maximum value.""" + + MINIMUM = (4, "min", "The axis crosses at the minimum value.") + """The axis crosses at the minimum value.""" + + +class XL_CATEGORY_TYPE(BaseEnum): + """Specifies the type of the category axis. + + Example:: + + from pptx.enum.chart import XL_CATEGORY_TYPE + + date_axis = chart.category_axis + assert date_axis.category_type == XL_CATEGORY_TYPE.TIME_SCALE + + MS API Name: `XlCategoryType` + + https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx + """ + + AUTOMATIC_SCALE = (-4105, "The application controls the axis type.") + """The application controls the axis type.""" + + CATEGORY_SCALE = (2, "Axis groups data by an arbitrary set of categories") + """Axis groups data by an arbitrary set of categories""" + + TIME_SCALE = (3, "Axis groups data on a time scale of days, months, or years.") + """Axis groups data on a time scale of days, months, or years.""" + + +class XL_CHART_TYPE(BaseEnum): + """Specifies the type of a chart. + + Example:: + + from pptx.enum.chart import XL_CHART_TYPE + + assert chart.chart_type == XL_CHART_TYPE.BAR_STACKED + + MS API Name: `XlChartType` + + http://msdn.microsoft.com/en-us/library/office/ff838409.aspx + """ + + THREE_D_AREA = (-4098, "3D Area.") + """3D Area.""" + + THREE_D_AREA_STACKED = (78, "3D Stacked Area.") + """3D Stacked Area.""" + + THREE_D_AREA_STACKED_100 = (79, "100% Stacked Area.") + """100% Stacked Area.""" + + THREE_D_BAR_CLUSTERED = (60, "3D Clustered Bar.") + """3D Clustered Bar.""" + + THREE_D_BAR_STACKED = (61, "3D Stacked Bar.") + """3D Stacked Bar.""" + + THREE_D_BAR_STACKED_100 = (62, "3D 100% Stacked Bar.") + """3D 100% Stacked Bar.""" + + THREE_D_COLUMN = (-4100, "3D Column.") + """3D Column.""" + + THREE_D_COLUMN_CLUSTERED = (54, "3D Clustered Column.") + """3D Clustered Column.""" + + THREE_D_COLUMN_STACKED = (55, "3D Stacked Column.") + """3D Stacked Column.""" + + THREE_D_COLUMN_STACKED_100 = (56, "3D 100% Stacked Column.") + """3D 100% Stacked Column.""" + + THREE_D_LINE = (-4101, "3D Line.") + """3D Line.""" + + THREE_D_PIE = (-4102, "3D Pie.") + """3D Pie.""" + + THREE_D_PIE_EXPLODED = (70, "Exploded 3D Pie.") + """Exploded 3D Pie.""" + + AREA = (1, "Area") + """Area""" + + AREA_STACKED = (76, "Stacked Area.") + """Stacked Area.""" + + AREA_STACKED_100 = (77, "100% Stacked Area.") + """100% Stacked Area.""" + + BAR_CLUSTERED = (57, "Clustered Bar.") + """Clustered Bar.""" + + BAR_OF_PIE = (71, "Bar of Pie.") + """Bar of Pie.""" + + BAR_STACKED = (58, "Stacked Bar.") + """Stacked Bar.""" + + BAR_STACKED_100 = (59, "100% Stacked Bar.") + """100% Stacked Bar.""" + + BUBBLE = (15, "Bubble.") + """Bubble.""" + + BUBBLE_THREE_D_EFFECT = (87, "Bubble with 3D effects.") + """Bubble with 3D effects.""" + + COLUMN_CLUSTERED = (51, "Clustered Column.") + """Clustered Column.""" + + COLUMN_STACKED = (52, "Stacked Column.") + """Stacked Column.""" + + COLUMN_STACKED_100 = (53, "100% Stacked Column.") + """100% Stacked Column.""" + + CONE_BAR_CLUSTERED = (102, "Clustered Cone Bar.") + """Clustered Cone Bar.""" + + CONE_BAR_STACKED = (103, "Stacked Cone Bar.") + """Stacked Cone Bar.""" + + CONE_BAR_STACKED_100 = (104, "100% Stacked Cone Bar.") + """100% Stacked Cone Bar.""" + + CONE_COL = (105, "3D Cone Column.") + """3D Cone Column.""" + + CONE_COL_CLUSTERED = (99, "Clustered Cone Column.") + """Clustered Cone Column.""" + + CONE_COL_STACKED = (100, "Stacked Cone Column.") + """Stacked Cone Column.""" + + CONE_COL_STACKED_100 = (101, "100% Stacked Cone Column.") + """100% Stacked Cone Column.""" + + CYLINDER_BAR_CLUSTERED = (95, "Clustered Cylinder Bar.") + """Clustered Cylinder Bar.""" + + CYLINDER_BAR_STACKED = (96, "Stacked Cylinder Bar.") + """Stacked Cylinder Bar.""" + + CYLINDER_BAR_STACKED_100 = (97, "100% Stacked Cylinder Bar.") + """100% Stacked Cylinder Bar.""" + + CYLINDER_COL = (98, "3D Cylinder Column.") + """3D Cylinder Column.""" + + CYLINDER_COL_CLUSTERED = (92, "Clustered Cone Column.") + """Clustered Cone Column.""" + + CYLINDER_COL_STACKED = (93, "Stacked Cone Column.") + """Stacked Cone Column.""" + + CYLINDER_COL_STACKED_100 = (94, "100% Stacked Cylinder Column.") + """100% Stacked Cylinder Column.""" + + DOUGHNUT = (-4120, "Doughnut.") + """Doughnut.""" + + DOUGHNUT_EXPLODED = (80, "Exploded Doughnut.") + """Exploded Doughnut.""" + + LINE = (4, "Line.") + """Line.""" + + LINE_MARKERS = (65, "Line with Markers.") + """Line with Markers.""" + + LINE_MARKERS_STACKED = (66, "Stacked Line with Markers.") + """Stacked Line with Markers.""" + + LINE_MARKERS_STACKED_100 = (67, "100% Stacked Line with Markers.") + """100% Stacked Line with Markers.""" + + LINE_STACKED = (63, "Stacked Line.") + """Stacked Line.""" + + LINE_STACKED_100 = (64, "100% Stacked Line.") + """100% Stacked Line.""" + + PIE = (5, "Pie.") + """Pie.""" + + PIE_EXPLODED = (69, "Exploded Pie.") + """Exploded Pie.""" + + PIE_OF_PIE = (68, "Pie of Pie.") + """Pie of Pie.""" + + PYRAMID_BAR_CLUSTERED = (109, "Clustered Pyramid Bar.") + """Clustered Pyramid Bar.""" + + PYRAMID_BAR_STACKED = (110, "Stacked Pyramid Bar.") + """Stacked Pyramid Bar.""" + + PYRAMID_BAR_STACKED_100 = (111, "100% Stacked Pyramid Bar.") + """100% Stacked Pyramid Bar.""" + + PYRAMID_COL = (112, "3D Pyramid Column.") + """3D Pyramid Column.""" + + PYRAMID_COL_CLUSTERED = (106, "Clustered Pyramid Column.") + """Clustered Pyramid Column.""" + + PYRAMID_COL_STACKED = (107, "Stacked Pyramid Column.") + """Stacked Pyramid Column.""" + + PYRAMID_COL_STACKED_100 = (108, "100% Stacked Pyramid Column.") + """100% Stacked Pyramid Column.""" + + RADAR = (-4151, "Radar.") + """Radar.""" + + RADAR_FILLED = (82, "Filled Radar.") + """Filled Radar.""" + + RADAR_MARKERS = (81, "Radar with Data Markers.") + """Radar with Data Markers.""" + + STOCK_HLC = (88, "High-Low-Close.") + """High-Low-Close.""" + + STOCK_OHLC = (89, "Open-High-Low-Close.") + """Open-High-Low-Close.""" + + STOCK_VHLC = (90, "Volume-High-Low-Close.") + """Volume-High-Low-Close.""" + + STOCK_VOHLC = (91, "Volume-Open-High-Low-Close.") + """Volume-Open-High-Low-Close.""" + + SURFACE = (83, "3D Surface.") + """3D Surface.""" + + SURFACE_TOP_VIEW = (85, "Surface (Top View).") + """Surface (Top View).""" + + SURFACE_TOP_VIEW_WIREFRAME = (86, "Surface (Top View wireframe).") + """Surface (Top View wireframe).""" + + SURFACE_WIREFRAME = (84, "3D Surface (wireframe).") + """3D Surface (wireframe).""" + + XY_SCATTER = (-4169, "Scatter.") + """Scatter.""" + + XY_SCATTER_LINES = (74, "Scatter with Lines.") + """Scatter with Lines.""" + + XY_SCATTER_LINES_NO_MARKERS = (75, "Scatter with Lines and No Data Markers.") + """Scatter with Lines and No Data Markers.""" + + XY_SCATTER_SMOOTH = (72, "Scatter with Smoothed Lines.") + """Scatter with Smoothed Lines.""" + + XY_SCATTER_SMOOTH_NO_MARKERS = (73, "Scatter with Smoothed Lines and No Data Markers.") + """Scatter with Smoothed Lines and No Data Markers.""" + + +class XL_DATA_LABEL_POSITION(BaseXmlEnum): + """Specifies where the data label is positioned. + + Example:: + + from pptx.enum.chart import XL_LABEL_POSITION + + data_labels = chart.plots[0].data_labels + data_labels.position = XL_LABEL_POSITION.OUTSIDE_END + + MS API Name: `XlDataLabelPosition` + + http://msdn.microsoft.com/en-us/library/office/ff745082.aspx + """ + + ABOVE = (0, "t", "The data label is positioned above the data point.") + """The data label is positioned above the data point.""" + + BELOW = (1, "b", "The data label is positioned below the data point.") + """The data label is positioned below the data point.""" + + BEST_FIT = (5, "bestFit", "Word sets the position of the data label.") + """Word sets the position of the data label.""" + + CENTER = ( + -4108, + "ctr", + "The data label is centered on the data point or inside a bar or a pie slice.", + ) + """The data label is centered on the data point or inside a bar or a pie slice.""" + + INSIDE_BASE = ( + 4, + "inBase", + "The data label is positioned inside the data point at the bottom edge.", + ) + """The data label is positioned inside the data point at the bottom edge.""" + + INSIDE_END = (3, "inEnd", "The data label is positioned inside the data point at the top edge.") + """The data label is positioned inside the data point at the top edge.""" + + LEFT = (-4131, "l", "The data label is positioned to the left of the data point.") + """The data label is positioned to the left of the data point.""" + + MIXED = (6, "", "Data labels are in multiple positions (read-only).") + """Data labels are in multiple positions (read-only).""" + + OUTSIDE_END = ( + 2, + "outEnd", + "The data label is positioned outside the data point at the top edge.", + ) + """The data label is positioned outside the data point at the top edge.""" + + RIGHT = (-4152, "r", "The data label is positioned to the right of the data point.") + """The data label is positioned to the right of the data point.""" + + +XL_LABEL_POSITION = XL_DATA_LABEL_POSITION + + +class XL_LEGEND_POSITION(BaseXmlEnum): + """Specifies the position of the legend on a chart. + + Example:: + + from pptx.enum.chart import XL_LEGEND_POSITION + + chart.has_legend = True + chart.legend.position = XL_LEGEND_POSITION.BOTTOM + + MS API Name: `XlLegendPosition` + + http://msdn.microsoft.com/en-us/library/office/ff745840.aspx + """ + + BOTTOM = (-4107, "b", "Below the chart.") + """Below the chart.""" + + CORNER = (2, "tr", "In the upper-right corner of the chart border.") + """In the upper-right corner of the chart border.""" + + CUSTOM = (-4161, "", "A custom position (read-only).") + """A custom position (read-only).""" + + LEFT = (-4131, "l", "Left of the chart.") + """Left of the chart.""" + + RIGHT = (-4152, "r", "Right of the chart.") + """Right of the chart.""" + + TOP = (-4160, "t", "Above the chart.") + """Above the chart.""" + + +class XL_MARKER_STYLE(BaseXmlEnum): + """Specifies the marker style for a point or series in a line, scatter, or radar chart. + + Example:: + + from pptx.enum.chart import XL_MARKER_STYLE + + series.marker.style = XL_MARKER_STYLE.CIRCLE + + MS API Name: `XlMarkerStyle` + + http://msdn.microsoft.com/en-us/library/office/ff197219.aspx + """ + + AUTOMATIC = (-4105, "auto", "Automatic markers") + """Automatic markers""" + + CIRCLE = (8, "circle", "Circular markers") + """Circular markers""" + + DASH = (-4115, "dash", "Long bar markers") + """Long bar markers""" + + DIAMOND = (2, "diamond", "Diamond-shaped markers") + """Diamond-shaped markers""" + + DOT = (-4118, "dot", "Short bar markers") + """Short bar markers""" + + NONE = (-4142, "none", "No markers") + """No markers""" + + PICTURE = (-4147, "picture", "Picture markers") + """Picture markers""" + + PLUS = (9, "plus", "Square markers with a plus sign") + """Square markers with a plus sign""" + + SQUARE = (1, "square", "Square markers") + """Square markers""" + + STAR = (5, "star", "Square markers with an asterisk") + """Square markers with an asterisk""" + + TRIANGLE = (3, "triangle", "Triangular markers") + """Triangular markers""" + + X = (-4168, "x", "Square markers with an X") + """Square markers with an X""" + + +class XL_TICK_MARK(BaseXmlEnum): + """Specifies a type of axis tick for a chart. + + Example:: + + from pptx.enum.chart import XL_TICK_MARK + + chart.value_axis.minor_tick_mark = XL_TICK_MARK.INSIDE + + MS API Name: `XlTickMark` + + http://msdn.microsoft.com/en-us/library/office/ff193878.aspx + """ + + CROSS = (4, "cross", "Tick mark crosses the axis") + """Tick mark crosses the axis""" + + INSIDE = (2, "in", "Tick mark appears inside the axis") + """Tick mark appears inside the axis""" + + NONE = (-4142, "none", "No tick mark") + """No tick mark""" + + OUTSIDE = (3, "out", "Tick mark appears outside the axis") + """Tick mark appears outside the axis""" + + +class XL_TICK_LABEL_POSITION(BaseXmlEnum): + """Specifies the position of tick-mark labels on a chart axis. + + Example:: + + from pptx.enum.chart import XL_TICK_LABEL_POSITION + + category_axis = chart.category_axis + category_axis.tick_label_position = XL_TICK_LABEL_POSITION.LOW + + MS API Name: `XlTickLabelPosition` + + http://msdn.microsoft.com/en-us/library/office/ff822561.aspx + """ + + HIGH = (-4127, "high", "Top or right side of the chart.") + """Top or right side of the chart.""" + + LOW = (-4134, "low", "Bottom or left side of the chart.") + """Bottom or left side of the chart.""" + + NEXT_TO_AXIS = (4, "nextTo", "Next to axis (where axis is not at either side of the chart).") + """Next to axis (where axis is not at either side of the chart).""" + + NONE = (-4142, "none", "No tick labels.") + """No tick labels.""" diff --git a/src/pptx/enum/dml.py b/src/pptx/enum/dml.py new file mode 100644 index 000000000..40d5c5cdf --- /dev/null +++ b/src/pptx/enum/dml.py @@ -0,0 +1,405 @@ +"""Enumerations used by DrawingML objects.""" + +from __future__ import annotations + +from pptx.enum.base import BaseEnum, BaseXmlEnum + + +class MSO_COLOR_TYPE(BaseEnum): + """ + Specifies the color specification scheme + + Example:: + + from pptx.enum.dml import MSO_COLOR_TYPE + + assert shape.fill.fore_color.type == MSO_COLOR_TYPE.SCHEME + + MS API Name: "MsoColorType" + + http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15).aspx + """ + + RGB = (1, "Color is specified by an |RGBColor| value.") + """Color is specified by an |RGBColor| value.""" + + SCHEME = (2, "Color is one of the preset theme colors") + """Color is one of the preset theme colors""" + + HSL = (101, "Color is specified using Hue, Saturation, and Luminosity values") + """Color is specified using Hue, Saturation, and Luminosity values""" + + PRESET = (102, "Color is specified using a named built-in color") + """Color is specified using a named built-in color""" + + SCRGB = (103, "Color is an scRGB color, a wide color gamut RGB color space") + """Color is an scRGB color, a wide color gamut RGB color space""" + + SYSTEM = ( + 104, + "Color is one specified by the operating system, such as the window background color.", + ) + """Color is one specified by the operating system, such as the window background color.""" + + +class MSO_FILL_TYPE(BaseEnum): + """ + Specifies the type of bitmap used for the fill of a shape. + + Alias: ``MSO_FILL`` + + Example:: + + from pptx.enum.dml import MSO_FILL + + assert shape.fill.type == MSO_FILL.SOLID + + MS API Name: `MsoFillType` + + http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx + """ + + BACKGROUND = ( + 5, + "The shape is transparent, such that whatever is behind the shape shows through." + " Often this is the slide background, but if a visible shape is behind, that will" + " show through.", + ) + """The shape is transparent, such that whatever is behind the shape shows through. + + Often this is the slide background, but if a visible shape is behind, that will show through. + """ + + GRADIENT = (3, "Shape is filled with a gradient") + """Shape is filled with a gradient""" + + GROUP = (101, "Shape is part of a group and should inherit the fill properties of the group.") + """Shape is part of a group and should inherit the fill properties of the group.""" + + PATTERNED = (2, "Shape is filled with a pattern") + """Shape is filled with a pattern""" + + PICTURE = (6, "Shape is filled with a bitmapped image") + """Shape is filled with a bitmapped image""" + + SOLID = (1, "Shape is filled with a solid color") + """Shape is filled with a solid color""" + + TEXTURED = (4, "Shape is filled with a texture") + """Shape is filled with a texture""" + + +MSO_FILL = MSO_FILL_TYPE + + +class MSO_LINE_DASH_STYLE(BaseXmlEnum): + """Specifies the dash style for a line. + + Alias: ``MSO_LINE`` + + Example:: + + from pptx.enum.dml import MSO_LINE + + shape.line.dash_style = MSO_LINE.DASH_DOT_DOT + + MS API name: `MsoLineDashStyle` + + https://learn.microsoft.com/en-us/office/vba/api/Office.MsoLineDashStyle + """ + + DASH = (4, "dash", "Line consists of dashes only.") + """Line consists of dashes only.""" + + DASH_DOT = (5, "dashDot", "Line is a dash-dot pattern.") + """Line is a dash-dot pattern.""" + + DASH_DOT_DOT = (6, "lgDashDotDot", "Line is a dash-dot-dot pattern.") + """Line is a dash-dot-dot pattern.""" + + LONG_DASH = (7, "lgDash", "Line consists of long dashes.") + """Line consists of long dashes.""" + + LONG_DASH_DOT = (8, "lgDashDot", "Line is a long dash-dot pattern.") + """Line is a long dash-dot pattern.""" + + ROUND_DOT = (3, "sysDot", "Line is made up of round dots.") + """Line is made up of round dots.""" + + SOLID = (1, "solid", "Line is solid.") + """Line is solid.""" + + SQUARE_DOT = (2, "sysDash", "Line is made up of square dots.") + """Line is made up of square dots.""" + + DASH_STYLE_MIXED = (-2, "", "Not supported.") + """Return value only, indicating more than one dash style applies.""" + + +MSO_LINE = MSO_LINE_DASH_STYLE + + +class MSO_PATTERN_TYPE(BaseXmlEnum): + """Specifies the fill pattern used in a shape. + + Alias: ``MSO_PATTERN`` + + Example:: + + from pptx.enum.dml import MSO_PATTERN + + fill = shape.fill + fill.patterned() + fill.pattern = MSO_PATTERN.WAVE + + MS API Name: `MsoPatternType` + + https://learn.microsoft.com/en-us/office/vba/api/Office.MsoPatternType + """ + + CROSS = (51, "cross", "Cross") + """Cross""" + + DARK_DOWNWARD_DIAGONAL = (15, "dkDnDiag", "Dark Downward Diagonal") + """Dark Downward Diagonal""" + + DARK_HORIZONTAL = (13, "dkHorz", "Dark Horizontal") + """Dark Horizontal""" + + DARK_UPWARD_DIAGONAL = (16, "dkUpDiag", "Dark Upward Diagonal") + """Dark Upward Diagonal""" + + DARK_VERTICAL = (14, "dkVert", "Dark Vertical") + """Dark Vertical""" + + DASHED_DOWNWARD_DIAGONAL = (28, "dashDnDiag", "Dashed Downward Diagonal") + """Dashed Downward Diagonal""" + + DASHED_HORIZONTAL = (32, "dashHorz", "Dashed Horizontal") + """Dashed Horizontal""" + + DASHED_UPWARD_DIAGONAL = (27, "dashUpDiag", "Dashed Upward Diagonal") + """Dashed Upward Diagonal""" + + DASHED_VERTICAL = (31, "dashVert", "Dashed Vertical") + """Dashed Vertical""" + + DIAGONAL_BRICK = (40, "diagBrick", "Diagonal Brick") + """Diagonal Brick""" + + DIAGONAL_CROSS = (54, "diagCross", "Diagonal Cross") + """Diagonal Cross""" + + DIVOT = (46, "divot", "Pattern Divot") + """Pattern Divot""" + + DOTTED_DIAMOND = (24, "dotDmnd", "Dotted Diamond") + """Dotted Diamond""" + + DOTTED_GRID = (45, "dotGrid", "Dotted Grid") + """Dotted Grid""" + + DOWNWARD_DIAGONAL = (52, "dnDiag", "Downward Diagonal") + """Downward Diagonal""" + + HORIZONTAL = (49, "horz", "Horizontal") + """Horizontal""" + + HORIZONTAL_BRICK = (35, "horzBrick", "Horizontal Brick") + """Horizontal Brick""" + + LARGE_CHECKER_BOARD = (36, "lgCheck", "Large Checker Board") + """Large Checker Board""" + + LARGE_CONFETTI = (33, "lgConfetti", "Large Confetti") + """Large Confetti""" + + LARGE_GRID = (34, "lgGrid", "Large Grid") + """Large Grid""" + + LIGHT_DOWNWARD_DIAGONAL = (21, "ltDnDiag", "Light Downward Diagonal") + """Light Downward Diagonal""" + + LIGHT_HORIZONTAL = (19, "ltHorz", "Light Horizontal") + """Light Horizontal""" + + LIGHT_UPWARD_DIAGONAL = (22, "ltUpDiag", "Light Upward Diagonal") + """Light Upward Diagonal""" + + LIGHT_VERTICAL = (20, "ltVert", "Light Vertical") + """Light Vertical""" + + NARROW_HORIZONTAL = (30, "narHorz", "Narrow Horizontal") + """Narrow Horizontal""" + + NARROW_VERTICAL = (29, "narVert", "Narrow Vertical") + """Narrow Vertical""" + + OUTLINED_DIAMOND = (41, "openDmnd", "Outlined Diamond") + """Outlined Diamond""" + + PERCENT_10 = (2, "pct10", "10% of the foreground color.") + """10% of the foreground color.""" + + PERCENT_20 = (3, "pct20", "20% of the foreground color.") + """20% of the foreground color.""" + + PERCENT_25 = (4, "pct25", "25% of the foreground color.") + """25% of the foreground color.""" + + PERCENT_30 = (5, "pct30", "30% of the foreground color.") + """30% of the foreground color.""" + + ERCENT_40 = (6, "pct40", "40% of the foreground color.") + """40% of the foreground color.""" + + PERCENT_5 = (1, "pct5", "5% of the foreground color.") + """5% of the foreground color.""" + + PERCENT_50 = (7, "pct50", "50% of the foreground color.") + """50% of the foreground color.""" + + PERCENT_60 = (8, "pct60", "60% of the foreground color.") + """60% of the foreground color.""" + + PERCENT_70 = (9, "pct70", "70% of the foreground color.") + """70% of the foreground color.""" + + PERCENT_75 = (10, "pct75", "75% of the foreground color.") + """75% of the foreground color.""" + + PERCENT_80 = (11, "pct80", "80% of the foreground color.") + """80% of the foreground color.""" + + PERCENT_90 = (12, "pct90", "90% of the foreground color.") + """90% of the foreground color.""" + + PLAID = (42, "plaid", "Plaid") + """Plaid""" + + SHINGLE = (47, "shingle", "Shingle") + """Shingle""" + + SMALL_CHECKER_BOARD = (17, "smCheck", "Small Checker Board") + """Small Checker Board""" + + SMALL_CONFETTI = (37, "smConfetti", "Small Confetti") + """Small Confetti""" + + SMALL_GRID = (23, "smGrid", "Small Grid") + """Small Grid""" + + SOLID_DIAMOND = (39, "solidDmnd", "Solid Diamond") + """Solid Diamond""" + + SPHERE = (43, "sphere", "Sphere") + """Sphere""" + + TRELLIS = (18, "trellis", "Trellis") + """Trellis""" + + UPWARD_DIAGONAL = (53, "upDiag", "Upward Diagonal") + """Upward Diagonal""" + + VERTICAL = (50, "vert", "Vertical") + """Vertical""" + + WAVE = (48, "wave", "Wave") + """Wave""" + + WEAVE = (44, "weave", "Weave") + """Weave""" + + WIDE_DOWNWARD_DIAGONAL = (25, "wdDnDiag", "Wide Downward Diagonal") + """Wide Downward Diagonal""" + + WIDE_UPWARD_DIAGONAL = (26, "wdUpDiag", "Wide Upward Diagonal") + """Wide Upward Diagonal""" + + ZIG_ZAG = (38, "zigZag", "Zig Zag") + """Zig Zag""" + + MIXED = (-2, "", "Mixed pattern (read-only).") + """Mixed pattern (read-only).""" + + +MSO_PATTERN = MSO_PATTERN_TYPE + + +class MSO_THEME_COLOR_INDEX(BaseXmlEnum): + """An Office theme color, one of those shown in the color gallery on the formatting ribbon. + + Alias: ``MSO_THEME_COLOR`` + + Example:: + + from pptx.enum.dml import MSO_THEME_COLOR + + shape.fill.solid() + shape.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_1 + + MS API Name: `MsoThemeColorIndex` + + http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx + """ + + NOT_THEME_COLOR = (0, "", "Indicates the color is not a theme color.") + """Indicates the color is not a theme color.""" + + ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.") + """Specifies the Accent 1 theme color.""" + + ACCENT_2 = (6, "accent2", "Specifies the Accent 2 theme color.") + """Specifies the Accent 2 theme color.""" + + ACCENT_3 = (7, "accent3", "Specifies the Accent 3 theme color.") + """Specifies the Accent 3 theme color.""" + + ACCENT_4 = (8, "accent4", "Specifies the Accent 4 theme color.") + """Specifies the Accent 4 theme color.""" + + ACCENT_5 = (9, "accent5", "Specifies the Accent 5 theme color.") + """Specifies the Accent 5 theme color.""" + + ACCENT_6 = (10, "accent6", "Specifies the Accent 6 theme color.") + """Specifies the Accent 6 theme color.""" + + BACKGROUND_1 = (14, "bg1", "Specifies the Background 1 theme color.") + """Specifies the Background 1 theme color.""" + + BACKGROUND_2 = (16, "bg2", "Specifies the Background 2 theme color.") + """Specifies the Background 2 theme color.""" + + DARK_1 = (1, "dk1", "Specifies the Dark 1 theme color.") + """Specifies the Dark 1 theme color.""" + + DARK_2 = (3, "dk2", "Specifies the Dark 2 theme color.") + """Specifies the Dark 2 theme color.""" + + FOLLOWED_HYPERLINK = (12, "folHlink", "Specifies the theme color for a clicked hyperlink.") + """Specifies the theme color for a clicked hyperlink.""" + + HYPERLINK = (11, "hlink", "Specifies the theme color for a hyperlink.") + """Specifies the theme color for a hyperlink.""" + + LIGHT_1 = (2, "lt1", "Specifies the Light 1 theme color.") + """Specifies the Light 1 theme color.""" + + LIGHT_2 = (4, "lt2", "Specifies the Light 2 theme color.") + """Specifies the Light 2 theme color.""" + + TEXT_1 = (13, "tx1", "Specifies the Text 1 theme color.") + """Specifies the Text 1 theme color.""" + + TEXT_2 = (15, "tx2", "Specifies the Text 2 theme color.") + """Specifies the Text 2 theme color.""" + + MIXED = ( + -2, + "", + "Indicates multiple theme colors are used, such as in a group shape (read-only).", + ) + """Indicates multiple theme colors are used, such as in a group shape (read-only).""" + + +MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX diff --git a/src/pptx/enum/lang.py b/src/pptx/enum/lang.py new file mode 100644 index 000000000..a6bc1c8b4 --- /dev/null +++ b/src/pptx/enum/lang.py @@ -0,0 +1,685 @@ +"""Enumerations used for specifying language.""" + +from __future__ import annotations + +from pptx.enum.base import BaseXmlEnum + + +class MSO_LANGUAGE_ID(BaseXmlEnum): + """ + Specifies the language identifier. + + Example:: + + from pptx.enum.lang import MSO_LANGUAGE_ID + + font.language_id = MSO_LANGUAGE_ID.POLISH + + MS API Name: `MsoLanguageId` + + https://msdn.microsoft.com/en-us/library/office/ff862134.aspx + """ + + NONE = (0, "", "No language specified.") + """No language specified.""" + + AFRIKAANS = (1078, "af-ZA", "The Afrikaans language.") + """The Afrikaans language.""" + + ALBANIAN = (1052, "sq-AL", "The Albanian language.") + """The Albanian language.""" + + AMHARIC = (1118, "am-ET", "The Amharic language.") + """The Amharic language.""" + + ARABIC = (1025, "ar-SA", "The Arabic language.") + """The Arabic language.""" + + ARABIC_ALGERIA = (5121, "ar-DZ", "The Arabic Algeria language.") + """The Arabic Algeria language.""" + + ARABIC_BAHRAIN = (15361, "ar-BH", "The Arabic Bahrain language.") + """The Arabic Bahrain language.""" + + ARABIC_EGYPT = (3073, "ar-EG", "The Arabic Egypt language.") + """The Arabic Egypt language.""" + + ARABIC_IRAQ = (2049, "ar-IQ", "The Arabic Iraq language.") + """The Arabic Iraq language.""" + + ARABIC_JORDAN = (11265, "ar-JO", "The Arabic Jordan language.") + """The Arabic Jordan language.""" + + ARABIC_KUWAIT = (13313, "ar-KW", "The Arabic Kuwait language.") + """The Arabic Kuwait language.""" + + ARABIC_LEBANON = (12289, "ar-LB", "The Arabic Lebanon language.") + """The Arabic Lebanon language.""" + + ARABIC_LIBYA = (4097, "ar-LY", "The Arabic Libya language.") + """The Arabic Libya language.""" + + ARABIC_MOROCCO = (6145, "ar-MA", "The Arabic Morocco language.") + """The Arabic Morocco language.""" + + ARABIC_OMAN = (8193, "ar-OM", "The Arabic Oman language.") + """The Arabic Oman language.""" + + ARABIC_QATAR = (16385, "ar-QA", "The Arabic Qatar language.") + """The Arabic Qatar language.""" + + ARABIC_SYRIA = (10241, "ar-SY", "The Arabic Syria language.") + """The Arabic Syria language.""" + + ARABIC_TUNISIA = (7169, "ar-TN", "The Arabic Tunisia language.") + """The Arabic Tunisia language.""" + + ARABIC_UAE = (14337, "ar-AE", "The Arabic UAE language.") + """The Arabic UAE language.""" + + ARABIC_YEMEN = (9217, "ar-YE", "The Arabic Yemen language.") + """The Arabic Yemen language.""" + + ARMENIAN = (1067, "hy-AM", "The Armenian language.") + """The Armenian language.""" + + ASSAMESE = (1101, "as-IN", "The Assamese language.") + """The Assamese language.""" + + AZERI_CYRILLIC = (2092, "az-AZ", "The Azeri Cyrillic language.") + """The Azeri Cyrillic language.""" + + AZERI_LATIN = (1068, "az-Latn-AZ", "The Azeri Latin language.") + """The Azeri Latin language.""" + + BASQUE = (1069, "eu-ES", "The Basque language.") + """The Basque language.""" + + BELGIAN_DUTCH = (2067, "nl-BE", "The Belgian Dutch language.") + """The Belgian Dutch language.""" + + BELGIAN_FRENCH = (2060, "fr-BE", "The Belgian French language.") + """The Belgian French language.""" + + BENGALI = (1093, "bn-IN", "The Bengali language.") + """The Bengali language.""" + + BOSNIAN = (4122, "hr-BA", "The Bosnian language.") + """The Bosnian language.""" + + BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC = ( + 8218, + "bs-BA", + "The Bosnian Bosnia Herzegovina Cyrillic language.", + ) + """The Bosnian Bosnia Herzegovina Cyrillic language.""" + + BOSNIAN_BOSNIA_HERZEGOVINA_LATIN = ( + 5146, + "bs-Latn-BA", + "The Bosnian Bosnia Herzegovina Latin language.", + ) + """The Bosnian Bosnia Herzegovina Latin language.""" + + BRAZILIAN_PORTUGUESE = (1046, "pt-BR", "The Brazilian Portuguese language.") + """The Brazilian Portuguese language.""" + + BULGARIAN = (1026, "bg-BG", "The Bulgarian language.") + """The Bulgarian language.""" + + BURMESE = (1109, "my-MM", "The Burmese language.") + """The Burmese language.""" + + BYELORUSSIAN = (1059, "be-BY", "The Byelorussian language.") + """The Byelorussian language.""" + + CATALAN = (1027, "ca-ES", "The Catalan language.") + """The Catalan language.""" + + CHEROKEE = (1116, "chr-US", "The Cherokee language.") + """The Cherokee language.""" + + CHINESE_HONG_KONG_SAR = (3076, "zh-HK", "The Chinese Hong Kong SAR language.") + """The Chinese Hong Kong SAR language.""" + + CHINESE_MACAO_SAR = (5124, "zh-MO", "The Chinese Macao SAR language.") + """The Chinese Macao SAR language.""" + + CHINESE_SINGAPORE = (4100, "zh-SG", "The Chinese Singapore language.") + """The Chinese Singapore language.""" + + CROATIAN = (1050, "hr-HR", "The Croatian language.") + """The Croatian language.""" + + CZECH = (1029, "cs-CZ", "The Czech language.") + """The Czech language.""" + + DANISH = (1030, "da-DK", "The Danish language.") + """The Danish language.""" + + DIVEHI = (1125, "div-MV", "The Divehi language.") + """The Divehi language.""" + + DUTCH = (1043, "nl-NL", "The Dutch language.") + """The Dutch language.""" + + EDO = (1126, "bin-NG", "The Edo language.") + """The Edo language.""" + + ENGLISH_AUS = (3081, "en-AU", "The English AUS language.") + """The English AUS language.""" + + ENGLISH_BELIZE = (10249, "en-BZ", "The English Belize language.") + """The English Belize language.""" + + ENGLISH_CANADIAN = (4105, "en-CA", "The English Canadian language.") + """The English Canadian language.""" + + ENGLISH_CARIBBEAN = (9225, "en-CB", "The English Caribbean language.") + """The English Caribbean language.""" + + ENGLISH_INDONESIA = (14345, "en-ID", "The English Indonesia language.") + """The English Indonesia language.""" + + ENGLISH_IRELAND = (6153, "en-IE", "The English Ireland language.") + """The English Ireland language.""" + + ENGLISH_JAMAICA = (8201, "en-JA", "The English Jamaica language.") + """The English Jamaica language.""" + + ENGLISH_NEW_ZEALAND = (5129, "en-NZ", "The English NewZealand language.") + """The English NewZealand language.""" + + ENGLISH_PHILIPPINES = (13321, "en-PH", "The English Philippines language.") + """The English Philippines language.""" + + ENGLISH_SOUTH_AFRICA = (7177, "en-ZA", "The English South Africa language.") + """The English South Africa language.""" + + ENGLISH_TRINIDAD_TOBAGO = (11273, "en-TT", "The English Trinidad Tobago language.") + """The English Trinidad Tobago language.""" + + ENGLISH_UK = (2057, "en-GB", "The English UK language.") + """The English UK language.""" + + ENGLISH_US = (1033, "en-US", "The English US language.") + """The English US language.""" + + ENGLISH_ZIMBABWE = (12297, "en-ZW", "The English Zimbabwe language.") + """The English Zimbabwe language.""" + + ESTONIAN = (1061, "et-EE", "The Estonian language.") + """The Estonian language.""" + + FAEROESE = (1080, "fo-FO", "The Faeroese language.") + """The Faeroese language.""" + + FARSI = (1065, "fa-IR", "The Farsi language.") + """The Farsi language.""" + + FILIPINO = (1124, "fil-PH", "The Filipino language.") + """The Filipino language.""" + + FINNISH = (1035, "fi-FI", "The Finnish language.") + """The Finnish language.""" + + FRANCH_CONGO_DRC = (9228, "fr-CD", "The French Congo DRC language.") + """The French Congo DRC language.""" + + FRENCH = (1036, "fr-FR", "The French language.") + """The French language.""" + + FRENCH_CAMEROON = (11276, "fr-CM", "The French Cameroon language.") + """The French Cameroon language.""" + + FRENCH_CANADIAN = (3084, "fr-CA", "The French Canadian language.") + """The French Canadian language.""" + + FRENCH_COTED_IVOIRE = (12300, "fr-CI", "The French Coted Ivoire language.") + """The French Coted Ivoire language.""" + + FRENCH_HAITI = (15372, "fr-HT", "The French Haiti language.") + """The French Haiti language.""" + + FRENCH_LUXEMBOURG = (5132, "fr-LU", "The French Luxembourg language.") + """The French Luxembourg language.""" + + FRENCH_MALI = (13324, "fr-ML", "The French Mali language.") + """The French Mali language.""" + + FRENCH_MONACO = (6156, "fr-MC", "The French Monaco language.") + """The French Monaco language.""" + + FRENCH_MOROCCO = (14348, "fr-MA", "The French Morocco language.") + """The French Morocco language.""" + + FRENCH_REUNION = (8204, "fr-RE", "The French Reunion language.") + """The French Reunion language.""" + + FRENCH_SENEGAL = (10252, "fr-SN", "The French Senegal language.") + """The French Senegal language.""" + + FRENCH_WEST_INDIES = (7180, "fr-WINDIES", "The French West Indies language.") + """The French West Indies language.""" + + FRISIAN_NETHERLANDS = (1122, "fy-NL", "The Frisian Netherlands language.") + """The Frisian Netherlands language.""" + + FULFULDE = (1127, "ff-NG", "The Fulfulde language.") + """The Fulfulde language.""" + + GAELIC_IRELAND = (2108, "ga-IE", "The Gaelic Ireland language.") + """The Gaelic Ireland language.""" + + GAELIC_SCOTLAND = (1084, "en-US", "The Gaelic Scotland language.") + """The Gaelic Scotland language.""" + + GALICIAN = (1110, "gl-ES", "The Galician language.") + """The Galician language.""" + + GEORGIAN = (1079, "ka-GE", "The Georgian language.") + """The Georgian language.""" + + GERMAN = (1031, "de-DE", "The German language.") + """The German language.""" + + GERMAN_AUSTRIA = (3079, "de-AT", "The German Austria language.") + """The German Austria language.""" + + GERMAN_LIECHTENSTEIN = (5127, "de-LI", "The German Liechtenstein language.") + """The German Liechtenstein language.""" + + GERMAN_LUXEMBOURG = (4103, "de-LU", "The German Luxembourg language.") + """The German Luxembourg language.""" + + GREEK = (1032, "el-GR", "The Greek language.") + """The Greek language.""" + + GUARANI = (1140, "gn-PY", "The Guarani language.") + """The Guarani language.""" + + GUJARATI = (1095, "gu-IN", "The Gujarati language.") + """The Gujarati language.""" + + HAUSA = (1128, "ha-NG", "The Hausa language.") + """The Hausa language.""" + + HAWAIIAN = (1141, "haw-US", "The Hawaiian language.") + """The Hawaiian language.""" + + HEBREW = (1037, "he-IL", "The Hebrew language.") + """The Hebrew language.""" + + HINDI = (1081, "hi-IN", "The Hindi language.") + """The Hindi language.""" + + HUNGARIAN = (1038, "hu-HU", "The Hungarian language.") + """The Hungarian language.""" + + IBIBIO = (1129, "ibb-NG", "The Ibibio language.") + """The Ibibio language.""" + + ICELANDIC = (1039, "is-IS", "The Icelandic language.") + """The Icelandic language.""" + + IGBO = (1136, "ig-NG", "The Igbo language.") + """The Igbo language.""" + + INDONESIAN = (1057, "id-ID", "The Indonesian language.") + """The Indonesian language.""" + + INUKTITUT = (1117, "iu-Cans-CA", "The Inuktitut language.") + """The Inuktitut language.""" + + ITALIAN = (1040, "it-IT", "The Italian language.") + """The Italian language.""" + + JAPANESE = (1041, "ja-JP", "The Japanese language.") + """The Japanese language.""" + + KANNADA = (1099, "kn-IN", "The Kannada language.") + """The Kannada language.""" + + KANURI = (1137, "kr-NG", "The Kanuri language.") + """The Kanuri language.""" + + KASHMIRI = (1120, "ks-Arab", "The Kashmiri language.") + """The Kashmiri language.""" + + KASHMIRI_DEVANAGARI = (2144, "ks-Deva", "The Kashmiri Devanagari language.") + """The Kashmiri Devanagari language.""" + + KAZAKH = (1087, "kk-KZ", "The Kazakh language.") + """The Kazakh language.""" + + KHMER = (1107, "kh-KH", "The Khmer language.") + """The Khmer language.""" + + KIRGHIZ = (1088, "ky-KG", "The Kirghiz language.") + """The Kirghiz language.""" + + KONKANI = (1111, "kok-IN", "The Konkani language.") + """The Konkani language.""" + + KOREAN = (1042, "ko-KR", "The Korean language.") + """The Korean language.""" + + KYRGYZ = (1088, "ky-KG", "The Kyrgyz language.") + """The Kyrgyz language.""" + + LAO = (1108, "lo-LA", "The Lao language.") + """The Lao language.""" + + LATIN = (1142, "la-Latn", "The Latin language.") + """The Latin language.""" + + LATVIAN = (1062, "lv-LV", "The Latvian language.") + """The Latvian language.""" + + LITHUANIAN = (1063, "lt-LT", "The Lithuanian language.") + """The Lithuanian language.""" + + MACEDONINAN_FYROM = (1071, "mk-MK", "The Macedonian FYROM language.") + """The Macedonian FYROM language.""" + + MALAY_BRUNEI_DARUSSALAM = (2110, "ms-BN", "The Malay Brunei Darussalam language.") + """The Malay Brunei Darussalam language.""" + + MALAYALAM = (1100, "ml-IN", "The Malayalam language.") + """The Malayalam language.""" + + MALAYSIAN = (1086, "ms-MY", "The Malaysian language.") + """The Malaysian language.""" + + MALTESE = (1082, "mt-MT", "The Maltese language.") + """The Maltese language.""" + + MANIPURI = (1112, "mni-IN", "The Manipuri language.") + """The Manipuri language.""" + + MAORI = (1153, "mi-NZ", "The Maori language.") + """The Maori language.""" + + MARATHI = (1102, "mr-IN", "The Marathi language.") + """The Marathi language.""" + + MEXICAN_SPANISH = (2058, "es-MX", "The Mexican Spanish language.") + """The Mexican Spanish language.""" + + MONGOLIAN = (1104, "mn-MN", "The Mongolian language.") + """The Mongolian language.""" + + NEPALI = (1121, "ne-NP", "The Nepali language.") + """The Nepali language.""" + + NO_PROOFING = (1024, "en-US", "No proofing.") + """No proofing.""" + + NORWEGIAN_BOKMOL = (1044, "nb-NO", "The Norwegian Bokmol language.") + """The Norwegian Bokmol language.""" + + NORWEGIAN_NYNORSK = (2068, "nn-NO", "The Norwegian Nynorsk language.") + """The Norwegian Nynorsk language.""" + + ORIYA = (1096, "or-IN", "The Oriya language.") + """The Oriya language.""" + + OROMO = (1138, "om-Ethi-ET", "The Oromo language.") + """The Oromo language.""" + + PASHTO = (1123, "ps-AF", "The Pashto language.") + """The Pashto language.""" + + POLISH = (1045, "pl-PL", "The Polish language.") + """The Polish language.""" + + PORTUGUESE = (2070, "pt-PT", "The Portuguese language.") + """The Portuguese language.""" + + PUNJABI = (1094, "pa-IN", "The Punjabi language.") + """The Punjabi language.""" + + QUECHUA_BOLIVIA = (1131, "quz-BO", "The Quechua Bolivia language.") + """The Quechua Bolivia language.""" + + QUECHUA_ECUADOR = (2155, "quz-EC", "The Quechua Ecuador language.") + """The Quechua Ecuador language.""" + + QUECHUA_PERU = (3179, "quz-PE", "The Quechua Peru language.") + """The Quechua Peru language.""" + + RHAETO_ROMANIC = (1047, "rm-CH", "The Rhaeto Romanic language.") + """The Rhaeto Romanic language.""" + + ROMANIAN = (1048, "ro-RO", "The Romanian language.") + """The Romanian language.""" + + ROMANIAN_MOLDOVA = (2072, "ro-MO", "The Romanian Moldova language.") + """The Romanian Moldova language.""" + + RUSSIAN = (1049, "ru-RU", "The Russian language.") + """The Russian language.""" + + RUSSIAN_MOLDOVA = (2073, "ru-MO", "The Russian Moldova language.") + """The Russian Moldova language.""" + + SAMI_LAPPISH = (1083, "se-NO", "The Sami Lappish language.") + """The Sami Lappish language.""" + + SANSKRIT = (1103, "sa-IN", "The Sanskrit language.") + """The Sanskrit language.""" + + SEPEDI = (1132, "ns-ZA", "The Sepedi language.") + """The Sepedi language.""" + + SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC = ( + 7194, + "sr-BA", + "The Serbian Bosnia Herzegovina Cyrillic language.", + ) + """The Serbian Bosnia Herzegovina Cyrillic language.""" + + SERBIAN_BOSNIA_HERZEGOVINA_LATIN = ( + 6170, + "sr-Latn-BA", + "The Serbian Bosnia Herzegovina Latin language.", + ) + """The Serbian Bosnia Herzegovina Latin language.""" + + SERBIAN_CYRILLIC = (3098, "sr-SP", "The Serbian Cyrillic language.") + """The Serbian Cyrillic language.""" + + SERBIAN_LATIN = (2074, "sr-Latn-CS", "The Serbian Latin language.") + """The Serbian Latin language.""" + + SESOTHO = (1072, "st-ZA", "The Sesotho language.") + """The Sesotho language.""" + + SIMPLIFIED_CHINESE = (2052, "zh-CN", "The Simplified Chinese language.") + """The Simplified Chinese language.""" + + SINDHI = (1113, "sd-Deva-IN", "The Sindhi language.") + """The Sindhi language.""" + + SINDHI_PAKISTAN = (2137, "sd-Arab-PK", "The Sindhi Pakistan language.") + """The Sindhi Pakistan language.""" + + SINHALESE = (1115, "si-LK", "The Sinhalese language.") + """The Sinhalese language.""" + + SLOVAK = (1051, "sk-SK", "The Slovak language.") + """The Slovak language.""" + + SLOVENIAN = (1060, "sl-SI", "The Slovenian language.") + """The Slovenian language.""" + + SOMALI = (1143, "so-SO", "The Somali language.") + """The Somali language.""" + + SORBIAN = (1070, "wen-DE", "The Sorbian language.") + """The Sorbian language.""" + + SPANISH = (1034, "es-ES_tradnl", "The Spanish language.") + """The Spanish language.""" + + SPANISH_ARGENTINA = (11274, "es-AR", "The Spanish Argentina language.") + """The Spanish Argentina language.""" + + SPANISH_BOLIVIA = (16394, "es-BO", "The Spanish Bolivia language.") + """The Spanish Bolivia language.""" + + SPANISH_CHILE = (13322, "es-CL", "The Spanish Chile language.") + """The Spanish Chile language.""" + + SPANISH_COLOMBIA = (9226, "es-CO", "The Spanish Colombia language.") + """The Spanish Colombia language.""" + + SPANISH_COSTA_RICA = (5130, "es-CR", "The Spanish Costa Rica language.") + """The Spanish Costa Rica language.""" + + SPANISH_DOMINICAN_REPUBLIC = (7178, "es-DO", "The Spanish Dominican Republic language.") + """The Spanish Dominican Republic language.""" + + SPANISH_ECUADOR = (12298, "es-EC", "The Spanish Ecuador language.") + """The Spanish Ecuador language.""" + + SPANISH_EL_SALVADOR = (17418, "es-SV", "The Spanish El Salvador language.") + """The Spanish El Salvador language.""" + + SPANISH_GUATEMALA = (4106, "es-GT", "The Spanish Guatemala language.") + """The Spanish Guatemala language.""" + + SPANISH_HONDURAS = (18442, "es-HN", "The Spanish Honduras language.") + """The Spanish Honduras language.""" + + SPANISH_MODERN_SORT = (3082, "es-ES", "The Spanish Modern Sort language.") + """The Spanish Modern Sort language.""" + + SPANISH_NICARAGUA = (19466, "es-NI", "The Spanish Nicaragua language.") + """The Spanish Nicaragua language.""" + + SPANISH_PANAMA = (6154, "es-PA", "The Spanish Panama language.") + """The Spanish Panama language.""" + + SPANISH_PARAGUAY = (15370, "es-PY", "The Spanish Paraguay language.") + """The Spanish Paraguay language.""" + + SPANISH_PERU = (10250, "es-PE", "The Spanish Peru language.") + """The Spanish Peru language.""" + + SPANISH_PUERTO_RICO = (20490, "es-PR", "The Spanish Puerto Rico language.") + """The Spanish Puerto Rico language.""" + + SPANISH_URUGUAY = (14346, "es-UR", "The Spanish Uruguay language.") + """The Spanish Uruguay language.""" + + SPANISH_VENEZUELA = (8202, "es-VE", "The Spanish Venezuela language.") + """The Spanish Venezuela language.""" + + SUTU = (1072, "st-ZA", "The Sutu language.") + """The Sutu language.""" + + SWAHILI = (1089, "sw-KE", "The Swahili language.") + """The Swahili language.""" + + SWEDISH = (1053, "sv-SE", "The Swedish language.") + """The Swedish language.""" + + SWEDISH_FINLAND = (2077, "sv-FI", "The Swedish Finland language.") + """The Swedish Finland language.""" + + SWISS_FRENCH = (4108, "fr-CH", "The Swiss French language.") + """The Swiss French language.""" + + SWISS_GERMAN = (2055, "de-CH", "The Swiss German language.") + """The Swiss German language.""" + + SWISS_ITALIAN = (2064, "it-CH", "The Swiss Italian language.") + """The Swiss Italian language.""" + + SYRIAC = (1114, "syr-SY", "The Syriac language.") + """The Syriac language.""" + + TAJIK = (1064, "tg-TJ", "The Tajik language.") + """The Tajik language.""" + + TAMAZIGHT = (1119, "tzm-Arab-MA", "The Tamazight language.") + """The Tamazight language.""" + + TAMAZIGHT_LATIN = (2143, "tmz-DZ", "The Tamazight Latin language.") + """The Tamazight Latin language.""" + + TAMIL = (1097, "ta-IN", "The Tamil language.") + """The Tamil language.""" + + TATAR = (1092, "tt-RU", "The Tatar language.") + """The Tatar language.""" + + TELUGU = (1098, "te-IN", "The Telugu language.") + """The Telugu language.""" + + THAI = (1054, "th-TH", "The Thai language.") + """The Thai language.""" + + TIBETAN = (1105, "bo-CN", "The Tibetan language.") + """The Tibetan language.""" + + TIGRIGNA_ERITREA = (2163, "ti-ER", "The Tigrigna Eritrea language.") + """The Tigrigna Eritrea language.""" + + TIGRIGNA_ETHIOPIC = (1139, "ti-ET", "The Tigrigna Ethiopic language.") + """The Tigrigna Ethiopic language.""" + + TRADITIONAL_CHINESE = (1028, "zh-TW", "The Traditional Chinese language.") + """The Traditional Chinese language.""" + + TSONGA = (1073, "ts-ZA", "The Tsonga language.") + """The Tsonga language.""" + + TSWANA = (1074, "tn-ZA", "The Tswana language.") + """The Tswana language.""" + + TURKISH = (1055, "tr-TR", "The Turkish language.") + """The Turkish language.""" + + TURKMEN = (1090, "tk-TM", "The Turkmen language.") + """The Turkmen language.""" + + UKRAINIAN = (1058, "uk-UA", "The Ukrainian language.") + """The Ukrainian language.""" + + URDU = (1056, "ur-PK", "The Urdu language.") + """The Urdu language.""" + + UZBEK_CYRILLIC = (2115, "uz-UZ", "The Uzbek Cyrillic language.") + """The Uzbek Cyrillic language.""" + + UZBEK_LATIN = (1091, "uz-Latn-UZ", "The Uzbek Latin language.") + """The Uzbek Latin language.""" + + VENDA = (1075, "ve-ZA", "The Venda language.") + """The Venda language.""" + + VIETNAMESE = (1066, "vi-VN", "The Vietnamese language.") + """The Vietnamese language.""" + + WELSH = (1106, "cy-GB", "The Welsh language.") + """The Welsh language.""" + + XHOSA = (1076, "xh-ZA", "The Xhosa language.") + """The Xhosa language.""" + + YI = (1144, "ii-CN", "The Yi language.") + """The Yi language.""" + + YIDDISH = (1085, "yi-Hebr", "The Yiddish language.") + """The Yiddish language.""" + + YORUBA = (1130, "yo-NG", "The Yoruba language.") + """The Yoruba language.""" + + ZULU = (1077, "zu-ZA", "The Zulu language.") + """The Zulu language.""" + + MIXED = (-2, "", "More than one language in specified range (read-only).") + """More than one language in specified range (read-only).""" diff --git a/src/pptx/enum/shapes.py b/src/pptx/enum/shapes.py new file mode 100644 index 000000000..86f521f40 --- /dev/null +++ b/src/pptx/enum/shapes.py @@ -0,0 +1,1029 @@ +"""Enumerations used by shapes and related objects.""" + +from __future__ import annotations + +import enum + +from pptx.enum.base import BaseEnum, BaseXmlEnum + + +class MSO_AUTO_SHAPE_TYPE(BaseXmlEnum): + """Specifies a type of AutoShape, e.g. DOWN_ARROW. + + Alias: ``MSO_SHAPE`` + + Example:: + + from pptx.enum.shapes import MSO_SHAPE + from pptx.util import Inches + + left = top = width = height = Inches(1.0) + slide.shapes.add_shape( + MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height + ) + + MS API Name: `MsoAutoShapeType` + + https://learn.microsoft.com/en-us/office/vba/api/Office.MsoAutoShapeType + """ + + ACTION_BUTTON_BACK_OR_PREVIOUS = ( + 129, + "actionButtonBackPrevious", + "Back or Previous button. Supports mouse-click and mouse-over actions", + ) + """Back or Previous button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_BEGINNING = ( + 131, + "actionButtonBeginning", + "Beginning button. Supports mouse-click and mouse-over actions", + ) + """Beginning button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_CUSTOM = ( + 125, + "actionButtonBlank", + "Button with no default picture or text. Supports mouse-click and mouse-over actions", + ) + """Button with no default picture or text. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_DOCUMENT = ( + 134, + "actionButtonDocument", + "Document button. Supports mouse-click and mouse-over actions", + ) + """Document button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_END = ( + 132, + "actionButtonEnd", + "End button. Supports mouse-click and mouse-over actions", + ) + """End button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_FORWARD_OR_NEXT = ( + 130, + "actionButtonForwardNext", + "Forward or Next button. Supports mouse-click and mouse-over actions", + ) + """Forward or Next button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_HELP = ( + 127, + "actionButtonHelp", + "Help button. Supports mouse-click and mouse-over actions", + ) + """Help button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_HOME = ( + 126, + "actionButtonHome", + "Home button. Supports mouse-click and mouse-over actions", + ) + """Home button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_INFORMATION = ( + 128, + "actionButtonInformation", + "Information button. Supports mouse-click and mouse-over actions", + ) + """Information button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_MOVIE = ( + 136, + "actionButtonMovie", + "Movie button. Supports mouse-click and mouse-over actions", + ) + """Movie button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_RETURN = ( + 133, + "actionButtonReturn", + "Return button. Supports mouse-click and mouse-over actions", + ) + """Return button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_SOUND = ( + 135, + "actionButtonSound", + "Sound button. Supports mouse-click and mouse-over actions", + ) + """Sound button. Supports mouse-click and mouse-over actions""" + + ARC = (25, "arc", "Arc") + """Arc""" + + BALLOON = (137, "wedgeRoundRectCallout", "Rounded Rectangular Callout") + """Rounded Rectangular Callout""" + + BENT_ARROW = (41, "bentArrow", "Block arrow that follows a curved 90-degree angle") + """Block arrow that follows a curved 90-degree angle""" + + BENT_UP_ARROW = ( + 44, + "bentUpArrow", + "Block arrow that follows a sharp 90-degree angle. Points up by default", + ) + """Block arrow that follows a sharp 90-degree angle. Points up by default""" + + BEVEL = (15, "bevel", "Bevel") + """Bevel""" + + BLOCK_ARC = (20, "blockArc", "Block arc") + """Block arc""" + + CAN = (13, "can", "Can") + """Can""" + + CHART_PLUS = (182, "chartPlus", "Chart Plus") + """Chart Plus""" + + CHART_STAR = (181, "chartStar", "Chart Star") + """Chart Star""" + + CHART_X = (180, "chartX", "Chart X") + """Chart X""" + + CHEVRON = (52, "chevron", "Chevron") + """Chevron""" + + CHORD = (161, "chord", "Geometric chord shape") + """Geometric chord shape""" + + CIRCULAR_ARROW = (60, "circularArrow", "Block arrow that follows a curved 180-degree angle") + """Block arrow that follows a curved 180-degree angle""" + + CLOUD = (179, "cloud", "Cloud") + """Cloud""" + + CLOUD_CALLOUT = (108, "cloudCallout", "Cloud callout") + """Cloud callout""" + + CORNER = (162, "corner", "Corner") + """Corner""" + + CORNER_TABS = (169, "cornerTabs", "Corner Tabs") + """Corner Tabs""" + + CROSS = (11, "plus", "Cross") + """Cross""" + + CUBE = (14, "cube", "Cube") + """Cube""" + + CURVED_DOWN_ARROW = (48, "curvedDownArrow", "Block arrow that curves down") + """Block arrow that curves down""" + + CURVED_DOWN_RIBBON = (100, "ellipseRibbon", "Ribbon banner that curves down") + """Ribbon banner that curves down""" + + CURVED_LEFT_ARROW = (46, "curvedLeftArrow", "Block arrow that curves left") + """Block arrow that curves left""" + + CURVED_RIGHT_ARROW = (45, "curvedRightArrow", "Block arrow that curves right") + """Block arrow that curves right""" + + CURVED_UP_ARROW = (47, "curvedUpArrow", "Block arrow that curves up") + """Block arrow that curves up""" + + CURVED_UP_RIBBON = (99, "ellipseRibbon2", "Ribbon banner that curves up") + """Ribbon banner that curves up""" + + DECAGON = (144, "decagon", "Decagon") + """Decagon""" + + DIAGONAL_STRIPE = (141, "diagStripe", "Diagonal Stripe") + """Diagonal Stripe""" + + DIAMOND = (4, "diamond", "Diamond") + """Diamond""" + + DODECAGON = (146, "dodecagon", "Dodecagon") + """Dodecagon""" + + DONUT = (18, "donut", "Donut") + """Donut""" + + DOUBLE_BRACE = (27, "bracePair", "Double brace") + """Double brace""" + + DOUBLE_BRACKET = (26, "bracketPair", "Double bracket") + """Double bracket""" + + DOUBLE_WAVE = (104, "doubleWave", "Double wave") + """Double wave""" + + DOWN_ARROW = (36, "downArrow", "Block arrow that points down") + """Block arrow that points down""" + + DOWN_ARROW_CALLOUT = (56, "downArrowCallout", "Callout with arrow that points down") + """Callout with arrow that points down""" + + DOWN_RIBBON = (98, "ribbon", "Ribbon banner with center area below ribbon ends") + """Ribbon banner with center area below ribbon ends""" + + EXPLOSION1 = (89, "irregularSeal1", "Explosion") + """Explosion""" + + EXPLOSION2 = (90, "irregularSeal2", "Explosion") + """Explosion""" + + FLOWCHART_ALTERNATE_PROCESS = ( + 62, + "flowChartAlternateProcess", + "Alternate process flowchart symbol", + ) + """Alternate process flowchart symbol""" + + FLOWCHART_CARD = (75, "flowChartPunchedCard", "Card flowchart symbol") + """Card flowchart symbol""" + + FLOWCHART_COLLATE = (79, "flowChartCollate", "Collate flowchart symbol") + """Collate flowchart symbol""" + + FLOWCHART_CONNECTOR = (73, "flowChartConnector", "Connector flowchart symbol") + """Connector flowchart symbol""" + + FLOWCHART_DATA = (64, "flowChartInputOutput", "Data flowchart symbol") + """Data flowchart symbol""" + + FLOWCHART_DECISION = (63, "flowChartDecision", "Decision flowchart symbol") + """Decision flowchart symbol""" + + FLOWCHART_DELAY = (84, "flowChartDelay", "Delay flowchart symbol") + """Delay flowchart symbol""" + + FLOWCHART_DIRECT_ACCESS_STORAGE = ( + 87, + "flowChartMagneticDrum", + "Direct access storage flowchart symbol", + ) + """Direct access storage flowchart symbol""" + + FLOWCHART_DISPLAY = (88, "flowChartDisplay", "Display flowchart symbol") + """Display flowchart symbol""" + + FLOWCHART_DOCUMENT = (67, "flowChartDocument", "Document flowchart symbol") + """Document flowchart symbol""" + + FLOWCHART_EXTRACT = (81, "flowChartExtract", "Extract flowchart symbol") + """Extract flowchart symbol""" + + FLOWCHART_INTERNAL_STORAGE = ( + 66, + "flowChartInternalStorage", + "Internal storage flowchart symbol", + ) + """Internal storage flowchart symbol""" + + FLOWCHART_MAGNETIC_DISK = (86, "flowChartMagneticDisk", "Magnetic disk flowchart symbol") + """Magnetic disk flowchart symbol""" + + FLOWCHART_MANUAL_INPUT = (71, "flowChartManualInput", "Manual input flowchart symbol") + """Manual input flowchart symbol""" + + FLOWCHART_MANUAL_OPERATION = ( + 72, + "flowChartManualOperation", + "Manual operation flowchart symbol", + ) + """Manual operation flowchart symbol""" + + FLOWCHART_MERGE = (82, "flowChartMerge", "Merge flowchart symbol") + """Merge flowchart symbol""" + + FLOWCHART_MULTIDOCUMENT = (68, "flowChartMultidocument", "Multi-document flowchart symbol") + """Multi-document flowchart symbol""" + + FLOWCHART_OFFLINE_STORAGE = (139, "flowChartOfflineStorage", "Offline Storage") + """Offline Storage""" + + FLOWCHART_OFFPAGE_CONNECTOR = ( + 74, + "flowChartOffpageConnector", + "Off-page connector flowchart symbol", + ) + """Off-page connector flowchart symbol""" + + FLOWCHART_OR = (78, "flowChartOr", '"Or" flowchart symbol') + """\"Or\" flowchart symbol""" + + FLOWCHART_PREDEFINED_PROCESS = ( + 65, + "flowChartPredefinedProcess", + "Predefined process flowchart symbol", + ) + """Predefined process flowchart symbol""" + + FLOWCHART_PREPARATION = (70, "flowChartPreparation", "Preparation flowchart symbol") + """Preparation flowchart symbol""" + + FLOWCHART_PROCESS = (61, "flowChartProcess", "Process flowchart symbol") + """Process flowchart symbol""" + + FLOWCHART_PUNCHED_TAPE = (76, "flowChartPunchedTape", "Punched tape flowchart symbol") + """Punched tape flowchart symbol""" + + FLOWCHART_SEQUENTIAL_ACCESS_STORAGE = ( + 85, + "flowChartMagneticTape", + "Sequential access storage flowchart symbol", + ) + """Sequential access storage flowchart symbol""" + + FLOWCHART_SORT = (80, "flowChartSort", "Sort flowchart symbol") + """Sort flowchart symbol""" + + FLOWCHART_STORED_DATA = (83, "flowChartOnlineStorage", "Stored data flowchart symbol") + """Stored data flowchart symbol""" + + FLOWCHART_SUMMING_JUNCTION = ( + 77, + "flowChartSummingJunction", + "Summing junction flowchart symbol", + ) + """Summing junction flowchart symbol""" + + FLOWCHART_TERMINATOR = (69, "flowChartTerminator", "Terminator flowchart symbol") + """Terminator flowchart symbol""" + + FOLDED_CORNER = (16, "foldedCorner", "Folded corner") + """Folded corner""" + + FRAME = (158, "frame", "Frame") + """Frame""" + + FUNNEL = (174, "funnel", "Funnel") + """Funnel""" + + GEAR_6 = (172, "gear6", "Gear 6") + """Gear 6""" + + GEAR_9 = (173, "gear9", "Gear 9") + """Gear 9""" + + HALF_FRAME = (159, "halfFrame", "Half Frame") + """Half Frame""" + + HEART = (21, "heart", "Heart") + """Heart""" + + HEPTAGON = (145, "heptagon", "Heptagon") + """Heptagon""" + + HEXAGON = (10, "hexagon", "Hexagon") + """Hexagon""" + + HORIZONTAL_SCROLL = (102, "horizontalScroll", "Horizontal scroll") + """Horizontal scroll""" + + ISOSCELES_TRIANGLE = (7, "triangle", "Isosceles triangle") + """Isosceles triangle""" + + LEFT_ARROW = (34, "leftArrow", "Block arrow that points left") + """Block arrow that points left""" + + LEFT_ARROW_CALLOUT = (54, "leftArrowCallout", "Callout with arrow that points left") + """Callout with arrow that points left""" + + LEFT_BRACE = (31, "leftBrace", "Left brace") + """Left brace""" + + LEFT_BRACKET = (29, "leftBracket", "Left bracket") + """Left bracket""" + + LEFT_CIRCULAR_ARROW = (176, "leftCircularArrow", "Left Circular Arrow") + """Left Circular Arrow""" + + LEFT_RIGHT_ARROW = ( + 37, + "leftRightArrow", + "Block arrow with arrowheads that point both left and right", + ) + """Block arrow with arrowheads that point both left and right""" + + LEFT_RIGHT_ARROW_CALLOUT = ( + 57, + "leftRightArrowCallout", + "Callout with arrowheads that point both left and right", + ) + """Callout with arrowheads that point both left and right""" + + LEFT_RIGHT_CIRCULAR_ARROW = (177, "leftRightCircularArrow", "Left Right Circular Arrow") + """Left Right Circular Arrow""" + + LEFT_RIGHT_RIBBON = (140, "leftRightRibbon", "Left Right Ribbon") + """Left Right Ribbon""" + + LEFT_RIGHT_UP_ARROW = ( + 40, + "leftRightUpArrow", + "Block arrow with arrowheads that point left, right, and up", + ) + """Block arrow with arrowheads that point left, right, and up""" + + LEFT_UP_ARROW = (43, "leftUpArrow", "Block arrow with arrowheads that point left and up") + """Block arrow with arrowheads that point left and up""" + + LIGHTNING_BOLT = (22, "lightningBolt", "Lightning bolt") + """Lightning bolt""" + + LINE_CALLOUT_1 = (109, "borderCallout1", "Callout with border and horizontal callout line") + """Callout with border and horizontal callout line""" + + LINE_CALLOUT_1_ACCENT_BAR = (113, "accentCallout1", "Callout with vertical accent bar") + """Callout with vertical accent bar""" + + LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR = ( + 121, + "accentBorderCallout1", + "Callout with border and vertical accent bar", + ) + """Callout with border and vertical accent bar""" + + LINE_CALLOUT_1_NO_BORDER = (117, "callout1", "Callout with horizontal line") + """Callout with horizontal line""" + + LINE_CALLOUT_2 = (110, "borderCallout2", "Callout with diagonal straight line") + """Callout with diagonal straight line""" + + LINE_CALLOUT_2_ACCENT_BAR = ( + 114, + "accentCallout2", + "Callout with diagonal callout line and accent bar", + ) + """Callout with diagonal callout line and accent bar""" + + LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR = ( + 122, + "accentBorderCallout2", + "Callout with border, diagonal straight line, and accent bar", + ) + """Callout with border, diagonal straight line, and accent bar""" + + LINE_CALLOUT_2_NO_BORDER = (118, "callout2", "Callout with no border and diagonal callout line") + """Callout with no border and diagonal callout line""" + + LINE_CALLOUT_3 = (111, "borderCallout3", "Callout with angled line") + """Callout with angled line""" + + LINE_CALLOUT_3_ACCENT_BAR = ( + 115, + "accentCallout3", + "Callout with angled callout line and accent bar", + ) + """Callout with angled callout line and accent bar""" + + LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR = ( + 123, + "accentBorderCallout3", + "Callout with border, angled callout line, and accent bar", + ) + """Callout with border, angled callout line, and accent bar""" + + LINE_CALLOUT_3_NO_BORDER = (119, "callout3", "Callout with no border and angled callout line") + """Callout with no border and angled callout line""" + + LINE_CALLOUT_4 = ( + 112, + "borderCallout3", + "Callout with callout line segments forming a U-shape.", + ) + """Callout with callout line segments forming a U-shape.""" + + LINE_CALLOUT_4_ACCENT_BAR = ( + 116, + "accentCallout3", + "Callout with accent bar and callout line segments forming a U-shape.", + ) + """Callout with accent bar and callout line segments forming a U-shape.""" + + LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR = ( + 124, + "accentBorderCallout3", + "Callout with border, accent bar, and callout line segments forming a U-shape.", + ) + """Callout with border, accent bar, and callout line segments forming a U-shape.""" + + LINE_CALLOUT_4_NO_BORDER = ( + 120, + "callout3", + "Callout with no border and callout line segments forming a U-shape.", + ) + """Callout with no border and callout line segments forming a U-shape.""" + + LINE_INVERSE = (183, "lineInv", "Straight Connector") + """Straight Connector""" + + MATH_DIVIDE = (166, "mathDivide", "Division") + """Division""" + + MATH_EQUAL = (167, "mathEqual", "Equal") + """Equal""" + + MATH_MINUS = (164, "mathMinus", "Minus") + """Minus""" + + MATH_MULTIPLY = (165, "mathMultiply", "Multiply") + """Multiply""" + + MATH_NOT_EQUAL = (168, "mathNotEqual", "Not Equal") + """Not Equal""" + + MATH_PLUS = (163, "mathPlus", "Plus") + """Plus""" + + MOON = (24, "moon", "Moon") + """Moon""" + + NON_ISOSCELES_TRAPEZOID = (143, "nonIsoscelesTrapezoid", "Non-isosceles Trapezoid") + """Non-isosceles Trapezoid""" + + NOTCHED_RIGHT_ARROW = (50, "notchedRightArrow", "Notched block arrow that points right") + """Notched block arrow that points right""" + + NO_SYMBOL = (19, "noSmoking", "'No' Symbol") + """'No' Symbol""" + + OCTAGON = (6, "octagon", "Octagon") + """Octagon""" + + OVAL = (9, "ellipse", "Oval") + """Oval""" + + OVAL_CALLOUT = (107, "wedgeEllipseCallout", "Oval-shaped callout") + """Oval-shaped callout""" + + PARALLELOGRAM = (2, "parallelogram", "Parallelogram") + """Parallelogram""" + + PENTAGON = (51, "homePlate", "Pentagon") + """Pentagon""" + + PIE = (142, "pie", "Pie") + """Pie""" + + PIE_WEDGE = (175, "pieWedge", "Pie") + """Pie""" + + PLAQUE = (28, "plaque", "Plaque") + """Plaque""" + + PLAQUE_TABS = (171, "plaqueTabs", "Plaque Tabs") + """Plaque Tabs""" + + QUAD_ARROW = (39, "quadArrow", "Block arrows that point up, down, left, and right") + """Block arrows that point up, down, left, and right""" + + QUAD_ARROW_CALLOUT = ( + 59, + "quadArrowCallout", + "Callout with arrows that point up, down, left, and right", + ) + """Callout with arrows that point up, down, left, and right""" + + RECTANGLE = (1, "rect", "Rectangle") + """Rectangle""" + + RECTANGULAR_CALLOUT = (105, "wedgeRectCallout", "Rectangular callout") + """Rectangular callout""" + + REGULAR_PENTAGON = (12, "pentagon", "Pentagon") + """Pentagon""" + + RIGHT_ARROW = (33, "rightArrow", "Block arrow that points right") + """Block arrow that points right""" + + RIGHT_ARROW_CALLOUT = (53, "rightArrowCallout", "Callout with arrow that points right") + """Callout with arrow that points right""" + + RIGHT_BRACE = (32, "rightBrace", "Right brace") + """Right brace""" + + RIGHT_BRACKET = (30, "rightBracket", "Right bracket") + """Right bracket""" + + RIGHT_TRIANGLE = (8, "rtTriangle", "Right triangle") + """Right triangle""" + + ROUNDED_RECTANGLE = (5, "roundRect", "Rounded rectangle") + """Rounded rectangle""" + + ROUNDED_RECTANGULAR_CALLOUT = (106, "wedgeRoundRectCallout", "Rounded rectangle-shaped callout") + """Rounded rectangle-shaped callout""" + + ROUND_1_RECTANGLE = (151, "round1Rect", "Round Single Corner Rectangle") + """Round Single Corner Rectangle""" + + ROUND_2_DIAG_RECTANGLE = (153, "round2DiagRect", "Round Diagonal Corner Rectangle") + """Round Diagonal Corner Rectangle""" + + ROUND_2_SAME_RECTANGLE = (152, "round2SameRect", "Round Same Side Corner Rectangle") + """Round Same Side Corner Rectangle""" + + SMILEY_FACE = (17, "smileyFace", "Smiley face") + """Smiley face""" + + SNIP_1_RECTANGLE = (155, "snip1Rect", "Snip Single Corner Rectangle") + """Snip Single Corner Rectangle""" + + SNIP_2_DIAG_RECTANGLE = (157, "snip2DiagRect", "Snip Diagonal Corner Rectangle") + """Snip Diagonal Corner Rectangle""" + + SNIP_2_SAME_RECTANGLE = (156, "snip2SameRect", "Snip Same Side Corner Rectangle") + """Snip Same Side Corner Rectangle""" + + SNIP_ROUND_RECTANGLE = (154, "snipRoundRect", "Snip and Round Single Corner Rectangle") + """Snip and Round Single Corner Rectangle""" + + SQUARE_TABS = (170, "squareTabs", "Square Tabs") + """Square Tabs""" + + STAR_10_POINT = (149, "star10", "10-Point Star") + """10-Point Star""" + + STAR_12_POINT = (150, "star12", "12-Point Star") + """12-Point Star""" + + STAR_16_POINT = (94, "star16", "16-point star") + """16-point star""" + + STAR_24_POINT = (95, "star24", "24-point star") + """24-point star""" + + STAR_32_POINT = (96, "star32", "32-point star") + """32-point star""" + + STAR_4_POINT = (91, "star4", "4-point star") + """4-point star""" + + STAR_5_POINT = (92, "star5", "5-point star") + """5-point star""" + + STAR_6_POINT = (147, "star6", "6-Point Star") + """6-Point Star""" + + STAR_7_POINT = (148, "star7", "7-Point Star") + """7-Point Star""" + + STAR_8_POINT = (93, "star8", "8-point star") + """8-point star""" + + STRIPED_RIGHT_ARROW = ( + 49, + "stripedRightArrow", + "Block arrow that points right with stripes at the tail", + ) + """Block arrow that points right with stripes at the tail""" + + SUN = (23, "sun", "Sun") + """Sun""" + + SWOOSH_ARROW = (178, "swooshArrow", "Swoosh Arrow") + """Swoosh Arrow""" + + TEAR = (160, "teardrop", "Teardrop") + """Teardrop""" + + TRAPEZOID = (3, "trapezoid", "Trapezoid") + """Trapezoid""" + + UP_ARROW = (35, "upArrow", "Block arrow that points up") + """Block arrow that points up""" + + UP_ARROW_CALLOUT = (55, "upArrowCallout", "Callout with arrow that points up") + """Callout with arrow that points up""" + + UP_DOWN_ARROW = (38, "upDownArrow", "Block arrow that points up and down") + """Block arrow that points up and down""" + + UP_DOWN_ARROW_CALLOUT = (58, "upDownArrowCallout", "Callout with arrows that point up and down") + """Callout with arrows that point up and down""" + + UP_RIBBON = (97, "ribbon2", "Ribbon banner with center area above ribbon ends") + """Ribbon banner with center area above ribbon ends""" + + U_TURN_ARROW = (42, "uturnArrow", "Block arrow forming a U shape") + """Block arrow forming a U shape""" + + VERTICAL_SCROLL = (101, "verticalScroll", "Vertical scroll") + """Vertical scroll""" + + WAVE = (103, "wave", "Wave") + """Wave""" + + +MSO_SHAPE = MSO_AUTO_SHAPE_TYPE + + +class MSO_CONNECTOR_TYPE(BaseXmlEnum): + """ + Specifies a type of connector. + + Alias: ``MSO_CONNECTOR`` + + Example:: + + from pptx.enum.shapes import MSO_CONNECTOR + from pptx.util import Cm + + shapes = prs.slides[0].shapes + connector = shapes.add_connector( + MSO_CONNECTOR.STRAIGHT, Cm(2), Cm(2), Cm(10), Cm(10) + ) + assert connector.left.cm == 2 + + MS API Name: `MsoConnectorType` + + http://msdn.microsoft.com/en-us/library/office/ff860918.aspx + """ + + CURVE = (3, "curvedConnector3", "Curved connector.") + """Curved connector.""" + + ELBOW = (2, "bentConnector3", "Elbow connector.") + """Elbow connector.""" + + STRAIGHT = (1, "line", "Straight line connector.") + """Straight line connector.""" + + MIXED = (-2, "", "Return value only; indicates a combination of other states.") + """Return value only; indicates a combination of other states.""" + + +MSO_CONNECTOR = MSO_CONNECTOR_TYPE + + +class MSO_SHAPE_TYPE(BaseEnum): + """Specifies the type of a shape, more specifically than the five base types. + + Alias: ``MSO`` + + Example:: + + from pptx.enum.shapes import MSO_SHAPE_TYPE + + assert shape.type == MSO_SHAPE_TYPE.PICTURE + + MS API Name: `MsoShapeType` + + http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15).aspx + """ + + AUTO_SHAPE = (1, "AutoShape") + """AutoShape""" + + CALLOUT = (2, "Callout shape") + """Callout shape""" + + CANVAS = (20, "Drawing canvas") + """Drawing canvas""" + + CHART = (3, "Chart, e.g. pie chart, bar chart") + """Chart, e.g. pie chart, bar chart""" + + COMMENT = (4, "Comment") + """Comment""" + + DIAGRAM = (21, "Diagram") + """Diagram""" + + EMBEDDED_OLE_OBJECT = (7, "Embedded OLE object") + """Embedded OLE object""" + + FORM_CONTROL = (8, "Form control") + """Form control""" + + FREEFORM = (5, "Freeform") + """Freeform""" + + GROUP = (6, "Group shape") + """Group shape""" + + IGX_GRAPHIC = (24, "SmartArt graphic") + """SmartArt graphic""" + + INK = (22, "Ink") + """Ink""" + + INK_COMMENT = (23, "Ink Comment") + """Ink Comment""" + + LINE = (9, "Line") + """Line""" + + LINKED_OLE_OBJECT = (10, "Linked OLE object") + """Linked OLE object""" + + LINKED_PICTURE = (11, "Linked picture") + """Linked picture""" + + MEDIA = (16, "Media") + """Media""" + + OLE_CONTROL_OBJECT = (12, "OLE control object") + """OLE control object""" + + PICTURE = (13, "Picture") + """Picture""" + + PLACEHOLDER = (14, "Placeholder") + """Placeholder""" + + SCRIPT_ANCHOR = (18, "Script anchor") + """Script anchor""" + + TABLE = (19, "Table") + """Table""" + + TEXT_BOX = (17, "Text box") + """Text box""" + + TEXT_EFFECT = (15, "Text effect") + """Text effect""" + + WEB_VIDEO = (26, "Web video") + """Web video""" + + MIXED = (-2, "Multiple shape types (read-only).") + """Multiple shape types (read-only).""" + + +MSO = MSO_SHAPE_TYPE + + +class PP_MEDIA_TYPE(BaseEnum): + """Indicates the OLE media type. + + Example:: + + from pptx.enum.shapes import PP_MEDIA_TYPE + + movie = slide.shapes[0] + assert movie.media_type == PP_MEDIA_TYPE.MOVIE + + MS API Name: `PpMediaType` + + https://msdn.microsoft.com/en-us/library/office/ff746008.aspx + """ + + MOVIE = (3, "Video media such as MP4.") + """Video media such as MP4.""" + + OTHER = (1, "Other media types") + """Other media types""" + + SOUND = (1, "Audio media such as MP3.") + """Audio media such as MP3.""" + + MIXED = ( + -2, + "Return value only; indicates multiple media types, typically for a collection of shapes." + " May not be applicable in python-pptx.", + ) + """Return value only; indicates multiple media types. + + Typically for a collection of shapes. May not be applicable in python-pptx. + """ + + +class PP_PLACEHOLDER_TYPE(BaseXmlEnum): + """Specifies one of the 18 distinct types of placeholder. + + Alias: ``PP_PLACEHOLDER`` + + Example:: + + from pptx.enum.shapes import PP_PLACEHOLDER + + placeholder = slide.placeholders[0] + assert placeholder.type == PP_PLACEHOLDER.TITLE + + MS API name: `PpPlaceholderType` + + http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15 ").aspx" + """ + + BITMAP = (9, "clipArt", "Clip art placeholder") + """Clip art placeholder""" + + BODY = (2, "body", "Body") + """Body""" + + CENTER_TITLE = (3, "ctrTitle", "Center Title") + """Center Title""" + + CHART = (8, "chart", "Chart") + """Chart""" + + DATE = (16, "dt", "Date") + """Date""" + + FOOTER = (15, "ftr", "Footer") + """Footer""" + + HEADER = (14, "hdr", "Header") + """Header""" + + MEDIA_CLIP = (10, "media", "Media Clip") + """Media Clip""" + + OBJECT = (7, "obj", "Object") + """Object""" + + ORG_CHART = (11, "dgm", "SmartArt placeholder. Organization chart is a legacy name.") + """SmartArt placeholder. Organization chart is a legacy name.""" + + PICTURE = (18, "pic", "Picture") + """Picture""" + + SLIDE_IMAGE = (101, "sldImg", "Slide Image") + """Slide Image""" + + SLIDE_NUMBER = (13, "sldNum", "Slide Number") + """Slide Number""" + + SUBTITLE = (4, "subTitle", "Subtitle") + """Subtitle""" + + TABLE = (12, "tbl", "Table") + """Table""" + + TITLE = (1, "title", "Title") + """Title""" + + VERTICAL_BODY = (6, "", "Vertical Body (read-only).") + """Vertical Body (read-only).""" + + VERTICAL_OBJECT = (17, "", "Vertical Object (read-only).") + """Vertical Object (read-only).""" + + VERTICAL_TITLE = (5, "", "Vertical Title (read-only).") + """Vertical Title (read-only).""" + + MIXED = (-2, "", "Return value only; multiple placeholders of differing types.") + """Return value only; multiple placeholders of differing types.""" + + +PP_PLACEHOLDER = PP_PLACEHOLDER_TYPE + + +class PROG_ID(enum.Enum): + """One-off Enum-like object for progId values. + + Indicates the type of an OLE object in terms of the program used to open it. + + A member of this enumeration can be used in a `SlideShapes.add_ole_object()` call to + specify a Microsoft Office file-type (Excel, PowerPoint, or Word), which will + then not require several of the arguments required to embed other object types. + + Example:: + + from pptx.enum.shapes import PROG_ID + from pptx.util import Inches + + embedded_xlsx_shape = slide.shapes.add_ole_object( + "workbook.xlsx", PROG_ID.XLSX, left=Inches(1), top=Inches(1) + ) + assert embedded_xlsx_shape.ole_format.prog_id == "Excel.Sheet.12" + """ + + _progId: str + _icon_filename: str + _width: int + _height: int + + def __new__(cls, value: str, progId: str, icon_filename: str, width: int, height: int): + self = object.__new__(cls) + self._value_ = value + self._progId = progId + self._icon_filename = icon_filename + self._width = width + self._height = height + return self + + @property + def height(self): + return self._height + + @property + def icon_filename(self): + return self._icon_filename + + @property + def progId(self): + return self._progId + + @property + def width(self): + return self._width + + DOCX = ("DOCX", "Word.Document.12", "docx-icon.emf", 965200, 609600) + """`progId` for an embedded Word 2007+ (.docx) document.""" + + PPTX = ("PPTX", "PowerPoint.Show.12", "pptx-icon.emf", 965200, 609600) + """`progId` for an embedded PowerPoint 2007+ (.pptx) document.""" + + XLSX = ("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600) + """`progId` for an embedded Excel 2007+ (.xlsx) document.""" diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py new file mode 100644 index 000000000..db266a3c9 --- /dev/null +++ b/src/pptx/enum/text.py @@ -0,0 +1,230 @@ +"""Enumerations used by text and related objects.""" + +from __future__ import annotations + +from pptx.enum.base import BaseEnum, BaseXmlEnum + + +class MSO_AUTO_SIZE(BaseEnum): + """Determines the type of automatic sizing allowed. + + The following names can be used to specify the automatic sizing behavior used to fit a shape's + text within the shape bounding box, for example:: + + from pptx.enum.text import MSO_AUTO_SIZE + + shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE + + The word-wrap setting of the text frame interacts with the auto-size setting to determine the + specific auto-sizing behavior. + + Note that `TextFrame.auto_size` can also be set to |None|, which removes the auto size setting + altogether. This causes the setting to be inherited, either from the layout placeholder, in the + case of a placeholder shape, or from the theme. + + MS API Name: `MsoAutoSize` + + http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15).aspx + """ + + NONE = ( + 0, + "No automatic sizing of the shape or text will be done.\n\nText can freely extend beyond" + " the horizontal and vertical edges of the shape bounding box.", + ) + """No automatic sizing of the shape or text will be done. + + Text can freely extend beyond the horizontal and vertical edges of the shape bounding box. + """ + + SHAPE_TO_FIT_TEXT = ( + 1, + "The shape height and possibly width are adjusted to fit the text.\n\nNote this setting" + " interacts with the TextFrame.word_wrap property setting. If word wrap is turned on," + " only the height of the shape will be adjusted; soft line breaks will be used to fit the" + " text horizontally.", + ) + """The shape height and possibly width are adjusted to fit the text. + + Note this setting interacts with the TextFrame.word_wrap property setting. If word wrap is + turned on, only the height of the shape will be adjusted; soft line breaks will be used to fit + the text horizontally. + """ + + TEXT_TO_FIT_SHAPE = ( + 2, + "The font size is reduced as necessary to fit the text within the shape.", + ) + """The font size is reduced as necessary to fit the text within the shape.""" + + MIXED = (-2, "Return value only; indicates a combination of automatic sizing schemes are used.") + """Return value only; indicates a combination of automatic sizing schemes are used.""" + + +class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum): + """ + Indicates the type of underline for text. Used with + :attr:`.Font.underline` to specify the style of text underlining. + + Alias: ``MSO_UNDERLINE`` + + Example:: + + from pptx.enum.text import MSO_UNDERLINE + + run.font.underline = MSO_UNDERLINE.DOUBLE_LINE + + MS API Name: `MsoTextUnderlineType` + + http://msdn.microsoft.com/en-us/library/aa432699.aspx + """ + + NONE = (0, "none", "Specifies no underline.") + """Specifies no underline.""" + + DASH_HEAVY_LINE = (8, "dashHeavy", "Specifies a dash underline.") + """Specifies a dash underline.""" + + DASH_LINE = (7, "dash", "Specifies a dash line underline.") + """Specifies a dash line underline.""" + + DASH_LONG_HEAVY_LINE = (10, "dashLongHeavy", "Specifies a long heavy line underline.") + """Specifies a long heavy line underline.""" + + DASH_LONG_LINE = (9, "dashLong", "Specifies a dashed long line underline.") + """Specifies a dashed long line underline.""" + + DOT_DASH_HEAVY_LINE = (12, "dotDashHeavy", "Specifies a dot dash heavy line underline.") + """Specifies a dot dash heavy line underline.""" + + DOT_DASH_LINE = (11, "dotDash", "Specifies a dot dash line underline.") + """Specifies a dot dash line underline.""" + + DOT_DOT_DASH_HEAVY_LINE = ( + 14, + "dotDotDashHeavy", + "Specifies a dot dot dash heavy line underline.", + ) + """Specifies a dot dot dash heavy line underline.""" + + DOT_DOT_DASH_LINE = (13, "dotDotDash", "Specifies a dot dot dash line underline.") + """Specifies a dot dot dash line underline.""" + + DOTTED_HEAVY_LINE = (6, "dottedHeavy", "Specifies a dotted heavy line underline.") + """Specifies a dotted heavy line underline.""" + + DOTTED_LINE = (5, "dotted", "Specifies a dotted line underline.") + """Specifies a dotted line underline.""" + + DOUBLE_LINE = (3, "dbl", "Specifies a double line underline.") + """Specifies a double line underline.""" + + HEAVY_LINE = (4, "heavy", "Specifies a heavy line underline.") + """Specifies a heavy line underline.""" + + SINGLE_LINE = (2, "sng", "Specifies a single line underline.") + """Specifies a single line underline.""" + + WAVY_DOUBLE_LINE = (17, "wavyDbl", "Specifies a wavy double line underline.") + """Specifies a wavy double line underline.""" + + WAVY_HEAVY_LINE = (16, "wavyHeavy", "Specifies a wavy heavy line underline.") + """Specifies a wavy heavy line underline.""" + + WAVY_LINE = (15, "wavy", "Specifies a wavy line underline.") + """Specifies a wavy line underline.""" + + WORDS = (1, "words", "Specifies underlining words.") + """Specifies underlining words.""" + + MIXED = (-2, "", "Specifies a mix of underline types (read-only).") + """Specifies a mix of underline types (read-only).""" + + +MSO_UNDERLINE = MSO_TEXT_UNDERLINE_TYPE + + +class MSO_VERTICAL_ANCHOR(BaseXmlEnum): + """Specifies the vertical alignment of text in a text frame. + + Used with the `.vertical_anchor` property of the |TextFrame| object. Note that the + `vertical_anchor` property can also have the value None, indicating there is no directly + specified vertical anchor setting and its effective value is inherited from its placeholder if + it has one or from the theme. |None| may also be assigned to remove an explicitly specified + vertical anchor setting. + + MS API Name: `MsoVerticalAnchor` + + http://msdn.microsoft.com/en-us/library/office/ff865255.aspx + """ + + TOP = (1, "t", "Aligns text to top of text frame") + """Aligns text to top of text frame""" + + MIDDLE = (3, "ctr", "Centers text vertically") + """Centers text vertically""" + + BOTTOM = (4, "b", "Aligns text to bottom of text frame") + """Aligns text to bottom of text frame""" + + MIXED = (-2, "", "Return value only; indicates a combination of the other states.") + """Return value only; indicates a combination of the other states.""" + + +MSO_ANCHOR = MSO_VERTICAL_ANCHOR + + +class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): + """Specifies the horizontal alignment for one or more paragraphs. + + Alias: `PP_ALIGN` + + Example:: + + from pptx.enum.text import PP_ALIGN + + shape.paragraphs[0].alignment = PP_ALIGN.CENTER + + MS API Name: `PpParagraphAlignment` + + http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15).aspx + """ + + CENTER = (2, "ctr", "Center align") + """Center align""" + + DISTRIBUTE = ( + 5, + "dist", + "Evenly distributes e.g. Japanese characters from left to right within a line", + ) + """Evenly distributes e.g. Japanese characters from left to right within a line""" + + JUSTIFY = ( + 4, + "just", + "Justified, i.e. each line both begins and ends at the margin.\n\nSpacing between words" + " is adjusted such that the line exactly fills the width of the paragraph.", + ) + """Justified, i.e. each line both begins and ends at the margin. + + Spacing between words is adjusted such that the line exactly fills the width of the paragraph. + """ + + JUSTIFY_LOW = (7, "justLow", "Justify using a small amount of space between words.") + """Justify using a small amount of space between words.""" + + LEFT = (1, "l", "Left aligned") + """Left aligned""" + + RIGHT = (3, "r", "Right aligned") + """Right aligned""" + + THAI_DISTRIBUTE = (6, "thaiDist", "Thai distributed") + """Thai distributed""" + + MIXED = (-2, "", "Multiple alignments are present in a set of paragraphs (read-only).") + """Multiple alignments are present in a set of paragraphs (read-only).""" + + +PP_ALIGN = PP_PARAGRAPH_ALIGNMENT diff --git a/pptx/exc.py b/src/pptx/exc.py similarity index 84% rename from pptx/exc.py rename to src/pptx/exc.py index 8641fe44f..0a1e03b81 100644 --- a/pptx/exc.py +++ b/src/pptx/exc.py @@ -1,11 +1,10 @@ -# encoding: utf-8 - -""" -Exceptions used with python-pptx. +"""Exceptions used with python-pptx. The base exception class is PythonPptxError. """ +from __future__ import annotations + class PythonPptxError(Exception): """Generic error class.""" diff --git a/pptx/media.py b/src/pptx/media.py similarity index 95% rename from pptx/media.py rename to src/pptx/media.py index c5adf24ea..7aaf47ca1 100644 --- a/pptx/media.py +++ b/src/pptx/media.py @@ -1,40 +1,38 @@ -# encoding: utf-8 - """Objects related to images, audio, and video.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import base64 import hashlib import os +from typing import IO -from .compat import is_string -from .opc.constants import CONTENT_TYPE as CT -from .util import lazyproperty +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.util import lazyproperty class Video(object): """Immutable value object representing a video such as MP4.""" - def __init__(self, blob, mime_type, filename): + def __init__(self, blob: bytes, mime_type: str | None, filename: str | None): super(Video, self).__init__() self._blob = blob self._mime_type = mime_type self._filename = filename @classmethod - def from_blob(cls, blob, mime_type, filename=None): + def from_blob(cls, blob: bytes, mime_type: str | None, filename: str | None = None): """Return a new |Video| object loaded from image binary in *blob*.""" return cls(blob, mime_type, filename) @classmethod - def from_path_or_file_like(cls, movie_file, mime_type): + def from_path_or_file_like(cls, movie_file: str | IO[bytes], mime_type: str | None) -> Video: """Return a new |Video| object containing video in *movie_file*. *movie_file* can be either a path (string) or a file-like (e.g. StringIO) object. """ - if is_string(movie_file): + if isinstance(movie_file, str): # treat movie_file as a path with open(movie_file, "rb") as f: blob = f.read() @@ -79,7 +77,7 @@ def ext(self): }.get(self._mime_type, "vid") @property - def filename(self): + def filename(self) -> str: """Return a filename.ext string appropriate to this video. The base filename from the original path is used if this image was diff --git a/pptx/opc/__init__.py b/src/pptx/opc/__init__.py similarity index 100% rename from pptx/opc/__init__.py rename to src/pptx/opc/__init__.py diff --git a/src/pptx/opc/constants.py b/src/pptx/opc/constants.py new file mode 100644 index 000000000..e1b08a93a --- /dev/null +++ b/src/pptx/opc/constants.py @@ -0,0 +1,331 @@ +"""Constant values related to the Open Packaging Convention. + +In particular, this includes content (MIME) types and relationship types. +""" + +from __future__ import annotations + + +class CONTENT_TYPE: + """Content type URIs (like MIME-types) that specify a part's format.""" + + ASF = "video/x-ms-asf" + AVI = "video/avi" + 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_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml" + DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" + DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" + GIF = "image/gif" + INK = "application/inkml+xml" + JPEG = "image/jpeg" + MOV = "video/quicktime" + MP4 = "video/mp4" + MPG = "video/mpeg" + MS_PHOTO = "image/vnd.ms-photo" + MS_VIDEO = "video/msvideo" + OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml" + OFC_CHART_EX = "application/vnd.ms-office.chartex+xml" + OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml" + OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml" + OFC_CUSTOM_XML_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" + ) + OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml" + OFC_EXTENDED_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.extended-properties+xml" + ) + OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" + OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" + OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml" + OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml" + OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing" + OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml" + OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( + "application/vnd.openxmlformats-package.digital-signature-certificate" + ) + OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin" + OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( + "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" + ) + OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" + PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" + PML_COMMENT_AUTHORS = ( + "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml" + ) + PML_HANDOUT_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml" + ) + PML_NOTES_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml" + ) + PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" + PML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation" + PML_PRESENTATION_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" + ) + PML_PRES_MACRO_MAIN = "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml" + PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" + PML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.presentationml.printerSettings" + ) + PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" + PML_SLIDESHOW_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" + ) + PML_SLIDE_LAYOUT = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" + ) + PML_SLIDE_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" + ) + PML_SLIDE_UPDATE_INFO = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml" + ) + PML_TABLE_STYLES = ( + "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml" + ) + PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml" + PML_TEMPLATE_MAIN = ( + "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_CUSTOM_PROPERTY = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" + ) + SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" + SML_EXTERNAL_LINK = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" + ) + SML_PIVOT_CACHE_DEFINITION = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" + ) + SML_PIVOT_CACHE_RECORDS = ( + "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_REVISION_HEADERS = ( + "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.sharedStrings+xml" + ) + SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" + SML_SHEET_METADATA = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.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.tableSingleCells+xml" + ) + SML_TEMPLATE_MAIN = ( + "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.volatileDependencies+xml" + ) + SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + SWF = "application/x-shockwave-flash" + TIFF = "image/tiff" + VIDEO = "video/unknown" + 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.document.glossary+xml" + ) + WML_DOCUMENT_MAIN = ( + "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.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.webSettings+xml" + ) + WMV = "video/x-ms-wmv" + XML = "application/xml" + X_EMF = "image/x-emf" + X_FONTDATA = "application/x-fontdata" + X_FONT_TTF = "application/x-font-ttf" + X_MS_VIDEO = "video/x-msvideo" + X_WMF = "image/x-wmf" + + +class NAMESPACE: + """Constant values for OPC XML namespaces""" + + DML_WORDPROCESSING_DRAWING = ( + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + ) + OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships" + OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types" + WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +class RELATIONSHIP_TARGET_MODE: + """Open XML relationship target modes""" + + EXTERNAL = "External" + INTERNAL = "Internal" + + +class RELATIONSHIP_TYPE: + AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" + A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" + CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" + CERTIFICATE = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" + "re/certificate" + ) + CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle" + CHART_USER_SHAPES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes" + ) + COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + COMMENT_AUTHORS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors" + ) + CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections" + CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" + CORE_PROPERTIES = ( + "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" + ) + CUSTOM_PROPERTIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" + ) + CUSTOM_PROPERTY = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty" + ) + CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" + CUSTOM_XML_PROPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps" + ) + DIAGRAM_COLORS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors" + ) + DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" + DIAGRAM_LAYOUT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout" + ) + DIAGRAM_QUICK_STYLE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle" + ) + DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" + EXTENDED_PROPERTIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" + ) + EXTERNAL_LINK = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink" + ) + FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" + FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" + FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" + FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" + GLOSSARY_DOCUMENT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument" + ) + HANDOUT_MASTER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster" + ) + HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" + HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media" + 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/digital-signature/origin" + PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" + PIVOT_CACHE_DEFINITION = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac" + "heDefinition" + ) + PIVOT_CACHE_RECORDS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh" + "eetml/pivotCacheRecords" + ) + PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" + PRINTER_SETTINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings" + ) + QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable" + REVISION_HEADERS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders" + ) + REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog" + SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" + SHARED_STRINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" + ) + SHEET_METADATA = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata" + ) + SIGNATURE = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" + "re/signature" + ) + SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" + SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" + SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" + SLIDE_UPDATE_INFO = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo" + ) + STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" + TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + TABLE_SINGLE_CELLS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells" + ) + TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles" + TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags" + THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" + THEME_OVERRIDE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride" + ) + THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/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" + VOLATILE_DEPENDENCIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile" + "Dependencies" + ) + WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" + WORKSHEET_SOURCE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource" + ) + XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" diff --git a/src/pptx/opc/oxml.py b/src/pptx/opc/oxml.py new file mode 100644 index 000000000..5dd902a55 --- /dev/null +++ b/src/pptx/opc/oxml.py @@ -0,0 +1,188 @@ +"""OPC-local oxml module to handle OPC-local concerns like relationship parsing.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast + +from lxml import etree + +from pptx.opc.constants import NAMESPACE as NS +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.oxml import parse_xml, register_element_cls +from pptx.oxml.simpletypes import ( + ST_ContentType, + ST_Extension, + ST_TargetMode, + XsdAnyUri, + XsdId, +) +from pptx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, +) + +if TYPE_CHECKING: + from pptx.opc.packuri import PackURI + +nsmap = { + "ct": NS.OPC_CONTENT_TYPES, + "pr": NS.OPC_RELATIONSHIPS, + "r": NS.OFC_RELATIONSHIPS, +} + + +def oxml_to_encoded_bytes( + element: BaseOxmlElement, + encoding: str = "utf-8", + pretty_print: bool = False, + standalone: bool | None = None, +) -> bytes: + return etree.tostring( + element, encoding=encoding, pretty_print=pretty_print, standalone=standalone + ) + + +def oxml_tostring( + elm: BaseOxmlElement, + encoding: str | None = None, + pretty_print: bool = False, + standalone: bool | None = None, +): + return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone) + + +def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes: + """Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file. + + Includes XML-declaration header. + """ + return etree.tostring(part_elm, encoding="UTF-8", standalone=True) + + +class CT_Default(BaseOxmlElement): + """`` element. + + Specifies the default content type to be applied to a part with the specified extension. + """ + + extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Extension", ST_Extension + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) + + +class CT_Override(BaseOxmlElement): + """`` element. + + Specifies the content type to be applied for a part with the specified partname. + """ + + partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "PartName", XsdAnyUri + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) + + +class CT_Relationship(BaseOxmlElement): + """`` element. + + Represents a single relationship from a source to a target part. + """ + + rId: str = RequiredAttribute("Id", XsdId) # pyright: ignore[reportAssignmentType] + reltype: str = RequiredAttribute("Type", XsdAnyUri) # pyright: ignore[reportAssignmentType] + target_ref: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Target", XsdAnyUri + ) + targetMode: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "TargetMode", ST_TargetMode, default=RTM.INTERNAL + ) + + @classmethod + def new( + cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL + ) -> CT_Relationship: + """Return a new `` element. + + `target_ref` is either a partname or a URI. + """ + relationship = cast(CT_Relationship, parse_xml(f'')) + relationship.rId = rId + relationship.reltype = reltype + relationship.target_ref = target_ref + relationship.targetMode = target_mode + return relationship + + +class CT_Relationships(BaseOxmlElement): + """`` element, the root element in a .rels file.""" + + relationship_lst: list[CT_Relationship] + _insert_relationship: Callable[[CT_Relationship], CT_Relationship] + + relationship = ZeroOrMore("pr:Relationship") + + def add_rel( + self, rId: str, reltype: str, target: str, is_external: bool = False + ) -> CT_Relationship: + """Add a child `` element with attributes set as specified.""" + target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL + relationship = CT_Relationship.new(rId, reltype, target, target_mode) + return self._insert_relationship(relationship) + + @classmethod + def new(cls) -> CT_Relationships: + """Return a new `` element.""" + return cast(CT_Relationships, parse_xml(f'')) + + @property + def xml_file_bytes(self) -> bytes: + """Return XML bytes, with XML-declaration, for this `` element. + + Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at + the top. + """ + return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True) + + +class CT_Types(BaseOxmlElement): + """`` element. + + The container element for Default and Override elements in [Content_Types].xml. + """ + + default_lst: list[CT_Default] + override_lst: list[CT_Override] + + _add_default: Callable[..., CT_Default] + _add_override: Callable[..., CT_Override] + + default = ZeroOrMore("ct:Default") + override = ZeroOrMore("ct:Override") + + def add_default(self, ext: str, content_type: str) -> CT_Default: + """Add a child `` element with attributes set to parameter values.""" + return self._add_default(extension=ext, contentType=content_type) + + def add_override(self, partname: PackURI, content_type: str) -> CT_Override: + """Add a child `` element with attributes set to parameter values.""" + return self._add_override(partName=partname, contentType=content_type) + + @classmethod + def new(cls) -> CT_Types: + """Return a new `` element.""" + return cast(CT_Types, parse_xml(f'')) + + +register_element_cls("ct:Default", CT_Default) +register_element_cls("ct:Override", CT_Override) +register_element_cls("ct:Types", CT_Types) + +register_element_cls("pr:Relationship", CT_Relationship) +register_element_cls("pr:Relationships", CT_Relationships) diff --git a/src/pptx/opc/package.py b/src/pptx/opc/package.py new file mode 100644 index 000000000..713759c54 --- /dev/null +++ b/src/pptx/opc/package.py @@ -0,0 +1,762 @@ +"""Fundamental Open Packaging Convention (OPC) objects. + +The :mod:`pptx.packaging` module coheres around the concerns of reading and writing +presentations to and from a .pptx file. +""" + +from __future__ import annotations + +import collections +from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast + +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.oxml import CT_Relationships, serialize_part_xml +from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI +from pptx.opc.serialized import PackageReader, PackageWriter +from pptx.opc.shared import CaseInsensitiveDict +from pptx.oxml import parse_xml +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from typing_extensions import Self + + from pptx.opc.oxml import CT_Relationship, CT_Types + from pptx.oxml.xmlchemy import BaseOxmlElement + from pptx.package import Package + from pptx.parts.presentation import PresentationPart + + +class _RelatableMixin: + """Provide relationship methods required by both the package and each part.""" + + def part_related_by(self, reltype: str) -> Part: + """Return (single) part having relationship to this package 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) + + def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str: + """Return rId key of relationship of `reltype` to `target`. + + If such a relationship already exists, its rId is returned. Otherwise the relationship is + added and its new rId returned. + """ + if isinstance(target, str): + assert is_external + return self._rels.get_or_add_ext_rel(reltype, target) + + return self._rels.get_or_add(reltype, target) + + def related_part(self, rId: str) -> Part: + """Return related |Part| subtype identified by `rId`.""" + return self._rels[rId].target_part + + def target_ref(self, rId: str) -> str: + """Return URL contained in target ref of relationship identified by `rId`.""" + return self._rels[rId].target_ref + + @lazyproperty + def _rels(self) -> _Relationships: + """|_Relationships| object containing relationships from this part to others.""" + raise NotImplementedError( # pragma: no cover + "`%s` must implement `.rels`" % type(self).__name__ + ) + + +class OpcPackage(_RelatableMixin): + """Main API class for |python-opc|. + + A new instance is constructed by calling the :meth:`open` classmethod with a path to a package + file or file-like object containing a package (.pptx file). + """ + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + @classmethod + def open(cls, pkg_file: str | IO[bytes]) -> Self: + """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" + return cls(pkg_file)._load() + + def drop_rel(self, rId: str) -> None: + """Remove relationship identified by `rId`.""" + self._rels.pop(rId) + + def iter_parts(self) -> Iterator[Part]: + """Generate exactly one reference to each part in the package.""" + visited: Set[Part] = set() + for rel in self.iter_rels(): + if rel.is_external: + continue + part = rel.target_part + if part in visited: + continue + yield part + visited.add(part) + + def iter_rels(self) -> Iterator[_Relationship]: + """Generate exactly one reference to each relationship in package. + + Performs a depth-first traversal of the rels graph. + """ + visited: Set[Part] = set() + + def walk_rels(rels: _Relationships) -> Iterator[_Relationship]: + for rel in rels.values(): + yield rel + # --- external items can have no relationships --- + if rel.is_external: + continue + # -- all relationships other than those for the package belong to a part. Once + # -- that part has been processed, processing it again would lead to the same + # -- relationships appearing more than once. + part = rel.target_part + if part in visited: + continue + visited.add(part) + # --- recurse into relationships of each unvisited target-part --- + yield from walk_rels(part.rels) + + yield from walk_rels(self._rels) + + @property + def main_document_part(self) -> PresentationPart: + """Return |Part| subtype serving as the main document part for this package. + + In this case it will be a |Presentation| part. + """ + return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT)) + + def next_partname(self, tmpl: str) -> PackURI: + """Return |PackURI| next available partname matching `tmpl`. + + `tmpl` 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: + '/ppt/slides/slide%d.xml' + """ + # --- expected next partname is tmpl % n where n is one greater than the number + # --- of existing partnames that match tmpl. Speed up finding the next one + # --- (maybe) by searching from the end downward rather than from 1 upward. + prefix = tmpl[: (tmpl % 42).find("42")] + partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)} + for n in range(len(partnames) + 1, 0, -1): + candidate_partname = tmpl % n + if candidate_partname not in partnames: + return PackURI(candidate_partname) + raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover + + def save(self, pkg_file: str | IO[bytes]) -> None: + """Save this package to `pkg_file`. + + `file` can be either a path to a file (a string) or a file-like object. + """ + PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts())) + + def _load(self) -> Self: + """Return the package after loading all parts and relationships.""" + pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self)) + self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts) + return self + + @lazyproperty + def _rels(self) -> _Relationships: + """|Relationships| object containing relationships of this package.""" + return _Relationships(PACKAGE_URI.baseURI) + + +class _PackageLoader: + """Function-object that loads a package from disk (or other store).""" + + def __init__(self, pkg_file: str | IO[bytes], package: Package): + self._pkg_file = pkg_file + self._package = package + + @classmethod + def load( + cls, pkg_file: str | IO[bytes], package: Package + ) -> tuple[CT_Relationships, dict[PackURI, Part]]: + """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`. + + The returned `parts` value is a {partname: part} mapping with each part in the package + included and constructed complete with its relationships to other parts in the package. + + The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed + package relationships. It is the caller's responsibility (the package object) to load + those relationships into its |_Relationships| object. + """ + return cls(pkg_file, package)._load() + + def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]: + """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file.""" + parts, xml_rels = self._parts, self._xml_rels + + for partname, part in parts.items(): + part.load_rels_from_xml(xml_rels[partname], parts) + + return xml_rels[PACKAGE_URI], parts + + @lazyproperty + def _content_types(self) -> _ContentTypeMap: + """|_ContentTypeMap| object providing content-types for items of this package. + + Provides a content-type (MIME-type) for any given partname. + """ + return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI]) + + @lazyproperty + def _package_reader(self) -> PackageReader: + """|PackageReader| object providing access to package-items in pkg_file.""" + return PackageReader(self._pkg_file) + + @lazyproperty + def _parts(self) -> dict[PackURI, Part]: + """dict {partname: Part} populated with parts loading from package. + + Among other duties, this collection is passed to each relationships collection so each + relationship can resolve a reference to its target part when required. This reference can + only be reliably carried out once the all parts have been loaded. + """ + content_types = self._content_types + package = self._package + package_reader = self._package_reader + + return { + partname: PartFactory( + partname, + content_types[partname], + package, + blob=package_reader[partname], + ) + for partname in (p for p in self._xml_rels if p != "/") + # -- invalid partnames can arise in some packages; ignore those rather than raise an + # -- exception. + if partname in package_reader + } + + @lazyproperty + def _xml_rels(self) -> dict[PackURI, CT_Relationships]: + """dict {partname: xml_rels} for package and all package parts. + + This is used as the basis for other loading operations such as loading parts and + populating their relationships. + """ + xml_rels: dict[PackURI, CT_Relationships] = {} + visited_partnames: Set[PackURI] = set() + + def load_rels(source_partname: PackURI, rels: CT_Relationships): + """Populate `xml_rels` dict by traversing relationships depth-first.""" + xml_rels[source_partname] = rels + visited_partnames.add(source_partname) + base_uri = source_partname.baseURI + + # --- recursion stops when there are no unvisited partnames in rels --- + for rel in rels.relationship_lst: + if rel.targetMode == RTM.EXTERNAL: + continue + target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref) + if target_partname in visited_partnames: + continue + load_rels(target_partname, self._xml_rels_for(target_partname)) + + load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI)) + return xml_rels + + def _xml_rels_for(self, partname: PackURI) -> CT_Relationships: + """Return CT_Relationships object formed by parsing rels XML for `partname`. + + A CT_Relationships object is returned in all cases. A part that has no relationships + receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects. + """ + rels_xml = self._package_reader.rels_xml_for(partname) + return ( + CT_Relationships.new() + if rels_xml is None + else cast(CT_Relationships, parse_xml(rels_xml)) + ) + + +class Part(_RelatableMixin): + """Base class for package parts. + + Provides common properties and methods, but intended to be subclassed in client code to + implement specific part behaviors. Also serves as the default class for parts that are not yet + given specific behaviors. + """ + + def __init__( + self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None + ): + # --- XmlPart subtypes, don't store a blob (the original XML) --- + self._partname = partname + self._content_type = content_type + self._package = package + self._blob = blob + + @classmethod + def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self: + """Return `cls` instance loaded from arguments. + + This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart + for an example. + """ + return cls(partname, content_type, package, blob) + + @property + def blob(self) -> bytes: + """Contents of this package part as a sequence of bytes. + + Intended to be overridden by subclasses. Default behavior is to return the blob initial + loaded during `Package.open()` operation. + """ + return self._blob or b"" + + @blob.setter + def blob(self, blob: bytes): + """Note that not all subclasses use the part blob as their blob source. + + In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on + demand. This works fine for binary parts though. + """ + self._blob = blob + + @lazyproperty + def content_type(self) -> str: + """Content-type (MIME-type) of this part.""" + return self._content_type + + def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None: + """load _Relationships for this part from `xml_rels`. + + Part references are resolved using the `parts` dict that maps each partname to the loaded + part with that partname. These relationships are loaded from a serialized package and so + already have assigned rIds. This method is only used during package loading. + """ + self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts) + + @lazyproperty + def package(self) -> Package: + """Package this part belongs to.""" + return self._package + + @property + def partname(self) -> PackURI: + """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" + return self._partname + + @partname.setter + def partname(self, partname: PackURI): + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( # pragma: no cover + "partname must be instance of PackURI, got '%s'" % type(partname).__name__ + ) + self._partname = partname + + @lazyproperty + def rels(self) -> _Relationships: + """Collection of relationships from this part to other parts.""" + # --- this must be public to allow the part graph to be traversed --- + return self._rels + + def _blob_from_file(self, file: str | IO[bytes]) -> bytes: + """Return bytes of `file`, which is either a str path or a file-like object.""" + # --- a str `file` is assumed to be a path --- + if isinstance(file, str): + with open(file, "rb") as f: + return f.read() + + # --- otherwise, assume `file` is a file-like object + # --- reposition file cursor if it has one + if callable(getattr(file, "seek")): + file.seek(0) + return file.read() + + @lazyproperty + def _rels(self) -> _Relationships: + """Relationships from this part to others.""" + return _Relationships(self._partname.baseURI) + + +class XmlPart(Part): + """Base class for package parts containing an XML payload, which is most of them. + + Provides additional methods to the |Part| base class that take care of parsing and + reserializing the XML payload and managing relationships to other parts. + """ + + def __init__( + self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement + ): + super(XmlPart, self).__init__(partname, content_type, package) + self._element = element + + @classmethod + def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes): + """Return instance of `cls` loaded with parsed XML from `blob`.""" + return cls( + partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob)) + ) + + @property + def blob(self) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride] + """bytes XML serialization of this part.""" + return serialize_part_xml(self._element) + + # -- XmlPart cannot set its blob, which is why pyright complains -- + + def drop_rel(self, rId: str) -> None: + """Remove relationship identified by `rId` if its reference count is under 2. + + Relationships with a reference count of 0 are implicit relationships. Note that only XML + parts can drop relationships. + """ + if self._rel_ref_count(rId) < 2: + self._rels.pop(rId) + + @property + def part(self): + """This part. + + This is 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 + + def _rel_ref_count(self, rId: str) -> int: + """Return int count of references in this part's XML to `rId`.""" + return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId]) + + +class PartFactory: + """Constructs a registered subtype of |Part|. + + Client code can register a subclass of |Part| to be used for a package blob based on its + content type. + """ + + part_type_for: dict[str, type[Part]] = {} + + def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part: + PartClass = cls._part_cls_for(content_type) + return PartClass.load(partname, content_type, package, blob) + + @classmethod + def _part_cls_for(cls, content_type: str) -> type[Part]: + """Return the custom part class registered for `content_type`. + + Returns |Part| 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 Part + + +class _ContentTypeMap: + """Value type providing dict semantics for looking up content type by partname.""" + + def __init__(self, overrides: dict[str, str], defaults: dict[str, str]): + self._overrides = overrides + self._defaults = defaults + + def __getitem__(self, partname: PackURI) -> str: + """Return content-type (MIME-type) for part identified by *partname*.""" + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + "_ContentTypeMap key must be , got %s" % type(partname).__name__ + ) + + if partname in self._overrides: + return self._overrides[partname] + + if partname.ext in self._defaults: + return self._defaults[partname.ext] + + raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname) + + @classmethod + def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap: + """Return |_ContentTypeMap| instance populated from `content_types_xml`.""" + types_elm = cast("CT_Types", parse_xml(content_types_xml)) + # -- note all partnames in [Content_Types].xml are absolute -- + overrides = CaseInsensitiveDict( + (o.partName.lower(), o.contentType) for o in types_elm.override_lst + ) + defaults = CaseInsensitiveDict( + (d.extension.lower(), d.contentType) for d in types_elm.default_lst + ) + return cls(overrides, defaults) + + +class _Relationships(Mapping[str, "_Relationship"]): + """Collection of |_Relationship| instances having `dict` semantics. + + Relationships are keyed by their rId, but may also be found in other ways, such as by their + relationship type. |Relationship| objects are keyed by their rId. + + Iterating this collection has normal mapping semantics, generating the keys (rIds) of the + mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a + `dict`. + """ + + def __init__(self, base_uri: str): + self._base_uri = base_uri + + def __contains__(self, rId: object) -> bool: + """Implement 'in' operation, like `"rId7" in relationships`.""" + return rId in self._rels + + def __getitem__(self, rId: str) -> _Relationship: + """Implement relationship lookup by rId using indexed access, like rels[rId].""" + try: + return self._rels[rId] + except KeyError: + raise KeyError("no relationship with key '%s'" % rId) + + def __iter__(self) -> Iterator[str]: + """Implement iteration of rIds (iterating a mapping produces its keys).""" + return iter(self._rels) + + def __len__(self) -> int: + """Return count of relationships in collection.""" + return len(self._rels) + + def get_or_add(self, reltype: str, target_part: Part) -> str: + """Return str rId of `reltype` to `target_part`. + + The rId of an existing matching relationship is used if present. Otherwise, a new + relationship is added and that rId is returned. + """ + existing_rId = self._get_matching(reltype, target_part) + return ( + self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId + ) + + def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str: + """Return str rId of external relationship of `reltype` to `target_ref`. + + The rId of an existing matching relationship is used if present. Otherwise, a new + relationship is added and that rId is returned. + """ + existing_rId = self._get_matching(reltype, target_ref, is_external=True) + return ( + self._add_relationship(reltype, target_ref, is_external=True) + if existing_rId is None + else existing_rId + ) + + def load_from_xml( + self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part] + ) -> None: + """Replace any relationships in this collection with those from `xml_rels`.""" + + def iter_valid_rels(): + """Filter out broken relationships such as those pointing to NULL.""" + for rel_elm in xml_rels.relationship_lst: + # --- Occasionally a PowerPoint plugin or other client will "remove" + # --- a relationship simply by "voiding" its Target value, like making + # --- it "/ppt/slides/NULL". Skip any relationships linking to a + # --- partname that is not present in the package. + if rel_elm.targetMode == RTM.INTERNAL: + partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref) + if partname not in parts: + continue + yield _Relationship.from_xml(base_uri, rel_elm, parts) + + self._rels.clear() + self._rels.update((rel.rId, rel) for rel in iter_valid_rels()) + + def part_with_reltype(self, reltype: str) -> Part: + """Return target part of relationship with matching `reltype`. + + Raises |KeyError| if not found and |ValueError| if more than one matching relationship is + found. + """ + rels_of_reltype = self._rels_by_reltype[reltype] + + if len(rels_of_reltype) == 0: + raise KeyError("no relationship of type '%s' in collection" % reltype) + + if len(rels_of_reltype) > 1: + raise ValueError("multiple relationships of type '%s' in collection" % reltype) + + return rels_of_reltype[0].target_part + + def pop(self, rId: str) -> _Relationship: + """Return |_Relationship| identified by `rId` after removing it from collection. + + The caller is responsible for ensuring it is no longer required. + """ + return self._rels.pop(rId) + + @property + def xml(self): + """bytes XML serialization of this relationship collection. + + This value is suitable for storage as a .rels file in an OPC package. Includes a ` elements deterministically (in numerical order) to + # -- simplify testing and manual inspection. + def iter_rels_in_numerical_order(): + sorted_num_rId_pairs = sorted( + ( + int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0, + rId, + ) + for rId in self.keys() + ) + return (self[rId] for _, rId in sorted_num_rId_pairs) + + for rel in iter_rels_in_numerical_order(): + rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external) + + return rels_elm.xml_file_bytes + + def _add_relationship(self, reltype: str, target: Part | str, is_external: bool = False) -> str: + """Return str rId of |_Relationship| newly added to spec.""" + rId = self._next_rId + self._rels[rId] = _Relationship( + self._base_uri, + rId, + reltype, + target_mode=RTM.EXTERNAL if is_external else RTM.INTERNAL, + target=target, + ) + return rId + + def _get_matching( + self, reltype: str, target: Part | str, is_external: bool = False + ) -> str | None: + """Return optional str rId of rel of `reltype`, `target`, and `is_external`. + + Returns `None` on no matching relationship + """ + for rel in self._rels_by_reltype[reltype]: + if rel.is_external != is_external: + continue + rel_target = rel.target_ref if rel.is_external else rel.target_part + if rel_target == target: + return rel.rId + + return None + + @property + def _next_rId(self) -> str: + """Next str rId available in collection. + + The next rId is the first unused key starting from "rId1" and making use of any gaps in + numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']. + """ + # --- The common case is where all sequential numbers starting at "rId1" are + # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start + # --- there and count down to produce the best performance. + for n in range(len(self) + 1, 0, -1): + rId_candidate = "rId%d" % n # like 'rId19' + if rId_candidate not in self._rels: + return rId_candidate + raise Exception( + "ProgrammingError: Impossible to have more distinct rIds than relationships" + ) + + @lazyproperty + def _rels(self) -> dict[str, _Relationship]: + """dict {rId: _Relationship} containing relationships of this collection.""" + return {} + + @property + def _rels_by_reltype(self) -> dict[str, list[_Relationship]]: + """defaultdict {reltype: [rels]} for all relationships in collection.""" + D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list) + for rel in self.values(): + D[rel.reltype].append(rel) + return D + + +class _Relationship: + """Value object describing link from a part or package to another part.""" + + def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str): + self._base_uri = base_uri + self._rId = rId + self._reltype = reltype + self._target_mode = target_mode + self._target = target + + @classmethod + def from_xml( + cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part] + ) -> _Relationship: + """Return |_Relationship| object based on CT_Relationship element `rel`.""" + target = ( + rel.target_ref + if rel.targetMode == RTM.EXTERNAL + else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)] + ) + return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target) + + @lazyproperty + def is_external(self) -> bool: + """True if target_mode is `RTM.EXTERNAL`. + + An external relationship is a link to a resource outside the package, such as a + web-resource (URL). + """ + return self._target_mode == RTM.EXTERNAL + + @lazyproperty + def reltype(self) -> str: + """Member of RELATIONSHIP_TYPE describing relationship of target to source.""" + return self._reltype + + @lazyproperty + def rId(self) -> str: + """str relationship-id, like 'rId9'. + + Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies + this relationship within its peers for the source-part or package. + """ + return self._rId + + @lazyproperty + def target_part(self) -> Part: + """|Part| or subtype referred to by this relationship.""" + if self.is_external: + raise ValueError( + "`.target_part` property on _Relationship is undefined when " + "target-mode is external" + ) + assert isinstance(self._target, Part) + return self._target + + @lazyproperty + def target_partname(self) -> PackURI: + """|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: + raise ValueError( + "`.target_partname` property on _Relationship is undefined when " + "target-mode is external" + ) + assert isinstance(self._target, Part) + return self._target.partname + + @lazyproperty + def target_ref(self) -> str: + """str reference to relationship target. + + For internal relationships this is the relative partname, suitable for serialization + purposes. For an external relationship it is typically a URL. + """ + if self.is_external: + assert isinstance(self._target, str) + return self._target + + return self.target_partname.relative_ref(self._base_uri) diff --git a/src/pptx/opc/packuri.py b/src/pptx/opc/packuri.py new file mode 100644 index 000000000..74ddd333f --- /dev/null +++ b/src/pptx/opc/packuri.py @@ -0,0 +1,109 @@ +"""Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI.""" + +from __future__ import annotations + +import posixpath +import re + + +class PackURI(str): + """Proxy for a pack URI (partname). + + Provides utility properties the baseURI and the filename slice. Behaves as |str| otherwise. + """ + + _filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?") + + def __new__(cls, pack_uri_str: str): + if not pack_uri_str[0] == "/": + raise ValueError(f"PackURI must begin with slash, got {repr(pack_uri_str)}") + return str.__new__(cls, pack_uri_str) + + @staticmethod + def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI: + """Construct an 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) -> 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-partname "/", the baseURI is "/". + """ + return posixpath.split(self)[0] + + @property + def ext(self) -> str: + """The extension portion of this pack URI. + + E.g. `"xml"` for `"/ppt/slides/slide1.xml"`. Note the leading period is not included. + """ + # -- raw_ext is either empty string or starts with period, e.g. ".xml" -- + raw_ext = posixpath.splitext(self)[1] + return raw_ext[1:] if raw_ext.startswith(".") else raw_ext + + @property + def filename(self) -> str: + """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) -> int | None: + """Optional int partname index. + + Value is an integer for an "array" partname or None for singleton partname, e.g. `21` for + `"/ppt/slides/slide21.xml"` and |None| for `"/ppt/presentation.xml"`. + """ + filename = self.filename + if not filename: + return None + name_part = posixpath.splitext(filename)[0] # filename w/ext removed + match = self._filename_re.match(name_part) + if match is None: + return None + if match.group(2): + return int(match.group(2)) + return None + + @property + def membername(self) -> str: + """The pack URI with the leading slash stripped off. + + This is 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: str) -> str: + """Return string containing relative reference to package item from `baseURI`. + + E.g. PackURI("/ppt/slideLayouts/slideLayout1.xml") would return + "../slideLayouts/slideLayout1.xml" for baseURI "/ppt/slides". + """ + # workaround for posixpath bug in 2.6, doesn't generate correct + # relative path when `start` (second) parameter is root ("/") + return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) + + @property + def rels_uri(self) -> PackURI: + """The pack URI of the .rels part corresponding to the current pack URI. + + Only produces sensible output if the pack URI is a partname or the package pseudo-partname + "/". + """ + rels_filename = "%s.rels" % self.filename + rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename) + return PackURI(rels_uri_str) + + +PACKAGE_URI = PackURI("/") +CONTENT_TYPES_URI = PackURI("/[Content_Types].xml") diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py new file mode 100644 index 000000000..92366708b --- /dev/null +++ b/src/pptx/opc/serialized.py @@ -0,0 +1,296 @@ +"""API for reading/writing serialized Open Packaging Convention (OPC) package.""" + +from __future__ import annotations + +import os +import posixpath +import zipfile +from typing import IO, TYPE_CHECKING, Any, Container, Sequence + +from pptx.exc import PackageNotFoundError +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.oxml import CT_Types, serialize_part_xml +from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI +from pptx.opc.shared import CaseInsensitiveDict +from pptx.opc.spec import default_content_types +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.opc.package import Part, _Relationships # pyright: ignore[reportPrivateUsage] + + +class PackageReader(Container[bytes]): + """Provides access to package-parts of an OPC package with dict semantics. + + The package may be in zip-format (a .pptx file) or expanded into a directory structure, + perhaps by unzipping a .pptx file. + """ + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + def __contains__(self, pack_uri: object) -> bool: + """Return True when part identified by `pack_uri` is present in package.""" + return pack_uri in self._blob_reader + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Return bytes for part corresponding to `pack_uri`.""" + return self._blob_reader[pack_uri] + + def rels_xml_for(self, partname: PackURI) -> bytes | None: + """Return optional rels item XML for `partname`. + + Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI| + instance. + """ + blob_reader, uri = self._blob_reader, partname.rels_uri + return blob_reader[uri] if uri in blob_reader else None + + @lazyproperty + def _blob_reader(self) -> _PhysPkgReader: + """|_PhysPkgReader| subtype providing read access to the package file.""" + return _PhysPkgReader.factory(self._pkg_file) + + +class PackageWriter: + """Writes a zip-format OPC package to `pkg_file`. + + `pkg_file` can be either a path to a zip file (a string) or a file-like object. `pkg_rels` is + the |_Relationships| object containing relationships for the package. `parts` is a sequence of + |Part| subtype instance to be written to the package. + + Its single API classmethod is :meth:`write`. This class is not intended to be instantiated. + """ + + def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]): + self._pkg_file = pkg_file + self._pkg_rels = pkg_rels + self._parts = parts + + @classmethod + def write( + cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part] + ) -> None: + """Write a physical package (.pptx file) to `pkg_file`. + + The serialized package contains `pkg_rels` and `parts`, a content-types stream based on + the content type of each part, and a .rels file for each part that has relationships. + """ + cls(pkg_file, pkg_rels, parts)._write() + + def _write(self) -> None: + """Write physical package (.pptx file).""" + with _PhysPkgWriter.factory(self._pkg_file) as phys_writer: + self._write_content_types_stream(phys_writer) + self._write_pkg_rels(phys_writer) + self._write_parts(phys_writer) + + def _write_content_types_stream(self, phys_writer: _PhysPkgWriter) -> None: + """Write `[Content_Types].xml` part to the physical package. + + This part must contain an appropriate content type lookup target for each part in the + package. + """ + phys_writer.write( + CONTENT_TYPES_URI, + serialize_part_xml(_ContentTypesItem.xml_for(self._parts)), + ) + + def _write_parts(self, phys_writer: _PhysPkgWriter) -> None: + """Write blob of each part in `parts` to the package. + + A rels item for each part is also written when the part has relationships. + """ + for part in self._parts: + phys_writer.write(part.partname, part.blob) + if part._rels: # pyright: ignore[reportPrivateUsage] + phys_writer.write(part.partname.rels_uri, part.rels.xml) + + def _write_pkg_rels(self, phys_writer: _PhysPkgWriter) -> None: + """Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package.""" + phys_writer.write(PACKAGE_URI.rels_uri, self._pkg_rels.xml) + + +class _PhysPkgReader(Container[PackURI]): + """Base class for physical package reader objects.""" + + def __contains__(self, item: object) -> bool: + """Must be implemented by each subclass.""" + raise NotImplementedError( # pragma: no cover + "`%s` must implement `.__contains__()`" % type(self).__name__ + ) + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Blob for part corresponding to `pack_uri`.""" + raise NotImplementedError( # pragma: no cover + f"`{type(self).__name__}` must implement `.__contains__()`" + ) + + @classmethod + def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader: + """Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`.""" + # --- for pkg_file other than str, assume it's a stream and pass it to Zip + # --- reader to sort out + if not isinstance(pkg_file, str): + return _ZipPkgReader(pkg_file) + + # --- otherwise we treat `pkg_file` as a path --- + if os.path.isdir(pkg_file): + return _DirPkgReader(pkg_file) + + if zipfile.is_zipfile(pkg_file): + return _ZipPkgReader(pkg_file) + + raise PackageNotFoundError("Package not found at '%s'" % pkg_file) + + +class _DirPkgReader(_PhysPkgReader): + """Implements |PhysPkgReader| interface for OPC package extracted into directory. + + `path` is the path to a directory containing an expanded package. + """ + + def __init__(self, path: str): + self._path = os.path.abspath(path) + + def __contains__(self, pack_uri: object) -> bool: + """Return True when part identified by `pack_uri` is present in zip archive.""" + if not isinstance(pack_uri, PackURI): + return False + return os.path.exists(posixpath.join(self._path, pack_uri.membername)) + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Return bytes of file corresponding to `pack_uri` in package directory.""" + path = os.path.join(self._path, pack_uri.membername) + try: + with open(path, "rb") as f: + return f.read() + except IOError: + raise KeyError("no member '%s' in package" % pack_uri) + + +class _ZipPkgReader(_PhysPkgReader): + """Implements |PhysPkgReader| interface for a zip-file OPC package.""" + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + def __contains__(self, pack_uri: object) -> bool: + """Return True when part identified by `pack_uri` is present in zip archive.""" + return pack_uri in self._blobs + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Return bytes for part corresponding to `pack_uri`. + + Raises |KeyError| if no matching member is present in zip archive. + """ + if pack_uri not in self._blobs: + raise KeyError("no member '%s' in package" % pack_uri) + return self._blobs[pack_uri] + + @lazyproperty + def _blobs(self) -> dict[PackURI, bytes]: + """dict mapping partname to package part binaries.""" + with zipfile.ZipFile(self._pkg_file, "r") as z: + return {PackURI("/%s" % name): z.read(name) for name in z.namelist()} + + +class _PhysPkgWriter: + """Base class for physical package writer objects.""" + + @classmethod + def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter: + """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`. + + Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented + or even a `_StreamPkgWriter`. + """ + return _ZipPkgWriter(pkg_file) + + def write(self, pack_uri: PackURI, blob: bytes) -> None: + """Write `blob` to package with membername corresponding to `pack_uri`.""" + raise NotImplementedError( # pragma: no cover + f"`{type(self).__name__}` must implement `.write()`" + ) + + +class _ZipPkgWriter(_PhysPkgWriter): + """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package.""" + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + def __enter__(self) -> _ZipPkgWriter: + """Enable use as a context-manager. Opening zip for writing happens here.""" + return self + + def __exit__(self, *exc: list[Any]) -> None: + """Close the zip archive on exit from context. + + Closing flushes any pending physical writes and releasing any resources it's using. + """ + self._zipf.close() + + def write(self, pack_uri: PackURI, blob: bytes) -> None: + """Write `blob` to zip package with membername corresponding to `pack_uri`.""" + self._zipf.writestr(pack_uri.membername, blob) + + @lazyproperty + def _zipf(self) -> zipfile.ZipFile: + """`ZipFile` instance open for writing.""" + return zipfile.ZipFile( + self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) + + +class _ContentTypesItem: + """Composes content-types "part" ([Content_Types].xml) for a collection of parts.""" + + def __init__(self, parts: Sequence[Part]): + self._parts = parts + + @classmethod + def xml_for(cls, parts: Sequence[Part]) -> CT_Types: + """Return content-types XML mapping each part in `parts` to a content-type. + + The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package. + """ + return cls(parts)._xml + + @lazyproperty + def _xml(self) -> CT_Types: + """lxml.etree._Element containing the content-types item. + + This XML object is suitable for serialization to the `[Content_Types].xml` item for 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. + """ + defaults, overrides = self._defaults_and_overrides + _types_elm = CT_Types.new() + + for ext, content_type in sorted(defaults.items()): + _types_elm.add_default(ext, content_type) + for partname, content_type in sorted(overrides.items()): + _types_elm.add_override(partname, content_type) + + return _types_elm + + @lazyproperty + def _defaults_and_overrides(self) -> tuple[dict[str, str], dict[PackURI, str]]: + """pair of dict (defaults, overrides) accounting for all parts. + + `defaults` is {ext: content_type} and overrides is {partname: content_type}. + """ + defaults = CaseInsensitiveDict(rels=CT.OPC_RELATIONSHIPS, xml=CT.XML) + overrides: dict[PackURI, str] = {} + + for part in self._parts: + partname, content_type = part.partname, part.content_type + ext = partname.ext + if (ext.lower(), content_type) in default_content_types: + defaults[ext] = content_type + else: + overrides[partname] = content_type + + return defaults, overrides diff --git a/src/pptx/opc/shared.py b/src/pptx/opc/shared.py new file mode 100644 index 000000000..cc7fce8c1 --- /dev/null +++ b/src/pptx/opc/shared.py @@ -0,0 +1,20 @@ +"""Objects shared by modules in the pptx.opc sub-package.""" + +from __future__ import annotations + + +class CaseInsensitiveDict(dict): + """Mapping type like dict except it matches key without respect to case. + + For example, D['A'] == D['a']. Note this is not general-purpose, just complete + enough to satisfy opc package needs. It assumes str keys for example. + """ + + def __contains__(self, key): + return super(CaseInsensitiveDict, self).__contains__(key.lower()) + + def __getitem__(self, key): + return super(CaseInsensitiveDict, self).__getitem__(key.lower()) + + def __setitem__(self, key, value): + return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) diff --git a/pptx/opc/spec.py b/src/pptx/opc/spec.py similarity index 85% rename from pptx/opc/spec.py rename to src/pptx/opc/spec.py index 5b63f425c..a83caf8bd 100644 --- a/pptx/opc/spec.py +++ b/src/pptx/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 pptx.opc.constants import CONTENT_TYPE as CT default_content_types = ( ("bin", CT.PML_PRINTER_SETTINGS), diff --git a/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py similarity index 85% rename from pptx/oxml/__init__.py rename to src/pptx/oxml/__init__.py index 3eebf4f10..21afaa921 100644 --- a/pptx/oxml/__init__.py +++ b/src/pptx/oxml/__init__.py @@ -1,70 +1,65 @@ -# encoding: utf-8 - """Initializes lxml parser, particularly the custom element classes. Also makes available a handful of functions that wrap its typical uses. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import os +from typing import TYPE_CHECKING, Type from lxml import etree -from .ns import NamespacePrefixedTag +from pptx.oxml.ns import NamespacePrefixedTag + +if TYPE_CHECKING: + from pptx.oxml.xmlchemy import BaseOxmlElement -# configure etree XML parser ------------------------------- +# -- configure etree 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_from_template(template_name): - """ - Return an element loaded from the XML in the template file identified by - *template_name*. - """ +def parse_from_template(template_file_name: str): + """Return an element loaded from the XML in the template file identified by `template_name`.""" thisdir = os.path.split(__file__)[0] - filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_name) + filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_file_name) with open(filename, "rb") as f: xml = f.read() return parse_xml(xml) -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. - """ - root_element = etree.fromstring(xml, oxml_parser) - return root_element +def parse_xml(xml: str | bytes): + """Return root lxml element obtained by parsing XML character string in `xml`.""" + return etree.fromstring(xml, oxml_parser) -def register_element_cls(nsptagname, cls): - """ - Register *cls* to be constructed when the oxml parser encounters an - element having name *nsptag_name*. *nsptag_name* is a string of the form - ``nspfx:tagroot``, e.g. ``'w:document'``. +def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]): + """Register `cls` to be constructed when oxml parser encounters element having `nsptag_name`. + + `nsptag_name` is a string of the form `nspfx:tagroot`, e.g. `"w:document"`. """ nsptag = NamespacePrefixedTag(nsptagname) namespace = element_class_lookup.get_namespace(nsptag.nsuri) namespace[nsptag.local_part] = cls -from .action import CT_Hyperlink # noqa: E402 +from pptx.oxml.action import CT_Hyperlink # noqa: E402 register_element_cls("a:hlinkClick", CT_Hyperlink) register_element_cls("a:hlinkHover", CT_Hyperlink) -from .chart.axis import ( # noqa: E402 +from pptx.oxml.chart.axis import ( # noqa: E402 CT_AxisUnit, CT_CatAx, CT_ChartLines, CT_Crosses, CT_DateAx, CT_LblOffset, + CT_Orientation, CT_Scaling, CT_TickLblPos, CT_TickMark, @@ -80,12 +75,13 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:majorUnit", CT_AxisUnit) register_element_cls("c:minorTickMark", CT_TickMark) register_element_cls("c:minorUnit", CT_AxisUnit) +register_element_cls("c:orientation", CT_Orientation) register_element_cls("c:scaling", CT_Scaling) register_element_cls("c:tickLblPos", CT_TickLblPos) register_element_cls("c:valAx", CT_ValAx) -from .chart.chart import ( # noqa: E402 +from pptx.oxml.chart.chart import ( # noqa: E402 CT_Chart, CT_ChartSpace, CT_ExternalData, @@ -100,27 +96,27 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:style", CT_Style) -from .chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls # noqa: E402 +from pptx.oxml.chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls # noqa: E402 register_element_cls("c:dLbl", CT_DLbl) register_element_cls("c:dLblPos", CT_DLblPos) register_element_cls("c:dLbls", CT_DLbls) -from .chart.legend import CT_Legend, CT_LegendPos # noqa: E402 +from pptx.oxml.chart.legend import CT_Legend, CT_LegendPos # noqa: E402 register_element_cls("c:legend", CT_Legend) register_element_cls("c:legendPos", CT_LegendPos) -from .chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle # noqa: E402 +from pptx.oxml.chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle # noqa: E402 register_element_cls("c:marker", CT_Marker) register_element_cls("c:size", CT_MarkerSize) register_element_cls("c:symbol", CT_MarkerStyle) -from .chart.plot import ( # noqa: E402 +from pptx.oxml.chart.plot import ( # noqa: E402 CT_Area3DChart, CT_AreaChart, CT_BarChart, @@ -153,7 +149,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:scatterChart", CT_ScatterChart) -from .chart.series import ( # noqa: E402 +from pptx.oxml.chart.series import ( # noqa: E402 CT_AxDataSource, CT_DPt, CT_Lvl, @@ -173,7 +169,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:yVal", CT_NumDataSource) -from .chart.shared import ( # noqa: E402 +from pptx.oxml.chart.shared import ( # noqa: E402 CT_Boolean, CT_Boolean_Explicit, CT_Double, @@ -216,12 +212,12 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:xMode", CT_LayoutMode) -from .coreprops import CT_CoreProperties # noqa: E402 +from pptx.oxml.coreprops import CT_CoreProperties # noqa: E402 register_element_cls("cp:coreProperties", CT_CoreProperties) -from .dml.color import ( # noqa: E402 +from pptx.oxml.dml.color import ( # noqa: E402 CT_Color, CT_HslColor, CT_Percentage, @@ -244,7 +240,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("a:sysClr", CT_SystemColor) -from .dml.fill import ( # noqa: E402 +from pptx.oxml.dml.fill import ( # noqa: E402 CT_Blip, CT_BlipFillProperties, CT_GradientFillProperties, @@ -271,12 +267,12 @@ def register_element_cls(nsptagname, cls): register_element_cls("a:srcRect", CT_RelativeRect) -from .dml.line import CT_PresetLineDashProperties # noqa: E402 +from pptx.oxml.dml.line import CT_PresetLineDashProperties # noqa: E402 register_element_cls("a:prstDash", CT_PresetLineDashProperties) -from .presentation import ( # noqa: E402 +from pptx.oxml.presentation import ( # noqa: E402 CT_Presentation, CT_SlideId, CT_SlideIdList, @@ -293,7 +289,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:sldSz", CT_SlideSize) -from .shapes.autoshape import ( # noqa: E402 +from pptx.oxml.shapes.autoshape import ( # noqa: E402 CT_AdjPoint2D, CT_CustomGeometry2D, CT_GeomGuide, @@ -324,7 +320,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:sp", CT_Shape) -from .shapes.connector import ( # noqa: E402 +from pptx.oxml.shapes.connector import ( # noqa: E402 CT_Connection, CT_Connector, CT_ConnectorNonVisual, @@ -338,7 +334,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:nvCxnSpPr", CT_ConnectorNonVisual) -from .shapes.graphfrm import ( # noqa: E402 +from pptx.oxml.shapes.graphfrm import ( # noqa: E402 CT_GraphicalObject, CT_GraphicalObjectData, CT_GraphicalObjectFrame, @@ -353,7 +349,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:oleObj", CT_OleObject) -from .shapes.groupshape import ( # noqa: E402 +from pptx.oxml.shapes.groupshape import ( # noqa: E402 CT_GroupShape, CT_GroupShapeNonVisual, CT_GroupShapeProperties, @@ -365,14 +361,14 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:spTree", CT_GroupShape) -from .shapes.picture import CT_Picture, CT_PictureNonVisual # noqa: E402 +from pptx.oxml.shapes.picture import CT_Picture, CT_PictureNonVisual # noqa: E402 register_element_cls("p:blipFill", CT_BlipFillProperties) register_element_cls("p:nvPicPr", CT_PictureNonVisual) register_element_cls("p:pic", CT_Picture) -from .shapes.shared import ( # noqa: E402 +from pptx.oxml.shapes.shared import ( # noqa: E402 CT_ApplicationNonVisualDrawingProps, CT_LineProperties, CT_NonVisualDrawingProps, @@ -397,7 +393,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:xfrm", CT_Transform2D) -from .slide import ( # noqa: E402 +from pptx.oxml.slide import ( # noqa: E402 CT_Background, CT_BackgroundProperties, CT_CommonSlideData, @@ -428,7 +424,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:video", CT_TLMediaNodeVideo) -from .table import ( # noqa: E402 +from pptx.oxml.table import ( # noqa: E402 CT_Table, CT_TableCell, CT_TableCellProperties, @@ -447,7 +443,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("a:tr", CT_TableRow) -from .text import ( # noqa: E402 +from pptx.oxml.text import ( # noqa: E402 CT_RegularTextRun, CT_TextBody, CT_TextBodyProperties, @@ -485,6 +481,6 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:txBody", CT_TextBody) -from .theme import CT_OfficeStyleSheet # noqa: E402 +from pptx.oxml.theme import CT_OfficeStyleSheet # noqa: E402 register_element_cls("a:theme", CT_OfficeStyleSheet) diff --git a/src/pptx/oxml/action.py b/src/pptx/oxml/action.py new file mode 100644 index 000000000..9b31a9e16 --- /dev/null +++ b/src/pptx/oxml/action.py @@ -0,0 +1,53 @@ +"""lxml custom element classes for text-related XML elements.""" + +from __future__ import annotations + +from pptx.oxml.simpletypes import XsdString +from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute + + +class CT_Hyperlink(BaseOxmlElement): + """Custom element class for elements.""" + + rId: str = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + action: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "action", XsdString + ) + + @property + def action_fields(self) -> dict[str, str]: + """Query portion of the `ppaction://` URL as dict. + + For example `{'id':'0', 'return':'true'}` in 'ppaction://customshow?id=0&return=true'. + + Returns an empty dict if the URL contains no query string or if no action attribute is + present. + """ + url = self.action + + if url is None: + return {} + + halves = url.split("?") + if len(halves) == 1: + return {} + + key_value_pairs = halves[1].split("&") + return dict([pair.split("=") for pair in key_value_pairs]) + + @property + def action_verb(self) -> str | None: + """The host portion of the `ppaction://` URL contained in the action attribute. + + For example 'customshow' in 'ppaction://customshow?id=0&return=true'. Returns |None| if no + action attribute is present. + """ + url = self.action + + if url is None: + return None + + protocol_and_host = url.split("?")[0] + host = protocol_and_host[11:] + + return host diff --git a/pptx/oxml/chart/__init__.py b/src/pptx/oxml/chart/__init__.py similarity index 100% rename from pptx/oxml/chart/__init__.py rename to src/pptx/oxml/chart/__init__.py diff --git a/pptx/oxml/chart/axis.py b/src/pptx/oxml/chart/axis.py similarity index 73% rename from pptx/oxml/chart/axis.py rename to src/pptx/oxml/chart/axis.py index d60f6fa8f..7129810c9 100644 --- a/pptx/oxml/chart/axis.py +++ b/src/pptx/oxml/chart/axis.py @@ -1,16 +1,12 @@ -# encoding: utf-8 +"""Axis-related oxml objects.""" -""" -Axis-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ...enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK -from .shared import CT_Title -from ..simpletypes import ST_AxisUnit, ST_LblOffset -from ..text import CT_TextBody -from ..xmlchemy import ( +from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK +from pptx.oxml.chart.shared import CT_Title +from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset, ST_Orientation +from pptx.oxml.text import CT_TextBody +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, @@ -20,9 +16,7 @@ class BaseAxisElement(BaseOxmlElement): - """ - Base class for catAx, valAx, and perhaps other axis elements. - """ + """Base class for catAx, dateAx, valAx, and perhaps other axis elements.""" @property def defRPr(self): @@ -34,6 +28,25 @@ def defRPr(self): defRPr = txPr.defRPr return defRPr + @property + def orientation(self): + """Value of `val` attribute of `c:scaling/c:orientation` grandchild element. + + Defaults to `ST_Orientation.MIN_MAX` if attribute or any ancestors are not + present. + """ + orientation = self.scaling.orientation + if orientation is None: + return ST_Orientation.MIN_MAX + return orientation.val + + @orientation.setter + def orientation(self, value): + """`value` is a member of `ST_Orientation`.""" + self.scaling._remove_orientation() + if value == ST_Orientation.MAX_MIN: + self.scaling.get_or_add_orientation().val = value + def _new_title(self): return CT_Title.new_title() @@ -42,17 +55,13 @@ def _new_txPr(self): class CT_AxisUnit(BaseOxmlElement): - """ - Used for ```` and ```` elements, and others. - """ + """Used for `c:majorUnit` and `c:minorUnit` elements, and others.""" val = RequiredAttribute("val", ST_AxisUnit) class CT_CatAx(BaseAxisElement): - """ - ```` element, defining a category axis. - """ + """`c:catAx` element, defining a category axis.""" _tag_seq = ( "c:axId", @@ -97,27 +106,22 @@ class CT_CatAx(BaseAxisElement): class CT_ChartLines(BaseOxmlElement): - """ - Used for c:majorGridlines and c:minorGridlines, specifies gridlines - visual properties such as color and width. + """Used for `c:majorGridlines` and `c:minorGridlines`. + + Specifies gridlines visual properties such as color and width. """ spPr = ZeroOrOne("c:spPr", successors=()) -class CT_Crosses(BaseAxisElement): - """ - ```` element, specifying where the other axis crosses this - one. - """ +class CT_Crosses(BaseOxmlElement): + """`c:crosses` element, specifying where the other axis crosses this one.""" val = RequiredAttribute("val", XL_AXIS_CROSSES) class CT_DateAx(BaseAxisElement): - """ - ```` element, defining a date (category) axis. - """ + """`c:dateAx` element, defining a date (category) axis.""" _tag_seq = ( "c:axId", @@ -163,21 +167,33 @@ class CT_DateAx(BaseAxisElement): class CT_LblOffset(BaseOxmlElement): - """ - ```` custom element class - """ + """`c:lblOffset` custom element class.""" val = OptionalAttribute("val", ST_LblOffset, default=100) -class CT_Scaling(BaseOxmlElement): +class CT_Orientation(BaseOxmlElement): + """`c:xAx/c:scaling/c:orientation` element, defining category order. + + Used to reverse the order categories appear in on a bar chart so they start at the + top rather than the bottom. Because we read top-to-bottom, the default way looks odd + to many and perhaps most folks. Also applicable to value and date axes. """ - ```` element, defining axis scale characteristics such as - maximum value, log vs. linear, etc. + + val = OptionalAttribute("val", ST_Orientation, default=ST_Orientation.MIN_MAX) + + +class CT_Scaling(BaseOxmlElement): + """`c:scaling` element. + + Defines axis scale characteristics such as maximum value, log vs. linear, etc. """ - max = ZeroOrOne("c:max", successors=("c:min", "c:extLst")) - min = ZeroOrOne("c:min", successors=("c:extLst",)) + _tag_seq = ("c:logBase", "c:orientation", "c:max", "c:min", "c:extLst") + orientation = ZeroOrOne("c:orientation", successors=_tag_seq[2:]) + max = ZeroOrOne("c:max", successors=_tag_seq[3:]) + min = ZeroOrOne("c:min", successors=_tag_seq[4:]) + del _tag_seq @property def maximum(self): @@ -225,25 +241,19 @@ def minimum(self, value): class CT_TickLblPos(BaseOxmlElement): - """ - ```` element. - """ + """`c:tickLblPos` element.""" val = OptionalAttribute("val", XL_TICK_LABEL_POSITION) class CT_TickMark(BaseOxmlElement): - """ - Used for ```` and ````. - """ + """Used for `c:minorTickMark` and `c:majorTickMark`.""" val = OptionalAttribute("val", XL_TICK_MARK, default=XL_TICK_MARK.CROSS) class CT_ValAx(BaseAxisElement): - """ - ```` element, defining a value axis. - """ + """`c:valAx` element, defining a value axis.""" _tag_seq = ( "c:axId", diff --git a/pptx/oxml/chart/chart.py b/src/pptx/oxml/chart/chart.py similarity index 94% rename from pptx/oxml/chart/chart.py rename to src/pptx/oxml/chart/chart.py index 65a0191e7..f4cd0dc7c 100644 --- a/pptx/oxml/chart/chart.py +++ b/src/pptx/oxml/chart/chart.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Custom element classes for top-level chart-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import cast from pptx.oxml import parse_xml from pptx.oxml.chart.shared import CT_Title @@ -40,9 +40,7 @@ class CT_Chart(BaseOxmlElement): autoTitleDeleted = ZeroOrOne("c:autoTitleDeleted", successors=_tag_seq[2:]) plotArea = OneAndOnlyOne("c:plotArea") legend = ZeroOrOne("c:legend", successors=_tag_seq[9:]) - rId = RequiredAttribute("r:id", XsdString) - - _chart_tmpl = '' % (nsdecls("c"), nsdecls("r")) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] @property def has_legend(self): @@ -69,13 +67,9 @@ def has_legend(self, bool_value): self._add_legend() @staticmethod - def new_chart(rId): - """ - Return a new ```` element - """ - xml = CT_Chart._chart_tmpl % (rId) - chart = parse_xml(xml) - return chart + def new_chart(rId: str) -> CT_Chart: + """Return a new `c:chart` element.""" + return cast(CT_Chart, parse_xml(f'')) def _new_title(self): return CT_Title.new_title() diff --git a/pptx/oxml/chart/datalabel.py b/src/pptx/oxml/chart/datalabel.py similarity index 98% rename from pptx/oxml/chart/datalabel.py rename to src/pptx/oxml/chart/datalabel.py index 091693919..b6aac2fd5 100644 --- a/pptx/oxml/chart/datalabel.py +++ b/src/pptx/oxml/chart/datalabel.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Chart data-label related oxml objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.enum.chart import XL_DATA_LABEL_POSITION from pptx.oxml import parse_xml diff --git a/pptx/oxml/chart/legend.py b/src/pptx/oxml/chart/legend.py similarity index 85% rename from pptx/oxml/chart/legend.py rename to src/pptx/oxml/chart/legend.py index 7a2eadb8e..196ca15de 100644 --- a/pptx/oxml/chart/legend.py +++ b/src/pptx/oxml/chart/legend.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""lxml custom element classes for legend-related XML elements.""" -""" -lxml custom element classes for legend-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ...enum.chart import XL_LEGEND_POSITION -from ..text import CT_TextBody -from ..xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne +from pptx.enum.chart import XL_LEGEND_POSITION +from pptx.oxml.text import CT_TextBody +from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne class CT_Legend(BaseOxmlElement): diff --git a/pptx/oxml/chart/marker.py b/src/pptx/oxml/chart/marker.py similarity index 84% rename from pptx/oxml/chart/marker.py rename to src/pptx/oxml/chart/marker.py index e849e3be2..34afd13d5 100644 --- a/pptx/oxml/chart/marker.py +++ b/src/pptx/oxml/chart/marker.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""Series-related oxml objects.""" -""" -Series-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals - -from ...enum.chart import XL_MARKER_STYLE -from ..simpletypes import ST_MarkerSize -from ..xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne +from pptx.enum.chart import XL_MARKER_STYLE +from pptx.oxml.simpletypes import ST_MarkerSize +from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne class CT_Marker(BaseOxmlElement): diff --git a/pptx/oxml/chart/plot.py b/src/pptx/oxml/chart/plot.py similarity index 97% rename from pptx/oxml/chart/plot.py rename to src/pptx/oxml/chart/plot.py index f917913df..9c695a43a 100644 --- a/pptx/oxml/chart/plot.py +++ b/src/pptx/oxml/chart/plot.py @@ -1,25 +1,21 @@ -# encoding: utf-8 +"""Plot-related oxml objects.""" -""" -Plot-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from .datalabel import CT_DLbls -from ..simpletypes import ( +from pptx.oxml.chart.datalabel import CT_DLbls +from pptx.oxml.simpletypes import ( ST_BarDir, ST_BubbleScale, ST_GapAmount, ST_Grouping, ST_Overlap, ) -from ..xmlchemy import ( +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, - ZeroOrOne, ZeroOrMore, + ZeroOrOne, ) diff --git a/pptx/oxml/chart/series.py b/src/pptx/oxml/chart/series.py similarity index 97% rename from pptx/oxml/chart/series.py rename to src/pptx/oxml/chart/series.py index 2974a2269..9264d552d 100644 --- a/pptx/oxml/chart/series.py +++ b/src/pptx/oxml/chart/series.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""Series-related oxml objects.""" -""" -Series-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from .datalabel import CT_DLbls -from ..simpletypes import XsdUnsignedInt -from ..xmlchemy import ( +from pptx.oxml.chart.datalabel import CT_DLbls +from pptx.oxml.simpletypes import XsdUnsignedInt +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OxmlElement, diff --git a/pptx/oxml/chart/shared.py b/src/pptx/oxml/chart/shared.py similarity index 96% rename from pptx/oxml/chart/shared.py rename to src/pptx/oxml/chart/shared.py index ddea5132c..5515aa4be 100644 --- a/pptx/oxml/chart/shared.py +++ b/src/pptx/oxml/chart/shared.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Shared oxml objects for charts.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.oxml import parse_xml from pptx.oxml.ns import nsdecls @@ -186,10 +184,7 @@ def tx_rich(self): def new_title(): """Return "loose" `c:title` element containing default children.""" return parse_xml( - "" - " " - ' ' - "" % nsdecls("c") + "" " " ' ' "" % nsdecls("c") ) diff --git a/pptx/oxml/coreprops.py b/src/pptx/oxml/coreprops.py similarity index 60% rename from pptx/oxml/coreprops.py rename to src/pptx/oxml/coreprops.py index 2993e88bc..de6b26b24 100644 --- a/pptx/oxml/coreprops.py +++ b/src/pptx/oxml/coreprops.py @@ -1,30 +1,29 @@ -# encoding: utf-8 +"""lxml custom element classes for core properties-related XML elements.""" -""" -lxml custom element classes for core properties-related XML elements. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations +import datetime as dt import re +from typing import Callable, cast -from datetime import datetime, timedelta +from lxml.etree import _Element # pyright: ignore[reportPrivateUsage] -from pptx.compat import to_unicode -from . import parse_xml -from .ns import nsdecls, qn -from .xmlchemy import BaseOxmlElement, ZeroOrOne +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls, qn +from pptx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne class CT_CoreProperties(BaseOxmlElement): - """ - ```` element, the root element of the Core Properties - part stored as ``/docProps/core.xml``. Implements many of the Dublin Core - document metadata elements. String elements resolve to an empty string - ('') if the element is not present in the XML. String elements are - limited in length to 255 unicode characters. + """`cp:coreProperties` 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. """ + get_or_add_revision: Callable[[], _Element] + category = ZeroOrOne("cp:category", successors=()) contentStatus = ZeroOrOne("cp:contentStatus", successors=()) created = ZeroOrOne("dcterms:created", successors=()) @@ -36,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: _Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "cp:revision", successors=() + ) subject = ZeroOrOne("dc:subject", successors=()) title = ZeroOrOne("dc:title", successors=()) version = ZeroOrOne("cp:version", successors=()) @@ -44,42 +45,40 @@ class CT_CoreProperties(BaseOxmlElement): _coreProperties_tmpl = "\n" % nsdecls("cp", "dc", "dcterms") @staticmethod - def new_coreProperties(): - """Return a new ```` element""" - xml = CT_CoreProperties._coreProperties_tmpl - coreProperties = parse_xml(xml) - return coreProperties + def new_coreProperties() -> CT_CoreProperties: + """Return a new `cp:coreProperties` element""" + return cast(CT_CoreProperties, parse_xml(CT_CoreProperties._coreProperties_tmpl)) @property - def author_text(self): + def author_text(self) -> str: 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 - def contentStatus_text(self): + def contentStatus_text(self) -> str: 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 @@ -87,39 +86,39 @@ 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 - def identifier_text(self): + def identifier_text(self) -> str: 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 - def keywords_text(self): + def keywords_text(self) -> str: 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 - def language_text(self): + def language_text(self) -> str: 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 - def lastModifiedBy_text(self): + def lastModifiedBy_text(self) -> str: 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 @@ -127,7 +126,7 @@ 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 @@ -135,104 +134,101 @@ def modified_datetime(self): 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 - def revision_number(self): - """ - Integer value of revision property. - """ + def revision_number(self) -> int: + """Integer value of revision property.""" revision = self.revision if revision is None: return 0 revision_str = revision.text + if revision_str is None: + return 0 try: revision = int(revision_str) except ValueError: - # non-integer revision strings also resolve to 0 - revision = 0 - # as do negative integers + # -- non-integer revision strings also resolve to 0 -- + return 0 + # -- as do negative integers -- if revision < 0: - revision = 0 + return 0 return revision @revision_number.setter - def revision_number(self, value): - """ - Set revision property to string value of integer *value*. - """ - if not isinstance(value, int) or value < 1: + def revision_number(self, value: int): + """Set revision property to string value of integer `value`.""" + 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() revision.text = str(value) @property - def subject_text(self): + def subject_text(self) -> str: 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 - def title_text(self): + def title_text(self) -> str: 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 - def version_text(self): + def version_text(self) -> str: 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): - element = getattr(self, property_name) + def _datetime_of_element(self, property_name: str) -> dt.datetime | None: + element = cast("_Element | None", getattr(self, property_name)) if element is None: return None datetime_str = element.text + if datetime_str is None: + return None try: return self._parse_W3CDTF_to_datetime(datetime_str) except ValueError: # invalid datetime strings are ignored return None - def _get_or_add(self, prop_name): - """ - Return element returned by 'get_or_add_' method for *prop_name*. - """ + def _get_or_add(self, prop_name: str): + """Return element returned by 'get_or_add_' method for `prop_name`.""" get_or_add_method_name = "get_or_add_%s" % prop_name get_or_add_method = getattr(self, get_or_add_method_name) element = get_or_add_method() return element @classmethod - def _offset_dt(cls, dt, offset_str): - """ - Return a |datetime| instance that is offset from datetime *dt* by - the timezone offset specified in *offset_str*, a string like - ``'-07:00'``. + def _offset_dt(cls, datetime: dt.datetime, offset_str: str): + """Return |datetime| instance offset from `datetime` by offset specified in `offset_str`. + + `offset_str` is a string like `'-07:00'`. """ match = cls._offset_pattern.match(offset_str) if match is None: - raise ValueError("'%s' is not a valid offset string" % offset_str) + raise ValueError(f"{repr(offset_str)} is not a valid offset string") sign, hours_str, minutes_str = match.groups() sign_factor = -1 if sign == "+" else 1 hours = int(hours_str) * sign_factor minutes = int(minutes_str) * sign_factor - td = timedelta(hours=hours, minutes=minutes) - return dt + td + td = dt.timedelta(hours=hours, minutes=minutes) + return datetime + 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' @@ -244,24 +240,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 + timestamp = None for tmpl in templates: try: - dt = datetime.strptime(parseable_part, tmpl) + timestamp = dt.datetime.strptime(parseable_part, tmpl) except ValueError: continue - if dt is None: + if timestamp 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(timestamp, offset_str) + return timestamp - 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): + 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" raise ValueError(tmpl % type(value)) element = self._get_or_add(prop_name) @@ -276,16 +270,16 @@ 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): - """Set string value of *name* property to *value*.""" - value = to_unicode(value) + def _set_element_text(self, prop_name: str, value: str) -> None: + """Set string value of `name` property to `value`.""" + value = str(value) if len(value) > 255: tmpl = "exceeded 255 char limit for property, got:\n\n'%s'" raise ValueError(tmpl % value) element = self._get_or_add(prop_name) element.text = value - def _text_of_element(self, property_name): + def _text_of_element(self, property_name: str) -> str: element = getattr(self, property_name) if element is None: return "" diff --git a/pptx/oxml/dml/__init__.py b/src/pptx/oxml/dml/__init__.py similarity index 100% rename from pptx/oxml/dml/__init__.py rename to src/pptx/oxml/dml/__init__.py diff --git a/pptx/oxml/dml/color.py b/src/pptx/oxml/dml/color.py similarity index 89% rename from pptx/oxml/dml/color.py rename to src/pptx/oxml/dml/color.py index 4aa796d5b..dfce90aa0 100644 --- a/pptx/oxml/dml/color.py +++ b/src/pptx/oxml/dml/color.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""lxml custom element classes for DrawingML-related XML elements.""" -""" -lxml custom element classes for DrawingML-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import - -from ...enum.dml import MSO_THEME_COLOR -from ..simpletypes import ST_HexColorRGB, ST_Percentage -from ..xmlchemy import ( +from pptx.enum.dml import MSO_THEME_COLOR +from pptx.oxml.simpletypes import ST_HexColorRGB, ST_Percentage +from pptx.oxml.xmlchemy import ( BaseOxmlElement, Choice, RequiredAttribute, diff --git a/pptx/oxml/dml/fill.py b/src/pptx/oxml/dml/fill.py similarity index 93% rename from pptx/oxml/dml/fill.py rename to src/pptx/oxml/dml/fill.py index a7b688a3e..2ff2255d7 100644 --- a/pptx/oxml/dml/fill.py +++ b/src/pptx/oxml/dml/fill.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""lxml custom element classes for DrawingML-related XML elements.""" -""" -lxml custom element classes for DrawingML-related XML elements. -""" - -from __future__ import absolute_import +from __future__ import annotations from pptx.enum.dml import MSO_PATTERN_TYPE from pptx.oxml import parse_xml @@ -165,17 +161,13 @@ class CT_PatternFillProperties(BaseOxmlElement): def _new_bgClr(self): """Override default to add minimum subtree.""" - xml = ( - "\n" ' \n' "\n" - ) % nsdecls("a") + xml = ("\n" ' \n' "\n") % nsdecls("a") bgClr = parse_xml(xml) return bgClr def _new_fgClr(self): """Override default to add minimum subtree.""" - xml = ( - "\n" ' \n' "\n" - ) % nsdecls("a") + xml = ("\n" ' \n' "\n") % nsdecls("a") fgClr = parse_xml(xml) return fgClr diff --git a/pptx/oxml/dml/line.py b/src/pptx/oxml/dml/line.py similarity index 77% rename from pptx/oxml/dml/line.py rename to src/pptx/oxml/dml/line.py index 02d4e59c2..720ca8e07 100644 --- a/pptx/oxml/dml/line.py +++ b/src/pptx/oxml/dml/line.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """lxml custom element classes for DrawingML line-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.enum.dml import MSO_LINE_DASH_STYLE from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute diff --git a/src/pptx/oxml/ns.py b/src/pptx/oxml/ns.py new file mode 100644 index 000000000..d900c33bf --- /dev/null +++ b/src/pptx/oxml/ns.py @@ -0,0 +1,129 @@ +"""Namespace related objects.""" + +from __future__ import annotations + + +# -- Maps namespace prefix to namespace name for all known PowerPoint XML namespaces -- +_nsmap = { + "a": "http://schemas.openxmlformats.org/drawingml/2006/main", + "c": "http://schemas.openxmlformats.org/drawingml/2006/chart", + "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", + "ct": "http://schemas.openxmlformats.org/package/2006/content-types", + "dc": "http://purl.org/dc/elements/1.1/", + "dcmitype": "http://purl.org/dc/dcmitype/", + "dcterms": "http://purl.org/dc/terms/", + "ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties", + "i": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + "m": "http://schemas.openxmlformats.org/officeDocument/2006/math", + "mo": "http://schemas.microsoft.com/office/mac/office/2008/main", + "mv": "urn:schemas-microsoft-com:mac:vml", + "o": "urn:schemas-microsoft-com:office:office", + "p": "http://schemas.openxmlformats.org/presentationml/2006/main", + "pd": "http://schemas.openxmlformats.org/drawingml/2006/presentationDrawing", + "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", + "pr": "http://schemas.openxmlformats.org/package/2006/relationships", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "sl": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout", + "v": "urn:schemas-microsoft-com:vml", + "ve": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "w10": "urn:schemas-microsoft-com:office:word", + "wne": "http://schemas.microsoft.com/office/word/2006/wordml", + "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", +} + +pfxmap = {value: key for key, value in _nsmap.items()} + + +class NamespacePrefixedTag(str): + """Value object that knows the semantics of an XML tag having a namespace prefix.""" + + def __new__(cls, nstag: str): + return super(NamespacePrefixedTag, cls).__new__(cls, nstag) + + def __init__(self, nstag: str): + self._pfx, self._local_part = nstag.split(":") + self._ns_uri = _nsmap[self._pfx] + + @classmethod + 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) + + @property + def clark_name(self): + return "{%s}%s" % (self._ns_uri, self._local_part) + + @property + def local_part(self): + """ + Return the local part of the tag as a string. E.g. 'foobar' is + returned for tag 'f:foobar'. + """ + return self._local_part + + @property + def nsmap(self): + """ + Return a dict having a single member, mapping the namespace prefix of + this tag to it's namespace name (e.g. {'f': 'http://foo/bar'}). This + is handy for passing to xpath calls and other uses. + """ + return {self._pfx: self._ns_uri} + + @property + def nspfx(self): + """ + Return the string namespace prefix for the tag, e.g. 'f' is returned + for tag 'f:foobar'. + """ + return self._pfx + + @property + def nsuri(self): + """ + Return the namespace URI for the tag, e.g. 'http://foo/bar' would be + returned for tag 'f:foobar' if the 'f' prefix maps to + 'http://foo/bar' in _nsmap. + """ + return self._ns_uri + + +def namespaces(*prefixes: str): + """Return a dict containing the subset namespace prefix mappings specified by *prefixes*. + + Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p'). + """ + return {pfx: _nsmap[pfx] for pfx in prefixes} + + +nsmap = namespaces # alias for more compact use with Element() + + +def nsdecls(*prefixes: str): + return " ".join(['xmlns:%s="%s"' % (pfx, _nsmap[pfx]) for pfx in prefixes]) + + +def nsuri(nspfx: str): + """Return the namespace URI corresponding to `nspfx`. + + Example: + + >>> nsuri("p") + "http://schemas.openxmlformats.org/presentationml/2006/main" + """ + return _nsmap[nspfx] + + +def qn(namespace_prefixed_tag: str) -> str: + """Return a Clark-notation qualified tag name corresponding to `namespace_prefixed_tag`. + + `namespace_prefixed_tag` is a string like 'p:body'. 'qn' stands for `qualified name`. + + As an example, `qn("p:cSld")` returns: + `"{http://schemas.openxmlformats.org/drawingml/2006/main}cSld"`. + """ + nsptag = NamespacePrefixedTag(namespace_prefixed_tag) + return nsptag.clark_name diff --git a/src/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py new file mode 100644 index 000000000..17997c2b1 --- /dev/null +++ b/src/pptx/oxml/presentation.py @@ -0,0 +1,130 @@ +"""Custom element classes for presentation-related XML elements.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast + +from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString +from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne + +if TYPE_CHECKING: + from pptx.util import Length + + +class CT_Presentation(BaseOxmlElement): + """`p:presentation` element, root of the Presentation part stored as `/ppt/presentation.xml`.""" + + get_or_add_sldSz: Callable[[], CT_SlideSize] + get_or_add_sldIdLst: Callable[[], CT_SlideIdList] + get_or_add_sldMasterIdLst: Callable[[], CT_SlideMasterIdList] + + sldMasterIdLst: CT_SlideMasterIdList | None = ( + ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldMasterIdLst", + successors=( + "p:notesMasterIdLst", + "p:handoutMasterIdLst", + "p:sldIdLst", + "p:sldSz", + "p:notesSz", + ), + ) + ) + sldIdLst: CT_SlideIdList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldIdLst", successors=("p:sldSz", "p:notesSz") + ) + sldSz: CT_SlideSize | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldSz", successors=("p:notesSz",) + ) + + +class CT_SlideId(BaseOxmlElement): + """`p:sldId` element. + + Direct child of `p:sldIdLst` that contains an `rId` reference to a slide in the presentation. + """ + + id: int = RequiredAttribute("id", ST_SlideId) # pyright: ignore[reportAssignmentType] + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + + +class CT_SlideIdList(BaseOxmlElement): + """`p:sldIdLst` element. + + Direct child of that contains a list of the slide parts in the presentation. + """ + + sldId_lst: list[CT_SlideId] + + _add_sldId: Callable[..., CT_SlideId] + sldId = ZeroOrMore("p:sldId") + + def add_sldId(self, rId: str) -> CT_SlideId: + """Create and return a reference to a new `p:sldId` child element. + + The new `p:sldId` element has its r:id attribute set to `rId`. + """ + return self._add_sldId(id=self._next_id, rId=rId) + + @property + def _next_id(self) -> int: + """The next available slide ID as an `int`. + + Valid slide IDs start at 256. The next integer value greater than the max value in use is + chosen, which minimizes that chance of reusing the id of a deleted slide. + """ + MIN_SLIDE_ID = 256 + MAX_SLIDE_ID = 2147483647 + + used_ids = [int(s) for s in cast("list[str]", self.xpath("./p:sldId/@id"))] + simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1 + if simple_next <= MAX_SLIDE_ID: + return simple_next + + # -- fall back to search for next unused from bottom -- + valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_ID <= id <= MAX_SLIDE_ID)) + return ( + next( + candidate_id + for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_ID) + if candidate_id != used_id + ) + if valid_used_ids + else 256 + ) + + +class CT_SlideMasterIdList(BaseOxmlElement): + """`p:sldMasterIdLst` element. + + Child of `p:presentation` containing references to the slide masters that belong to the + presentation. + """ + + sldMasterId_lst: list[CT_SlideMasterIdListEntry] + + sldMasterId = ZeroOrMore("p:sldMasterId") + + +class CT_SlideMasterIdListEntry(BaseOxmlElement): + """ + ```` element, child of ```` containing + a reference to a slide master. + """ + + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + + +class CT_SlideSize(BaseOxmlElement): + """`p:sldSz` element. + + Direct child of that contains the width and height of slides in the + presentation. + """ + + cx: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "cx", ST_SlideSizeCoordinate + ) + cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "cy", ST_SlideSizeCoordinate + ) diff --git a/src/pptx/oxml/shapes/__init__.py b/src/pptx/oxml/shapes/__init__.py new file mode 100644 index 000000000..37f8ef60e --- /dev/null +++ b/src/pptx/oxml/shapes/__init__.py @@ -0,0 +1,19 @@ +"""Base shape-related objects such as BaseShape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from pptx.oxml.shapes.autoshape import CT_Shape + from pptx.oxml.shapes.connector import CT_Connector + from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame + from pptx.oxml.shapes.groupshape import CT_GroupShape + from pptx.oxml.shapes.picture import CT_Picture + + +ShapeElement: TypeAlias = ( + "CT_Connector | CT_GraphicalObjectFrame | CT_GroupShape | CT_Picture | CT_Shape" +) diff --git a/pptx/oxml/shapes/autoshape.py b/src/pptx/oxml/shapes/autoshape.py similarity index 60% rename from pptx/oxml/shapes/autoshape.py rename to src/pptx/oxml/shapes/autoshape.py index 3da31d132..5d78f624f 100644 --- a/pptx/oxml/shapes/autoshape.py +++ b/src/pptx/oxml/shapes/autoshape.py @@ -1,10 +1,10 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -lxml custom element classes for shape-related XML elements. -""" +"""lxml custom element classes for shape-related XML elements.""" -from __future__ import absolute_import +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.oxml import parse_xml @@ -22,69 +22,91 @@ OneAndOnlyOne, OptionalAttribute, RequiredAttribute, - ZeroOrOne, ZeroOrMore, + ZeroOrOne, ) +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import ( + CT_ApplicationNonVisualDrawingProps, + CT_NonVisualDrawingProps, + CT_ShapeProperties, + ) + from pptx.util import Length + class CT_AdjPoint2D(BaseOxmlElement): """`a:pt` custom element class.""" - x = RequiredAttribute("x", ST_Coordinate) - y = RequiredAttribute("y", ST_Coordinate) + x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType] + y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType] class CT_CustomGeometry2D(BaseOxmlElement): """`a:custGeom` custom element class.""" + get_or_add_pathLst: Callable[[], CT_Path2DList] + _tag_seq = ("a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst", "a:rect", "a:pathLst") - pathLst = ZeroOrOne("a:pathLst", successors=_tag_seq[6:]) + pathLst: CT_Path2DList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:pathLst", successors=_tag_seq[6:] + ) class CT_GeomGuide(BaseOxmlElement): - """ - ```` custom element class, defining a "guide", corresponding to - a yellow diamond-shaped handle on an autoshape. + """`a:gd` custom element class. + + Defines a "guide", corresponding to a yellow diamond-shaped handle on an autoshape. """ - name = RequiredAttribute("name", XsdString) - fmla = RequiredAttribute("fmla", XsdString) + name: str = RequiredAttribute("name", XsdString) # pyright: ignore[reportAssignmentType] + fmla: str = RequiredAttribute("fmla", XsdString) # pyright: ignore[reportAssignmentType] class CT_GeomGuideList(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:avLst` custom element class.""" + + _add_gd: Callable[[], CT_GeomGuide] + + gd_lst: list[CT_GeomGuide] gd = ZeroOrMore("a:gd") class CT_NonVisualDrawingShapeProps(BaseShapeElement): - """ - ```` custom element class - """ + """`p:cNvSpPr` custom element class.""" spLocks = ZeroOrOne("a:spLocks") - txBox = OptionalAttribute("txBox", XsdBoolean) + txBox: bool | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "txBox", XsdBoolean + ) class CT_Path2D(BaseOxmlElement): """`a:path` custom element class.""" + _add_close: Callable[[], CT_Path2DClose] + _add_lnTo: Callable[[], CT_Path2DLineTo] + _add_moveTo: Callable[[], CT_Path2DMoveTo] + close = ZeroOrMore("a:close", successors=()) lnTo = ZeroOrMore("a:lnTo", successors=()) moveTo = ZeroOrMore("a:moveTo", successors=()) - w = OptionalAttribute("w", ST_PositiveCoordinate) - h = OptionalAttribute("h", ST_PositiveCoordinate) - - def add_close(self): + w: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w", ST_PositiveCoordinate + ) + h: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "h", ST_PositiveCoordinate + ) + + def add_close(self) -> CT_Path2DClose: """Return a newly created `a:close` element. The new `a:close` element is appended to this `a:path` element. """ return self._add_close() - def add_lnTo(self, x, y): + def add_lnTo(self, x: Length, y: Length) -> CT_Path2DLineTo: """Return a newly created `a:lnTo` subtree with end point *(x, y)*. The new `a:lnTo` element is appended to this `a:path` element. @@ -94,8 +116,8 @@ def add_lnTo(self, x, y): pt.x, pt.y = x, y return lnTo - def add_moveTo(self, x, y): - """Return a newly created `a:moveTo` subtree with point *(x, y)*. + def add_moveTo(self, x: Length, y: Length): + """Return a newly created `a:moveTo` subtree with point `(x, y)`. The new `a:moveTo` element is appended to this `a:path` element. """ @@ -112,15 +134,19 @@ class CT_Path2DClose(BaseOxmlElement): class CT_Path2DLineTo(BaseOxmlElement): """`a:lnTo` custom element class.""" + _add_pt: Callable[[], CT_AdjPoint2D] + pt = ZeroOrOne("a:pt", successors=()) class CT_Path2DList(BaseOxmlElement): """`a:pathLst` custom element class.""" + _add_path: Callable[[], CT_Path2D] + path = ZeroOrMore("a:path", successors=()) - def add_path(self, w, h): + def add_path(self, w: Length, h: Length): """Return a newly created `a:path` child element.""" path = self._add_path() path.w, path.h = w, h @@ -130,33 +156,32 @@ def add_path(self, w, h): class CT_Path2DMoveTo(BaseOxmlElement): """`a:moveTo` custom element class.""" + _add_pt: Callable[[], CT_AdjPoint2D] + pt = ZeroOrOne("a:pt", successors=()) class CT_PresetGeometry2D(BaseOxmlElement): - """ - custom element class - """ + """`a:prstGeom` custom element class.""" - avLst = ZeroOrOne("a:avLst") - prst = RequiredAttribute("prst", MSO_AUTO_SHAPE_TYPE) + _add_avLst: Callable[[], CT_GeomGuideList] + _remove_avLst: Callable[[], None] + + avLst: CT_GeomGuideList | None = ZeroOrOne("a:avLst") # pyright: ignore[reportAssignmentType] + prst: MSO_AUTO_SHAPE_TYPE = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "prst", MSO_AUTO_SHAPE_TYPE + ) @property - def gd_lst(self): - """ - Sequence containing the ``gd`` element children of ```` - child element, empty if none are present. - """ + def gd_lst(self) -> list[CT_GeomGuide]: + """Sequence of `a:gd` element children of `a:avLst`. Empty if none are present.""" avLst = self.avLst if avLst is None: return [] return avLst.gd_lst - def rewrite_guides(self, guides): - """ - Remove any ```` element children of ```` and replace - them with ones having (name, val) in *guides*. - """ + def rewrite_guides(self, guides: list[tuple[str, int]]): + """Replace any `a:gd` element children of `a:avLst` with ones forme from `guides`.""" self._remove_avLst() avLst = self._add_avLst() for name, val in guides: @@ -166,16 +191,15 @@ def rewrite_guides(self, guides): class CT_Shape(BaseShapeElement): - """ - ```` custom element class - """ + """`p:sp` custom element class.""" + + get_or_add_txBody: Callable[[], CT_TextBody] - nvSpPr = OneAndOnlyOne("p:nvSpPr") - spPr = OneAndOnlyOne("p:spPr") - txBody = ZeroOrOne("p:txBody", successors=("p:extLst",)) + nvSpPr: CT_ShapeNonVisual = OneAndOnlyOne("p:nvSpPr") # pyright: ignore[reportAssignmentType] + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] + txBody: CT_TextBody | None = ZeroOrOne("p:txBody", successors=("p:extLst",)) # pyright: ignore - def add_path(self, w, h): - """Reference to `a:custGeom` descendant or |None| if not present.""" + def add_path(self, w: Length, h: Length) -> CT_Path2D: custGeom = self.spPr.custGeom if custGeom is None: raise ValueError("shape must be freeform") @@ -183,9 +207,7 @@ def add_path(self, w, h): return pathLst.add_path(w=w, h=h) def get_or_add_ln(self): - """ - Return the grandchild element, newly added if not present. - """ + """Return the `a:ln` grandchild element, newly added if not present.""" return self.spPr.get_or_add_ln() @property @@ -199,120 +221,36 @@ def has_custom_geometry(self): @property def is_autoshape(self): - """ - True if this shape is an auto shape. A shape is an auto shape if it - has a ```` element and does not have a txBox="1" attribute - on cNvSpPr. + """True if this shape is an auto shape. + + A shape is an auto shape if it has a `a:prstGeom` element and does not have a txBox="1" + attribute on cNvSpPr. """ prstGeom = self.prstGeom if prstGeom is None: return False - if self.nvSpPr.cNvSpPr.txBox is True: - return False - return True + return self.nvSpPr.cNvSpPr.txBox is not True @property def is_textbox(self): + """True if this shape is a text box. + + A shape is a text box if it has a `txBox` attribute on cNvSpPr that resolves to |True|. + The default when the txBox attribute is missing is |False|. """ - True if this shape is a text box. A shape is a text box if it has a - ``txBox`` attribute on cNvSpPr that resolves to |True|. The default - when the txBox attribute is missing is |False|. - """ - if self.nvSpPr.cNvSpPr.txBox is True: - return True - return False + return self.nvSpPr.cNvSpPr.txBox is True @property def ln(self): - """ - ```` grand-child element or |None| if not present - """ + """`a:ln` grand-child element or |None| if not present.""" return self.spPr.ln @staticmethod - def new_autoshape_sp(id_, name, prst, left, top, width, height): - """ - Return a new ```` element tree configured as a base auto shape. - """ - tmpl = CT_Shape._autoshape_sp_tmpl() - xml = tmpl % (id_, name, left, top, width, height, prst) - sp = parse_xml(xml) - return sp - - @staticmethod - def new_freeform_sp(shape_id, name, x, y, cx, cy): - """Return new `p:sp` element tree configured as freeform shape. - - The returned shape has a `a:custGeom` subtree but no paths in its - path list. - """ - tmpl = CT_Shape._freeform_sp_tmpl() - xml = tmpl % (shape_id, name, x, y, cx, cy) - sp = parse_xml(xml) - return sp - - @staticmethod - def new_placeholder_sp(id_, name, ph_type, orient, sz, idx): - """ - Return a new ```` element tree configured as a placeholder - shape. - """ - tmpl = CT_Shape._ph_sp_tmpl() - xml = tmpl % (id_, name) - sp = parse_xml(xml) - - ph = sp.nvSpPr.nvPr.get_or_add_ph() - ph.type = ph_type - ph.idx = idx - ph.orient = orient - ph.sz = sz - - placeholder_types_that_have_a_text_frame = ( - PP_PLACEHOLDER.TITLE, - PP_PLACEHOLDER.CENTER_TITLE, - PP_PLACEHOLDER.SUBTITLE, - PP_PLACEHOLDER.BODY, - PP_PLACEHOLDER.OBJECT, - ) - - if ph_type in placeholder_types_that_have_a_text_frame: - sp.append(CT_TextBody.new()) - - return sp - - @staticmethod - def new_textbox_sp(id_, name, left, top, width, height): - """ - Return a new ```` element tree configured as a base textbox - shape. - """ - tmpl = CT_Shape._textbox_sp_tmpl() - xml = tmpl % (id_, name, left, top, width, height) - sp = parse_xml(xml) - return sp - - @property - def prst(self): - """ - Value of ``prst`` attribute of ```` element or |None| if - not present. - """ - prstGeom = self.prstGeom - if prstGeom is None: - return None - return prstGeom.prst - - @property - def prstGeom(self): - """ - Reference to ```` child element or |None| if this shape - doesn't have one, for example, if it's a placeholder shape. - """ - return self.spPr.prstGeom - - @staticmethod - def _autoshape_sp_tmpl(): - return ( + def new_autoshape_sp( + id_: int, name: str, prst: str, left: int, top: int, width: int, height: int + ) -> CT_Shape: + """Return a new `p:sp` element tree configured as a base auto shape.""" + xml = ( "\n" " \n" ' \n' @@ -350,11 +288,17 @@ def _autoshape_sp_tmpl(): " \n" " \n" "" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d", "%s") - ) + ) % (id_, name, left, top, width, height, prst) + return cast(CT_Shape, parse_xml(xml)) @staticmethod - def _freeform_sp_tmpl(): - return ( + def new_freeform_sp(shape_id: int, name: str, x: int, y: int, cx: int, cy: int): + """Return new `p:sp` element tree configured as freeform shape. + + The returned shape has a `a:custGeom` subtree but no paths in its + path list. + """ + xml = ( "\n" " \n" ' \n' @@ -397,26 +341,76 @@ def _freeform_sp_tmpl(): " \n" " \n" "" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d") + ) % (shape_id, name, x, y, cx, cy) + return cast(CT_Shape, parse_xml(xml)) + + @staticmethod + def new_placeholder_sp( + id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz, idx + ) -> CT_Shape: + """Return a new `p:sp` element tree configured as a placeholder shape.""" + sp = cast( + CT_Shape, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), ) - def _new_txBody(self): - return CT_TextBody.new_p_txBody() + ph = sp.nvSpPr.nvPr.get_or_add_ph() + ph.type = ph_type + ph.idx = idx + ph.orient = orient + ph.sz = sz - @staticmethod - def _ph_sp_tmpl(): - return ( - "\n" - " \n" - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - "" % (nsdecls("a", "p"), "%d", "%s") + placeholder_types_that_have_a_text_frame = ( + PP_PLACEHOLDER.TITLE, + PP_PLACEHOLDER.CENTER_TITLE, + PP_PLACEHOLDER.SUBTITLE, + PP_PLACEHOLDER.BODY, + PP_PLACEHOLDER.OBJECT, ) + if ph_type in placeholder_types_that_have_a_text_frame: + sp.append(CT_TextBody.new()) + + return sp + + @staticmethod + def new_textbox_sp(id_, name, left, top, width, height): + """Return a new `p:sp` element tree configured as a base textbox shape.""" + tmpl = CT_Shape._textbox_sp_tmpl() + xml = tmpl % (id_, name, left, top, width, height) + sp = parse_xml(xml) + return sp + + @property + def prst(self): + """Value of `prst` attribute of `a:prstGeom` element or |None| if not present.""" + prstGeom = self.prstGeom + if prstGeom is None: + return None + return prstGeom.prst + + @property + def prstGeom(self) -> CT_PresetGeometry2D: + """Reference to `a:prstGeom` child element. + + |None| if this shape doesn't have one, for example, if it's a placeholder shape. + """ + return self.spPr.prstGeom + + def _new_txBody(self): + return CT_TextBody.new_p_txBody() + @staticmethod def _textbox_sp_tmpl(): return ( @@ -448,10 +442,14 @@ def _textbox_sp_tmpl(): class CT_ShapeNonVisual(BaseShapeElement): - """ - ```` custom element class - """ - - cNvPr = OneAndOnlyOne("p:cNvPr") - cNvSpPr = OneAndOnlyOne("p:cNvSpPr") - nvPr = OneAndOnlyOne("p:nvPr") + """`p:nvSpPr` custom element class.""" + + cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvPr" + ) + cNvSpPr: CT_NonVisualDrawingShapeProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvSpPr" + ) + nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvPr") + ) diff --git a/src/pptx/oxml/shapes/connector.py b/src/pptx/oxml/shapes/connector.py new file mode 100644 index 000000000..91261f780 --- /dev/null +++ b/src/pptx/oxml/shapes/connector.py @@ -0,0 +1,107 @@ +"""lxml custom element classes for XML elements related to the Connector shape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls +from pptx.oxml.shapes.shared import BaseShapeElement +from pptx.oxml.simpletypes import ST_DrawingElementId, XsdUnsignedInt +from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne + +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import CT_ShapeProperties + + +class CT_Connection(BaseShapeElement): + """A `a:stCxn` or `a:endCxn` element. + + Specifies a connection between an end-point of a connector and a shape connection point. + """ + + id = RequiredAttribute("id", ST_DrawingElementId) + idx = RequiredAttribute("idx", XsdUnsignedInt) + + +class CT_Connector(BaseShapeElement): + """A line/connector shape `p:cxnSp` element""" + + _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst") + nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr") + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] + del _tag_seq + + @classmethod + def new_cxnSp( + cls, + id_: int, + name: str, + prst: str, + x: int, + y: int, + cx: int, + cy: int, + flipH: bool, + flipV: bool, + ) -> CT_Connector: + """Return a new `p:cxnSp` element tree configured as a base connector.""" + flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "") + return cast( + CT_Connector, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f' \n' + f" \n" + f" \n" + f"" + ), + ) + + +class CT_ConnectorNonVisual(BaseOxmlElement): + """ + `p:nvCxnSpPr` element, container for the non-visual properties of + a connector, such as name, id, etc. + """ + + cNvPr = OneAndOnlyOne("p:cNvPr") + cNvCxnSpPr = OneAndOnlyOne("p:cNvCxnSpPr") + nvPr = OneAndOnlyOne("p:nvPr") + + +class CT_NonVisualConnectorProperties(BaseOxmlElement): + """ + `p:cNvCxnSpPr` element, container for the non-visual properties specific + to a connector shape, such as connections and connector locking. + """ + + _tag_seq = ("a:cxnSpLocks", "a:stCxn", "a:endCxn", "a:extLst") + stCxn = ZeroOrOne("a:stCxn", successors=_tag_seq[2:]) + endCxn = ZeroOrOne("a:endCxn", successors=_tag_seq[3:]) + del _tag_seq diff --git a/src/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py new file mode 100644 index 000000000..efa0b3632 --- /dev/null +++ b/src/pptx/oxml/shapes/graphfrm.py @@ -0,0 +1,342 @@ +"""lxml custom element class for CT_GraphicalObjectFrame XML element.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pptx.oxml import parse_xml +from pptx.oxml.chart.chart import CT_Chart +from pptx.oxml.ns import nsdecls +from pptx.oxml.shapes.shared import BaseShapeElement +from pptx.oxml.simpletypes import XsdBoolean, XsdString +from pptx.oxml.table import CT_Table +from pptx.oxml.xmlchemy import ( + BaseOxmlElement, + OneAndOnlyOne, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, +) +from pptx.spec import ( + GRAPHIC_DATA_URI_CHART, + GRAPHIC_DATA_URI_OLEOBJ, + GRAPHIC_DATA_URI_TABLE, +) + +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import ( + CT_ApplicationNonVisualDrawingProps, + CT_NonVisualDrawingProps, + CT_Transform2D, + ) + + +class CT_GraphicalObject(BaseOxmlElement): + """`a:graphic` element. + + The container for the reference to or definition of the framed graphical object (table, chart, + etc.). + """ + + graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:graphicData" + ) + + @property + def chart(self) -> CT_Chart | None: + """The `c:chart` grandchild element, or |None| if not present.""" + return self.graphicData.chart + + +class CT_GraphicalObjectData(BaseShapeElement): + """`p:graphicData` element. + + The direct container for a table, a chart, or another graphical object. + """ + + chart: CT_Chart | None = ZeroOrOne("c:chart") # pyright: ignore[reportAssignmentType] + tbl: CT_Table | None = ZeroOrOne("a:tbl") # pyright: ignore[reportAssignmentType] + uri: str = RequiredAttribute("uri", XsdString) # pyright: ignore[reportAssignmentType] + + @property + def blob_rId(self) -> str | None: + """Optional `r:id` attribute value of `p:oleObj` descendent element. + + This value is `None` when this `p:graphicData` element does not enclose an OLE object. + This value could also be `None` if an enclosed OLE object does not specify this attribute + (it is specified optional in the schema) but so far, all OLE objects we've encountered + specify this value. + """ + return None if self._oleObj is None else self._oleObj.rId + + @property + def is_embedded_ole_obj(self) -> bool | None: + """Optional boolean indicating an embedded OLE object. + + Returns `None` when this `p:graphicData` element does not enclose an OLE object. `True` + indicates an embedded OLE object and `False` indicates a linked OLE object. + """ + return None if self._oleObj is None else self._oleObj.is_embedded + + @property + def progId(self) -> str | None: + """Optional str value of "progId" attribute of `p:oleObj` descendent. + + This value identifies the "type" of the embedded object in terms of the application used + to open it. + + This value is `None` when this `p:graphicData` element does not enclose an OLE object. + This could also be `None` if an enclosed OLE object does not specify this attribute (it is + specified optional in the schema) but so far, all OLE objects we've encountered specify + this value. + """ + return None if self._oleObj is None else self._oleObj.progId + + @property + def showAsIcon(self) -> bool | None: + """Optional value of "showAsIcon" attribute value of `p:oleObj` descendent. + + This value is `None` when this `p:graphicData` element does not enclose an OLE object. It + is False when the `showAsIcon` attribute is omitted on the `p:oleObj` element. + """ + return None if self._oleObj is None else self._oleObj.showAsIcon + + @property + def _oleObj(self) -> CT_OleObject | None: + """Optional `p:oleObj` element contained in this `p:graphicData' element. + + Returns `None` when this graphic-data element does not enclose an OLE object. Note that + this returns the last `p:oleObj` element found. There can be more than one `p:oleObj` + element because an `mc.AlternateContent` element may appear as the child of + `p:graphicData` and that alternate-content subtree can contain multiple compatibility + choices. The last one should suit best for reading purposes because it contains the lowest + common denominator. + """ + oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj")) + return oleObjs[-1] if oleObjs else None + + +class CT_GraphicalObjectFrame(BaseShapeElement): + """`p:graphicFrame` element. + + A container for a table, a chart, or another graphical object. + """ + + nvGraphicFramePr: CT_GraphicalObjectFrameNonVisual = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvGraphicFramePr") + ) + xfrm: CT_Transform2D = OneAndOnlyOne("p:xfrm") # pyright: ignore + graphic: CT_GraphicalObject = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:graphic" + ) + + @property + def chart(self) -> CT_Chart | None: + """The `c:chart` great-grandchild element, or |None| if not present.""" + return self.graphic.chart + + @property + def chart_rId(self) -> str | None: + """The `rId` attribute of the `c:chart` great-grandchild element. + + |None| if not present. + """ + chart = self.chart + if chart is None: + return None + return chart.rId + + def get_or_add_xfrm(self) -> CT_Transform2D: + """Return the required `p:xfrm` child element. + + Overrides version on BaseShapeElement. + """ + return self.xfrm + + @property + def graphicData(self) -> CT_GraphicalObjectData: + """`a:graphicData` grandchild of this graphic-frame element.""" + return self.graphic.graphicData + + @property + def graphicData_uri(self) -> str: + """str value of `uri` attribute of `a:graphicData` grandchild.""" + return self.graphic.graphicData.uri + + @property + def has_oleobj(self) -> bool: + """`True` for graphicFrame containing an OLE object, `False` otherwise.""" + return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ + + @property + def is_embedded_ole_obj(self) -> bool | None: + """Optional boolean indicating an embedded OLE object. + + Returns `None` when this `p:graphicFrame` element does not enclose an OLE object. `True` + indicates an embedded OLE object and `False` indicates a linked OLE object. + """ + return self.graphicData.is_embedded_ole_obj + + @classmethod + def new_chart_graphicFrame( + cls, id_: int, name: str, rId: str, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Return a `p:graphicFrame` element tree populated with a chart element.""" + graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy) + graphicData = graphicFrame.graphic.graphicData + graphicData.uri = GRAPHIC_DATA_URI_CHART + graphicData.append(CT_Chart.new_chart(rId)) + return graphicFrame + + @classmethod + def new_graphicFrame( + cls, id_: int, name: str, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Return a new `p:graphicFrame` element tree suitable for containing a table or chart. + + Note that a graphicFrame element is not a valid shape until it contains a graphical object + such as a table. + """ + return cast( + CT_GraphicalObjectFrame, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), + ) + + @classmethod + def new_ole_object_graphicFrame( + cls, + id_: int, + name: str, + ole_object_rId: str, + progId: str, + icon_rId: str, + x: int, + y: int, + cx: int, + cy: int, + imgW: int, + imgH: int, + ) -> CT_GraphicalObjectFrame: + """Return newly-created `p:graphicFrame` for embedded OLE-object. + + `ole_object_rId` identifies the relationship to the OLE-object part. + + `progId` is a str identifying the object-type in terms of the application (program) used + to open it. This becomes an attribute of the same name in the `p:oleObj` element. + + `icon_rId` identifies the relationship to an image part used to display the OLE-object as + an icon (vs. a preview). + """ + return cast( + CT_GraphicalObjectFrame, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f" \n" + f" \n' + f' \n' + f" \n" + f" \n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), + ) + + @classmethod + def new_table_graphicFrame( + cls, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Return a `p:graphicFrame` element tree populated with a table element.""" + graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy) + graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE + graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy)) + return graphicFrame + + +class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement): + """`p:nvGraphicFramePr` element. + + This contains the non-visual properties of a graphic frame, such as name, id, etc. + """ + + cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvPr" + ) + nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvPr") + ) + + +class CT_OleObject(BaseOxmlElement): + """`p:oleObj` element, container for an OLE object (e.g. Excel file). + + An OLE object can be either linked or embedded (hence the name). + """ + + progId: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "progId", XsdString + ) + rId: str | None = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + showAsIcon: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "showAsIcon", XsdBoolean, default=False + ) + + @property + def is_embedded(self) -> bool: + """True when this OLE object is embedded, False when it is linked.""" + return len(self.xpath("./p:embed")) > 0 diff --git a/pptx/oxml/shapes/groupshape.py b/src/pptx/oxml/shapes/groupshape.py similarity index 67% rename from pptx/oxml/shapes/groupshape.py rename to src/pptx/oxml/shapes/groupshape.py index e428bd79e..f62bc6662 100644 --- a/pptx/oxml/shapes/groupshape.py +++ b/src/pptx/oxml/shapes/groupshape.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """lxml custom element classes for shape-tree-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Iterator from pptx.enum.shapes import MSO_CONNECTOR_TYPE from pptx.oxml import parse_xml @@ -15,15 +15,21 @@ from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne from pptx.util import Emu +if TYPE_CHECKING: + from pptx.enum.shapes import PP_PLACEHOLDER + from pptx.oxml.shapes import ShapeElement + from pptx.oxml.shapes.shared import CT_Transform2D + class CT_GroupShape(BaseShapeElement): - """ - Used for the shape tree (````) element as well as the group - shape (````) element. - """ + """Used for shape tree (`p:spTree`) as well as the group shape (`p:grpSp`) elements.""" - nvGrpSpPr = OneAndOnlyOne("p:nvGrpSpPr") - grpSpPr = OneAndOnlyOne("p:grpSpPr") + nvGrpSpPr: CT_GroupShapeNonVisual = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:nvGrpSpPr" + ) + grpSpPr: CT_GroupShapeProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:grpSpPr" + ) _shape_tags = ( qn("p:sp"), @@ -34,26 +40,33 @@ class CT_GroupShape(BaseShapeElement): qn("p:contentPart"), ) - def add_autoshape(self, id_, name, prst, x, y, cx, cy): - """ - Append a new ```` shape to the group/shapetree having the - properties specified in call. - """ + def add_autoshape( + self, id_: int, name: str, prst: str, x: int, y: int, cx: int, cy: int + ) -> CT_Shape: + """Return new `p:sp` appended to the group/shapetree with specified attributes.""" sp = CT_Shape.new_autoshape_sp(id_, name, prst, x, y, cx, cy) self.insert_element_before(sp, "p:extLst") return sp - def add_cxnSp(self, id_, name, type_member, x, y, cx, cy, flipH, flipV): - """ - Append a new ```` shape to the group/shapetree having the - properties specified in call. - """ + def add_cxnSp( + self, + id_: int, + name: str, + type_member: MSO_CONNECTOR_TYPE, + x: int, + y: int, + cx: int, + cy: int, + flipH: bool, + flipV: bool, + ) -> CT_Connector: + """Return new `p:cxnSp` appended to the group/shapetree with the specified attribues.""" prst = MSO_CONNECTOR_TYPE.to_xml(type_member) cxnSp = CT_Connector.new_cxnSp(id_, name, prst, x, y, cx, cy, flipH, flipV) self.insert_element_before(cxnSp, "p:extLst") return cxnSp - def add_freeform_sp(self, x, y, cx, cy): + def add_freeform_sp(self, x: int, y: int, cx: int, cy: int) -> CT_Shape: """Append a new freeform `p:sp` with specified position and size.""" shape_id = self._next_shape_id name = "Freeform %d" % (shape_id - 1,) @@ -61,7 +74,7 @@ def add_freeform_sp(self, x, y, cx, cy): self.insert_element_before(sp, "p:extLst") return sp - def add_grpSp(self): + def add_grpSp(self) -> CT_GroupShape: """Return `p:grpSp` element newly appended to this shape tree. The element contains no sub-shapes, is positioned at (0, 0), and has @@ -73,40 +86,34 @@ def add_grpSp(self): self.insert_element_before(grpSp, "p:extLst") return grpSp - def add_pic(self, id_, name, desc, rId, x, y, cx, cy): - """ - Append a ```` shape to the group/shapetree having properties - as specified in call. - """ + def add_pic( + self, id_: int, name: str, desc: str, rId: str, x: int, y: int, cx: int, cy: int + ) -> CT_Picture: + """Append a `p:pic` shape to the group/shapetree having properties as specified in call.""" pic = CT_Picture.new_pic(id_, name, desc, rId, x, y, cx, cy) self.insert_element_before(pic, "p:extLst") return pic - def add_placeholder(self, id_, name, ph_type, orient, sz, idx): - """ - Append a newly-created placeholder ```` shape having the - specified placeholder properties. - """ + def add_placeholder( + self, id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz: str, idx: int + ) -> CT_Shape: + """Append a newly-created placeholder `p:sp` shape having the specified properties.""" sp = CT_Shape.new_placeholder_sp(id_, name, ph_type, orient, sz, idx) self.insert_element_before(sp, "p:extLst") return sp - def add_table(self, id_, name, rows, cols, x, y, cx, cy): - """ - Append a ```` shape containing a table as specified - in call. - """ + def add_table( + self, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Append a `p:graphicFrame` shape containing a table as specified in call.""" graphicFrame = CT_GraphicalObjectFrame.new_table_graphicFrame( id_, name, rows, cols, x, y, cx, cy ) self.insert_element_before(graphicFrame, "p:extLst") return graphicFrame - def add_textbox(self, id_, name, x, y, cx, cy): - """ - Append a newly-created textbox ```` shape having the specified - position and size. - """ + def add_textbox(self, id_: int, name: str, x: int, y: int, cx: int, cy: int) -> CT_Shape: + """Append a newly-created textbox `p:sp` shape having the specified position and size.""" sp = CT_Shape.new_textbox_sp(id_, name, x, y, cx, cy) self.insert_element_before(sp, "p:extLst") return sp @@ -121,32 +128,27 @@ def chOff(self): """Descendent `p:grpSpPr/a:xfrm/a:chOff` element.""" return self.grpSpPr.get_or_add_xfrm().get_or_add_chOff() - def get_or_add_xfrm(self): - """ - Return the ```` grandchild element, newly-added if not - present. - """ + def get_or_add_xfrm(self) -> CT_Transform2D: + """Return the `a:xfrm` grandchild element, newly-added if not present.""" return self.grpSpPr.get_or_add_xfrm() def iter_ph_elms(self): - """ - Generate each placeholder shape child element in document order. - """ + """Generate each placeholder shape child element in document order.""" for e in self.iter_shape_elms(): if e.has_ph_elm: yield e - def iter_shape_elms(self): - """ - Generate each child of this ```` element that corresponds - to a shape, in the sequence they appear in the XML. + def iter_shape_elms(self) -> Iterator[ShapeElement]: + """Generate each child of this `p:spTree` element that corresponds to a shape. + + Items appear in XML document order. """ for elm in self.iterchildren(): if elm.tag in self._shape_tags: yield elm @property - def max_shape_id(self): + def max_shape_id(self) -> int: """Maximum int value assigned as @id in this slide. This is generally a shape-id, but ids can be assigned to other @@ -161,8 +163,8 @@ def max_shape_id(self): return max(used_ids) if used_ids else 0 @classmethod - def new_grpSp(cls, id_, name): - """Return new "loose" `p:grpSp` element having *id_* and *name*.""" + def new_grpSp(cls, id_: int, name: str) -> CT_GroupShape: + """Return new "loose" `p:grpSp` element having `id_` and `name`.""" xml = ( "\n" " \n" @@ -183,7 +185,7 @@ def new_grpSp(cls, id_, name): grpSp = parse_xml(xml) return grpSp - def recalculate_extents(self): + def recalculate_extents(self) -> None: """Adjust x, y, cx, and cy to incorporate all contained shapes. This would typically be called when a contained shape is added, @@ -204,14 +206,12 @@ def recalculate_extents(self): self.getparent().recalculate_extents() @property - def xfrm(self): - """ - The ```` grandchild element or |None| if not found - """ + def xfrm(self) -> CT_Transform2D | None: + """The `a:xfrm` grandchild element or |None| if not found.""" return self.grpSpPr.xfrm @property - def _child_extents(self): + def _child_extents(self) -> tuple[int, int, int, int]: """(x, y, cx, cy) tuple representing net position and size. The values are formed as a composite of the contained child shapes. @@ -234,7 +234,7 @@ def _child_extents(self): return x, y, cx, cy @property - def _next_shape_id(self): + def _next_shape_id(self) -> int: """Return unique shape id suitable for use with a new shape element. The returned id is the next available positive integer drawing object @@ -250,15 +250,15 @@ def _next_shape_id(self): class CT_GroupShapeNonVisual(BaseShapeElement): - """ - ```` element. - """ + """`p:nvGrpSpPr` element.""" cNvPr = OneAndOnlyOne("p:cNvPr") class CT_GroupShapeProperties(BaseOxmlElement): - """p:grpSpPr element """ + """p:grpSpPr element""" + + get_or_add_xfrm: Callable[[], CT_Transform2D] _tag_seq = ( "a:xfrm", @@ -273,6 +273,8 @@ class CT_GroupShapeProperties(BaseOxmlElement): "a:scene3d", "a:extLst", ) - xfrm = ZeroOrOne("a:xfrm", successors=_tag_seq[1:]) + xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:xfrm", successors=_tag_seq[1:] + ) effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[8:]) del _tag_seq diff --git a/pptx/oxml/shapes/picture.py b/src/pptx/oxml/shapes/picture.py similarity index 82% rename from pptx/oxml/shapes/picture.py rename to src/pptx/oxml/shapes/picture.py index e4bbe6d92..bacc97194 100644 --- a/pptx/oxml/shapes/picture.py +++ b/src/pptx/oxml/shapes/picture.py @@ -1,27 +1,32 @@ -# encoding: utf-8 - """lxml custom element classes for picture-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, cast +from xml.sax.saxutils import escape -from .. import parse_xml -from ..ns import nsdecls -from .shared import BaseShapeElement -from ..xmlchemy import BaseOxmlElement, OneAndOnlyOne +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls +from pptx.oxml.shapes.shared import BaseShapeElement +from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne + +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import CT_ShapeProperties + from pptx.util import Length class CT_Picture(BaseShapeElement): - """ - ```` element, which represents a picture shape (an image placement - on a slide). + """`p:pic` element. + + Represents a picture shape (an image placement on a slide). """ nvPicPr = OneAndOnlyOne("p:nvPicPr") blipFill = OneAndOnlyOne("p:blipFill") - spPr = OneAndOnlyOne("p:spPr") + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] @property - def blip_rId(self): + def blip_rId(self) -> str | None: """Value of `p:blipFill/a:blip/@r:embed`. Returns |None| if not present. @@ -61,33 +66,40 @@ def new_ph_pic(cls, id_, name, desc, rId): return parse_xml(cls._pic_ph_tmpl() % (id_, name, desc, rId)) @classmethod - def new_pic(cls, id_, name, desc, rId, left, top, width, height): - """ - Return a new ```` element tree configured with the supplied - parameters. - """ - xml = cls._pic_tmpl() % (id_, name, desc, rId, left, top, width, height) - pic = parse_xml(xml) - return pic + def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy): + """Return new `` element tree configured with supplied parameters.""" + return parse_xml(cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy)) @classmethod def new_video_pic( - cls, shape_id, shape_name, video_rId, media_rId, poster_frame_rId, x, y, cx, cy - ): + cls, + shape_id: int, + shape_name: str, + video_rId: str, + media_rId: str, + poster_frame_rId: str, + x: Length, + y: Length, + cx: Length, + cy: Length, + ) -> CT_Picture: """Return a new `p:pic` populated with the specified video.""" - return parse_xml( - cls._pic_video_tmpl() - % ( - shape_id, - shape_name, - video_rId, - media_rId, - poster_frame_rId, - x, - y, - cx, - cy, - ) + return cast( + CT_Picture, + parse_xml( + cls._pic_video_tmpl() + % ( + shape_id, + shape_name, + video_rId, + media_rId, + poster_frame_rId, + x, + y, + cx, + cy, + ) + ), ) @property diff --git a/pptx/oxml/shapes/shared.py b/src/pptx/oxml/shapes/shared.py similarity index 68% rename from pptx/oxml/shapes/shared.py rename to src/pptx/oxml/shapes/shared.py index 74eb562d3..d9f945697 100644 --- a/pptx/oxml/shapes/shared.py +++ b/src/pptx/oxml/shapes/shared.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Common shape-related oxml objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable from pptx.dml.fill import CT_GradientFillProperties from pptx.enum.shapes import PP_PLACEHOLDER @@ -30,15 +30,19 @@ ) from pptx.util import Emu +if TYPE_CHECKING: + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.shapes.autoshape import CT_CustomGeometry2D, CT_PresetGeometry2D + from pptx.util import Length + class BaseShapeElement(BaseOxmlElement): - """ - Provides common behavior for shape element classes like CT_Shape, - CT_Picture, etc. - """ + """Provides common behavior for shape element classes like CT_Shape, CT_Picture, etc.""" + + spPr: CT_ShapeProperties @property - def cx(self): + def cx(self) -> Length: return self._get_xfrm_attr("cx") @cx.setter @@ -46,7 +50,7 @@ def cx(self, value): self._set_xfrm_attr("cx", value) @property - def cy(self): + def cy(self) -> Length: return self._get_xfrm_attr("cy") @cy.setter @@ -70,36 +74,34 @@ def flipV(self, value): self._set_xfrm_attr("flipV", value) def get_or_add_xfrm(self): - """ - Return the ```` grandchild element, newly-added if not - present. This version works for ````, ````, and - ```` elements, others will need to override. + """Return the `a:xfrm` grandchild element, newly-added if not present. + + This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to + override. """ return self.spPr.get_or_add_xfrm() @property def has_ph_elm(self): """ - True if this shape element has a ```` descendant, indicating it + True if this shape element has a `p:ph` descendant, indicating it is a placeholder shape. False otherwise. """ return self.ph is not None @property - def ph(self): - """ - The ```` descendant element if there is one, None otherwise. - """ + def ph(self) -> CT_Placeholder | None: + """The `p:ph` descendant element if there is one, None otherwise.""" ph_elms = self.xpath("./*[1]/p:nvPr/p:ph") if len(ph_elms) == 0: return None return ph_elms[0] @property - def ph_idx(self): - """ - Integer value of placeholder idx attribute. Raises |ValueError| if - shape is not a placeholder. + def ph_idx(self) -> int: + """Integer value of placeholder idx attribute. + + Raises |ValueError| if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -107,10 +109,10 @@ def ph_idx(self): return ph.idx @property - def ph_orient(self): - """ - Placeholder orientation, e.g. 'vert'. Raises |ValueError| if shape is - not a placeholder. + def ph_orient(self) -> str: + """Placeholder orientation, e.g. 'vert'. + + Raises |ValueError| if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -118,10 +120,10 @@ def ph_orient(self): return ph.orient @property - def ph_sz(self): - """ - Placeholder size, e.g. ST_PlaceholderSize.HALF, None if shape has no - ```` descendant. + def ph_sz(self) -> str: + """Placeholder size, e.g. ST_PlaceholderSize.HALF. + + Raises `ValueError` if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -130,9 +132,9 @@ def ph_sz(self): @property def ph_type(self): - """ - Placeholder type, e.g. ST_PlaceholderType.TITLE ('title'), none if - shape has no ```` descendant. + """Placeholder type, e.g. ST_PlaceholderType.TITLE ('title'). + + Raises `ValueError` if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -140,17 +142,15 @@ def ph_type(self): return ph.type @property - def rot(self): - """ - Float representing degrees this shape is rotated clockwise. - """ + def rot(self) -> float: + """Float representing degrees this shape is rotated clockwise.""" xfrm = self.xfrm - if xfrm is None: + if xfrm is None or xfrm.rot is None: return 0.0 return xfrm.rot @rot.setter - def rot(self, value): + def rot(self, value: float): self.get_or_add_xfrm().rot = value @property @@ -169,13 +169,11 @@ def shape_name(self): @property def txBody(self): - """ - Child ```` element, None if not present - """ + """Child `p:txBody` element, None if not present.""" return self.find(qn("p:txBody")) @property - def x(self): + def x(self) -> Length: return self._get_xfrm_attr("x") @x.setter @@ -184,15 +182,15 @@ def x(self, value): @property def xfrm(self): - """ - The ```` grandchild element or |None| if not found. This - version works for ````, ````, and ```` - elements, others will need to override. + """The `a:xfrm` grandchild element or |None| if not found. + + This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to + override. """ return self.spPr.xfrm @property - def y(self): + def y(self) -> Length: return self._get_xfrm_attr("y") @y.setter @@ -203,12 +201,12 @@ def y(self, value): def _nvXxPr(self): """ Required non-visual shape properties element for this shape. Actual - name depends on the shape type, e.g. ```` for picture + name depends on the shape type, e.g. `p:nvPicPr` for picture shape. """ return self.xpath("./*[1]")[0] - def _get_xfrm_attr(self, name): + def _get_xfrm_attr(self, name: str) -> Length | None: xfrm = self.xfrm if xfrm is None: return None @@ -220,9 +218,9 @@ def _set_xfrm_attr(self, name, value): class CT_ApplicationNonVisualDrawingProps(BaseOxmlElement): - """ - ```` element - """ + """`p:nvPr` element.""" + + get_or_add_ph: Callable[[], CT_Placeholder] ph = ZeroOrOne( "p:ph", @@ -295,27 +293,34 @@ def prstDash_val(self, val): class CT_NonVisualDrawingProps(BaseOxmlElement): - """ - ```` custom element class. - """ + """`p:cNvPr` custom element class.""" + + get_or_add_hlinkClick: Callable[[], CT_Hyperlink] + get_or_add_hlinkHover: Callable[[], CT_Hyperlink] _tag_seq = ("a:hlinkClick", "a:hlinkHover", "a:extLst") - hlinkClick = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:]) - hlinkHover = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:]) + hlinkClick: CT_Hyperlink | None = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:]) + hlinkHover: CT_Hyperlink | None = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:]) id = RequiredAttribute("id", ST_DrawingElementId) name = RequiredAttribute("name", XsdString) del _tag_seq class CT_Placeholder(BaseOxmlElement): - """ - ```` custom element class. - """ + """`p:ph` custom element class.""" - type = OptionalAttribute("type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT) - orient = OptionalAttribute("orient", ST_Direction, default=ST_Direction.HORZ) - sz = OptionalAttribute("sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL) - idx = OptionalAttribute("idx", XsdUnsignedInt, default=0) + type: PP_PLACEHOLDER = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT + ) + orient: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "orient", ST_Direction, default=ST_Direction.HORZ + ) + sz: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL + ) + idx: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "idx", XsdUnsignedInt, default=0 + ) class CT_Point2D(BaseOxmlElement): @@ -323,8 +328,8 @@ class CT_Point2D(BaseOxmlElement): Custom element class for element. """ - x = RequiredAttribute("x", ST_Coordinate) - y = RequiredAttribute("y", ST_Coordinate) + x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType] + y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType] class CT_PositiveSize2D(BaseOxmlElement): @@ -339,10 +344,14 @@ class CT_PositiveSize2D(BaseOxmlElement): class CT_ShapeProperties(BaseOxmlElement): """Custom element class for `p:spPr` element. - Shared by `p:sp`, `p:cxnSp`, and `p:pic` elements as well as a few more - obscure ones. + Shared by `p:sp`, `p:cxnSp`, and `p:pic` elements as well as a few more obscure ones. """ + get_or_add_xfrm: Callable[[], CT_Transform2D] + get_or_add_ln: Callable[[], CT_LineProperties] + _add_prstGeom: Callable[[], CT_PresetGeometry2D] + _remove_custGeom: Callable[[], None] + _tag_seq = ( "a:xfrm", "a:custGeom", @@ -360,9 +369,15 @@ class CT_ShapeProperties(BaseOxmlElement): "a:sp3d", "a:extLst", ) - xfrm = ZeroOrOne("a:xfrm", successors=_tag_seq[1:]) - custGeom = ZeroOrOne("a:custGeom", successors=_tag_seq[2:]) - prstGeom = ZeroOrOne("a:prstGeom", successors=_tag_seq[3:]) + xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:xfrm", successors=_tag_seq[1:] + ) + custGeom: CT_CustomGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:custGeom", successors=_tag_seq[2:] + ) + prstGeom: CT_PresetGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:prstGeom", successors=_tag_seq[3:] + ) eg_fillProperties = ZeroOrOneChoice( ( Choice("a:noFill"), @@ -374,7 +389,9 @@ class CT_ShapeProperties(BaseOxmlElement): ), successors=_tag_seq[9:], ) - ln = ZeroOrOne("a:ln", successors=_tag_seq[10:]) + ln: CT_LineProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:ln", successors=_tag_seq[10:] + ) effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[11:]) del _tag_seq @@ -399,11 +416,10 @@ def cy(self): return Emu(cy_str_lst[0]) @property - def x(self): - """ - The offset of the left edge of the shape from the left edge of the - slide, as an instance of Emu. Corresponds to the value of the - `./xfrm/off/@x` attribute. None if not present. + def x(self) -> Length | None: + """Distance between the left edge of the slide and left edge of the shape. + + 0 if not present. """ x_str_lst = self.xpath("./a:xfrm/a:off/@x") if not x_str_lst: @@ -433,12 +449,16 @@ class CT_Transform2D(BaseOxmlElement): """ _tag_seq = ("a:off", "a:ext", "a:chOff", "a:chExt") - off = ZeroOrOne("a:off", successors=_tag_seq[1:]) + off: CT_Point2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:off", successors=_tag_seq[1:] + ) ext = ZeroOrOne("a:ext", successors=_tag_seq[2:]) chOff = ZeroOrOne("a:chOff", successors=_tag_seq[3:]) chExt = ZeroOrOne("a:chExt", successors=_tag_seq[4:]) del _tag_seq - rot = OptionalAttribute("rot", ST_Angle, default=0.0) + rot: float | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "rot", ST_Angle, default=0.0 + ) flipH = OptionalAttribute("flipH", XsdBoolean, default=False) flipV = OptionalAttribute("flipV", XsdBoolean, default=False) diff --git a/pptx/oxml/simpletypes.py b/src/pptx/oxml/simpletypes.py similarity index 92% rename from pptx/oxml/simpletypes.py rename to src/pptx/oxml/simpletypes.py index d77d9a494..6ceb06f7c 100644 --- a/pptx/oxml/simpletypes.py +++ b/src/pptx/oxml/simpletypes.py @@ -1,35 +1,35 @@ -# encoding: utf-8 +"""Simple-type classes. -""" -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. +A "simple-type" is a scalar type, generally serving as an XML attribute. This is in contrast to a +"complex-type" which would specify an XML element. + +These objects 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, print_function +from __future__ import annotations import numbers +from typing import Any from pptx.exc import InvalidXmlError from pptx.util import Centipoints, Emu -class BaseSimpleType(object): +class BaseSimpleType: @classmethod - def from_xml(cls, str_value): - return cls.convert_from_xml(str_value) + 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 validate_float(cls, value): - """ - Note that int values are accepted. - """ + def validate_float(cls, value: Any): + """Note that int values are accepted.""" if not isinstance(value, (int, float)): raise TypeError("value must be a number, got %s" % type(value)) @@ -149,8 +149,7 @@ def convert_to_xml(cls, value): def validate(cls, value): 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 ) @@ -229,7 +228,7 @@ class ST_Angle(XsdInt): THREE_SIXTY = 360 * DEGREE_INCREMENTS @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> float: rot = int(str_value) % cls.THREE_SIXTY return float(rot) / cls.DEGREE_INCREMENTS @@ -347,9 +346,7 @@ def validate(cls, value): class ST_Direction(XsdTokenEnumeration): - """ - Valid values for attribute - """ + """Valid values for `` attribute.""" HORZ = "horz" VERT = "vert" @@ -417,17 +414,13 @@ def validate(cls, value): # must be 6 chars long---------- if len(str_value) != 6: - raise ValueError( - "RGB string must be six characters long, got '%s'" % str_value - ) + raise ValueError("RGB string must be six characters long, got '%s'" % str_value) # must parse as hex int -------- try: int(str_value, 16) except ValueError: - raise ValueError( - "RGB string must be valid hex string, got '%s'" % str_value - ) + raise ValueError("RGB string must be valid hex string, got '%s'" % str_value) class ST_LayoutMode(XsdStringEnumeration): @@ -469,8 +462,7 @@ def validate(cls, value): super(ST_LineWidth, cls).validate(value) if value < 0 or value > 20116800: raise ValueError( - "value must be in range 0-20116800 inclusive (0-1584 points)" - ", got %d" % value + "value must be in range 0-20116800 inclusive (0-1584 points)" ", got %d" % value ) @@ -480,6 +472,15 @@ def validate(cls, value): cls.validate_int_in_range(value, 2, 72) +class ST_Orientation(XsdStringEnumeration): + """Valid values for `val` attribute on c:orientation (CT_Orientation).""" + + MAX_MIN = "maxMin" + MIN_MAX = "minMax" + + _members = (MAX_MIN, MIN_MAX) + + class ST_Overlap(BaseIntType): """ String value is an integer in range -100..100, representing a percent, @@ -604,8 +605,7 @@ def validate(cls, value): cls.validate_int(value) if value < 914400 or value > 51206400: raise ValueError( - "value must be in range(914400, 51206400) (1-56 inches), got" - " %d" % value + "value must be in range(914400, 51206400) (1-56 inches), got" " %d" % value ) @@ -625,9 +625,7 @@ class ST_TargetMode(XsdString): def validate(cls, value): cls.validate_string(value) if value not in ("External", "Internal"): - raise ValueError( - "must be one of 'Internal' or 'External', got '%s'" % value - ) + raise ValueError("must be one of 'Internal' or 'External', got '%s'" % value) class ST_TextFontScalePercentOrPercentString(BaseFloatType): @@ -650,9 +648,7 @@ def convert_to_xml(cls, value): def validate(cls, value): BaseFloatType.validate(value) if value < 1.0 or value > 100.0: - raise ValueError( - "value must be in range 1.0..100.0 (percent), got %s" % value - ) + raise ValueError("value must be in range 1.0..100.0 (percent), got %s" % value) class ST_TextFontSize(BaseIntType): diff --git a/pptx/oxml/slide.py b/src/pptx/oxml/slide.py similarity index 70% rename from pptx/oxml/slide.py rename to src/pptx/oxml/slide.py index 36b868cf8..37a9780f6 100644 --- a/pptx/oxml/slide.py +++ b/src/pptx/oxml/slide.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Slide-related custom element classes, including those for masters.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast from pptx.oxml import parse_from_template, parse_xml from pptx.oxml.dml.fill import CT_GradientFillProperties @@ -19,39 +19,39 @@ ZeroOrOneChoice, ) +if TYPE_CHECKING: + from pptx.oxml.shapes.groupshape import CT_GroupShape + class _BaseSlideElement(BaseOxmlElement): - """ - Base class for the six slide types, providing common methods. - """ + """Base class for the six slide types, providing common methods.""" + + cSld: CT_CommonSlideData @property - def spTree(self): - """ - Return required `p:cSld/p:spTree` grandchild. - """ + def spTree(self) -> CT_GroupShape: + """Return required `p:cSld/p:spTree` grandchild.""" return self.cSld.spTree class CT_Background(BaseOxmlElement): """`p:bg` element.""" + _insert_bgPr: Callable[[CT_BackgroundProperties], None] + # ---these two are actually a choice, not a sequence, but simpler for # ---present purposes this way. _tag_seq = ("p:bgPr", "p:bgRef") - bgPr = ZeroOrOne("p:bgPr", successors=()) + bgPr: CT_BackgroundProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:bgPr", successors=() + ) bgRef = ZeroOrOne("p:bgRef", successors=()) del _tag_seq def add_noFill_bgPr(self): """Return a new `p:bgPr` element with noFill properties.""" - xml = ( - "\n" - " \n" - " \n" - "" % nsdecls("a", "p") - ) - bgPr = parse_xml(xml) + xml = "\n" " \n" " \n" "" % nsdecls("a", "p") + bgPr = cast(CT_BackgroundProperties, parse_xml(xml)) self._insert_bgPr(bgPr) return bgPr @@ -91,24 +91,31 @@ def _new_gradFill(self): class CT_CommonSlideData(BaseOxmlElement): """`p:cSld` element.""" + _remove_bg: Callable[[], None] + get_or_add_bg: Callable[[], CT_Background] + _tag_seq = ("p:bg", "p:spTree", "p:custDataLst", "p:controls", "p:extLst") - bg = ZeroOrOne("p:bg", successors=_tag_seq[1:]) - spTree = OneAndOnlyOne("p:spTree") + bg: CT_Background | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:bg", successors=_tag_seq[1:] + ) + spTree: CT_GroupShape = OneAndOnlyOne("p:spTree") # pyright: ignore[reportAssignmentType] del _tag_seq - name = OptionalAttribute("name", XsdString, default="") + name: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "name", XsdString, default="" + ) - def get_or_add_bgPr(self): + def get_or_add_bgPr(self) -> CT_BackgroundProperties: """Return `p:bg/p:bgPr` grandchild. - If no such grandchild is present, any existing `p:bg` child is first - removed and a new default `p:bg` with noFill settings is added. + If no such grandchild is present, any existing `p:bg` child is first removed and a new + default `p:bg` with noFill settings is added. """ bg = self.bg if bg is None or bg.bgPr is None: - self._change_to_noFill_bg() - return self.bg.bgPr + bg = self._change_to_noFill_bg() + return cast(CT_BackgroundProperties, bg.bgPr) - def _change_to_noFill_bg(self): + def _change_to_noFill_bg(self) -> CT_Background: """Establish a `p:bg` child with no-fill settings. Any existing `p:bg` child is first removed. @@ -120,55 +127,48 @@ def _change_to_noFill_bg(self): class CT_NotesMaster(_BaseSlideElement): - """ - ```` element, root of a notes master part - """ + """`p:notesMaster` element, root of a notes master part.""" _tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] del _tag_seq @classmethod - def new_default(cls): - """ - Return a new ```` element based on the built-in - default template. - """ - return parse_from_template("notesMaster") + def new_default(cls) -> CT_NotesMaster: + """Return a new `p:notesMaster` element based on the built-in default template.""" + return cast(CT_NotesMaster, parse_from_template("notesMaster")) class CT_NotesSlide(_BaseSlideElement): - """ - ```` element, root of a notes slide part - """ + """`p:notes` element, root of a notes slide part.""" _tag_seq = ("p:cSld", "p:clrMapOvr", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] del _tag_seq @classmethod - def new(cls): - """ - Return a new ```` element based on the default template. - Note that the template does not include placeholders, which must be - subsequently cloned from the notes master. + def new(cls) -> CT_NotesSlide: + """Return a new ```` element based on the default template. + + Note that the template does not include placeholders, which must be subsequently cloned + from the notes master. """ - return parse_from_template("notes") + return cast(CT_NotesSlide, parse_from_template("notes")) class CT_Slide(_BaseSlideElement): """`p:sld` element, root element of a slide part (XML document).""" _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=_tag_seq[2:]) timing = ZeroOrOne("p:timing", successors=_tag_seq[4:]) del _tag_seq @classmethod - def new(cls): + def new(cls) -> CT_Slide: """Return new `p:sld` element configured as base slide shape.""" - return parse_xml(cls._sld_xml()) + return cast(CT_Slide, parse_xml(cls._sld_xml())) @property def bg(self): @@ -252,37 +252,37 @@ def _sld_xml(): class CT_SlideLayout(_BaseSlideElement): - """ - ```` element, root of a slide layout part - """ + """`p:sldLayout` element, root of a slide layout part.""" _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] del _tag_seq class CT_SlideLayoutIdList(BaseOxmlElement): + """`p:sldLayoutIdLst` element, child of `p:sldMaster`. + + Contains references to the slide layouts that inherit from the slide master. """ - ```` element, child of ```` containing - references to the slide layouts that inherit from the slide master. - """ + + sldLayoutId_lst: list[CT_SlideLayoutIdListEntry] sldLayoutId = ZeroOrMore("p:sldLayoutId") class CT_SlideLayoutIdListEntry(BaseOxmlElement): - """ - ```` element, child of ```` containing - a reference to a slide layout. + """`p:sldLayoutId` element, child of `p:sldLayoutIdLst`. + + Contains a reference to a slide layout. """ - rId = RequiredAttribute("r:id", XsdString) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] class CT_SlideMaster(_BaseSlideElement): - """ - ```` element, root of a slide master part - """ + """`p:sldMaster` element, root of a slide master part.""" + + get_or_add_sldLayoutIdLst: Callable[[], CT_SlideLayoutIdList] _tag_seq = ( "p:cSld", @@ -294,8 +294,10 @@ class CT_SlideMaster(_BaseSlideElement): "p:txStyles", "p:extLst", ) - cSld = OneAndOnlyOne("p:cSld") - sldLayoutIdLst = ZeroOrOne("p:sldLayoutIdLst", successors=_tag_seq[3:]) + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] + sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldLayoutIdLst", successors=_tag_seq[3:] + ) del _tag_seq diff --git a/pptx/oxml/table.py b/src/pptx/oxml/table.py similarity index 55% rename from pptx/oxml/table.py rename to src/pptx/oxml/table.py index 5b0bd5b6d..cd3e9ebc3 100644 --- a/pptx/oxml/table.py +++ b/src/pptx/oxml/table.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Custom element classes for table-related XML elements""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Iterator, cast from pptx.enum.text import MSO_VERTICAL_ANCHOR from pptx.oxml import parse_xml @@ -22,87 +22,95 @@ ) from pptx.util import Emu, lazyproperty +if TYPE_CHECKING: + from pptx.util import Length + class CT_Table(BaseOxmlElement): """`a:tbl` custom element class""" + get_or_add_tblPr: Callable[[], CT_TableProperties] + tr_lst: list[CT_TableRow] + _add_tr: Callable[..., CT_TableRow] + _tag_seq = ("a:tblPr", "a:tblGrid", "a:tr") - tblPr = ZeroOrOne("a:tblPr", successors=_tag_seq[1:]) - tblGrid = OneAndOnlyOne("a:tblGrid") + tblPr: CT_TableProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:tblPr", successors=_tag_seq[1:] + ) + tblGrid: CT_TableGrid = OneAndOnlyOne("a:tblGrid") # pyright: ignore[reportAssignmentType] tr = ZeroOrMore("a:tr", successors=_tag_seq[3:]) del _tag_seq - def add_tr(self, height): - """ - Return a reference to a newly created child element having its - ``h`` attribute set to *height*. - """ + def add_tr(self, height: Length) -> CT_TableRow: + """Return a newly created `a:tr` child element having its `h` attribute set to `height`.""" return self._add_tr(h=height) @property - def bandCol(self): + def bandCol(self) -> bool: return self._get_boolean_property("bandCol") @bandCol.setter - def bandCol(self, value): + def bandCol(self, value: bool): self._set_boolean_property("bandCol", value) @property - def bandRow(self): + def bandRow(self) -> bool: return self._get_boolean_property("bandRow") @bandRow.setter - def bandRow(self, value): + def bandRow(self, value: bool): self._set_boolean_property("bandRow", value) @property - def firstCol(self): + def firstCol(self) -> bool: return self._get_boolean_property("firstCol") @firstCol.setter - def firstCol(self, value): + def firstCol(self, value: bool): self._set_boolean_property("firstCol", value) @property - def firstRow(self): + def firstRow(self) -> bool: return self._get_boolean_property("firstRow") @firstRow.setter - def firstRow(self, value): + def firstRow(self, value: bool): self._set_boolean_property("firstRow", value) - def iter_tcs(self): + def iter_tcs(self) -> Iterator[CT_TableCell]: """Generate each `a:tc` element in this tbl. - tc elements are generated left-to-right, top-to-bottom. + `a:tc` elements are generated left-to-right, top-to-bottom. """ return (tc for tr in self.tr_lst for tc in tr.tc_lst) @property - def lastCol(self): + def lastCol(self) -> bool: return self._get_boolean_property("lastCol") @lastCol.setter - def lastCol(self, value): + def lastCol(self, value: bool): self._set_boolean_property("lastCol", value) @property - def lastRow(self): + def lastRow(self) -> bool: return self._get_boolean_property("lastRow") @lastRow.setter - def lastRow(self, value): + def lastRow(self, value: bool): self._set_boolean_property("lastRow", value) @classmethod - def new_tbl(cls, rows, cols, width, height, tableStyleId=None): - """Return a new ```` element tree.""" + def new_tbl( + cls, rows: int, cols: int, width: int, height: int, tableStyleId: str | None = None + ) -> CT_Table: + """Return a new `p:tbl` element tree.""" # working hypothesis is this is the default table style GUID if tableStyleId is None: tableStyleId = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}" xml = cls._tbl_tmpl() % (tableStyleId) - tbl = parse_xml(xml) + tbl = cast(CT_Table, parse_xml(xml)) # add specified number of rows and columns rowheight = height // rows @@ -112,27 +120,27 @@ def new_tbl(cls, rows, cols, width, height, tableStyleId=None): # adjust width of last col to absorb any div error if col == cols - 1: colwidth = width - ((cols - 1) * colwidth) - tbl.tblGrid.add_gridCol(width=colwidth) + tbl.tblGrid.add_gridCol(width=Emu(colwidth)) for row in range(rows): # adjust height of last row to absorb any div error if row == rows - 1: rowheight = height - ((rows - 1) * rowheight) - tr = tbl.add_tr(height=rowheight) + tr = tbl.add_tr(height=Emu(rowheight)) for col in range(cols): tr.add_tc() return tbl - def tc(self, row_idx, col_idx): - """Return `a:tc` element at *row_idx*, *col_idx*.""" + def tc(self, row_idx: int, col_idx: int) -> CT_TableCell: + """Return `a:tc` element at `row_idx`, `col_idx`.""" return self.tr_lst[row_idx].tc_lst[col_idx] - def _get_boolean_property(self, propname): - """ - Generalized getter for the boolean properties on the ```` - child element. Defaults to False if *propname* attribute is missing - or ```` element itself is not present. + def _get_boolean_property(self, propname: str) -> bool: + """Generalized getter for the boolean properties on the `a:tblPr` child element. + + Defaults to False if `propname` attribute is missing or `a:tblPr` element itself is not + present. """ tblPr = self.tblPr if tblPr is None: @@ -140,19 +148,16 @@ def _get_boolean_property(self, propname): propval = getattr(tblPr, propname) return {True: True, False: False, None: False}[propval] - def _set_boolean_property(self, propname, value): - """ - Generalized setter for boolean properties on the ```` child - element, setting *propname* attribute appropriately based on *value*. - If *value* is True, the attribute is set to "1"; a tblPr child - element is added if necessary. If *value* is False, the *propname* - attribute is removed if present, allowing its default value of False - to be its effective value. + def _set_boolean_property(self, propname: str, value: bool) -> None: + """Generalized setter for boolean properties on the `a:tblPr` child element. + + Sets `propname` attribute appropriately based on `value`. If `value` is True, the + attribute is set to "1"; a tblPr child element is added if necessary. If `value` is False, + the `propname` attribute is removed if present, allowing its default value of False to be + its effective value. """ if value not in (True, False): - raise ValueError( - "assigned value must be either True or False, got %s" % value - ) + raise ValueError("assigned value must be either True or False, got %s" % value) tblPr = self.get_or_add_tblPr() setattr(tblPr, propname, value) @@ -171,43 +176,52 @@ def _tbl_tmpl(cls): class CT_TableCell(BaseOxmlElement): """`a:tc` custom element class""" + get_or_add_tcPr: Callable[[], CT_TableCellProperties] + get_or_add_txBody: Callable[[], CT_TextBody] + _tag_seq = ("a:txBody", "a:tcPr", "a:extLst") - txBody = ZeroOrOne("a:txBody", successors=_tag_seq[1:]) - tcPr = ZeroOrOne("a:tcPr", successors=_tag_seq[2:]) + txBody: CT_TextBody | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:txBody", successors=_tag_seq[1:] + ) + tcPr: CT_TableCellProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:tcPr", successors=_tag_seq[2:] + ) del _tag_seq - gridSpan = OptionalAttribute("gridSpan", XsdInt, default=1) - rowSpan = OptionalAttribute("rowSpan", XsdInt, default=1) - hMerge = OptionalAttribute("hMerge", XsdBoolean, default=False) - vMerge = OptionalAttribute("vMerge", XsdBoolean, default=False) + gridSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "gridSpan", XsdInt, default=1 + ) + rowSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "rowSpan", XsdInt, default=1 + ) + hMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "hMerge", XsdBoolean, default=False + ) + vMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "vMerge", XsdBoolean, default=False + ) @property - def anchor(self): - """ - String held in ``anchor`` attribute of ```` child element of - this ```` element. - """ + def anchor(self) -> MSO_VERTICAL_ANCHOR | None: + """String held in `anchor` attribute of `a:tcPr` child element of this `a:tc` element.""" if self.tcPr is None: return None return self.tcPr.anchor @anchor.setter - def anchor(self, anchor_enum_idx): - """ - Set value of anchor attribute on ```` child element - """ + def anchor(self, anchor_enum_idx: MSO_VERTICAL_ANCHOR | None): + """Set value of anchor attribute on `a:tcPr` child element.""" if anchor_enum_idx is None and self.tcPr is None: return tcPr = self.get_or_add_tcPr() tcPr.anchor = anchor_enum_idx - def append_ps_from(self, spanned_tc): - """Append `a:p` elements taken from *spanned_tc*. + def append_ps_from(self, spanned_tc: CT_TableCell): + """Append `a:p` elements taken from `spanned_tc`. - Any non-empty paragraph elements in *spanned_tc* are removed and - appended to the text-frame of this cell. If *spanned_tc* is left with - no content after this process, a single empty `a:p` element is added - to ensure the cell is compliant with the spec. + Any non-empty paragraph elements in `spanned_tc` are removed and appended to the + text-frame of this cell. If `spanned_tc` is left with no content after this process, a + single empty `a:p` element is added to ensure the cell is compliant with the spec. """ source_txBody = spanned_tc.get_or_add_txBody() target_txBody = self.get_or_add_txBody() @@ -228,94 +242,96 @@ def append_ps_from(self, spanned_tc): target_txBody.unclear_content() @property - def col_idx(self): + def col_idx(self) -> int: """Offset of this cell's column in its table.""" # ---tc elements come before any others in `a:tr` element--- - return self.getparent().index(self) + return cast(CT_TableRow, self.getparent()).index(self) @property - def is_merge_origin(self): + def is_merge_origin(self) -> bool: """True if cell is top-left in merged cell range.""" if self.gridSpan > 1 and not self.vMerge: return True - if self.rowSpan > 1 and not self.hMerge: - return True - return False + return self.rowSpan > 1 and not self.hMerge @property - def is_spanned(self): + def is_spanned(self) -> bool: """True if cell is in merged cell range but not merge origin cell.""" return self.hMerge or self.vMerge @property - def marT(self): - """ - Read/write integer top margin value represented in ``marT`` attribute - of the ```` child element of this ```` element. If the - attribute is not present, the default value ``45720`` (0.05 inches) - is returned for top and bottom; ``91440`` (0.10 inches) is the - default for left and right. Assigning |None| to any ``marX`` - property clears that attribute from the element, effectively setting - it to the default value. + def marT(self) -> Length: + """Top margin for this cell. + + This value is stored in the `marT` attribute of the `a:tcPr` child element of this `a:tc`. + + Read/write. If the attribute is not present, the default value `45720` (0.05 inches) is + returned for top and bottom; `91440` (0.10 inches) is the default for left and right. + Assigning |None| to any `marX` property clears that attribute from the element, + effectively setting it to the default value. """ - return self._get_marX("marT", 45720) + return self._get_marX("marT", Emu(45720)) @marT.setter - def marT(self, value): + def marT(self, value: Length | None): self._set_marX("marT", value) @property - def marR(self): - """ - Right margin value represented in ``marR`` attribute. - """ - return self._get_marX("marR", 91440) + def marR(self) -> Length: + """Right margin value represented in `marR` attribute.""" + return self._get_marX("marR", Emu(91440)) @marR.setter - def marR(self, value): + def marR(self, value: Length | None): self._set_marX("marR", value) @property - def marB(self): - """ - Bottom margin value represented in ``marB`` attribute. - """ - return self._get_marX("marB", 45720) + def marB(self) -> Length: + """Bottom margin value represented in `marB` attribute.""" + return self._get_marX("marB", Emu(45720)) @marB.setter - def marB(self, value): + def marB(self, value: Length | None): self._set_marX("marB", value) @property - def marL(self): - """ - Left margin value represented in ``marL`` attribute. - """ - return self._get_marX("marL", 91440) + def marL(self) -> Length: + """Left margin value represented in `marL` attribute.""" + return self._get_marX("marL", Emu(91440)) @marL.setter - def marL(self, value): + def marL(self, value: Length | None): self._set_marX("marL", value) @classmethod - def new(cls): + def new(cls) -> CT_TableCell: """Return a new `a:tc` element subtree.""" - xml = cls._tc_tmpl() - tc = parse_xml(xml) - return tc + return cast( + CT_TableCell, + parse_xml( + f"\n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), + ) @property - def row_idx(self): + def row_idx(self) -> int: """Offset of this cell's row in its table.""" - return self.getparent().row_idx + return cast(CT_TableRow, self.getparent()).row_idx @property - def tbl(self): + def tbl(self) -> CT_Table: """Table element this cell belongs to.""" - return self.xpath("ancestor::a:tbl")[0] + return cast(CT_Table, self.xpath("ancestor::a:tbl")[0]) @property - def text(self): + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] """str text contained in cell""" # ---note this shadows lxml _Element.text--- txBody = self.txBody @@ -323,41 +339,26 @@ def text(self): return "" return "\n".join([p.text for p in txBody.p_lst]) - def _get_marX(self, attr_name, default): - """ - Generalized method to get margin values. - """ + def _get_marX(self, attr_name: str, default: Length) -> Length: + """Generalized method to get margin values.""" if self.tcPr is None: return Emu(default) return Emu(int(self.tcPr.get(attr_name, default))) - def _new_txBody(self): + def _new_txBody(self) -> CT_TextBody: return CT_TextBody.new_a_txBody() - def _set_marX(self, marX, value): - """ - Set value of marX attribute on ```` child element. If *marX* - is |None|, the marX attribute is removed. *marX* is a string, one of - ``('marL', 'marR', 'marT', 'marB')``. + def _set_marX(self, marX: str, value: Length | None) -> None: + """Set value of marX attribute on `a:tcPr` child element. + + If `marX` is |None|, the marX attribute is removed. `marX` is a string, one of `('marL', + 'marR', 'marT', 'marB')`. """ if value is None and self.tcPr is None: return tcPr = self.get_or_add_tcPr() setattr(tcPr, marX, value) - @classmethod - def _tc_tmpl(cls): - return ( - "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "" % nsdecls("a") - ) - class CT_TableCellProperties(BaseOxmlElement): """`a:tcPr` custom element class""" @@ -373,43 +374,47 @@ class CT_TableCellProperties(BaseOxmlElement): ), successors=("a:headers", "a:extLst"), ) - anchor = OptionalAttribute("anchor", MSO_VERTICAL_ANCHOR) - marL = OptionalAttribute("marL", ST_Coordinate32) - marR = OptionalAttribute("marR", ST_Coordinate32) - marT = OptionalAttribute("marT", ST_Coordinate32) - marB = OptionalAttribute("marB", ST_Coordinate32) + anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "anchor", MSO_VERTICAL_ANCHOR + ) + marL: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marL", ST_Coordinate32 + ) + marR: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marR", ST_Coordinate32 + ) + marT: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marT", ST_Coordinate32 + ) + marB: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marB", ST_Coordinate32 + ) def _new_gradFill(self): return CT_GradientFillProperties.new_gradFill() class CT_TableCol(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:gridCol` custom element class.""" - w = RequiredAttribute("w", ST_Coordinate) + w: Length = RequiredAttribute("w", ST_Coordinate) # pyright: ignore[reportAssignmentType] class CT_TableGrid(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:tblGrid` custom element class.""" + + gridCol_lst: list[CT_TableCol] + _add_gridCol: Callable[..., CT_TableCol] gridCol = ZeroOrMore("a:gridCol") - def add_gridCol(self, width): - """ - Return a reference to a newly created child element - having its ``w`` attribute set to *width*. - """ + def add_gridCol(self, width: Length) -> CT_TableCol: + """A newly appended `a:gridCol` child element having its `w` attribute set to `width`.""" return self._add_gridCol(w=width) class CT_TableProperties(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:tblPr` custom element class.""" bandRow = OptionalAttribute("bandRow", XsdBoolean, default=False) bandCol = OptionalAttribute("bandCol", XsdBoolean, default=False) @@ -420,24 +425,22 @@ class CT_TableProperties(BaseOxmlElement): class CT_TableRow(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:tr` custom element class.""" + + tc_lst: list[CT_TableCell] + _add_tc: Callable[[], CT_TableCell] tc = ZeroOrMore("a:tc", successors=("a:extLst",)) - h = RequiredAttribute("h", ST_Coordinate) + h: Length = RequiredAttribute("h", ST_Coordinate) # pyright: ignore[reportAssignmentType] - def add_tc(self): - """ - Return a reference to a newly added minimal valid ```` child - element. - """ + def add_tc(self) -> CT_TableCell: + """A newly added minimal valid `a:tc` child element.""" return self._add_tc() @property - def row_idx(self): + def row_idx(self) -> int: """Offset of this row in its table.""" - return self.getparent().tr_lst.index(self) + return cast(CT_Table, self.getparent()).tr_lst.index(self) def _new_tc(self): return CT_TableCell.new() @@ -446,21 +449,19 @@ def _new_tc(self): class TcRange(object): """A 2D block of `a:tc` cell elements in a table. - This object assumes the structure of the underlying table does not change - during its lifetime. Structural changes in this context would be - insertion or removal of rows or columns. + This object assumes the structure of the underlying table does not change during its lifetime. + Structural changes in this context would be insertion or removal of rows or columns. - The client is expected to create, use, and then abandon an instance in - the context of a single user operation that is known to have no - structural side-effects of this type. + The client is expected to create, use, and then abandon an instance in the context of a single + user operation that is known to have no structural side-effects of this type. """ - def __init__(self, tc, other_tc): + def __init__(self, tc: CT_TableCell, other_tc: CT_TableCell): self._tc = tc self._other_tc = other_tc @classmethod - def from_merge_origin(cls, tc): + def from_merge_origin(cls, tc: CT_TableCell): """Return instance created from merge-origin tc element.""" other_tc = tc.tbl.tc( tc.row_idx + tc.rowSpan - 1, # ---other_row_idx @@ -469,7 +470,7 @@ def from_merge_origin(cls, tc): return cls(tc, other_tc) @lazyproperty - def contains_merged_cell(self): + def contains_merged_cell(self) -> bool: """True if one or more cells in range are part of a merged cell.""" for tc in self.iter_tcs(): if tc.gridSpan > 1: @@ -483,7 +484,7 @@ def contains_merged_cell(self): return False @lazyproperty - def dimensions(self): + def dimensions(self) -> tuple[int, int]: """(row_count, col_count) pair describing size of range.""" _, _, width, height = self._extents return height, width @@ -544,16 +545,15 @@ def _bottom(self): return top + height @lazyproperty - def _extents(self): + def _extents(self) -> tuple[int, int, int, int]: """A (left, top, width, height) tuple describing range extents. - Note this is normalized to accommodate the various orderings of the - corner cells provided on construction, which may be in any of four - configurations such as (top-left, bottom-right), - (bottom-left, top-right), etc. + Note this is normalized to accommodate the various orderings of the corner cells provided + on construction, which may be in any of four configurations such as (top-left, + bottom-right), (bottom-left, top-right), etc. """ - def start_and_size(idx, other_idx): + def start_and_size(idx: int, other_idx: int) -> tuple[int, int]: """Return beginning and length of range based on two indexes.""" return min(idx, other_idx), abs(idx - other_idx) + 1 @@ -566,23 +566,23 @@ def start_and_size(idx, other_idx): @lazyproperty def _left(self): - """Index of leftmost column in range""" + """Index of leftmost column in range.""" left, _, _, _ = self._extents return left @lazyproperty def _right(self): - """Index of column following the last column in range""" + """Index of column following the last column in range.""" left, _, width, _ = self._extents return left + width @lazyproperty def _tbl(self): - """`a:tbl` element containing this cell range""" + """`a:tbl` element containing this cell range.""" return self._tc.tbl @lazyproperty def _top(self): - """Index of topmost row in range""" + """Index of topmost row in range.""" _, top, _, _ = self._extents return top diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py new file mode 100644 index 000000000..0f9ecc152 --- /dev/null +++ b/src/pptx/oxml/text.py @@ -0,0 +1,618 @@ +"""Custom element classes for text-related XML elements""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Callable, cast + +from pptx.enum.lang import MSO_LANGUAGE_ID +from pptx.enum.text import ( + MSO_AUTO_SIZE, + MSO_TEXT_UNDERLINE_TYPE, + MSO_VERTICAL_ANCHOR, + PP_PARAGRAPH_ALIGNMENT, +) +from pptx.exc import InvalidXmlError +from pptx.oxml import parse_xml +from pptx.oxml.dml.fill import CT_GradientFillProperties +from pptx.oxml.ns import nsdecls +from pptx.oxml.simpletypes import ( + ST_Coordinate32, + ST_TextFontScalePercentOrPercentString, + ST_TextFontSize, + ST_TextIndentLevelType, + ST_TextSpacingPercentOrPercentString, + ST_TextSpacingPoint, + ST_TextTypeface, + ST_TextWrappingType, + XsdBoolean, +) +from pptx.oxml.xmlchemy import ( + BaseOxmlElement, + Choice, + OneAndOnlyOne, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, + ZeroOrOneChoice, +) +from pptx.util import Emu, Length + +if TYPE_CHECKING: + from pptx.oxml.action import CT_Hyperlink + + +class CT_RegularTextRun(BaseOxmlElement): + """`a:r` custom element class""" + + get_or_add_rPr: Callable[[], CT_TextCharacterProperties] + + rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:rPr", successors=("a:t",) + ) + t: BaseOxmlElement = OneAndOnlyOne("a:t") # pyright: ignore[reportAssignmentType] + + @property + def text(self) -> str: + """All text of (required) `a:t` child.""" + text = self.t.text + # -- t.text is None when t element is empty, e.g. '' -- + return text or "" + + @text.setter + def text(self, value: str): # pyright: ignore[reportIncompatibleMethodOverride] + self.t.text = self._escape_ctrl_chars(value) + + @staticmethod + def _escape_ctrl_chars(s: str) -> str: + """Return str after replacing each control character with a plain-text escape. + + For example, a BEL character (x07) would appear as "_x0007_". Horizontal-tab + (x09) and line-feed (x0A) are not escaped. All other characters in the range + x00-x1F are escaped. + """ + return re.sub(r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s) + + +class CT_TextBody(BaseOxmlElement): + """`p:txBody` custom element class. + + Also used for `c:txPr` in charts and perhaps other elements. + """ + + add_p: Callable[[], CT_TextParagraph] + p_lst: list[CT_TextParagraph] + + bodyPr: CT_TextBodyProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:bodyPr" + ) + p: CT_TextParagraph = OneOrMore("a:p") # pyright: ignore[reportAssignmentType] + + def clear_content(self): + """Remove all `a:p` children, but leave any others. + + cf. lxml `_Element.clear()` method which removes all children. + """ + for p in self.p_lst: + self.remove(p) + + @property + def defRPr(self) -> CT_TextCharacterProperties: + """`a:defRPr` element of required first `p` child, added with its ancestors if not present. + + Used when element is a ``c:txPr`` in a chart and the `p` element is used only to specify + formatting, not content. + """ + p = self.p_lst[0] + pPr = p.get_or_add_pPr() + defRPr = pPr.get_or_add_defRPr() + return defRPr + + @property + def is_empty(self) -> bool: + """True if only a single empty `a:p` element is present.""" + ps = self.p_lst + if len(ps) > 1: + return False + + if not ps: + raise InvalidXmlError("p:txBody must have at least one a:p") + + if ps[0].text != "": + return False + return True + + @classmethod + def new(cls): + """Return a new `p:txBody` element tree.""" + xml = cls._txBody_tmpl() + txBody = parse_xml(xml) + return txBody + + @classmethod + def new_a_txBody(cls) -> CT_TextBody: + """Return a new `a:txBody` element tree. + + Suitable for use in a table cell and possibly other situations. + """ + xml = cls._a_txBody_tmpl() + txBody = cast(CT_TextBody, parse_xml(xml)) + return txBody + + @classmethod + def new_p_txBody(cls): + """Return a new `p:txBody` element tree, suitable for use in an `p:sp` element.""" + xml = cls._p_txBody_tmpl() + return parse_xml(xml) + + @classmethod + def new_txPr(cls): + """Return a `c:txPr` element tree. + + Suitable for use in a chart object like data labels or tick labels. + """ + xml = ( + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + ) % nsdecls("c", "a") + txPr = parse_xml(xml) + return txPr + + def unclear_content(self): + """Ensure p:txBody has at least one a:p child. + + Intuitively, reverse a ".clear_content()" operation to minimum conformance with spec + (single empty paragraph). + """ + if len(self.p_lst) > 0: + return + self.add_p() + + @classmethod + def _a_txBody_tmpl(cls): + return "\n" " \n" " \n" "\n" % (nsdecls("a")) + + @classmethod + def _p_txBody_tmpl(cls): + return ( + "\n" " \n" " \n" "\n" % (nsdecls("p", "a")) + ) + + @classmethod + def _txBody_tmpl(cls): + return ( + "\n" + " \n" + " \n" + " \n" + "\n" % (nsdecls("a", "p")) + ) + + +class CT_TextBodyProperties(BaseOxmlElement): + """`a:bodyPr` custom element class.""" + + _add_noAutofit: Callable[[], BaseOxmlElement] + _add_normAutofit: Callable[[], CT_TextNormalAutofit] + _add_spAutoFit: Callable[[], BaseOxmlElement] + _remove_eg_textAutoFit: Callable[[], None] + + noAutofit: BaseOxmlElement | None + normAutofit: CT_TextNormalAutofit | None + spAutoFit: BaseOxmlElement | None + + eg_textAutoFit = ZeroOrOneChoice( + (Choice("a:noAutofit"), Choice("a:normAutofit"), Choice("a:spAutoFit")), + successors=("a:scene3d", "a:sp3d", "a:flatTx", "a:extLst"), + ) + lIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "lIns", ST_Coordinate32, default=Emu(91440) + ) + tIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "tIns", ST_Coordinate32, default=Emu(45720) + ) + rIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "rIns", ST_Coordinate32, default=Emu(91440) + ) + bIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "bIns", ST_Coordinate32, default=Emu(45720) + ) + anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "anchor", MSO_VERTICAL_ANCHOR + ) + wrap: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "wrap", ST_TextWrappingType + ) + + @property + def autofit(self): + """The autofit setting for the text frame, a member of the `MSO_AUTO_SIZE` enumeration.""" + if self.noAutofit is not None: + return MSO_AUTO_SIZE.NONE + if self.normAutofit is not None: + return MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE + if self.spAutoFit is not None: + return MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT + return None + + @autofit.setter + def autofit(self, value: MSO_AUTO_SIZE | None): + if value is not None and value not in MSO_AUTO_SIZE: + raise ValueError( + f"only None or a member of the MSO_AUTO_SIZE enumeration can be assigned to" + f" CT_TextBodyProperties.autofit, got {value}" + ) + self._remove_eg_textAutoFit() + if value == MSO_AUTO_SIZE.NONE: + self._add_noAutofit() + elif value == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE: + self._add_normAutofit() + elif value == MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT: + self._add_spAutoFit() + + +class CT_TextCharacterProperties(BaseOxmlElement): + """Custom element class for `a:rPr`, `a:defRPr`, and `a:endParaRPr`. + + 'rPr' is short for 'run properties', and it corresponds to the |Font| proxy class. + """ + + get_or_add_hlinkClick: Callable[[], CT_Hyperlink] + get_or_add_latin: Callable[[], CT_TextFont] + _remove_latin: Callable[[], None] + _remove_hlinkClick: Callable[[], None] + + eg_fillProperties = ZeroOrOneChoice( + ( + Choice("a:noFill"), + Choice("a:solidFill"), + Choice("a:gradFill"), + Choice("a:blipFill"), + Choice("a:pattFill"), + Choice("a:grpFill"), + ), + successors=( + "a:effectLst", + "a:effectDag", + "a:highlight", + "a:uLnTx", + "a:uLn", + "a:uFillTx", + "a:uFill", + "a:latin", + "a:ea", + "a:cs", + "a:sym", + "a:hlinkClick", + "a:hlinkMouseOver", + "a:rtl", + "a:extLst", + ), + ) + latin: CT_TextFont | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:latin", + successors=( + "a:ea", + "a:cs", + "a:sym", + "a:hlinkClick", + "a:hlinkMouseOver", + "a:rtl", + "a:extLst", + ), + ) + hlinkClick: CT_Hyperlink | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst") + ) + + lang: MSO_LANGUAGE_ID | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "lang", MSO_LANGUAGE_ID + ) + sz: int | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "sz", ST_TextFontSize + ) + b: bool | None = OptionalAttribute("b", XsdBoolean) # pyright: ignore[reportAssignmentType] + i: bool | None = OptionalAttribute("i", XsdBoolean) # pyright: ignore[reportAssignmentType] + u: MSO_TEXT_UNDERLINE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "u", MSO_TEXT_UNDERLINE_TYPE + ) + + def _new_gradFill(self): + return CT_GradientFillProperties.new_gradFill() + + def add_hlinkClick(self, rId: str) -> CT_Hyperlink: + """Add an `a:hlinkClick` child element with r:id attribute set to `rId`.""" + hlinkClick = self.get_or_add_hlinkClick() + hlinkClick.rId = rId + return hlinkClick + + +class CT_TextField(BaseOxmlElement): + """`a:fld` field element, for either a slide number or date field.""" + + get_or_add_rPr: Callable[[], CT_TextCharacterProperties] + + rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:rPr", successors=("a:pPr", "a:t") + ) + t: BaseOxmlElement | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:t", successors=() + ) + + @property + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] + """The text of the `a:t` child element.""" + t = self.t + if t is None: + return "" + return t.text or "" + + +class CT_TextFont(BaseOxmlElement): + """Custom element class for `a:latin`, `a:ea`, `a:cs`, and `a:sym`. + + These occur as child elements of CT_TextCharacterProperties, e.g. `a:rPr`. + """ + + typeface: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "typeface", ST_TextTypeface + ) + + +class CT_TextLineBreak(BaseOxmlElement): + """`a:br` line break element""" + + get_or_add_rPr: Callable[[], CT_TextCharacterProperties] + + rPr = ZeroOrOne("a:rPr", successors=()) + + @property + def text(self): # pyright: ignore[reportIncompatibleMethodOverride] + """Unconditionally a single vertical-tab character. + + A line break element can contain no text other than the implicit line feed it + represents. + """ + return "\v" + + +class CT_TextNormalAutofit(BaseOxmlElement): + """`a:normAutofit` element specifying fit text to shape font reduction, etc.""" + + fontScale = OptionalAttribute( + "fontScale", ST_TextFontScalePercentOrPercentString, default=100.0 + ) + + +class CT_TextParagraph(BaseOxmlElement): + """`a:p` custom element class""" + + get_or_add_endParaRPr: Callable[[], CT_TextCharacterProperties] + get_or_add_pPr: Callable[[], CT_TextParagraphProperties] + r_lst: list[CT_RegularTextRun] + _add_br: Callable[[], CT_TextLineBreak] + _add_r: Callable[[], CT_RegularTextRun] + + pPr: CT_TextParagraphProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr") + ) + r = ZeroOrMore("a:r", successors=("a:endParaRPr",)) + br = ZeroOrMore("a:br", successors=("a:endParaRPr",)) + endParaRPr: CT_TextCharacterProperties | None = ZeroOrOne( + "a:endParaRPr", successors=() + ) # pyright: ignore[reportAssignmentType] + + def add_br(self) -> CT_TextLineBreak: + """Return a newly appended `a:br` element.""" + return self._add_br() + + def add_r(self, text: str | None = None) -> CT_RegularTextRun: + """Return a newly appended `a:r` element.""" + r = self._add_r() + if text: + r.text = text + return r + + def append_text(self, text: str): + """Append `a:r` and `a:br` elements to `p` based on `text`. + + Any `\n` or `\v` (vertical-tab) characters in `text` delimit `a:r` (run) elements and + themselves are translated to `a:br` (line-break) elements. The vertical-tab character + appears in clipboard text from PowerPoint at "soft" line-breaks (new-line, but not new + paragraph). + """ + for idx, r_str in enumerate(re.split("\n|\v", text)): + # ---breaks are only added _between_ items, not at start--- + if idx > 0: + self.add_br() + # ---runs that would be empty are not added--- + if r_str: + self.add_r(r_str) + + @property + def content_children(self) -> tuple[CT_RegularTextRun | CT_TextLineBreak | CT_TextField, ...]: + """Sequence containing text-container child elements of this `a:p` element. + + These include `a:r`, `a:br`, and `a:fld`. + """ + return tuple( + e for e in self if isinstance(e, (CT_RegularTextRun, CT_TextLineBreak, CT_TextField)) + ) + + @property + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] + """str text contained in this paragraph.""" + # ---note this shadows the lxml _Element.text--- + return "".join([child.text for child in self.content_children]) + + def _new_r(self): + r_xml = "" % nsdecls("a") + return parse_xml(r_xml) + + +class CT_TextParagraphProperties(BaseOxmlElement): + """`a:pPr` custom element class.""" + + get_or_add_defRPr: Callable[[], CT_TextCharacterProperties] + _add_lnSpc: Callable[[], CT_TextSpacing] + _add_spcAft: Callable[[], CT_TextSpacing] + _add_spcBef: Callable[[], CT_TextSpacing] + _remove_lnSpc: Callable[[], None] + _remove_spcAft: Callable[[], None] + _remove_spcBef: Callable[[], None] + + _tag_seq = ( + "a:lnSpc", + "a:spcBef", + "a:spcAft", + "a:buClrTx", + "a:buClr", + "a:buSzTx", + "a:buSzPct", + "a:buSzPts", + "a:buFontTx", + "a:buFont", + "a:buNone", + "a:buAutoNum", + "a:buChar", + "a:buBlip", + "a:tabLst", + "a:defRPr", + "a:extLst", + ) + lnSpc: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:lnSpc", successors=_tag_seq[1:] + ) + spcBef: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcBef", successors=_tag_seq[2:] + ) + spcAft: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcAft", successors=_tag_seq[3:] + ) + defRPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:defRPr", successors=_tag_seq[16:] + ) + lvl: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "lvl", ST_TextIndentLevelType, default=0 + ) + algn: PP_PARAGRAPH_ALIGNMENT | None = OptionalAttribute( + "algn", PP_PARAGRAPH_ALIGNMENT + ) # pyright: ignore[reportAssignmentType] + del _tag_seq + + @property + def line_spacing(self) -> float | Length | None: + """The spacing between baselines of successive lines in this paragraph. + + A float value indicates a number of lines. A |Length| value indicates a fixed spacing. + Value is contained in `./a:lnSpc/a:spcPts/@val` or `./a:lnSpc/a:spcPct/@val`. Value is + |None| if no element is present. + """ + lnSpc = self.lnSpc + if lnSpc is None: + return None + if lnSpc.spcPts is not None: + return lnSpc.spcPts.val + return cast(CT_TextSpacingPercent, lnSpc.spcPct).val + + @line_spacing.setter + def line_spacing(self, value: float | Length | None): + self._remove_lnSpc() + if value is None: + return + if isinstance(value, Length): + self._add_lnSpc().set_spcPts(value) + else: + self._add_lnSpc().set_spcPct(value) + + @property + def space_after(self) -> Length | None: + """The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`.""" + spcAft = self.spcAft + if spcAft is None: + return None + spcPts = spcAft.spcPts + if spcPts is None: + return None + return spcPts.val + + @space_after.setter + def space_after(self, value: Length | None): + self._remove_spcAft() + if value is not None: + self._add_spcAft().set_spcPts(value) + + @property + def space_before(self): + """The EMU equivalent of the centipoints value in `./a:spcBef/a:spcPts/@val`.""" + spcBef = self.spcBef + if spcBef is None: + return None + spcPts = spcBef.spcPts + if spcPts is None: + return None + return spcPts.val + + @space_before.setter + def space_before(self, value: Length | None): + self._remove_spcBef() + if value is not None: + self._add_spcBef().set_spcPts(value) + + +class CT_TextSpacing(BaseOxmlElement): + """Used for `a:lnSpc`, `a:spcBef`, and `a:spcAft` elements.""" + + get_or_add_spcPct: Callable[[], CT_TextSpacingPercent] + get_or_add_spcPts: Callable[[], CT_TextSpacingPoint] + _remove_spcPct: Callable[[], None] + _remove_spcPts: Callable[[], None] + + # this should actually be a OneAndOnlyOneChoice, but that's not + # implemented yet. + spcPct: CT_TextSpacingPercent | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcPct" + ) + spcPts: CT_TextSpacingPoint | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcPts" + ) + + def set_spcPct(self, value: float): + """Set spacing to `value` lines, e.g. 1.75 lines. + + A ./a:spcPts child is removed if present. + """ + self._remove_spcPts() + spcPct = self.get_or_add_spcPct() + spcPct.val = value + + def set_spcPts(self, value: Length): + """Set spacing to `value` points. A ./a:spcPct child is removed if present.""" + self._remove_spcPct() + spcPts = self.get_or_add_spcPts() + spcPts.val = value + + +class CT_TextSpacingPercent(BaseOxmlElement): + """`a:spcPct` element, specifying spacing in thousandths of a percent in its `val` attribute.""" + + val: float = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "val", ST_TextSpacingPercentOrPercentString + ) + + +class CT_TextSpacingPoint(BaseOxmlElement): + """`a:spcPts` element, specifying spacing in centipoints in its `val` attribute.""" + + val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "val", ST_TextSpacingPoint + ) diff --git a/pptx/oxml/theme.py b/src/pptx/oxml/theme.py similarity index 77% rename from pptx/oxml/theme.py rename to src/pptx/oxml/theme.py index 9e3737311..19ac8dea6 100644 --- a/pptx/oxml/theme.py +++ b/src/pptx/oxml/theme.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""lxml custom element classes for theme-related XML elements.""" -""" -lxml custom element classes for theme-related XML elements. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from . import parse_from_template from .xmlchemy import BaseOxmlElement diff --git a/pptx/oxml/xmlchemy.py b/src/pptx/oxml/xmlchemy.py similarity index 54% rename from pptx/oxml/xmlchemy.py rename to src/pptx/oxml/xmlchemy.py index b84ef4ddb..41fb2e171 100644 --- a/pptx/oxml/xmlchemy.py +++ b/src/pptx/oxml/xmlchemy.py @@ -1,36 +1,49 @@ -# encoding: utf-8 +"""Base and meta classes enabling declarative definition of custom element classes.""" -""" -Base and meta classes that enable declarative definition of custom element -classes. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import re +from typing import Any, Callable, Iterable, Protocol, Sequence, Type, cast from lxml import etree +from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage] + +from pptx.exc import InvalidXmlError +from pptx.oxml import oxml_parser +from pptx.oxml.ns import NamespacePrefixedTag, _nsmap, qn # pyright: ignore[reportPrivateUsage] +from pptx.util import lazyproperty -from . import oxml_parser -from ..compat import Unicode -from ..exc import InvalidXmlError -from .ns import NamespacePrefixedTag, _nsmap, qn -from ..util import lazyproperty +class AttributeType(Protocol): + """Interface for an object that can act as an attribute type. -def OxmlElement(nsptag_str, nsmap=None): + An attribute-type specifies how values are transformed to and from the XML "string" value of the + attribute. """ - 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. + + @classmethod + def from_xml(cls, xml_value: str) -> Any: + """Transform an attribute value to a Python value.""" + ... + + @classmethod + def to_xml(cls, value: Any) -> str: + """Transform a Python value to a str value suitable to this XML attribute.""" + ... + + +def OxmlElement(nsptag_str: str, nsmap: dict[str, str] | None = None) -> BaseOxmlElement: + """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. """ nsptag = NamespacePrefixedTag(nsptag_str) nsmap = nsmap if nsmap is not None else nsptag.nsmap return oxml_parser.makeelement(nsptag.clark_name, nsmap=nsmap) -def serialize_for_reading(element): +def serialize_for_reading(element: ElementBase): """ Serialize *element* to human-readable XML suitable for tests. No XML declaration. @@ -39,11 +52,8 @@ def serialize_for_reading(element): return XmlString(xml) -class XmlString(Unicode): - """ - Provides string comparison override suitable for serialized XML that is - useful for tests. - """ +class XmlString(str): + """Provides string comparison override suitable for serialized XML; useful for tests.""" # ' text' # | | || | @@ -53,7 +63,9 @@ class XmlString(Unicode): _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): @@ -63,22 +75,22 @@ 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): - """ - Return a sequence of attribute strings parsed from *attrs*. Each - attribute string is stripped of whitespace on both ends. + 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. """ 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*. + def _eq_elm_strs(self, line: str, line_2: str) -> bool: + """True if the element in `line_2` is XML-equivalent to the element in `line`. + + In particular, the order of attributes in XML is not significant. """ front, attrs, close, text = self._parse_line(line) front_2, attrs_2, close_2, text_2 = self._parse_line(line_2) @@ -92,22 +104,19 @@ def _eq_elm_strs(self, line, line_2): return False return True - def _parse_line(self, line): - """ - Return front, attrs, close, text 4-tuple result of parsing XML element - string *line*. - """ + def _parse_line(self, line: str): + """Return front, attrs, close, text 4-tuple result of parsing XML element string `line`.""" match = self._xml_elm_line_patt.match(line) + if match is None: + raise ValueError("`line` does not match pattern for an XML element") front, attrs, close, text = [match.group(n) for n in range(1, 5)] return front, attrs, close, text class MetaOxmlElement(type): - """ - Metaclass for BaseOxmlElement - """ + """Metaclass for BaseOxmlElement.""" - def __init__(cls, clsname, bases, clsdict): + def __init__(cls, clsname: str, bases: tuple[type, ...], clsdict: dict[str, Any]): dispatchable = ( OneAndOnlyOne, OneOrMore, @@ -122,18 +131,14 @@ def __init__(cls, clsname, bases, clsdict): value.populate_class_members(cls, key) -class BaseAttribute(object): - """ - Base class for OptionalAttribute and RequiredAttribute, providing common - methods. - """ +class BaseAttribute: + """Base class for OptionalAttribute and RequiredAttribute, providing common methods.""" - def __init__(self, attr_name, simple_type): - super(BaseAttribute, self).__init__() + def __init__(self, attr_name: str, simple_type: type[AttributeType]): 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): """ Add the appropriate methods to *element_cls*. """ @@ -143,10 +148,10 @@ def populate_class_members(self, element_cls, 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. + + 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 @@ -158,15 +163,25 @@ def _clark_name(self): return qn(self._attr_name) return self._attr_name + @property + def _getter(self) -> Callable[[BaseOxmlElement], Any]: + """Callable suitable for the "get" side of the attribute property descriptor.""" + raise NotImplementedError("must be implemented by each subclass") + + @property + def _setter(self) -> Callable[[BaseOxmlElement, Any], None]: + """Callable suitable for the "set" side of the attribute property descriptor.""" + raise NotImplementedError("must be implemented by each subclass") + 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. + """Defines an optional attribute on a custom element class. + + An optional attribute returns a default value when not present for reading. When assigned + |None|, the attribute is removed. """ - def __init__(self, attr_name, simple_type, default=None): + def __init__(self, attr_name: str, simple_type: type[AttributeType], default: Any = None): super(OptionalAttribute, self).__init__(attr_name, simple_type) self._default = default @@ -184,13 +199,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]: + """Callable suitable for the "get" side of the attribute property descriptor.""" - def get_attr_value(obj): + def get_attr_value(obj: BaseOxmlElement) -> Any: attr_str_value = obj.get(self._clark_name) if attr_str_value is None: return self._default @@ -200,13 +212,12 @@ 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]: + """Callable suitable for the "set" side of the attribute property descriptor.""" - def set_attr_value(obj, value): + def set_attr_value(obj: BaseOxmlElement, value: Any) -> None: + # -- when an XML attribute has a default value, setting it to that default removes the + # -- attribute from the element (when it is present) if value == self._default: if self._clark_name in obj.attrib: del obj.attrib[self._clark_name] @@ -218,28 +229,23 @@ 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. - """ + def _getter(self) -> Callable[[BaseOxmlElement], Any]: + """Callable suitable for the "get" side of the attribute property descriptor.""" - def get_attr_value(obj): + def get_attr_value(obj: BaseOxmlElement) -> Any: 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) @@ -258,45 +264,36 @@ def _docstring(self): ) @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]: + """Callable suitable for the "set" side of the attribute property descriptor.""" - def set_attr_value(obj, value): + def set_attr_value(obj: BaseOxmlElement, value: Any) -> None: str_value = self._simple_type.to_xml(value) obj.set(self._clark_name, str_value) return set_attr_value -class _BaseChildElement(object): - """ - Base class for the child element classes corresponding to varying - cardinalities, such as ZeroOrOne and ZeroOrMore. +class _BaseChildElement: + """Base class for the child element classes corresponding to varying cardinalities. + + Subclasses include ZeroOrOne and ZeroOrMore. """ - def __init__(self, nsptagname, successors=()): + def __init__(self, nsptagname: str, successors: Sequence[str] = ()): super(_BaseChildElement, self).__init__() self._nsptagname = nsptagname self._successors = successors - def populate_class_members(self, element_cls, prop_name): - """ - Baseline behavior for adding the appropriate methods to - *element_cls*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """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): + def _add_child(obj: BaseOxmlElement, **attrs: Any): new_method = getattr(obj, self._new_method_name) child = new_method() for key, value in attrs.items(): @@ -312,9 +309,9 @@ 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. + + This method creates a new, empty element of the correct type, having no attributes. """ creator = self._creator creator.__doc__ = ( @@ -324,21 +321,18 @@ 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 parent element class. + + The property locates and returns this child element or `None` if not present. """ 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): + def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement): obj.insert_element_before(child, *self._successors) return child @@ -353,7 +347,7 @@ 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 = f"{self._prop_name}_lst" property_ = property(self._list_getter, None, None) setattr(self._element_cls, prop_name, property_) @@ -361,36 +355,30 @@ def _add_list_getter(self): def _add_method_name(self): return "_add_%s" % self._prop_name - def _add_to_class(self, name, method): - """ - Add *method* to the target class as *name*, unless *name* is already - defined on the class. - """ + def _add_to_class(self, name: str, method: Callable[..., Any]): + """Add `method` to the target class as `name`, unless `name` is already defined there.""" 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. - """ + def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]: + """Callable that creates a new, empty element of the child type, having no attributes.""" - def new_child_element(obj): + def new_child_element(obj: BaseOxmlElement): return OxmlElement(self._nsptagname) return new_child_element @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the property - descriptor. This default getter returns the child element with - matching tag name or |None| if not present. + def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement | None]: + """Callable 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): + def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement | None: return obj.find(qn(self._nsptagname)) get_child_element.__doc__ = ( @@ -403,14 +391,11 @@ def _insert_method_name(self): return "_insert_%s" % self._prop_name @property - def _list_getter(self): - """ - Return a function object suitable for the "get" side of a list - property descriptor. - """ + def _list_getter(self) -> Callable[[BaseOxmlElement], list[BaseOxmlElement]]: + """Callable suitable for the "get" side of a list property descriptor.""" - def get_child_element_list(obj): - return obj.findall(qn(self._nsptagname)) + def get_child_element_list(obj: BaseOxmlElement) -> list[BaseOxmlElement]: + return cast("list[BaseOxmlElement]", obj.findall(qn(self._nsptagname))) get_child_element_list.__doc__ = ( "A list containing each of the ``<%s>`` child elements, in the o" @@ -428,19 +413,16 @@ 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*. - """ + def populate_class_members( # pyright: ignore[reportIncompatibleMethodOverride] + self, element_cls: Type[BaseOxmlElement], group_prop_name: str, successors: Sequence[str] + ): + """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._group_prop_name = group_prop_name self._successors = successors @@ -451,13 +433,10 @@ def populate_class_members(self, element_cls, group_prop_name, successors): self._add_adder() 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. - """ + def _add_get_or_change_to_method(self) -> None: + """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 @@ -493,14 +472,12 @@ 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 __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: Type[BaseOxmlElement], prop_name: str): """ Add the appropriate methods to *element_cls*. """ @@ -508,13 +485,10 @@ def populate_class_members(self, element_cls, prop_name): self._add_getter() @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the property - descriptor. - """ + def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]: + """Callable suitable for the "get" side of the property descriptor.""" - def get_child_element(obj): + def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement: child = obj.find(qn(self._nsptagname)) if child is None: raise InvalidXmlError( @@ -522,22 +496,15 @@ def get_child_element(obj): ) 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 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*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """Add the appropriate methods to *element_cls*.""" super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() @@ -546,12 +513,10 @@ def populate_class_members(self, element_cls, prop_name): self._add_public_adder() delattr(element_cls, prop_name) - def _add_public_adder(self): - """ - Add a public ``add_x()`` method to the parent element class. - """ + def _add_public_adder(self) -> None: + """Add a public `.add_x()` method to the parent element class.""" - def add_child(obj): + def add_child(obj: BaseOxmlElement) -> BaseOxmlElement: private_add_method = getattr(obj, self._add_method_name) child = private_add_method() return child @@ -578,7 +543,7 @@ 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: Type[BaseOxmlElement], prop_name: str): """ Add the appropriate methods to *element_cls*. """ @@ -591,14 +556,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*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """Add the appropriate methods to `element_cls`.""" super(ZeroOrOne, self).populate_class_members(element_cls, prop_name) self._add_getter() self._add_creator() @@ -608,12 +569,9 @@ 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): + def get_or_add_child(obj: BaseOxmlElement) -> BaseOxmlElement: child = getattr(obj, self._prop_name) if child is None: add_method = getattr(obj, self._add_method_name) @@ -626,17 +584,12 @@ 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): + def _remove_child(obj: BaseOxmlElement) -> None: obj.remove_all(self._nsptagname) - _remove_child.__doc__ = ( - "Remove all ``<%s>`` child elements." - ) % self._nsptagname + _remove_child.__doc__ = f"Remove all `{self._nsptagname}` child elements." self._add_to_class(self._remove_method_name, _remove_child) @lazyproperty @@ -645,50 +598,37 @@ 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. - """ + """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 __init__(self, choices: Iterable[Choice], successors: Iterable[str] = ()): + self._choices = tuple(choices) + self._successors = tuple(successors) - def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """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): - """ - 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. + + The property 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): + def _remove_choice_group(obj: BaseOxmlElement) -> None: 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 @@ -698,8 +638,10 @@ def _choice_getter(self): descriptor. """ - def get_group_member_element(obj): - return obj.first_child_found_in(*self._member_nsptagnames) + def get_group_member_element(obj: BaseOxmlElement) -> BaseOxmlElement | None: + return cast( + "BaseOxmlElement | None", obj.first_child_found_in(*self._member_nsptagnames) + ) get_group_member_element.__doc__ = ( "Return the child element belonging to this element group, or " @@ -708,49 +650,39 @@ def get_group_member_element(obj): return get_group_member_element @lazyproperty - def _member_nsptagnames(self): - """ - Sequence of namespace-prefixed tagnames, one for each of the member - elements of this choice group. - """ + def _member_nsptagnames(self) -> list[str]: + """Sequence of namespace-prefixed tagnames, one for each member element of choice group.""" return [choice.nsptagname for choice in self._choices] @lazyproperty def _remove_choice_group_method_name(self): - return "_remove_%s" % self._prop_name + """Function-name for choice remover.""" + return f"_remove_{self._prop_name}" -class _OxmlElementBase(etree.ElementBase): - """ - Provides common behavior for oxml element classes - """ +# -- lxml typing isn't quite right here, just ignore this error on _Element -- +class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement): + """Effective base class for all custom element classes. - @classmethod - def child_tagnames_after(cls, tagname): - """ - Return a sequence containing the namespace prefixed child tagnames, - e.g. 'a:prstGeom', that occur after *tagname* in this element. - """ - return cls.child_tagnames.tagnames_after(tagname) + Adds standardized behavior to all classes in one place. + """ - def delete(self): - """ - Remove this element from the XML tree. - """ - self.getparent().remove(self) + def __repr__(self): + return "<%s '<%s>' at 0x%0x>" % ( + self.__class__.__name__, + self._nsptag, + id(self), + ) - def first_child_found_in(self, *tagnames): - """ - Return the first child found with tag in *tagnames*, or None if - not found. - """ + def first_child_found_in(self, *tagnames: str) -> _Element | None: + """First child with tag in `tagnames`, or None if not found.""" for tagname in tagnames: child = self.find(qn(tagname)) if child is not None: return child return None - def insert_element_before(self, elm, *tagnames): + def insert_element_before(self, elm: ElementBase, *tagnames: str): successor = self.first_child_found_in(*tagnames) if successor is not None: successor.addprevious(elm) @@ -758,40 +690,28 @@ def insert_element_before(self, elm, *tagnames): self.append(elm) return elm - def remove_all(self, tagname): - """ - Remove all child elements having *tagname*. - """ - matching = self.findall(qn(tagname)) - for child in matching: - self.remove(child) - - def remove_if_present(self, *tagnames): - """ - Remove all child elements having tagname in *tagnames*. - """ + def remove_all(self, *tagnames: str) -> None: + """Remove child elements with tagname (e.g. "a:p") in `tagnames`.""" for tagname in tagnames: - element = self.find(qn(tagname)) - if element is not None: - self.remove(element) + matching = self.findall(qn(tagname)) + for child in matching: + self.remove(child) @property - def xml(self): - """ - Return XML string for this element, suitable for testing purposes. - Pretty printed for readability and without an XML declaration at the - top. + def xml(self) -> str: + """XML string for this element, suitable for testing purposes. + + Pretty printed for readability and without an XML declaration at the top. """ return serialize_for_reading(self) - def xpath(self, xpath_str): - """ - Override of ``lxml`` _Element.xpath() method to provide standard Open - XML namespace mapping in centralized location. - """ - return super(BaseOxmlElement, self).xpath(xpath_str, namespaces=_nsmap) + 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. + """ + return super().xpath(xpath_str, namespaces=_nsmap) -BaseOxmlElement = MetaOxmlElement( - "BaseOxmlElement", (etree.ElementBase,), dict(_OxmlElementBase.__dict__) -) + @property + def _nsptag(self) -> str: + return NamespacePrefixedTag.from_clark_name(self.tag) diff --git a/pptx/package.py b/src/pptx/package.py similarity index 73% rename from pptx/package.py rename to src/pptx/package.py index e03ec271c..79703cd6c 100644 --- a/pptx/package.py +++ b/src/pptx/package.py @@ -1,46 +1,35 @@ -# encoding: utf-8 +"""Overall .pptx package.""" -""" -API classes for dealing with presentations and other objects one typically -encounters as an end-user of the PowerPoint user interface. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import IO, Iterator -from .opc.constants import RELATIONSHIP_TYPE as RT -from .opc.package import OpcPackage -from .opc.packuri import PackURI -from .parts.coreprops import CorePropertiesPart -from .parts.image import Image, ImagePart -from .parts.media import MediaPart -from .util import lazyproperty +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.package import OpcPackage +from pptx.opc.packuri import PackURI +from pptx.parts.coreprops import CorePropertiesPart +from pptx.parts.image import Image, ImagePart +from pptx.parts.media import MediaPart +from pptx.util import lazyproperty class Package(OpcPackage): - """ - Return an instance of |Package| loaded from *file*, where *file* can be a - path (a string) or a file-like object. If *file* is a path, it can be - either a path to a PowerPoint `.pptx` file or a path to a directory - containing an expanded presentation file, as would result from unzipping - a `.pptx` file. If *file* is |None|, the default presentation template is - loaded. - """ + """An overall .pptx package.""" @lazyproperty - def core_properties(self): - """ - Instance of |CoreProperties| holding the read/write Dublin Core - document properties for this presentation. Creates a default core - properties part if one is not present (not common). + def core_properties(self) -> CorePropertiesPart: + """Instance of |CoreProperties| holding read/write Dublin Core doc properties. + + Creates a default core properties part if one is not present (not common). """ try: return self.part_related_by(RT.CORE_PROPERTIES) except KeyError: - core_props = CorePropertiesPart.default() + core_props = CorePropertiesPart.default(self) self.relate_to(core_props, RT.CORE_PROPERTIES) return core_props - def get_or_add_image_part(self, image_file): + def get_or_add_image_part(self, image_file: str | IO[bytes]): """ Return an |ImagePart| object containing the image in *image_file*. If the image part already exists in this package, it is reused, @@ -56,10 +45,10 @@ def get_or_add_media_part(self, media): """ return self._media_parts.get_or_add_media_part(media) - def next_image_partname(self, ext): - """ - Return a |PackURI| instance representing the next available image - partname, by sequence number. *ext* is used as the extention on the + def next_image_partname(self, ext: str) -> PackURI: + """Return a |PackURI| instance representing the next available image partname. + + Partname uses the next available sequence number. *ext* is used as the extention on the returned partname. """ @@ -68,8 +57,10 @@ def first_available_image_idx(): [ part.partname.idx for part in self.iter_parts() - if part.partname.startswith("/ppt/media/image") - and part.partname.idx is not None + if ( + part.partname.startswith("/ppt/media/image") + and part.partname.idx is not None + ) ] ) for i, image_idx in enumerate(image_idxs): @@ -138,10 +129,8 @@ def __init__(self, package): super(_ImageParts, self).__init__() self._package = package - def __iter__(self): - """ - Generate a reference to each |ImagePart| object in the package. - """ + def __iter__(self) -> Iterator[ImagePart]: + """Generate a reference to each |ImagePart| object in the package.""" image_parts = [] for rel in self._package.iter_rels(): if rel.is_external: @@ -154,21 +143,18 @@ def __iter__(self): image_parts.append(image_part) yield image_part - def get_or_add_image_part(self, image_file): - """ - Return an |ImagePart| object containing the image in *image_file*, - which is either a path to an image file or a file-like object - containing an image. If an image part containing this same image - already exists, that instance is returned, otherwise a new image part - is created. + def get_or_add_image_part(self, image_file: str | IO[bytes]) -> ImagePart: + """Return |ImagePart| object containing the image in `image_file`. + + `image_file` can be either a path to an image file or a file-like object + containing an image. If an image part containing this same image already exists, + that instance is returned, otherwise a new image part is created. """ image = Image.from_file(image_file) image_part = self._find_by_sha1(image.sha1) - if image_part is None: - image_part = ImagePart.new(self._package, image) - return image_part + return image_part if image_part else ImagePart.new(self._package, image) - def _find_by_sha1(self, sha1): + def _find_by_sha1(self, sha1: str) -> ImagePart | None: """ Return an |ImagePart| object belonging to this package or |None| if no matching image part is found. The image part is identified by the diff --git a/pptx/oxml/shapes/__init__.py b/src/pptx/parts/__init__.py similarity index 100% rename from pptx/oxml/shapes/__init__.py rename to src/pptx/parts/__init__.py diff --git a/src/pptx/parts/chart.py b/src/pptx/parts/chart.py new file mode 100644 index 000000000..7208071b4 --- /dev/null +++ b/src/pptx/parts/chart.py @@ -0,0 +1,95 @@ +"""Chart part objects, including Chart and Charts.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pptx.chart.chart import Chart +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.package import XmlPart +from pptx.parts.embeddedpackage import EmbeddedXlsxPart +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.chart.data import ChartData + from pptx.enum.chart import XL_CHART_TYPE + from pptx.package import Package + + +class ChartPart(XmlPart): + """A chart part. + + Corresponds to parts having partnames matching ppt/charts/chart[1-9][0-9]*.xml + """ + + partname_template = "/ppt/charts/chart%d.xml" + + @classmethod + def new(cls, chart_type: XL_CHART_TYPE, chart_data: ChartData, package: Package): + """Return new |ChartPart| instance added to `package`. + + Returned chart-part contains a chart of `chart_type` depicting `chart_data`. + """ + chart_part = cls.load( + package.next_partname(cls.partname_template), + CT.DML_CHART, + package, + chart_data.xml_bytes(chart_type), + ) + chart_part.chart_workbook.update_from_xlsx_blob(chart_data.xlsx_blob) + return chart_part + + @lazyproperty + def chart(self): + """|Chart| object representing the chart in this part.""" + return Chart(self._element, self) + + @lazyproperty + def chart_workbook(self): + """ + The |ChartWorkbook| object providing access to the external chart + data in a linked or embedded Excel workbook. + """ + return ChartWorkbook(self._element, self) + + +class ChartWorkbook(object): + """Provides access to external chart data in a linked or embedded Excel workbook.""" + + def __init__(self, chartSpace, chart_part): + super(ChartWorkbook, self).__init__() + self._chartSpace = chartSpace + self._chart_part = chart_part + + def update_from_xlsx_blob(self, xlsx_blob): + """ + Replace the Excel spreadsheet in the related |EmbeddedXlsxPart| with + the Excel binary in *xlsx_blob*, adding a new |EmbeddedXlsxPart| if + there isn't one. + """ + xlsx_part = self.xlsx_part + if xlsx_part is None: + self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._chart_part.package) + return + xlsx_part.blob = xlsx_blob + + @property + def xlsx_part(self): + """Optional |EmbeddedXlsxPart| object containing data for this chart. + + This related part has its rId at `c:chartSpace/c:externalData/@rId`. This value + is |None| if there is no `` element. + """ + xlsx_part_rId = self._chartSpace.xlsx_part_rId + return None if xlsx_part_rId is None else self._chart_part.related_part(xlsx_part_rId) + + @xlsx_part.setter + def xlsx_part(self, xlsx_part): + """ + Set the related |EmbeddedXlsxPart| to *xlsx_part*. Assume one does + not already exist. + """ + rId = self._chart_part.relate_to(xlsx_part, RT.PACKAGE) + externalData = self._chartSpace.get_or_add_externalData() + externalData.rId = rId diff --git a/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py similarity index 51% rename from pptx/parts/coreprops.py rename to src/pptx/parts/coreprops.py index 5cfdb4301..8471cc8ef 100644 --- a/pptx/parts/coreprops.py +++ b/src/pptx/parts/coreprops.py @@ -1,64 +1,71 @@ -# encoding: utf-8 +"""Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" -""" -Core properties part, corresponds to ``/docProps/core.xml`` part in package. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +import datetime as dt +from typing import TYPE_CHECKING -from datetime import datetime +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.package import XmlPart +from pptx.opc.packuri import PackURI +from pptx.oxml.coreprops import CT_CoreProperties -from ..opc.constants import CONTENT_TYPE as CT -from ..opc.package import XmlPart -from ..opc.packuri import PackURI -from ..oxml.coreprops import CT_CoreProperties +if TYPE_CHECKING: + from pptx.package import Package 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`. + + Contains the core document properties for this document package. """ + _element: CT_CoreProperties + @classmethod - def default(cls): - core_props = cls._new() + def default(cls, package: Package): + """Return default new |CorePropertiesPart| instance suitable as starting point. + + This provides a base for adding core-properties to a package that doesn't yet + have any. + """ + core_props = cls._new(package) core_props.title = "PowerPoint Presentation" core_props.last_modified_by = "python-pptx" core_props.revision = 1 - core_props.modified = datetime.utcnow() + core_props.modified = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) return core_props @property - def author(self): + def author(self) -> str: return self._element.author_text @author.setter - def author(self, value): + def author(self, value: str): self._element.author_text = value @property - def category(self): + def category(self) -> str: return self._element.category_text @category.setter - def category(self, value): + def category(self, value: str): self._element.category_text = value @property - def comments(self): + def comments(self) -> str: return self._element.comments_text @comments.setter - def comments(self, value): + def comments(self, value: str): self._element.comments_text = value @property - def content_status(self): + def content_status(self) -> str: 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 @@ -66,39 +73,39 @@ 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 - def identifier(self): + def identifier(self) -> str: return self._element.identifier_text @identifier.setter - def identifier(self, value): + def identifier(self, value: str): self._element.identifier_text = value @property - def keywords(self): + def keywords(self) -> str: return self._element.keywords_text @keywords.setter - def keywords(self, value): + def keywords(self, value: str): self._element.keywords_text = value @property - def language(self): + def language(self) -> str: return self._element.language_text @language.setter - def language(self, value): + def language(self, value: str): self._element.language_text = value @property - def last_modified_by(self): + def last_modified_by(self) -> str: 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 @@ -106,7 +113,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 @@ -114,7 +121,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 @@ -122,36 +129,39 @@ 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 - def subject(self): + def subject(self) -> str: return self._element.subject_text @subject.setter - def subject(self, value): + def subject(self, value: str): self._element.subject_text = value @property - def title(self): + def title(self) -> str: return self._element.title_text @title.setter - def title(self, value): + def title(self, value: str): self._element.title_text = value @property - def version(self): + def version(self) -> str: return self._element.version_text @version.setter - def version(self, value): + def version(self, value: str): self._element.version_text = value @classmethod - def _new(cls): - partname = PackURI("/docProps/core.xml") - content_type = CT.OPC_CORE_PROPERTIES - core_props_elm = CT_CoreProperties.new_coreProperties() - return CorePropertiesPart(partname, content_type, core_props_elm) + def _new(cls, package: Package) -> CorePropertiesPart: + """Return new empty |CorePropertiesPart| instance.""" + return CorePropertiesPart( + PackURI("/docProps/core.xml"), + CT.OPC_CORE_PROPERTIES, + package, + CT_CoreProperties.new_coreProperties(), + ) diff --git a/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py similarity index 84% rename from pptx/parts/embeddedpackage.py rename to src/pptx/parts/embeddedpackage.py index 8754f61e5..7aa2cf408 100644 --- a/pptx/parts/embeddedpackage.py +++ b/src/pptx/parts/embeddedpackage.py @@ -1,14 +1,19 @@ -# encoding: utf-8 - """Embedded Package part objects. "Package" in this context means another OPC package, i.e. a DOCX, PPTX, or XLSX "file". """ +from __future__ import annotations + +from typing import TYPE_CHECKING + from pptx.enum.shapes import PROG_ID from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import Part +if TYPE_CHECKING: + from pptx.package import Package + class EmbeddedPackagePart(Part): """A distinct OPC package, e.g. an Excel file, embedded in this PPTX package. @@ -17,7 +22,7 @@ class EmbeddedPackagePart(Part): """ @classmethod - def factory(cls, prog_id, object_blob, package): + def factory(cls, prog_id: PROG_ID | str, object_blob: bytes, package: Package): """Return a new |EmbeddedPackagePart| subclass instance added to *package*. The subclass is determined by `prog_id` which corresponds to the "application" @@ -25,12 +30,12 @@ def factory(cls, prog_id, object_blob, package): bytes of `object_blob` and has the content-type also determined by `prog_id`. """ # --- a generic OLE object has no subclass --- - if prog_id not in PROG_ID: + if not isinstance(prog_id, PROG_ID): return cls( package.next_partname("/ppt/embeddings/oleObject%d.bin"), CT.OFC_OLE_OBJECT, - object_blob, package, + object_blob, ) # --- A Microsoft Office file-type is a distinguished package object --- @@ -43,13 +48,17 @@ def factory(cls, prog_id, object_blob, package): return EmbeddedPartCls.new(object_blob, package) @classmethod - def new(cls, blob, package): + def new(cls, blob: bytes, package: Package): """Return new |EmbeddedPackagePart| subclass object. The returned part object contains `blob` and is added to `package`. """ - partname = package.next_partname(cls.partname_template) - return cls(partname, cls.content_type, blob, package) + return cls( + package.next_partname(cls.partname_template), + cls.content_type, + package, + blob, + ) class EmbeddedDocxPart(EmbeddedPackagePart): diff --git a/src/pptx/parts/image.py b/src/pptx/parts/image.py new file mode 100644 index 000000000..9be5d02d6 --- /dev/null +++ b/src/pptx/parts/image.py @@ -0,0 +1,275 @@ +"""ImagePart and related objects.""" + +from __future__ import annotations + +import hashlib +import io +import os +from typing import IO, TYPE_CHECKING, Any, cast + +from PIL import Image as PIL_Image + +from pptx.opc.package import Part +from pptx.opc.spec import image_content_types +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from pptx.opc.packuri import PackURI + from pptx.package import Package + from pptx.util import Length + + +class ImagePart(Part): + """An image part. + + An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`. + """ + + def __init__( + self, + partname: PackURI, + content_type: str, + package: Package, + blob: bytes, + filename: str | None = None, + ): + super(ImagePart, self).__init__(partname, content_type, package, blob) + self._blob = blob + self._filename = filename + + @classmethod + def new(cls, package: Package, image: Image) -> ImagePart: + """Return new |ImagePart| instance containing `image`. + + `image` is an |Image| object. + """ + return cls( + package.next_image_partname(image.ext), + image.content_type, + package, + image.blob, + image.filename, + ) + + @property + def desc(self) -> str: + """The filename associated with this image. + + Either the filename of the original image or a generic name of the form `image.ext` where + `ext` is appropriate to the image file format, e.g. `'jpg'`. An image created using a path + will have that filename; one created with a file-like object will have a generic name. + """ + # -- return generic filename if original filename is unknown -- + if self._filename is None: + return f"image.{self.ext}" + return self._filename + + @property + def ext(self) -> str: + """File-name extension for this image e.g. `'png'`.""" + return self.partname.ext + + @property + def image(self) -> Image: + """An |Image| object containing the image in this image part. + + Note this is a `pptx.image.Image` object, not a PIL Image. + """ + return Image(self._blob, self.desc) + + def scale(self, scaled_cx: int | None, scaled_cy: int | None) -> tuple[int, int]: + """Return scaled image dimensions in EMU based on the combination of parameters supplied. + + If `scaled_cx` and `scaled_cy` are both |None|, the native image size is returned. If + neither `scaled_cx` nor `scaled_cy` is |None|, their values are returned unchanged. If a + value is provided for either `scaled_cx` or `scaled_cy` and the other is |None|, the + missing value is calculated such that the image's aspect ratio is preserved. + """ + image_cx, image_cy = self._native_size + + if scaled_cx and scaled_cy: + return scaled_cx, scaled_cy + + if scaled_cx and not scaled_cy: + scaling_factor = float(scaled_cx) / float(image_cx) + scaled_cy = int(round(image_cy * scaling_factor)) + return scaled_cx, scaled_cy + + if not scaled_cx and scaled_cy: + scaling_factor = float(scaled_cy) / float(image_cy) + scaled_cx = int(round(image_cx * scaling_factor)) + return scaled_cx, scaled_cy + + # -- only remaining case is both `scaled_cx` and `scaled_cy` are `None` -- + return image_cx, image_cy + + @lazyproperty + def sha1(self) -> str: + """The 40-character SHA1 hash digest for the image binary of this image part. + + like: `"1be010ea47803b00e140b852765cdf84f491da47"`. + """ + return hashlib.sha1(self._blob).hexdigest() + + @property + def _dpi(self) -> tuple[int, int]: + """(horz_dpi, vert_dpi) pair representing the dots-per-inch resolution of this image.""" + image = Image.from_blob(self._blob) + return image.dpi + + @property + def _native_size(self) -> tuple[Length, Length]: + """A (width, height) 2-tuple representing the native dimensions of the image in EMU. + + Calculated based on the image DPI value, if present, assuming 72 dpi as a default. + """ + EMU_PER_INCH = 914400 + horz_dpi, vert_dpi = self._dpi + width_px, height_px = self._px_size + + width = EMU_PER_INCH * width_px / horz_dpi + height = EMU_PER_INCH * height_px / vert_dpi + + return Emu(int(width)), Emu(int(height)) + + @property + def _px_size(self) -> tuple[int, int]: + """A (width, height) 2-tuple representing the dimensions of this image in pixels.""" + image = Image.from_blob(self._blob) + return image.size + + +class Image(object): + """Immutable value object representing an image such as a JPEG, PNG, or GIF.""" + + def __init__(self, blob: bytes, filename: str | None): + super(Image, self).__init__() + self._blob = blob + self._filename = filename + + @classmethod + def from_blob(cls, blob: bytes, filename: str | None = None) -> Image: + """Return a new |Image| object loaded from the image binary in `blob`.""" + return cls(blob, filename) + + @classmethod + def from_file(cls, image_file: str | IO[bytes]) -> Image: + """Return a new |Image| object loaded from `image_file`. + + `image_file` can be either a path (str) or a file-like object. + """ + if isinstance(image_file, str): + # treat image_file as a path + with open(image_file, "rb") as f: + blob = f.read() + filename = os.path.basename(image_file) + else: + # assume image_file is a file-like object + # ---reposition file cursor if it has one--- + if callable(getattr(image_file, "seek")): + image_file.seek(0) + blob = image_file.read() + filename = None + + return cls.from_blob(blob, filename) + + @property + def blob(self) -> bytes: + """The binary image bytestream of this image.""" + return self._blob + + @lazyproperty + def content_type(self) -> str: + """MIME-type of this image, e.g. `"image/jpeg"`.""" + return image_content_types[self.ext] + + @lazyproperty + def dpi(self) -> tuple[int, int]: + """A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch resolution of this image. + + A default value of (72, 72) is used if the dpi is not specified in the image file. + """ + + def int_dpi(dpi: Any): + """Return an integer dots-per-inch value corresponding to `dpi`. + + If `dpi` is |None|, a non-numeric type, less than 1 or greater than 2048, 72 is + returned. + """ + try: + int_dpi = int(round(float(dpi))) + if int_dpi < 1 or int_dpi > 2048: + int_dpi = 72 + except (TypeError, ValueError): + int_dpi = 72 + return int_dpi + + def normalize_pil_dpi(pil_dpi: tuple[int, int] | None): + """Return a (horz_dpi, vert_dpi) 2-tuple corresponding to `pil_dpi`. + + The value for the 'dpi' key in the `info` dict of a PIL image. If the 'dpi' key is not + present or contains an invalid value, `(72, 72)` is returned. + """ + if isinstance(pil_dpi, tuple): + return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1])) + return (72, 72) + + return normalize_pil_dpi(self._pil_props[2]) + + @lazyproperty + def ext(self) -> str: + """Canonical file extension for this image e.g. `'png'`. + + The returned extension is all lowercase and is the canonical extension for the content type + of this image, regardless of what extension may have been used in its filename, if any. + """ + ext_map = { + "BMP": "bmp", + "GIF": "gif", + "JPEG": "jpg", + "PNG": "png", + "TIFF": "tiff", + "WMF": "wmf", + } + format = self._format + if format not in ext_map: + tmpl = "unsupported image format, expected one of: %s, got '%s'" + raise ValueError(tmpl % (ext_map.keys(), format)) + return ext_map[format] + + @property + def filename(self) -> str | None: + """Filename from path used to load this image, if loaded from the filesystem. + + |None| if no filename was used in loading, such as when loaded from an in-memory stream. + """ + return self._filename + + @lazyproperty + def sha1(self) -> str: + """SHA1 hash digest of the image blob.""" + return hashlib.sha1(self._blob).hexdigest() + + @lazyproperty + def size(self) -> tuple[int, int]: + """A (width, height) 2-tuple specifying the dimensions of this image in pixels.""" + return self._pil_props[1] + + @property + def _format(self) -> str | None: + """The PIL Image format of this image, e.g. 'PNG'.""" + return self._pil_props[0] + + @lazyproperty + def _pil_props(self) -> tuple[str | None, tuple[int, int], tuple[int, int] | None]: + """tuple of image properties extracted from this image using Pillow.""" + stream = io.BytesIO(self._blob) + pil_image = PIL_Image.open(stream) # pyright: ignore[reportUnknownMemberType] + format = pil_image.format + width_px, height_px = pil_image.size + dpi = cast( + "tuple[int, int] | None", + pil_image.info.get("dpi"), # pyright: ignore[reportUnknownMemberType] + ) + stream.close() + return (format, (width_px, height_px), dpi) diff --git a/pptx/parts/media.py b/src/pptx/parts/media.py similarity index 54% rename from pptx/parts/media.py rename to src/pptx/parts/media.py index 4d4fa2300..7e8bc2f21 100644 --- a/pptx/parts/media.py +++ b/src/pptx/parts/media.py @@ -1,30 +1,32 @@ -# encoding: utf-8 - """MediaPart and related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import hashlib -from ..opc.package import Part -from ..util import lazyproperty +from pptx.opc.package import Part +from pptx.util import lazyproperty class MediaPart(Part): """A media part, containing an audio or video resource. A media part generally has a partname matching the regex - ``ppt/media/media[1-9][0-9]*.*``. + `ppt/media/media[1-9][0-9]*.*`. """ @classmethod def new(cls, package, media): - """Return new |MediaPart| instance containing *media*. + """Return new |MediaPart| instance containing `media`. - *media* must be a |Media| object. + `media` must be a |Media| object. """ - partname = package.next_media_partname(media.ext) - return cls(partname, media.content_type, media.blob, package) + return cls( + package.next_media_partname(media.ext), + media.content_type, + package, + media.blob, + ) @lazyproperty def sha1(self): diff --git a/src/pptx/parts/presentation.py b/src/pptx/parts/presentation.py new file mode 100644 index 000000000..1413de457 --- /dev/null +++ b/src/pptx/parts/presentation.py @@ -0,0 +1,126 @@ +"""Presentation part, the main part in a .pptx package.""" + +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, Iterable + +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.package import XmlPart +from pptx.opc.packuri import PackURI +from pptx.parts.slide import NotesMasterPart, SlidePart +from pptx.presentation import Presentation +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.parts.coreprops import CorePropertiesPart + from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster + + +class PresentationPart(XmlPart): + """Top level class in object model. + + Represents the contents of the /ppt directory of a .pptx file. + """ + + def add_slide(self, slide_layout: SlideLayout): + """Return (rId, slide) pair of a newly created blank slide. + + New slide inherits appearance from `slide_layout`. + """ + partname = self._next_slide_partname + slide_layout_part = slide_layout.part + slide_part = SlidePart.new(partname, self.package, slide_layout_part) + rId = self.relate_to(slide_part, RT.SLIDE) + return rId, slide_part.slide + + @property + def core_properties(self) -> CorePropertiesPart: + """A |CoreProperties| object for the presentation. + + Provides read/write access to the Dublin Core properties of this presentation. + """ + return self.package.core_properties + + def get_slide(self, slide_id: int) -> Slide | None: + """Return optional related |Slide| object identified by `slide_id`. + + Returns |None| if no slide with `slide_id` is related to this presentation. + """ + for sldId in self._element.sldIdLst: + if sldId.id == slide_id: + return self.related_part(sldId.rId).slide + return None + + @lazyproperty + def notes_master(self) -> NotesMaster: + """ + Return the |NotesMaster| object for this presentation. If the + presentation does not have a notes master, one is created from + a default template. The same single instance is returned on each + call. + """ + return self.notes_master_part.notes_master + + @lazyproperty + def notes_master_part(self) -> NotesMasterPart: + """Return the |NotesMasterPart| object for this presentation. + + If the presentation does not have a notes master, one is created from a default template. + The same single instance is returned on each call. + """ + try: + return self.part_related_by(RT.NOTES_MASTER) + except KeyError: + notes_master_part = NotesMasterPart.create_default(self.package) + self.relate_to(notes_master_part, RT.NOTES_MASTER) + return notes_master_part + + @lazyproperty + def presentation(self): + """ + A |Presentation| object providing access to the content of this + presentation. + """ + return Presentation(self._element, self) + + def related_slide(self, rId: str) -> Slide: + """Return |Slide| object for related |SlidePart| related by `rId`.""" + return self.related_part(rId).slide + + def related_slide_master(self, rId: str) -> SlideMaster: + """Return |SlideMaster| object for |SlideMasterPart| related by `rId`.""" + return self.related_part(rId).slide_master + + def rename_slide_parts(self, rIds: Iterable[str]): + """Assign incrementing partnames to the slide parts identified by `rIds`. + + Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their id appears + in the `rIds` sequence. The name portion is always `slide`. The number part forms a + continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always + `.xml`. + """ + for idx, rId in enumerate(rIds): + slide_part = self.related_part(rId) + slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1)) + + def save(self, path_or_stream: str | IO[bytes]): + """Save this presentation package to `path_or_stream`. + + `path_or_stream` can be either a path to a filesystem location (a string) or a + file-like object. + """ + self.package.save(path_or_stream) + + def slide_id(self, slide_part): + """Return the slide-id associated with `slide_part`.""" + for sldId in self._element.sldIdLst: + if self.related_part(sldId.rId) is slide_part: + return sldId.id + raise ValueError("matching slide_part not found") + + @property + def _next_slide_partname(self): + """Return |PackURI| instance containing next available slide partname.""" + sldIdLst = self._element.get_or_add_sldIdLst() + partname_str = "/ppt/slides/slide%d.xml" % (len(sldIdLst) + 1) + return PackURI(partname_str) diff --git a/pptx/parts/slide.py b/src/pptx/parts/slide.py similarity index 61% rename from pptx/parts/slide.py rename to src/pptx/parts/slide.py index 1f83c9689..6650564a5 100644 --- a/pptx/parts/slide.py +++ b/src/pptx/parts/slide.py @@ -1,9 +1,12 @@ -# encoding: utf-8 - """Slide and related objects.""" +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, cast + from pptx.enum.shapes import PROG_ID -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import XmlPart from pptx.opc.packuri import PackURI from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide @@ -13,6 +16,12 @@ from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.chart.data import ChartData + from pptx.enum.chart import XL_CHART_TYPE + from pptx.media import Video + from pptx.parts.image import Image, ImagePart + class BaseSlidePart(XmlPart): """Base class for slide parts. @@ -21,31 +30,30 @@ class BaseSlidePart(XmlPart): notes-master, and handout-master parts. """ - def get_image(self, rId): - """ - Return an |Image| object containing the image related to this slide - by *rId*. Raises |KeyError| if no image is related by that id, which - would generally indicate a corrupted .pptx file. - """ - return self.related_parts[rId].image + _element: CT_Slide - def get_or_add_image_part(self, image_file): + def get_image(self, rId: str) -> Image: + """Return an |Image| object containing the image related to this slide by *rId*. + + Raises |KeyError| if no image is related by that id, which would generally indicate a + corrupted .pptx file. """ - Return an ``(image_part, rId)`` 2-tuple corresponding to an - |ImagePart| object containing the image in *image_file*, and related - to this slide with the key *rId*. If either the image part or - relationship already exists, they are reused, otherwise they are - newly created. + return cast("ImagePart", self.related_part(rId)).image + + def get_or_add_image_part(self, image_file: str | IO[bytes]): + """Return `(image_part, rId)` pair corresponding to `image_file`. + + The returned |ImagePart| object contains the image in `image_file` and is + related to this slide with the key `rId`. If either the image part or + relationship already exists, they are reused, otherwise they are newly created. """ image_part = self._package.get_or_add_image_part(image_file) rId = self.relate_to(image_part, RT.IMAGE) return image_part, rId @property - def name(self): - """ - Internal name of this slide. - """ + def name(self) -> str: + """Internal name of this slide.""" return self._element.cSld.name @@ -79,21 +87,22 @@ def _new(cls, package): Create and return a standalone, default notes master part based on the built-in template (without any related parts, such as theme). """ - partname = PackURI("/ppt/notesMasters/notesMaster1.xml") - content_type = CT.PML_NOTES_MASTER - notesMaster = CT_NotesMaster.new_default() - return NotesMasterPart(partname, content_type, notesMaster, package) + return NotesMasterPart( + PackURI("/ppt/notesMasters/notesMaster1.xml"), + CT.PML_NOTES_MASTER, + package, + CT_NotesMaster.new_default(), + ) @classmethod def _new_theme_part(cls, package): - """ - Create and return a default theme part suitable for use with a notes - master. - """ - partname = package.next_partname("/ppt/theme/theme%d.xml") - content_type = CT.OFC_THEME - theme = CT_OfficeStyleSheet.new_default() - return XmlPart(partname, content_type, theme, package) + """Return new default theme-part suitable for use with a notes master.""" + return XmlPart( + package.next_partname("/ppt/theme/theme%d.xml"), + CT.OFC_THEME, + package, + CT_OfficeStyleSheet.new_default(), + ) class NotesSlidePart(BaseSlidePart): @@ -105,44 +114,42 @@ class NotesSlidePart(BaseSlidePart): @classmethod def new(cls, package, slide_part): - """ - Create and return a new notes slide part based on the notes master - and related to both the notes master part and *slide_part*. If no - notes master is present, create one based on the default template. + """Return new |NotesSlidePart| for the slide in `slide_part`. + + The new notes-slide part is based on the (singleton) notes master and related to + both the notes-master part and `slide_part`. If no notes-master is present, + one is created based on the default template. """ notes_master_part = package.presentation_part.notes_master_part - notes_slide_part = cls._add_notes_slide_part( - package, slide_part, notes_master_part - ) + notes_slide_part = cls._add_notes_slide_part(package, slide_part, notes_master_part) notes_slide = notes_slide_part.notes_slide notes_slide.clone_master_placeholders(notes_master_part.notes_master) return notes_slide_part @lazyproperty def notes_master(self): - """ - Return the |NotesMaster| object this notes slide inherits from. - """ + """Return the |NotesMaster| object this notes slide inherits from.""" notes_master_part = self.part_related_by(RT.NOTES_MASTER) return notes_master_part.notes_master @lazyproperty def notes_slide(self): - """ - Return the |NotesSlide| object that proxies this notes slide part. - """ + """Return the |NotesSlide| object that proxies this notes slide part.""" return NotesSlide(self._element, self) @classmethod def _add_notes_slide_part(cls, package, slide_part, notes_master_part): + """Create and return a new notes-slide part. + + The return part is fully related, but has no shape content (i.e. placeholders + not cloned). """ - Create and return a new notes slide part that is fully related, but - has no shape content (i.e. placeholders not cloned). - """ - partname = package.next_partname("/ppt/notesSlides/notesSlide%d.xml") - content_type = CT.PML_NOTES_SLIDE - notes = CT_NotesSlide.new() - notes_slide_part = NotesSlidePart(partname, content_type, notes, package) + notes_slide_part = NotesSlidePart( + package.next_partname("/ppt/notesSlides/notesSlide%d.xml"), + CT.PML_NOTES_SLIDE, + package, + CT_NotesSlide.new(), + ) notes_slide_part.relate_to(notes_master_part, RT.NOTES_MASTER) notes_slide_part.relate_to(slide_part, RT.SLIDE) return notes_slide_part @@ -153,28 +160,27 @@ class SlidePart(BaseSlidePart): @classmethod def new(cls, partname, package, slide_layout_part): + """Return newly-created blank slide part. + + The new slide-part has `partname` and a relationship to `slide_layout_part`. """ - Return a newly-created blank slide part having *partname* and related - to *slide_layout_part*. - """ - sld = CT_Slide.new() - slide_part = cls(partname, CT.PML_SLIDE, sld, package) + slide_part = cls(partname, CT.PML_SLIDE, package, CT_Slide.new()) slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT) return slide_part - def add_chart_part(self, chart_type, chart_data): - """ - Return the rId of a new |ChartPart| object containing a chart of - *chart_type*, displaying *chart_data*, and related to the slide - contained in this part. + def add_chart_part(self, chart_type: XL_CHART_TYPE, chart_data: ChartData): + """Return str rId of new |ChartPart| object containing chart of `chart_type`. + + The chart depicts `chart_data` and is related to the slide contained in this + part by `rId`. """ - chart_part = ChartPart.new(chart_type, chart_data, self.package) - rId = self.relate_to(chart_part, RT.CHART) - return rId + return self.relate_to(ChartPart.new(chart_type, chart_data, self._package), RT.CHART) - def add_embedded_ole_object_part(self, prog_id, ole_object_file): + def add_embedded_ole_object_part( + self, prog_id: PROG_ID | str, ole_object_file: str | IO[bytes] + ): """Return rId of newly-added OLE-object part formed from `ole_object_file`.""" - relationship_type = RT.PACKAGE if prog_id in PROG_ID else RT.OLE_OBJECT + relationship_type = RT.PACKAGE if isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT return self.relate_to( EmbeddedPackagePart.factory( prog_id, self._blob_from_file(ole_object_file), self._package @@ -182,7 +188,7 @@ def add_embedded_ole_object_part(self, prog_id, ole_object_file): relationship_type, ) - def get_or_add_video_media_part(self, video): + def get_or_add_video_media_part(self, video: Video) -> tuple[str, str]: """Return rIds for media and video relationships to media part. A new |MediaPart| object is created if it does not already exist @@ -212,11 +218,11 @@ def has_notes_slide(self): return True @lazyproperty - def notes_slide(self): - """ - The |NotesSlide| instance associated with this slide. If the slide - does not have a notes slide, a new one is created. The same single - instance is returned on each call. + def notes_slide(self) -> NotesSlide: + """The |NotesSlide| instance associated with this slide. + + If the slide does not have a notes slide, a new one is created. The same single instance + is returned on each call. """ try: notes_slide_part = self.part_related_by(RT.NOTES_SLIDE) @@ -232,19 +238,14 @@ def slide(self): return Slide(self._element, self) @property - def slide_id(self): - """ - Return the slide identifier stored in the presentation part for this - slide part. - """ + def slide_id(self) -> int: + """Return the slide identifier stored in the presentation part for this slide part.""" presentation_part = self.package.presentation_part return presentation_part.slide_id(self) @property - def slide_layout(self): - """ - |SlideLayout| object the slide in this part inherits from. - """ + def slide_layout(self) -> SlideLayout: + """|SlideLayout| object the slide in this part inherits appearance from.""" slide_layout_part = self.part_related_by(RT.SLIDE_LAYOUT) return slide_layout_part.slide_layout @@ -273,10 +274,8 @@ def slide_layout(self): return SlideLayout(self._element, self) @property - def slide_master(self): - """ - Slide master from which this slide layout inherits properties. - """ + def slide_master(self) -> SlideMaster: + """Slide master from which this slide layout inherits properties.""" return self.part_related_by(RT.SLIDE_MASTER).slide_master @@ -286,12 +285,9 @@ class SlideMasterPart(BaseSlidePart): Corresponds to package files ppt/slideMasters/slideMaster[1-9][0-9]*.xml. """ - def related_slide_layout(self, rId): - """ - Return the |SlideLayout| object of the related |SlideLayoutPart| - corresponding to relationship key *rId*. - """ - return self.related_parts[rId].slide_layout + def related_slide_layout(self, rId: str) -> SlideLayout: + """Return |SlideLayout| related to this slide-master by key `rId`.""" + return self.related_part(rId).slide_layout @lazyproperty def slide_master(self): diff --git a/src/pptx/presentation.py b/src/pptx/presentation.py new file mode 100644 index 000000000..a41bfd59a --- /dev/null +++ b/src/pptx/presentation.py @@ -0,0 +1,113 @@ +"""Main presentation object.""" + +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, cast + +from pptx.shared import PartElementProxy +from pptx.slide import SlideMasters, Slides +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.oxml.presentation import CT_Presentation, CT_SlideId + from pptx.parts.presentation import PresentationPart + from pptx.slide import NotesMaster, SlideLayouts + from pptx.util import Length + + +class Presentation(PartElementProxy): + """PresentationML (PML) presentation. + + Not intended to be constructed directly. Use :func:`pptx.Presentation` to open or + create a presentation. + """ + + _element: CT_Presentation + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + + @property + def core_properties(self): + """|CoreProperties| instance for this presentation. + + Provides read/write access to the Dublin Core document properties for the presentation. + """ + return self.part.core_properties + + @property + def notes_master(self) -> NotesMaster: + """Instance of |NotesMaster| for this presentation. + + If the presentation does not have a notes master, one is created from a default template + and returned. The same single instance is returned on each call. + """ + return self.part.notes_master + + def save(self, file: str | IO[bytes]): + """Writes this presentation to `file`. + + `file` can be either a file-path or a file-like object open for writing bytes. + """ + self.part.save(file) + + @property + def slide_height(self) -> Length | None: + """Height of slides in this presentation, in English Metric Units (EMU). + + Returns |None| if no slide width is defined. Read/write. + """ + sldSz = self._element.sldSz + if sldSz is None: + return None + return sldSz.cy + + @slide_height.setter + def slide_height(self, height: Length): + sldSz = self._element.get_or_add_sldSz() + sldSz.cy = height + + @property + def slide_layouts(self) -> SlideLayouts: + """|SlideLayouts| collection belonging to the first |SlideMaster| of this presentation. + + A presentation can have more than one slide master and each master will have its own set + of layouts. This property is a convenience for the common case where the presentation has + only a single slide master. + """ + return self.slide_masters[0].slide_layouts + + @property + def slide_master(self): + """ + First |SlideMaster| object belonging to this presentation. Typically, + presentations have only a single slide master. This property provides + simpler access in that common case. + """ + return self.slide_masters[0] + + @lazyproperty + def slide_masters(self) -> SlideMasters: + """|SlideMasters| collection of slide-masters belonging to this presentation.""" + return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self) + + @property + def slide_width(self): + """ + Width of slides in this presentation, in English Metric Units (EMU). + Returns |None| if no slide width is defined. Read/write. + """ + sldSz = self._element.sldSz + if sldSz is None: + return None + return sldSz.cx + + @slide_width.setter + def slide_width(self, width: Length): + sldSz = self._element.get_or_add_sldSz() + sldSz.cx = width + + @lazyproperty + def slides(self): + """|Slides| object containing the slides in this presentation.""" + sldIdLst = self._element.get_or_add_sldIdLst() + self.part.rename_slide_parts([cast("CT_SlideId", sldId).rId for sldId in sldIdLst]) + return Slides(sldIdLst, self) diff --git a/pptx/parts/__init__.py b/src/pptx/py.typed similarity index 100% rename from pptx/parts/__init__.py rename to src/pptx/py.typed diff --git a/src/pptx/shapes/__init__.py b/src/pptx/shapes/__init__.py new file mode 100644 index 000000000..332109a31 --- /dev/null +++ b/src/pptx/shapes/__init__.py @@ -0,0 +1,26 @@ +"""Objects used across sub-package.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.types import ProvidesPart + + +class Subshape(object): + """Provides access to the containing part for drawing elements that occur below a shape. + + Access to the part is required for example to add or drop a relationship. Provides + `self._parent` attribute to subclasses. + """ + + def __init__(self, parent: ProvidesPart): + super(Subshape, self).__init__() + self._parent = parent + + @property + def part(self) -> XmlPart: + """The package part containing this object.""" + return self._parent.part diff --git a/src/pptx/shapes/autoshape.py b/src/pptx/shapes/autoshape.py new file mode 100644 index 000000000..c7f8cd93e --- /dev/null +++ b/src/pptx/shapes/autoshape.py @@ -0,0 +1,355 @@ +"""Autoshape-related objects such as Shape and Adjustment.""" + +from __future__ import annotations + +from numbers import Number +from typing import TYPE_CHECKING, Iterable +from xml.sax import saxutils + +from pptx.dml.fill import FillFormat +from pptx.dml.line import LineFormat +from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE +from pptx.shapes.base import BaseShape +from pptx.spec import autoshape_types +from pptx.text.text import TextFrame +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.oxml.shapes.autoshape import CT_GeomGuide, CT_PresetGeometry2D, CT_Shape + from pptx.spec import AdjustmentValue + from pptx.types import ProvidesPart + + +class Adjustment: + """An adjustment value for an autoshape. + + An adjustment value corresponds to the position of an adjustment handle on an auto shape. + Adjustment handles are the small yellow diamond-shaped handles that appear on certain auto + shapes and allow the outline of the shape to be adjusted. For example, a rounded rectangle has + an adjustment handle that allows the radius of its corner rounding to be adjusted. + + Values are |float| and generally range from 0.0 to 1.0, although the value can be negative or + greater than 1.0 in certain circumstances. + """ + + def __init__(self, name: str, def_val: int, actual: int | None = None): + super(Adjustment, self).__init__() + self.name = name + self.def_val = def_val + self.actual = actual + + @property + def effective_value(self) -> float: + """Read/write |float| representing normalized adjustment value for this adjustment. + + Actual values are a large-ish integer expressed in shape coordinates, nominally between 0 + and 100,000. The effective value is normalized to a corresponding value nominally between + 0.0 and 1.0. Intuitively this represents the proportion of the width or height of the shape + at which the adjustment value is located from its starting point. For simple shapes such as + a rounded rectangle, this intuitive correspondence holds. For more complicated shapes and + at more extreme shape proportions (e.g. width is much greater than height), the value can + become negative or greater than 1.0. + """ + raw_value = self.actual if self.actual is not None else self.def_val + return self._normalize(raw_value) + + @effective_value.setter + def effective_value(self, value: float): + if not isinstance(value, Number): + raise ValueError(f"adjustment value must be numeric, got {repr(value)}") + self.actual = self._denormalize(value) + + @staticmethod + def _denormalize(value: float) -> int: + """Return integer corresponding to normalized `raw_value` on unit basis of 100,000. + + See Adjustment.normalize for additional details. + """ + return int(value * 100000.0) + + @staticmethod + def _normalize(raw_value: int) -> float: + """Return normalized value for `raw_value`. + + A normalized value is a |float| between 0.0 and 1.0 for nominal raw values between 0 and + 100,000. Raw values less than 0 and greater than 100,000 are valid and return values + calculated on the same unit basis of 100,000. + """ + return raw_value / 100000.0 + + @property + def val(self) -> int: + """Denormalized effective value. + + Expressed in shape coordinates, this is suitable for using in the XML. + """ + return self.actual if self.actual is not None else self.def_val + + +class AdjustmentCollection: + """Sequence of |Adjustment| instances for an auto shape. + + Each represents an available adjustment for a shape of its type. Supports `len()` and indexed + access, e.g. `shape.adjustments[1] = 0.15`. + """ + + def __init__(self, prstGeom: CT_PresetGeometry2D): + super(AdjustmentCollection, self).__init__() + self._adjustments_ = self._initialized_adjustments(prstGeom) + self._prstGeom = prstGeom + + def __getitem__(self, idx: int) -> float: + """Provides indexed access, (e.g. 'adjustments[9]').""" + return self._adjustments_[idx].effective_value + + def __setitem__(self, idx: int, value: float): + """Provides item assignment via an indexed expression, e.g. `adjustments[9] = 999.9`. + + Causes all adjustment values in collection to be written to the XML. + """ + self._adjustments_[idx].effective_value = value + self._rewrite_guides() + + def _initialized_adjustments(self, prstGeom: CT_PresetGeometry2D | None) -> list[Adjustment]: + """Return an initialized list of adjustment values based on the contents of `prstGeom`.""" + if prstGeom is None: + return [] + davs = AutoShapeType.default_adjustment_values(prstGeom.prst) + adjustments = [Adjustment(name, def_val) for name, def_val in davs] + self._update_adjustments_with_actuals(adjustments, prstGeom.gd_lst) + return adjustments + + def _rewrite_guides(self): + """Write `a:gd` elements to the XML, one for each adjustment value. + + Any existing guide elements are overwritten. + """ + guides = [(adj.name, adj.val) for adj in self._adjustments_] + self._prstGeom.rewrite_guides(guides) + + @staticmethod + def _update_adjustments_with_actuals( + adjustments: Iterable[Adjustment], guides: Iterable[CT_GeomGuide] + ): + """Update |Adjustment| instances in `adjustments` with actual values held in `guides`. + + `guides` is a list of `a:gd` elements. Guides with a name that does not match an adjustment + object are skipped. + """ + adjustments_by_name = dict((adj.name, adj) for adj in adjustments) + for gd in guides: + name = gd.name + actual = int(gd.fmla[4:]) + try: + adjustment = adjustments_by_name[name] + except KeyError: + continue + adjustment.actual = actual + return + + @property + def _adjustments(self) -> tuple[Adjustment, ...]: + """Sequence of |Adjustment| objects contained in collection.""" + return tuple(self._adjustments_) + + def __len__(self): + """Implement built-in function len()""" + return len(self._adjustments_) + + +class AutoShapeType: + """Provides access to metadata for an auto-shape of type identified by `autoshape_type_id`. + + Instances are cached, so no more than one instance for a particular auto shape type is in + memory. + + Instances provide the following attributes: + + .. attribute:: autoshape_type_id + + Integer uniquely identifying this auto shape type. Corresponds to a + value in `pptx.constants.MSO` like `MSO_SHAPE.ROUNDED_RECTANGLE`. + + .. attribute:: basename + + Base part of shape name for auto shapes of this type, e.g. `Rounded + Rectangle` becomes `Rounded Rectangle 99` when the distinguishing + integer is added to the shape name. + + .. attribute:: prst + + String identifier for this auto shape type used in the `a:prstGeom` + element. + + """ + + _instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {} + + def __new__(cls, autoshape_type_id: MSO_AUTO_SHAPE_TYPE) -> AutoShapeType: + """Only create new instance on first call for content_type. + + After that, use cached instance. + """ + # -- if there's not a matching instance in the cache, create one -- + if autoshape_type_id not in cls._instances: + inst = super(AutoShapeType, cls).__new__(cls) + cls._instances[autoshape_type_id] = inst + # -- return the instance; note that __init__() gets called either way -- + return cls._instances[autoshape_type_id] + + def __init__(self, autoshape_type_id: MSO_AUTO_SHAPE_TYPE): + """Initialize attributes from constant values in `pptx.spec`.""" + # -- skip loading if this instance is from the cache -- + if hasattr(self, "_loaded"): + return + # -- raise on bad autoshape_type_id -- + if autoshape_type_id not in autoshape_types: + raise KeyError( + "no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id + ) + # -- otherwise initialize new instance -- + autoshape_type = autoshape_types[autoshape_type_id] + self._autoshape_type_id = autoshape_type_id + self._basename = autoshape_type["basename"] + self._loaded = True + + @property + def autoshape_type_id(self) -> MSO_AUTO_SHAPE_TYPE: + """MSO_AUTO_SHAPE_TYPE enumeration member identifying this auto shape type.""" + return self._autoshape_type_id + + @property + def basename(self) -> str: + """Base of shape name for this auto shape type. + + A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for example at + `p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less the distinguishing + integer. This value is escaped because at least one autoshape-type name includes double + quotes ('"No" Symbol'). + """ + return saxutils.escape(self._basename, {'"': """}) + + @classmethod + def default_adjustment_values(cls, prst: MSO_AUTO_SHAPE_TYPE) -> tuple[AdjustmentValue, ...]: + """Sequence of (name, value) pair adjustment value defaults for `prst` autoshape-type.""" + return autoshape_types[prst]["avLst"] + + @classmethod + def id_from_prst(cls, prst: str) -> MSO_AUTO_SHAPE_TYPE: + """Select auto shape type with matching `prst`. + + e.g. `MSO_SHAPE.RECTANGLE` corresponding to preset geometry keyword `"rect"`. + """ + return MSO_AUTO_SHAPE_TYPE.from_xml(prst) + + @property + def prst(self): + """ + Preset geometry identifier string for this auto shape. Used in the + `prst` attribute of `a:prstGeom` element to specify the geometry + to be used in rendering the shape, for example `'roundRect'`. + """ + return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id) + + +class Shape(BaseShape): + """A shape that can appear on a slide. + + Corresponds to the `p:sp` element that can appear in any of the slide-type parts + (slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster). + """ + + def __init__(self, sp: CT_Shape, parent: ProvidesPart): + super(Shape, self).__init__(sp, parent) + self._sp = sp + + @lazyproperty + def adjustments(self) -> AdjustmentCollection: + """Read-only reference to |AdjustmentCollection| instance for this shape.""" + return AdjustmentCollection(self._sp.prstGeom) + + @property + def auto_shape_type(self): + """Enumeration value identifying the type of this auto shape. + + Like `MSO_SHAPE.ROUNDED_RECTANGLE`. Raises |ValueError| if this shape is not an auto shape. + """ + if not self._sp.is_autoshape: + raise ValueError("shape is not an auto shape") + return self._sp.prst + + @lazyproperty + def fill(self): + """|FillFormat| instance for this shape. + + Provides access to fill properties such as fill color. + """ + return FillFormat.from_fill_parent(self._sp.spPr) + + def get_or_add_ln(self): + """Return the `a:ln` element containing the line format properties XML for this shape.""" + return self._sp.get_or_add_ln() + + @property + def has_text_frame(self) -> bool: + """|True| if this shape can contain text. Always |True| for an AutoShape.""" + return True + + @lazyproperty + def line(self): + """|LineFormat| instance for this shape. + + Provides access to line properties such as line color. + """ + return LineFormat(self) + + @property + def ln(self): + """The `a:ln` element containing the line format properties such as line color and width. + + |None| if no `a:ln` element is present. + """ + return self._sp.ln + + @property + def shape_type(self) -> MSO_SHAPE_TYPE: + """Unique integer identifying the type of this shape, like `MSO_SHAPE_TYPE.TEXT_BOX`.""" + if self.is_placeholder: + return MSO_SHAPE_TYPE.PLACEHOLDER + if self._sp.has_custom_geometry: + return MSO_SHAPE_TYPE.FREEFORM + if self._sp.is_autoshape: + return MSO_SHAPE_TYPE.AUTO_SHAPE + if self._sp.is_textbox: + return MSO_SHAPE_TYPE.TEXT_BOX + raise NotImplementedError("Shape instance of unrecognized shape type") + + @property + def text(self) -> str: + """Read/write. Text in shape as a single string. + + The returned string will contain a newline character (`"\\n"`) separating each paragraph + and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the + shape's text. + + Assignment to `text` replaces any text previously contained in the shape, along with any + paragraph or font formatting applied to it. A newline character (`"\\n"`) in the assigned + text causes a new paragraph to be started. A vertical-tab (`"\\v"`) character in the + assigned text causes a line-break (soft carriage-return) to be inserted. (The vertical-tab + character appears in clipboard text copied from PowerPoint as its str encoding of + line-breaks.) + """ + return self.text_frame.text + + @text.setter + def text(self, text: str): + self.text_frame.text = text + + @property + def text_frame(self): + """|TextFrame| instance for this shape. + + Contains the text of the shape and provides access to text formatting properties. + """ + txBody = self._sp.get_or_add_txBody() + return TextFrame(txBody, self) diff --git a/src/pptx/shapes/base.py b/src/pptx/shapes/base.py new file mode 100644 index 000000000..751235023 --- /dev/null +++ b/src/pptx/shapes/base.py @@ -0,0 +1,244 @@ +"""Base shape-related objects such as BaseShape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pptx.action import ActionSetting +from pptx.dml.effect import ShadowFormat +from pptx.shared import ElementProxy +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER + from pptx.oxml.shapes import ShapeElement + from pptx.oxml.shapes.shared import CT_Placeholder + from pptx.parts.slide import BaseSlidePart + from pptx.types import ProvidesPart + from pptx.util import Length + + +class BaseShape(object): + """Base class for shape objects. + + Subclasses include |Shape|, |Picture|, and |GraphicFrame|. + """ + + def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart): + super().__init__() + self._element = shape_elm + self._parent = parent + + def __eq__(self, other: object) -> bool: + """|True| if this shape object proxies the same element as *other*. + + 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, BaseShape): + return False + return self._element is other._element + + def __ne__(self, other: object) -> bool: + if not isinstance(other, BaseShape): + return True + return self._element is not other._element + + @lazyproperty + def click_action(self) -> ActionSetting: + """|ActionSetting| instance providing access to click behaviors. + + Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page) + or to another slide in the presentation. The click action is that defined on the overall + shape, not a run of text within the shape. An |ActionSetting| object is always returned, + even when no click behavior is defined on the shape. + """ + cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage] + return ActionSetting(cNvPr, self) + + @property + def element(self) -> ShapeElement: + """`lxml` element for this shape, e.g. a CT_Shape instance. + + Note that manipulating this element improperly can produce an invalid presentation file. + Make sure you know what you're doing if you use this to change the underlying XML. + """ + return self._element + + @property + def has_chart(self) -> bool: + """|True| if this shape is a graphic frame containing a chart object. + + |False| otherwise. When |True|, the chart object can be accessed using the ``.chart`` + property. + """ + # This implementation is unconditionally False, the True version is + # on GraphicFrame subclass. + return False + + @property + def has_table(self) -> bool: + """|True| if this shape is a graphic frame containing a table object. + + |False| otherwise. When |True|, the table object can be accessed using the ``.table`` + property. + """ + # This implementation is unconditionally False, the True version is + # on GraphicFrame subclass. + return False + + @property + def has_text_frame(self) -> bool: + """|True| if this shape can contain text.""" + # overridden on Shape to return True. Only has text frame + return False + + @property + def height(self) -> Length: + """Read/write. Integer distance between top and bottom extents of shape in EMUs.""" + return self._element.cy + + @height.setter + def height(self, value: Length): + self._element.cy = value + + @property + def is_placeholder(self) -> bool: + """True if this shape is a placeholder. + + A shape is a placeholder if it has a element. + """ + return self._element.has_ph_elm + + @property + def left(self) -> Length: + """Integer distance of the left edge of this shape from the left edge of the slide. + + Read/write. Expressed in English Metric Units (EMU) + """ + return self._element.x + + @left.setter + def left(self, value: Length): + self._element.x = value + + @property + def name(self) -> str: + """Name of this shape, e.g. 'Picture 7'.""" + return self._element.shape_name + + @name.setter + def name(self, value: str): + self._element._nvXxPr.cNvPr.name = value # pyright: ignore[reportPrivateUsage] + + @property + def part(self) -> BaseSlidePart: + """The package part containing this shape. + + A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if + you are extending the behavior of |pp| API objects. + """ + return cast("BaseSlidePart", self._parent.part) + + @property + def placeholder_format(self) -> _PlaceholderFormat: + """Provides access to placeholder-specific properties such as placeholder type. + + Raises |ValueError| on access if the shape is not a placeholder. + """ + ph = self._element.ph + if ph is None: + raise ValueError("shape is not a placeholder") + return _PlaceholderFormat(ph) + + @property + def rotation(self) -> float: + """Degrees of clockwise rotation. + + Read/write float. Negative values can be assigned to indicate counter-clockwise rotation, + e.g. assigning -45.0 will change setting to 315.0. + """ + return self._element.rot + + @rotation.setter + def rotation(self, value: float): + self._element.rot = value + + @lazyproperty + def shadow(self) -> ShadowFormat: + """|ShadowFormat| object providing access to shadow for this shape. + + A |ShadowFormat| object is always returned, even when no shadow is + explicitly defined on this shape (i.e. it inherits its shadow + behavior). + """ + return ShadowFormat(self._element.spPr) + + @property + def shape_id(self) -> int: + """Read-only positive integer identifying this shape. + + The id of a shape is unique among all shapes on a slide. + """ + return self._element.shape_id + + @property + def shape_type(self) -> MSO_SHAPE_TYPE: + """A member of MSO_SHAPE_TYPE classifying this shape by type. + + Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses. + """ + raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`") + + @property + def top(self) -> Length: + """Distance from the top edge of the slide to the top edge of this shape. + + Read/write. Expressed in English Metric Units (EMU) + """ + return self._element.y + + @top.setter + def top(self, value: Length): + self._element.y = value + + @property + def width(self) -> Length: + """Distance between left and right extents of this shape. + + Read/write. Expressed in English Metric Units (EMU). + """ + return self._element.cx + + @width.setter + def width(self, value: Length): + self._element.cx = value + + +class _PlaceholderFormat(ElementProxy): + """Provides properties specific to placeholders, such as the placeholder type. + + Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape, + """ + + def __init__(self, element: CT_Placeholder): + super().__init__(element) + self._ph = element + + @property + def element(self) -> CT_Placeholder: + """The `p:ph` element proxied by this object.""" + return self._ph + + @property + def idx(self) -> int: + """Integer placeholder 'idx' attribute.""" + return self._ph.idx + + @property + def type(self) -> PP_PLACEHOLDER: + """Placeholder type. + + A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART + """ + return self._ph.type diff --git a/pptx/shapes/connector.py b/src/pptx/shapes/connector.py similarity index 98% rename from pptx/shapes/connector.py rename to src/pptx/shapes/connector.py index ecd8ec9a9..070b080d5 100644 --- a/pptx/shapes/connector.py +++ b/src/pptx/shapes/connector.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - """Connector (line) shape and related objects. A connector is a line shape having end-points that can be connected to other @@ -7,7 +5,7 @@ elbows, or can be curved. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.dml.line import LineFormat from pptx.enum.shapes import MSO_SHAPE_TYPE diff --git a/src/pptx/shapes/freeform.py b/src/pptx/shapes/freeform.py new file mode 100644 index 000000000..afe87385e --- /dev/null +++ b/src/pptx/shapes/freeform.py @@ -0,0 +1,337 @@ +"""Objects related to construction of freeform shapes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable, Iterator, Sequence + +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from pptx.oxml.shapes.autoshape import ( + CT_Path2D, + CT_Path2DClose, + CT_Path2DLineTo, + CT_Path2DMoveTo, + CT_Shape, + ) + from pptx.shapes.shapetree import _BaseGroupShapes # pyright: ignore[reportPrivateUsage] + from pptx.util import Length + +CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo" +DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close" + + +class FreeformBuilder(Sequence[DrawingOperation]): + """Allows a freeform shape to be specified and created. + + The initial pen position is provided on construction. From there, drawing proceeds using + successive calls to draw line segments. The freeform shape may be closed by calling the + :meth:`close` method. + + A shape may have more than one contour, in which case overlapping areas are "subtracted". A + contour is a sequence of line segments beginning with a "move-to" operation. A move-to + operation is automatically inserted in each new freeform; additional move-to ops can be + inserted with the `.move_to()` method. + """ + + def __init__( + self, + shapes: _BaseGroupShapes, + start_x: Length, + start_y: Length, + x_scale: float, + y_scale: float, + ): + super(FreeformBuilder, self).__init__() + self._shapes = shapes + self._start_x = start_x + self._start_y = start_y + self._x_scale = x_scale + self._y_scale = y_scale + + def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride] + self, idx: int + ) -> DrawingOperation: + return self._drawing_operations.__getitem__(idx) + + def __iter__(self) -> Iterator[DrawingOperation]: + return self._drawing_operations.__iter__() + + def __len__(self): + return self._drawing_operations.__len__() + + @classmethod + def new( + cls, + shapes: _BaseGroupShapes, + start_x: float, + start_y: float, + x_scale: float, + y_scale: float, + ): + """Return a new |FreeformBuilder| object. + + The initial pen location is specified (in local coordinates) by + (`start_x`, `start_y`). + """ + return cls(shapes, Emu(int(round(start_x))), Emu(int(round(start_y))), x_scale, y_scale) + + def add_line_segments(self, vertices: Iterable[tuple[float, float]], close: bool = True): + """Add a straight line segment to each point in `vertices`. + + `vertices` must be an iterable of (x, y) pairs (2-tuples). Each x and y value is rounded + to the nearest integer before use. The optional `close` parameter determines whether the + resulting contour is `closed` or left `open`. + + Returns this |FreeformBuilder| object so it can be used in chained calls. + """ + for x, y in vertices: + self._add_line_segment(x, y) + if close: + self._add_close() + return self + + def convert_to_shape(self, origin_x: Length = Emu(0), origin_y: Length = Emu(0)): + """Return new freeform shape positioned relative to specified offset. + + `origin_x` and `origin_y` locate the origin of the local coordinate system in slide + coordinates (EMU), perhaps most conveniently by use of a |Length| object. + + Note that this method may be called more than once to add multiple shapes of the same + geometry in different locations on the slide. + """ + sp = self._add_freeform_sp(origin_x, origin_y) + path = self._start_path(sp) + for drawing_operation in self: + drawing_operation.apply_operation_to(path) + return self._shapes._shape_factory(sp) # pyright: ignore[reportPrivateUsage] + + def move_to(self, x: float, y: float): + """Move pen to (x, y) (local coordinates) without drawing line. + + Returns this |FreeformBuilder| object so it can be used in chained calls. + """ + self._drawing_operations.append(_MoveTo.new(self, x, y)) + return self + + @property + def shape_offset_x(self) -> Length: + """Return x distance of shape origin from local coordinate origin. + + The returned integer represents the leftmost extent of the freeform shape, in local + coordinates. Note that the bounding box of the shape need not start at the local origin. + """ + min_x = self._start_x + for drawing_operation in self: + if isinstance(drawing_operation, _Close): + continue + min_x = min(min_x, drawing_operation.x) + return Emu(min_x) + + @property + def shape_offset_y(self) -> Length: + """Return y distance of shape origin from local coordinate origin. + + The returned integer represents the topmost extent of the freeform shape, in local + coordinates. Note that the bounding box of the shape need not start at the local origin. + """ + min_y = self._start_y + for drawing_operation in self: + if isinstance(drawing_operation, _Close): + continue + min_y = min(min_y, drawing_operation.y) + return Emu(min_y) + + def _add_close(self): + """Add a close |_Close| operation to the drawing sequence.""" + self._drawing_operations.append(_Close.new()) + + def _add_freeform_sp(self, origin_x: Length, origin_y: Length): + """Add a freeform `p:sp` element having no drawing elements. + + `origin_x` and `origin_y` are specified in slide coordinates, and represent the location + of the local coordinates origin on the slide. + """ + spTree = self._shapes._spTree # pyright: ignore[reportPrivateUsage] + return spTree.add_freeform_sp( + origin_x + self._left, origin_y + self._top, self._width, self._height + ) + + def _add_line_segment(self, x: float, y: float) -> None: + """Add a |_LineSegment| operation to the drawing sequence.""" + self._drawing_operations.append(_LineSegment.new(self, x, y)) + + @lazyproperty + def _drawing_operations(self) -> list[DrawingOperation]: + """Return the sequence of drawing operation objects for freeform.""" + return [] + + @property + def _dx(self) -> Length: + """Return width of this shape's path in local units.""" + min_x = max_x = self._start_x + for drawing_operation in self: + if isinstance(drawing_operation, _Close): + continue + min_x = min(min_x, drawing_operation.x) + max_x = max(max_x, drawing_operation.x) + return Emu(max_x - min_x) + + @property + def _dy(self) -> Length: + """Return integer height of this shape's path in local units.""" + min_y = max_y = self._start_y + for drawing_operation in self: + if isinstance(drawing_operation, _Close): + continue + min_y = min(min_y, drawing_operation.y) + max_y = max(max_y, drawing_operation.y) + return Emu(max_y - min_y) + + @property + def _height(self): + """Return vertical size of this shape's path in slide coordinates. + + This value is based on the actual extents of the shape and does not include any + positioning offset. + """ + return int(round(self._dy * self._y_scale)) + + @property + def _left(self): + """Return leftmost extent of this shape's path in slide coordinates. + + Note that this value does not include any positioning offset; it assumes the drawing + (local) coordinate origin is at (0, 0) on the slide. + """ + return int(round(self.shape_offset_x * self._x_scale)) + + def _local_to_shape(self, local_x: Length, local_y: Length) -> tuple[Length, Length]: + """Translate local coordinates point to shape coordinates. + + Shape coordinates have the same unit as local coordinates, but are offset such that the + origin of the shape coordinate system (0, 0) is located at the top-left corner of the + shape bounding box. + """ + return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y) + + def _start_path(self, sp: CT_Shape) -> CT_Path2D: + """Return a newly created `a:path` element added to `sp`. + + The returned `a:path` element has an `a:moveTo` element representing the shape starting + point as its only child. + """ + path = sp.add_path(w=self._dx, h=self._dy) + path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y)) + return path + + @property + def _top(self): + """Return topmost extent of this shape's path in slide coordinates. + + Note that this value does not include any positioning offset; it assumes the drawing + (local) coordinate origin is located at slide coordinates (0, 0) (top-left corner of + slide). + """ + return int(round(self.shape_offset_y * self._y_scale)) + + @property + def _width(self): + """Return width of this shape's path in slide coordinates. + + This value is based on the actual extents of the shape path and does not include any + positioning offset. + """ + return int(round(self._dx * self._x_scale)) + + +class _BaseDrawingOperation(object): + """Base class for freeform drawing operations. + + A drawing operation has at least one location (x, y) in local coordinates. + """ + + def __init__(self, freeform_builder: FreeformBuilder, x: Length, y: Length): + super(_BaseDrawingOperation, self).__init__() + self._freeform_builder = freeform_builder + self._x = x + self._y = y + + def apply_operation_to(self, path: CT_Path2D) -> CT_DrawingOperation: + """Add the XML element(s) implementing this operation to `path`. + + Must be implemented by each subclass. + """ + raise NotImplementedError("must be implemented by each subclass") + + @property + def x(self) -> Length: + """Return the horizontal (x) target location of this operation. + + The returned value is an integer in local coordinates. + """ + return self._x + + @property + def y(self) -> Length: + """Return the vertical (y) target location of this operation. + + The returned value is an integer in local coordinates. + """ + return self._y + + +class _Close(object): + """Specifies adding a `` element to the current contour.""" + + @classmethod + def new(cls) -> _Close: + """Return a new _Close object.""" + return cls() + + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose: + """Add `a:close` element to `path`.""" + return path.add_close() + + +class _LineSegment(_BaseDrawingOperation): + """Specifies a straight line segment ending at the specified point.""" + + @classmethod + def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _LineSegment: + """Return a new _LineSegment object ending at point *(x, y)*. + + Both `x` and `y` are rounded to the nearest integer before use. + """ + return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y)))) + + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DLineTo: + """Add `a:lnTo` element to `path` for this line segment. + + Returns the `a:lnTo` element newly added to the path. + """ + return path.add_lnTo( + Emu(self._x - self._freeform_builder.shape_offset_x), + Emu(self._y - self._freeform_builder.shape_offset_y), + ) + + +class _MoveTo(_BaseDrawingOperation): + """Specifies a new pen position.""" + + @classmethod + def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _MoveTo: + """Return a new _MoveTo object for move to point `(x, y)`. + + Both `x` and `y` are rounded to the nearest integer before use. + """ + return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y)))) + + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo: + """Add `a:moveTo` element to `path` for this line segment.""" + return path.add_moveTo( + Emu(self._x - self._freeform_builder.shape_offset_x), + Emu(self._y - self._freeform_builder.shape_offset_y), + ) diff --git a/pptx/shapes/graphfrm.py b/src/pptx/shapes/graphfrm.py similarity index 51% rename from pptx/shapes/graphfrm.py rename to src/pptx/shapes/graphfrm.py index f7f817a1e..c0ed2bbab 100644 --- a/pptx/shapes/graphfrm.py +++ b/src/pptx/shapes/graphfrm.py @@ -1,11 +1,13 @@ -# encoding: utf-8 - """Graphic Frame shape and related objects. A graphic frame is a common container for table, chart, smart art, and media objects. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.shapes.base import BaseShape from pptx.shared import ParentedElementProxy @@ -15,16 +17,29 @@ GRAPHIC_DATA_URI_TABLE, ) from pptx.table import Table +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.chart.chart import Chart + from pptx.dml.effect import ShadowFormat + from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectData, CT_GraphicalObjectFrame + from pptx.parts.chart import ChartPart + from pptx.parts.slide import BaseSlidePart + from pptx.types import ProvidesPart class GraphicFrame(BaseShape): """Container shape for table, chart, smart art, and media objects. - Corresponds to a ```` element in the shape tree. + Corresponds to a `p:graphicFrame` element in the shape tree. """ + def __init__(self, graphicFrame: CT_GraphicalObjectFrame, parent: ProvidesPart): + super().__init__(graphicFrame, parent) + self._graphicFrame = graphicFrame + @property - def chart(self): + def chart(self) -> Chart: """The |Chart| object containing the chart in this graphic frame. Raises |ValueError| if this graphic frame does not contain a chart. @@ -34,61 +49,62 @@ def chart(self): return self.chart_part.chart @property - def chart_part(self): + def chart_part(self) -> ChartPart: """The |ChartPart| object containing the chart in this graphic frame.""" - return self.part.related_parts[self._element.chart_rId] + chart_rId = self._graphicFrame.chart_rId + if chart_rId is None: + raise ValueError("this graphic frame does not contain a chart") + return cast("ChartPart", self.part.related_part(chart_rId)) @property - def has_chart(self): + def has_chart(self) -> bool: """|True| if this graphic frame contains a chart object. |False| otherwise. - When |True|, the chart object can be accessed using the ``.chart`` property. + When |True|, the chart object can be accessed using the `.chart` property. """ - return self._element.graphicData_uri == GRAPHIC_DATA_URI_CHART + return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_CHART @property - def has_table(self): + def has_table(self) -> bool: """|True| if this graphic frame contains a table object, |False| otherwise. When |True|, the table object can be accessed using the `.table` property. """ - return self._element.graphicData_uri == GRAPHIC_DATA_URI_TABLE + return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_TABLE @property - def ole_format(self): - """Optional _OleFormat object for this graphic-frame shape. + def ole_format(self) -> _OleFormat: + """_OleFormat object for this graphic-frame shape. - Raises `ValueError` on a GraphicFrame instance that does not contain an OLE - object. + Raises `ValueError` on a GraphicFrame instance that does not contain an OLE object. An shape that contains an OLE object will have `.shape_type` of either `EMBEDDED_OLE_OBJECT` or `LINKED_OLE_OBJECT`. """ - if not self._element.has_oleobj: + if not self._graphicFrame.has_oleobj: raise ValueError("not an OLE-object shape") - return _OleFormat(self._element.graphicData, self._parent) + return _OleFormat(self._graphicFrame.graphicData, self._parent) - @property - def shadow(self): + @lazyproperty + def shadow(self) -> ShadowFormat: """Unconditionally raises |NotImplementedError|. - Access to the shadow effect for graphic-frame objects is - content-specific (i.e. different for charts, tables, etc.) and has - not yet been implemented. + Access to the shadow effect for graphic-frame objects is content-specific (i.e. different + for charts, tables, etc.) and has not yet been implemented. """ raise NotImplementedError("shadow property on GraphicFrame not yet supported") @property - def shape_type(self): + def shape_type(self) -> MSO_SHAPE_TYPE: """Optional member of `MSO_SHAPE_TYPE` identifying the type of this shape. - Possible values are ``MSO_SHAPE_TYPE.CHART``, ``MSO_SHAPE_TYPE.TABLE``, - ``MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT``, ``MSO_SHAPE_TYPE.LINKED_OLE_OBJECT``. + Possible values are `MSO_SHAPE_TYPE.CHART`, `MSO_SHAPE_TYPE.TABLE`, + `MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT`, `MSO_SHAPE_TYPE.LINKED_OLE_OBJECT`. - This value is `None` when none of these four types apply, for example when the - shape contains SmartArt. + This value is `None` when none of these four types apply, for example when the shape + contains SmartArt. """ - graphicData_uri = self._element.graphicData_uri + graphicData_uri = self._graphicFrame.graphicData_uri if graphicData_uri == GRAPHIC_DATA_URI_CHART: return MSO_SHAPE_TYPE.CHART elif graphicData_uri == GRAPHIC_DATA_URI_TABLE: @@ -96,50 +112,55 @@ def shape_type(self): elif graphicData_uri == GRAPHIC_DATA_URI_OLEOBJ: return ( MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT - if self._element.is_embedded_ole_obj + if self._graphicFrame.is_embedded_ole_obj else MSO_SHAPE_TYPE.LINKED_OLE_OBJECT ) else: - return None + return None # pyright: ignore[reportReturnType] @property - def table(self): - """ - The |Table| object contained in this graphic frame. Raises - |ValueError| if this graphic frame does not contain a table. + def table(self) -> Table: + """The |Table| object contained in this graphic frame. + + Raises |ValueError| if this graphic frame does not contain a table. """ if not self.has_table: raise ValueError("shape does not contain a table") - tbl = self._element.graphic.graphicData.tbl + tbl = self._graphicFrame.graphic.graphicData.tbl return Table(tbl, self) class _OleFormat(ParentedElementProxy): """Provides attributes on an embedded OLE object.""" - def __init__(self, graphicData, parent): - super(_OleFormat, self).__init__(graphicData, parent) + part: BaseSlidePart # pyright: ignore[reportIncompatibleMethodOverride] + + def __init__(self, graphicData: CT_GraphicalObjectData, parent: ProvidesPart): + super().__init__(graphicData, parent) self._graphicData = graphicData @property - def blob(self): + def blob(self) -> bytes | None: """Optional bytes of OLE object, suitable for loading or saving as a file. - This value is None if the embedded object does not represent a "file". + This value is `None` if the embedded object does not represent a "file". """ - return self.part.related_parts[self._graphicData.blob_rId].blob + blob_rId = self._graphicData.blob_rId + if blob_rId is None: + return None + return self.part.related_part(blob_rId).blob @property - def prog_id(self): + def prog_id(self) -> str | None: """str "progId" attribute of this embedded OLE object. - The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the - embedded object, or perhaps more precisely, the application (aka. "server" in - OLE parlance) to be used to open this object. + The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the embedded + object, or perhaps more precisely, the application (aka. "server" in OLE parlance) to be + used to open this object. """ return self._graphicData.progId @property - def show_as_icon(self): + def show_as_icon(self) -> bool | None: """True when OLE object should appear as an icon (rather than preview).""" return self._graphicData.showAsIcon diff --git a/pptx/shapes/group.py b/src/pptx/shapes/group.py similarity index 56% rename from pptx/shapes/group.py rename to src/pptx/shapes/group.py index 0de06f853..717375851 100644 --- a/pptx/shapes/group.py +++ b/src/pptx/shapes/group.py @@ -1,20 +1,30 @@ -# encoding: utf-8 - """GroupShape and related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING from pptx.dml.effect import ShadowFormat from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.shapes.base import BaseShape from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.action import ActionSetting + from pptx.oxml.shapes.groupshape import CT_GroupShape + from pptx.shapes.shapetree import GroupShapes + from pptx.types import ProvidesPart + class GroupShape(BaseShape): """A shape that acts as a container for other shapes.""" - @property - def click_action(self): + def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart): + super().__init__(grpSp, parent) + self._grpSp = grpSp + + @lazyproperty + def click_action(self) -> ActionSetting: """Unconditionally raises `TypeError`. A group shape cannot have a click action or hover action. @@ -22,27 +32,25 @@ def click_action(self): raise TypeError("a group shape cannot have a click action") @property - def has_text_frame(self): + def has_text_frame(self) -> bool: """Unconditionally |False|. - A group shape does not have a textframe and cannot itself contain - text. This does not impact the ability of shapes contained by the - group to each have their own text. + A group shape does not have a textframe and cannot itself contain text. This does not + impact the ability of shapes contained by the group to each have their own text. """ return False @lazyproperty - def shadow(self): + def shadow(self) -> ShadowFormat: """|ShadowFormat| object representing shadow effect for this group. - A |ShadowFormat| object is always returned, even when no shadow is - explicitly defined on this group shape (i.e. when the group inherits - its shadow behavior). + A |ShadowFormat| object is always returned, even when no shadow is explicitly defined on + this group shape (i.e. when the group inherits its shadow behavior). """ - return ShadowFormat(self._element.grpSpPr) + return ShadowFormat(self._grpSp.grpSpPr) @property - def shape_type(self): + def shape_type(self) -> MSO_SHAPE_TYPE: """Member of :ref:`MsoShapeType` identifying the type of this shape. Unconditionally `MSO_SHAPE_TYPE.GROUP` in this case @@ -50,11 +58,11 @@ def shape_type(self): return MSO_SHAPE_TYPE.GROUP @lazyproperty - def shapes(self): + def shapes(self) -> GroupShapes: """|GroupShapes| object for this group. - The |GroupShapes| object provides access to the group's member shapes - and provides methods for adding new ones. + The |GroupShapes| object provides access to the group's member shapes and provides methods + for adding new ones. """ from pptx.shapes.shapetree import GroupShapes diff --git a/pptx/shapes/picture.py b/src/pptx/shapes/picture.py similarity index 50% rename from pptx/shapes/picture.py rename to src/pptx/shapes/picture.py index 1cb660e6f..59182860d 100644 --- a/pptx/shapes/picture.py +++ b/src/pptx/shapes/picture.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Shapes based on the `p:pic` element, including Picture and Movie.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING from pptx.dml.line import LineFormat from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE @@ -10,84 +10,87 @@ from pptx.shared import ParentedElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.shapes.picture import CT_Picture + from pptx.oxml.shapes.shared import CT_LineProperties + from pptx.types import ProvidesPart + class _BasePicture(BaseShape): """Base class for shapes based on a `p:pic` element.""" - def __init__(self, pic, parent): + def __init__(self, pic: CT_Picture, parent: ProvidesPart): super(_BasePicture, self).__init__(pic, parent) self._pic = pic @property - def crop_bottom(self): + def crop_bottom(self) -> float: """|float| representing relative portion cropped from shape bottom. - Read/write. 1.0 represents 100%. For example, 25% is represented by - 0.25. Negative values are valid as are values greater than 1.0. + Read/write. 1.0 represents 100%. For example, 25% is represented by 0.25. Negative values + are valid as are values greater than 1.0. """ - return self._element.srcRect_b + return self._pic.srcRect_b @crop_bottom.setter - def crop_bottom(self, value): - self._element.srcRect_b = value + def crop_bottom(self, value: float): + self._pic.srcRect_b = value @property - def crop_left(self): + def crop_left(self) -> float: """|float| representing relative portion cropped from left of shape. - Read/write. 1.0 represents 100%. A negative value extends the side - beyond the image boundary. + Read/write. 1.0 represents 100%. A negative value extends the side beyond the image + boundary. """ - return self._element.srcRect_l + return self._pic.srcRect_l @crop_left.setter - def crop_left(self, value): - self._element.srcRect_l = value + def crop_left(self, value: float): + self._pic.srcRect_l = value @property - def crop_right(self): + def crop_right(self) -> float: """|float| representing relative portion cropped from right of shape. Read/write. 1.0 represents 100%. """ - return self._element.srcRect_r + return self._pic.srcRect_r @crop_right.setter - def crop_right(self, value): - self._element.srcRect_r = value + def crop_right(self, value: float): + self._pic.srcRect_r = value @property - def crop_top(self): + def crop_top(self) -> float: """|float| representing relative portion cropped from shape top. Read/write. 1.0 represents 100%. """ - return self._element.srcRect_t + return self._pic.srcRect_t @crop_top.setter - def crop_top(self, value): - self._element.srcRect_t = value + def crop_top(self, value: float): + self._pic.srcRect_t = value def get_or_add_ln(self): - """ - Return the `a:ln` element containing the line format properties XML - for this `p:pic`-based shape. + """Return the `a:ln` element for this `p:pic`-based image. + + The `a:ln` element contains the line format properties XML. """ return self._pic.get_or_add_ln() @lazyproperty - def line(self): - """ - An instance of |LineFormat|, providing access to the properties of - the outline bordering this shape, such as its color and width. - """ + def line(self) -> LineFormat: + """Provides access to properties of the picture outline, such as its color and width.""" return LineFormat(self) @property - def ln(self): - """ - The ```` element containing the line format properties such as - line color and width. |None| if no ```` element is present. + def ln(self) -> CT_LineProperties | None: + """The `a:ln` element for this `p:pic`. + + Contains the line format properties such as line color and width. |None| if no `a:ln` + element is present. """ return self._pic.ln @@ -95,26 +98,23 @@ def ln(self): class Movie(_BasePicture): """A movie shape, one that places a video on a slide. - Like |Picture|, a movie shape is based on the `p:pic` element. A movie is - composed of a video and a *poster frame*, the placeholder image that - represents the video before it is played. + Like |Picture|, a movie shape is based on the `p:pic` element. A movie is composed of a video + and a *poster frame*, the placeholder image that represents the video before it is played. """ @lazyproperty - def media_format(self): + def media_format(self) -> _MediaFormat: """The |_MediaFormat| object for this movie. - The |_MediaFormat| object provides access to formatting properties - for the movie. + The |_MediaFormat| object provides access to formatting properties for the movie. """ - return _MediaFormat(self._element, self) + return _MediaFormat(self._pic, self) @property - def media_type(self): + def media_type(self) -> PP_MEDIA_TYPE: """Member of :ref:`PpMediaType` describing this shape. - The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this - case. + The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this case. """ return PP_MEDIA_TYPE.MOVIE @@ -124,16 +124,16 @@ def poster_frame(self): Returns |None| if this movie has no poster frame (uncommon). """ - slide_part, rId = self.part, self._element.blip_rId + slide_part, rId = self.part, self._pic.blip_rId if rId is None: return None return slide_part.get_image(rId) @property - def shape_type(self): + def shape_type(self) -> MSO_SHAPE_TYPE: """Return member of :ref:`MsoShapeType` describing this shape. - The return value is unconditionally ``MSO_SHAPE_TYPE.MEDIA`` in this + The return value is unconditionally `MSO_SHAPE_TYPE.MEDIA` in this case. """ return MSO_SHAPE_TYPE.MEDIA @@ -146,27 +146,22 @@ class Picture(_BasePicture): """ @property - def auto_shape_type(self): + def auto_shape_type(self) -> MSO_SHAPE | None: """Member of MSO_SHAPE indicating masking shape. - A picture can be masked by any of the so-called "auto-shapes" - available in PowerPoint, such as an ellipse or triangle. When - a picture is masked by a shape, the shape assumes the same dimensions - as the picture and the portion of the picture outside the shape - boundaries does not appear. Note the default value for - a newly-inserted picture is `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which - performs no cropping because the extents of the rectangle exactly - correspond to the extents of the picture. - - The available shapes correspond to the members of - :ref:`MsoAutoShapeType`. - - The return value can also be |None|, indicating the picture either - has no geometry (not expected) or has custom geometry, like - a freeform shape. A picture with no geometry will have no visible - representation on the slide, although it can be selected. This is - because without geometry, there is no "inside-the-shape" for it to - appear in. + A picture can be masked by any of the so-called "auto-shapes" available in PowerPoint, + such as an ellipse or triangle. When a picture is masked by a shape, the shape assumes the + same dimensions as the picture and the portion of the picture outside the shape boundaries + does not appear. Note the default value for a newly-inserted picture is + `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which performs no cropping because the extents of the + rectangle exactly correspond to the extents of the picture. + + The available shapes correspond to the members of :ref:`MsoAutoShapeType`. + + The return value can also be |None|, indicating the picture either has no geometry (not + expected) or has custom geometry, like a freeform shape. A picture with no geometry will + have no visible representation on the slide, although it can be selected. This is because + without geometry, there is no "inside-the-shape" for it to appear in. """ prstGeom = self._pic.spPr.prstGeom if prstGeom is None: # ---generally means cropped with freeform--- @@ -174,32 +169,29 @@ def auto_shape_type(self): return prstGeom.prst @auto_shape_type.setter - def auto_shape_type(self, member): + def auto_shape_type(self, member: MSO_SHAPE): MSO_SHAPE.validate(member) spPr = self._pic.spPr prstGeom = spPr.prstGeom if prstGeom is None: - spPr._remove_custGeom() - prstGeom = spPr._add_prstGeom() + spPr._remove_custGeom() # pyright: ignore[reportPrivateUsage] + prstGeom = spPr._add_prstGeom() # pyright: ignore[reportPrivateUsage] prstGeom.prst = member @property def image(self): + """The |Image| object for this picture. + + Provides access to the properties and bytes of the image in this picture shape. """ - An |Image| object providing access to the properties and bytes of the - image in this picture shape. - """ - slide_part, rId = self.part, self._element.blip_rId + slide_part, rId = self.part, self._pic.blip_rId if rId is None: raise ValueError("no embedded image") return slide_part.get_image(rId) @property - def shape_type(self): - """ - Unique integer identifying the type of this shape, unconditionally - ``MSO_SHAPE_TYPE.PICTURE`` in this case. - """ + def shape_type(self) -> MSO_SHAPE_TYPE: + """Unconditionally `MSO_SHAPE_TYPE.PICTURE` in this case.""" return MSO_SHAPE_TYPE.PICTURE diff --git a/pptx/shapes/placeholder.py b/src/pptx/shapes/placeholder.py similarity index 83% rename from pptx/shapes/placeholder.py rename to src/pptx/shapes/placeholder.py index 8f1bd8efa..c44837bef 100644 --- a/pptx/shapes/placeholder.py +++ b/src/pptx/shapes/placeholder.py @@ -1,21 +1,24 @@ -# encoding: utf-8 +"""Placeholder-related objects. +Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors +depending on whether it appears on a slide, layout, or master. Hence there is a +non-trivial class inheritance structure. """ -Placeholder-related objects, specific to shapes having a `p:ph` element. -A placeholder has distinct behaviors depending on whether it appears on -a slide, layout, or master. Hence there is a non-trivial class inheritance -structure. -""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER +from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame +from pptx.oxml.shapes.picture import CT_Picture +from pptx.shapes.autoshape import Shape +from pptx.shapes.graphfrm import GraphicFrame +from pptx.shapes.picture import Picture +from pptx.util import Emu -from .autoshape import Shape -from ..enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER -from .graphfrm import GraphicFrame -from ..oxml.shapes.graphfrm import CT_GraphicalObjectFrame -from ..oxml.shapes.picture import CT_Picture -from .picture import Picture -from ..util import Emu +if TYPE_CHECKING: + from pptx.oxml.shapes.autoshape import CT_Shape class _InheritsDimensions(object): @@ -122,9 +125,9 @@ def _inherited_value(self, attr_name): class _BaseSlidePlaceholder(_InheritsDimensions, Shape): - """ - Base class for placeholders on slides. Provides common behaviors such as - inherited dimensions. + """Base class for placeholders on slides. + + Provides common behaviors such as inherited dimensions. """ @property @@ -210,12 +213,13 @@ def sz(self): class LayoutPlaceholder(_InheritsDimensions, Shape): + """Placeholder shape on a slide layout. + + Provides differentiated behavior for slide layout placeholders, in particular, inheriting + shape properties from the master placeholder having the same type, when a matching one exists. """ - Placeholder shape on a slide layout, providing differentiated behavior - for slide layout placeholders, in particular, inheriting shape properties - from the master placeholder having the same type, when a matching one - exists. - """ + + element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride] @property def _base_placeholder(self): @@ -243,9 +247,9 @@ def _base_placeholder(self): class MasterPlaceholder(BasePlaceholder): - """ - Placeholder shape on a slide master. - """ + """Placeholder shape on a slide master.""" + + element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride] class NotesSlidePlaceholder(_InheritsDimensions, Shape): @@ -274,9 +278,7 @@ class SlidePlaceholder(_BaseSlidePlaceholder): class ChartPlaceholder(_BaseSlidePlaceholder): - """ - Placeholder shape that can only accept a chart. - """ + """Placeholder shape that can only accept a chart.""" def insert_chart(self, chart_type, chart_data): """ @@ -303,24 +305,19 @@ def _new_chart_graphicFrame(self, rId, x, y, cx, cy): position and size and containing the chart identified by *rId*. """ id_, name = self.shape_id, self.name - return CT_GraphicalObjectFrame.new_chart_graphicFrame( - id_, name, rId, x, y, cx, cy - ) + return CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy) class PicturePlaceholder(_BaseSlidePlaceholder): - """ - Placeholder shape that can only accept a picture. - """ + """Placeholder shape that can only accept a picture.""" def insert_picture(self, image_file): - """ - Return a |PlaceholderPicture| object depicting the image in - *image_file*, which may be either a path (string) or a file-like - object. The image is cropped to fill the entire space of the - placeholder. A |PlaceholderPicture| object has all the properties and - methods of a |Picture| shape except that the value of its - :attr:`~._BaseSlidePlaceholder.shape_type` property is + """Return a |PlaceholderPicture| object depicting the image in `image_file`. + + `image_file` may be either a path (string) or a file-like object. The image is + cropped to fill the entire space of the placeholder. A |PlaceholderPicture| + object has all the properties and methods of a |Picture| shape except that the + value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`. """ pic = self._new_placeholder_pic(image_file) @@ -379,21 +376,17 @@ def _base_placeholder(self): class TablePlaceholder(_BaseSlidePlaceholder): - """ - Placeholder shape that can only accept a picture. - """ + """Placeholder shape that can only accept a table.""" def insert_table(self, rows, cols): - """ - Return a |PlaceholderGraphicFrame| object containing a table of - *rows* rows and *cols* columns. The position and width of the table - are those of the placeholder and its height is proportional to the - number of rows. A |PlaceholderGraphicFrame| object has all the - properties and methods of a |GraphicFrame| shape except that the - value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is - unconditionally `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return - value is not the new table but rather *contains* the new table. The - table can be accessed using the + """Return |PlaceholderGraphicFrame| object containing a `rows` by `cols` table. + + The position and width of the table are those of the placeholder and its height + is proportional to the number of rows. A |PlaceholderGraphicFrame| object has + all the properties and methods of a |GraphicFrame| shape except that the value + of its :attr:`~._BaseSlidePlaceholder.shape_type` property is unconditionally + `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return value is not the new table + but rather *contains* the new table. The table can be accessed using the :attr:`~.PlaceholderGraphicFrame.table` property of the returned |PlaceholderGraphicFrame| object. """ diff --git a/src/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py new file mode 100644 index 000000000..29623f1f5 --- /dev/null +++ b/src/pptx/shapes/shapetree.py @@ -0,0 +1,1190 @@ +"""The shape tree, the structure that holds a slide's shapes.""" + +from __future__ import annotations + +import io +import os +from typing import IO, TYPE_CHECKING, Callable, Iterable, Iterator, cast + +from pptx.enum.shapes import PP_PLACEHOLDER, PROG_ID +from pptx.media import SPEAKER_IMAGE_BYTES, Video +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.oxml.ns import qn +from pptx.oxml.shapes.autoshape import CT_Shape +from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame +from pptx.oxml.shapes.picture import CT_Picture +from pptx.oxml.simpletypes import ST_Direction +from pptx.shapes.autoshape import AutoShapeType, Shape +from pptx.shapes.base import BaseShape +from pptx.shapes.connector import Connector +from pptx.shapes.freeform import FreeformBuilder +from pptx.shapes.graphfrm import GraphicFrame +from pptx.shapes.group import GroupShape +from pptx.shapes.picture import Movie, Picture +from pptx.shapes.placeholder import ( + ChartPlaceholder, + LayoutPlaceholder, + MasterPlaceholder, + NotesSlidePlaceholder, + PicturePlaceholder, + PlaceholderGraphicFrame, + PlaceholderPicture, + SlidePlaceholder, + TablePlaceholder, +) +from pptx.shared import ParentedElementProxy +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from pptx.chart.chart import Chart + from pptx.chart.data import ChartData + from pptx.enum.chart import XL_CHART_TYPE + from pptx.enum.shapes import MSO_CONNECTOR_TYPE, MSO_SHAPE + from pptx.oxml.shapes import ShapeElement + from pptx.oxml.shapes.connector import CT_Connector + from pptx.oxml.shapes.groupshape import CT_GroupShape + from pptx.parts.image import ImagePart + from pptx.parts.slide import SlidePart + from pptx.slide import Slide, SlideLayout + from pptx.types import ProvidesPart + from pptx.util import Length + +# +-- _BaseShapes +# | | +# | +-- _BaseGroupShapes +# | | | +# | | +-- GroupShapes +# | | | +# | | +-- SlideShapes +# | | +# | +-- LayoutShapes +# | | +# | +-- MasterShapes +# | | +# | +-- NotesSlideShapes +# | | +# | +-- BasePlaceholders +# | | +# | +-- LayoutPlaceholders +# | | +# | +-- MasterPlaceholders +# | | +# | +-- NotesSlidePlaceholders +# | +# +-- SlidePlaceholders + + +class _BaseShapes(ParentedElementProxy): + """Base class for a shape collection appearing in a slide-type object. + + Subclasses include Slide, SlideLayout, and SlideMaster. Provides common methods. + """ + + def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart): + super(_BaseShapes, self).__init__(spTree, parent) + self._spTree = spTree + self._cached_max_shape_id = None + + def __getitem__(self, idx: int) -> BaseShape: + """Return shape at `idx` in sequence, e.g. `shapes[2]`.""" + shape_elms = list(self._iter_member_elms()) + try: + shape_elm = shape_elms[idx] + except IndexError: + raise IndexError("shape index out of range") + return self._shape_factory(shape_elm) + + def __iter__(self) -> Iterator[BaseShape]: + """Generate a reference to each shape in the collection, in sequence.""" + for shape_elm in self._iter_member_elms(): + yield self._shape_factory(shape_elm) + + def __len__(self) -> int: + """Return count of shapes in this shape tree. + + A group shape contributes 1 to the total, without regard to the number of shapes contained + in the group. + """ + shape_elms = list(self._iter_member_elms()) + return len(shape_elms) + + def clone_placeholder(self, placeholder: LayoutPlaceholder) -> None: + """Add a new placeholder shape based on `placeholder`.""" + sp = placeholder.element + ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx) + id_ = self._next_shape_id + name = self._next_ph_name(ph_type, id_, orient) + self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx) + + def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str: + """Return the base name for a placeholder of `ph_type` in this shape collection. + + There is some variance between slide types, for example a notes slide uses a different + name for the body placeholder, so this method can be overriden by subclasses. + """ + return { + PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder", + PP_PLACEHOLDER.BODY: "Text Placeholder", + PP_PLACEHOLDER.CENTER_TITLE: "Title", + PP_PLACEHOLDER.CHART: "Chart Placeholder", + PP_PLACEHOLDER.DATE: "Date Placeholder", + PP_PLACEHOLDER.FOOTER: "Footer Placeholder", + PP_PLACEHOLDER.HEADER: "Header Placeholder", + PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder", + PP_PLACEHOLDER.OBJECT: "Content Placeholder", + PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder", + PP_PLACEHOLDER.PICTURE: "Picture Placeholder", + PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder", + PP_PLACEHOLDER.SUBTITLE: "Subtitle", + PP_PLACEHOLDER.TABLE: "Table Placeholder", + PP_PLACEHOLDER.TITLE: "Title", + }[ph_type] + + @property + def turbo_add_enabled(self) -> bool: + """True if "turbo-add" mode is enabled. Read/Write. + + EXPERIMENTAL: This feature can radically improve performance when adding large numbers + (hundreds of shapes) to a slide. It works by caching the last shape ID used and + incrementing that value to assign the next shape id. This avoids repeatedly searching all + shape ids in the slide each time a new ID is required. + + Performance is not noticeably improved for a slide with a relatively small number of + shapes, but because the search time rises with the square of the shape count, this option + can be useful for optimizing generation of a slide composed of many shapes. + + Shape-id collisions can occur (causing a repair error on load) if more than one |Slide| + object is used to interact with the same slide in the presentation. Note that the |Slides| + collection creates a new |Slide| object each time a slide is accessed (e.g. `slide = + prs.slides[0]`, so you must be careful to limit use to a single |Slide| object. + """ + return self._cached_max_shape_id is not None + + @turbo_add_enabled.setter + def turbo_add_enabled(self, value: bool): + enable = bool(value) + self._cached_max_shape_id = self._spTree.max_shape_id if enable else None + + @staticmethod + def _is_member_elm(shape_elm: ShapeElement) -> bool: + """Return true if `shape_elm` represents a member of this collection, False otherwise.""" + return True + + def _iter_member_elms(self) -> Iterator[ShapeElement]: + """Generate each child of the `p:spTree` element that corresponds to a shape. + + Items appear in XML document order. + """ + for shape_elm in self._spTree.iter_shape_elms(): + if self._is_member_elm(shape_elm): + yield shape_elm + + def _next_ph_name(self, ph_type: PP_PLACEHOLDER, id: int, orient: str) -> str: + """Next unique placeholder name for placeholder shape of type `ph_type`. + + Usually will be standard placeholder root name suffixed with id-1, e.g. + _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> 'Table Placeholder 3'. The number is + incremented as necessary to make the name unique within the collection. If `orient` is + `'vert'`, the placeholder name is prefixed with `'Vertical '`. + """ + basename = self.ph_basename(ph_type) + + # prefix rootname with 'Vertical ' if orient is 'vert' + if orient == ST_Direction.VERT: + basename = "Vertical %s" % basename + + # increment numpart as necessary to make name unique + numpart = id - 1 + names = self._spTree.xpath("//p:cNvPr/@name") + while True: + name = "%s %d" % (basename, numpart) + if name not in names: + break + numpart += 1 + + return name + + @property + def _next_shape_id(self) -> int: + """Return a unique shape id suitable for use with a new shape. + + The returned id is 1 greater than the maximum shape id used so far. In practice, the + minimum id is 2 because the spTree element is always assigned id="1". + """ + # ---presence of cached-max-shape-id indicates turbo mode is on--- + if self._cached_max_shape_id is not None: + self._cached_max_shape_id += 1 + return self._cached_max_shape_id + + return self._spTree.max_shape_id + 1 + + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return BaseShapeFactory(shape_elm, self) + + +class _BaseGroupShapes(_BaseShapes): + """Base class for shape-trees that can add shapes.""" + + part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride] + _element: CT_GroupShape + + def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart): + super(_BaseGroupShapes, self).__init__(grpSp, parent) + self._grpSp = grpSp + + def add_chart( + self, + chart_type: XL_CHART_TYPE, + x: Length, + y: Length, + cx: Length, + cy: Length, + chart_data: ChartData, + ) -> Chart: + """Add a new chart of `chart_type` to the slide. + + The chart is positioned at (`x`, `y`), has size (`cx`, `cy`), and depicts `chart_data`. + `chart_type` is one of the :ref:`XlChartType` enumeration values. `chart_data` is a + |ChartData| object populated with the categories and series values for the chart. + + Note that a |GraphicFrame| shape object is returned, not the |Chart| object contained in + that graphic frame shape. The chart object may be accessed using the :attr:`chart` + property of the returned |GraphicFrame| object. + """ + rId = self.part.add_chart_part(chart_type, chart_data) + graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy) + self._recalculate_extents() + return cast("Chart", self._shape_factory(graphicFrame)) + + def add_connector( + self, + connector_type: MSO_CONNECTOR_TYPE, + begin_x: Length, + begin_y: Length, + end_x: Length, + end_y: Length, + ) -> Connector: + """Add a newly created connector shape to the end of this shape tree. + + `connector_type` is a member of the :ref:`MsoConnectorType` enumeration and the end-point + values are specified as EMU values. The returned connector is of type `connector_type` and + has begin and end points as specified. + """ + cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y) + self._recalculate_extents() + return cast(Connector, self._shape_factory(cxnSp)) + + def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape: + """Return a |GroupShape| object newly appended to this shape tree. + + The group shape is empty and must be populated with shapes using methods on its shape + tree, available on its `.shapes` property. The position and extents of the group shape are + determined by the shapes it contains; its position and extents are recalculated each time + a shape is added to it. + """ + shapes = tuple(shapes) + grpSp = self._element.add_grpSp() + for shape in shapes: + grpSp.insert_element_before( + shape._element, "p:extLst" # pyright: ignore[reportPrivateUsage] + ) + if shapes: + grpSp.recalculate_extents() + return cast(GroupShape, self._shape_factory(grpSp)) + + def add_ole_object( + self, + object_file: str | IO[bytes], + prog_id: str, + left: Length, + top: Length, + width: Length | None = None, + height: Length | None = None, + icon_file: str | IO[bytes] | None = None, + icon_width: Length | None = None, + icon_height: Length | None = None, + ) -> GraphicFrame: + """Return newly-created GraphicFrame shape embedding `object_file`. + + The returned graphic-frame shape contains `object_file` as an embedded OLE object. It is + displayed as an icon at `left`, `top` with size `width`, `height`. `width` and `height` + may be omitted when `prog_id` is a member of `PROG_ID`, in which case the default icon + size is used. This is advised for best appearance where applicable because it avoids an + icon with a "stretched" appearance. + + `object_file` may either be a str path to a file or file-like object (such as + `io.BytesIO`) containing the bytes of the object to be embedded (such as an Excel file). + + `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value like + `"Adobe.Exchange.7"` determined by inspecting the XML generated by PowerPoint for an + object of the desired type. + + `icon_file` may either be a str path to an image file or a file-like object containing the + image. The image provided will be displayed in lieu of the OLE object; double-clicking on + the image opens the object (subject to operating-system limitations). The image file can + be any supported image file. Those produced by PowerPoint itself are generally EMF and can + be harvested from a PPTX package that embeds such an object. PNG and JPG also work fine. + + `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that describe + the size of the icon image within the shape. These should be omitted unless a custom + `icon_file` is provided. The dimensions must be discovered by inspecting the XML. + Automatic resizing of the OLE-object shape can occur when the icon is double-clicked if + these values are not as set by PowerPoint. This behavior may only manifest in the Windows + version of PowerPoint. + """ + graphicFrame = _OleObjectElementCreator.graphicFrame( + self, + self._next_shape_id, + object_file, + prog_id, + left, + top, + width, + height, + icon_file, + icon_width, + icon_height, + ) + self._spTree.append(graphicFrame) + self._recalculate_extents() + return cast(GraphicFrame, self._shape_factory(graphicFrame)) + + def add_picture( + self, + image_file: str | IO[bytes], + left: Length, + top: Length, + width: Length | None = None, + height: Length | None = None, + ) -> Picture: + """Add picture shape displaying image in `image_file`. + + `image_file` can be either a path to a file (a string) or a file-like object. The picture + is positioned with its top-left corner at (`top`, `left`). If `width` and `height` are + both |None|, the native size of the image is used. If only one of `width` or `height` is + used, the unspecified dimension is calculated to preserve the aspect ratio of the image. + If both are specified, the picture is stretched to fit, without regard to its native + aspect ratio. + """ + image_part, rId = self.part.get_or_add_image_part(image_file) + pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height) + self._recalculate_extents() + return cast(Picture, self._shape_factory(pic)) + + def add_shape( + self, autoshape_type_id: MSO_SHAPE, left: Length, top: Length, width: Length, height: Length + ) -> Shape: + """Return new |Shape| object appended to this shape tree. + + `autoshape_type_id` is a member of :ref:`MsoAutoShapeType` e.g. `MSO_SHAPE.RECTANGLE` + specifying the type of shape to be added. The remaining arguments specify the new shape's + position and size. + """ + autoshape_type = AutoShapeType(autoshape_type_id) + sp = self._add_sp(autoshape_type, left, top, width, height) + self._recalculate_extents() + return cast(Shape, self._shape_factory(sp)) + + def add_textbox(self, left: Length, top: Length, width: Length, height: Length) -> Shape: + """Return newly added text box shape appended to this shape tree. + + The text box is of the specified size, located at the specified position on the slide. + """ + sp = self._add_textbox_sp(left, top, width, height) + self._recalculate_extents() + return cast(Shape, self._shape_factory(sp)) + + def build_freeform( + self, start_x: float = 0, start_y: float = 0, scale: tuple[float, float] | float = 1.0 + ) -> FreeformBuilder: + """Return |FreeformBuilder| object to specify a freeform shape. + + The optional `start_x` and `start_y` arguments specify the starting pen position in local + coordinates. They will be rounded to the nearest integer before use and each default to + zero. + + The optional `scale` argument specifies the size of local coordinates proportional to + slide coordinates (EMU). If the vertical scale is different than the horizontal scale + (local coordinate units are "rectangular"), a pair of numeric values can be provided as + the `scale` argument, e.g. `scale=(1.0, 2.0)`. In this case the first number is + interpreted as the horizontal (X) scale and the second as the vertical (Y) scale. + + A convenient method for calculating scale is to divide a |Length| object by an equivalent + count of local coordinate units, e.g. `scale = Inches(1)/1000` for 1000 local units per + inch. + """ + x_scale, y_scale = scale if isinstance(scale, tuple) else (scale, scale) + + return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale) + + def index(self, shape: BaseShape) -> int: + """Return the index of `shape` in this sequence. + + Raises |ValueError| if `shape` is not in the collection. + """ + shape_elms = list(self._element.iter_shape_elms()) + return shape_elms.index(shape.element) + + def _add_chart_graphicFrame( + self, rId: str, x: Length, y: Length, cx: Length, cy: Length + ) -> CT_GraphicalObjectFrame: + """Return new `p:graphicFrame` element appended to this shape tree. + + The `p:graphicFrame` element has the specified position and size and refers to the chart + part identified by `rId`. + """ + shape_id = self._next_shape_id + name = "Chart %d" % (shape_id - 1) + graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame( + shape_id, name, rId, x, y, cx, cy + ) + self._spTree.append(graphicFrame) + return graphicFrame + + def _add_cxnSp( + self, + connector_type: MSO_CONNECTOR_TYPE, + begin_x: Length, + begin_y: Length, + end_x: Length, + end_y: Length, + ) -> CT_Connector: + """Return a newly-added `p:cxnSp` element as specified. + + The `p:cxnSp` element is for a connector of `connector_type` beginning at (`begin_x`, + `begin_y`) and extending to (`end_x`, `end_y`). + """ + id_ = self._next_shape_id + name = "Connector %d" % (id_ - 1) + + flipH, flipV = begin_x > end_x, begin_y > end_y + x, y = min(begin_x, end_x), min(begin_y, end_y) + cx, cy = abs(end_x - begin_x), abs(end_y - begin_y) + + return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV) + + def _add_pic_from_image_part( + self, + image_part: ImagePart, + rId: str, + x: Length, + y: Length, + cx: Length | None, + cy: Length | None, + ) -> CT_Picture: + """Return a newly appended `p:pic` element as specified. + + The `p:pic` element displays the image in `image_part` with size and position specified by + `x`, `y`, `cx`, and `cy`. The element is appended to the shape tree, causing it to be + displayed first in z-order on the slide. + """ + id_ = self._next_shape_id + scaled_cx, scaled_cy = image_part.scale(cx, cy) + name = "Picture %d" % (id_ - 1) + desc = image_part.desc + pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy) + return pic + + def _add_sp( + self, autoshape_type: AutoShapeType, x: Length, y: Length, cx: Length, cy: Length + ) -> CT_Shape: + """Return newly-added `p:sp` element as specified. + + `p:sp` element is of `autoshape_type` at position (`x`, `y`) and of size (`cx`, `cy`). + """ + id_ = self._next_shape_id + name = "%s %d" % (autoshape_type.basename, id_ - 1) + sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy) + return sp + + def _add_textbox_sp(self, x: Length, y: Length, cx: Length, cy: Length) -> CT_Shape: + """Return newly-appended textbox `p:sp` element. + + Element has position (`x`, `y`) and size (`cx`, `cy`). + """ + id_ = self._next_shape_id + name = "TextBox %d" % (id_ - 1) + sp = self._spTree.add_textbox(id_, name, x, y, cx, cy) + return sp + + def _recalculate_extents(self) -> None: + """Adjust position and size to incorporate all contained shapes. + + This would typically be called when a contained shape is added, removed, or its position + or size updated. + """ + # ---default behavior is to do nothing, GroupShapes overrides to + # produce the distinctive behavior of groups and subgroups.--- + pass + + +class GroupShapes(_BaseGroupShapes): + """The sequence of child shapes belonging to a group shape. + + Note that this collection can itself contain a group shape, making this part of a recursive, + tree data structure (acyclic graph). + """ + + def _recalculate_extents(self) -> None: + """Adjust position and size to incorporate all contained shapes. + + This would typically be called when a contained shape is added, removed, or its position + or size updated. + """ + self._grpSp.recalculate_extents() + + +class SlideShapes(_BaseGroupShapes): + """Sequence of shapes appearing on a slide. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. + Supports indexed access, len(), index(), and iteration. + """ + + parent: Slide # pyright: ignore[reportIncompatibleMethodOverride] + + def add_movie( + self, + movie_file: str | IO[bytes], + left: Length, + top: Length, + width: Length, + height: Length, + poster_frame_image: str | IO[bytes] | None = None, + mime_type: str = CT.VIDEO, + ) -> GraphicFrame: + """Return newly added movie shape displaying video in `movie_file`. + + **EXPERIMENTAL.** This method has important limitations: + + * The size must be specified; no auto-scaling such as that provided by :meth:`add_picture` + is performed. + * The MIME type of the video file should be specified, e.g. 'video/mp4'. The provided + video file is not interrogated for its type. The MIME type `video/unknown` is used by + default (and works fine in tests as of this writing). + * A poster frame image must be provided, it cannot be automatically extracted from the + video file. If no poster frame is provided, the default "media loudspeaker" image will + be used. + + Return a newly added movie shape to the slide, positioned at (`left`, `top`), having size + (`width`, `height`), and containing `movie_file`. Before the video is started, + `poster_frame_image` is displayed as a placeholder for the video. + """ + movie_pic = _MoviePicElementCreator.new_movie_pic( + self, + self._next_shape_id, + movie_file, + left, + top, + width, + height, + poster_frame_image, + mime_type, + ) + self._spTree.append(movie_pic) + self._add_video_timing(movie_pic) + return cast(GraphicFrame, self._shape_factory(movie_pic)) + + def add_table( + self, rows: int, cols: int, left: Length, top: Length, width: Length, height: Length + ) -> GraphicFrame: + """Add a |GraphicFrame| object containing a table. + + The table has the specified number of `rows` and `cols` and the specified position and + size. `width` is evenly distributed between the columns of the new table. Likewise, + `height` is evenly distributed between the rows. Note that the `.table` property on the + returned |GraphicFrame| shape must be used to access the enclosed |Table| object. + """ + graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height) + return cast(GraphicFrame, self._shape_factory(graphicFrame)) + + def clone_layout_placeholders(self, slide_layout: SlideLayout) -> None: + """Add placeholder shapes based on those in `slide_layout`. + + Z-order of placeholders is preserved. Latent placeholders (date, slide number, and footer) + are not cloned. + """ + for placeholder in slide_layout.iter_cloneable_placeholders(): + self.clone_placeholder(placeholder) + + @property + def placeholders(self) -> SlidePlaceholders: + """Sequence of placeholder shapes in this slide.""" + return self.parent.placeholders + + @property + def title(self) -> Shape | None: + """The title placeholder shape on the slide. + + |None| if the slide has no title placeholder. + """ + for elm in self._spTree.iter_ph_elms(): + if elm.ph_idx == 0: + return cast(Shape, self._shape_factory(elm)) + return None + + def _add_graphicFrame_containing_table( + self, rows: int, cols: int, x: Length, y: Length, cx: Length, cy: Length + ) -> CT_GraphicalObjectFrame: + """Return a newly added `p:graphicFrame` element containing a table as specified.""" + _id = self._next_shape_id + name = "Table %d" % (_id - 1) + graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy) + return graphicFrame + + def _add_video_timing(self, pic: CT_Picture) -> None: + """Add a `p:video` element under `p:sld/p:timing`. + + The element will refer to the specified `pic` element by its shape id, and cause the video + play controls to appear for that video. + """ + sld = self._spTree.xpath("/p:sld")[0] + childTnLst = sld.get_or_add_childTnLst() + childTnLst.add_video(pic.shape_id) + + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return SlideShapeFactory(shape_elm, self) + + +class LayoutShapes(_BaseShapes): + """Sequence of shapes appearing on a slide layout. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. + Supports indexed access, len(), index(), and iteration. + """ + + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return _LayoutShapeFactory(shape_elm, self) + + +class MasterShapes(_BaseShapes): + """Sequence of shapes appearing on a slide master. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. + Supports indexed access, len(), and iteration. + """ + + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return _MasterShapeFactory(shape_elm, self) + + +class NotesSlideShapes(_BaseShapes): + """Sequence of shapes appearing on a notes slide. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. + Supports indexed access, len(), index(), and iteration. + """ + + def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str: + """Return the base name for a placeholder of `ph_type` in this shape collection. + + A notes slide uses a different name for the body placeholder and has some unique + placeholder types, so this method overrides the default in the base class. + """ + return { + PP_PLACEHOLDER.BODY: "Notes Placeholder", + PP_PLACEHOLDER.DATE: "Date Placeholder", + PP_PLACEHOLDER.FOOTER: "Footer Placeholder", + PP_PLACEHOLDER.HEADER: "Header Placeholder", + PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder", + PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder", + }[ph_type] + + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return appropriate shape object for `shape_elm` appearing on a notes slide.""" + return _NotesSlideShapeFactory(shape_elm, self) + + +class BasePlaceholders(_BaseShapes): + """Base class for placeholder collections. + + Subclasses differentiate behaviors for a master, layout, and slide. By default, placeholder + shapes are constructed using |BaseShapeFactory|. Subclasses should override + :method:`_shape_factory` to use custom placeholder classes. + """ + + @staticmethod + def _is_member_elm(shape_elm: ShapeElement) -> bool: + """True if `shape_elm` is a placeholder shape, False otherwise.""" + return shape_elm.has_ph_elm + + +class LayoutPlaceholders(BasePlaceholders): + """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout.""" + + __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride] + [], Iterator[LayoutPlaceholder] + ] + + def get(self, idx: int, default: LayoutPlaceholder | None = None) -> LayoutPlaceholder | None: + """The first placeholder shape with matching `idx` value, or `default` if not found.""" + for placeholder in self: + if placeholder.element.ph_idx == idx: + return placeholder + return default + + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return _LayoutShapeFactory(shape_elm, self) + + +class MasterPlaceholders(BasePlaceholders): + """Sequence of MasterPlaceholder representing the placeholder shapes on a slide master.""" + + __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride] + [], Iterator[MasterPlaceholder] + ] + + def get(self, ph_type: PP_PLACEHOLDER, default: MasterPlaceholder | None = None): + """Return the first placeholder shape with type `ph_type` (e.g. 'body'). + + Returns `default` if no such placeholder shape is present in the collection. + """ + for placeholder in self: + if placeholder.ph_type == ph_type: + return placeholder + return default + + def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride] + self, placeholder_elm: CT_Shape + ) -> MasterPlaceholder: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return cast(MasterPlaceholder, _MasterShapeFactory(placeholder_elm, self)) + + +class NotesSlidePlaceholders(MasterPlaceholders): + """Sequence of placeholder shapes on a notes slide.""" + + __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride] + [], Iterator[NotesSlidePlaceholder] + ] + + def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride] + self, placeholder_elm: CT_Shape + ) -> NotesSlidePlaceholder: + """Return an instance of the appropriate placeholder proxy class for `placeholder_elm`.""" + return cast(NotesSlidePlaceholder, _NotesSlideShapeFactory(placeholder_elm, self)) + + +class SlidePlaceholders(ParentedElementProxy): + """Collection of placeholder shapes on a slide. + + Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the + placeholders it contains. + """ + + _element: CT_GroupShape + + def __getitem__(self, idx: int): + """Access placeholder shape having `idx`. + + Note that while this looks like list access, idx is actually a dictionary key and will + raise |KeyError| if no placeholder with that idx value is in the collection. + """ + for e in self._element.iter_ph_elms(): + if e.ph_idx == idx: + return SlideShapeFactory(e, self) + raise KeyError("no placeholder on this slide with idx == %d" % idx) + + def __iter__(self): + """Generate placeholder shapes in `idx` order.""" + ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx) + return (SlideShapeFactory(e, self) for e in ph_elms) + + def __len__(self) -> int: + """Return count of placeholder shapes.""" + return len(list(self._element.iter_ph_elms())) + + +def BaseShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + tag = shape_elm.tag + + if isinstance(shape_elm, CT_Picture): + videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile") + if videoFiles: + return Movie(shape_elm, parent) + return Picture(shape_elm, parent) + + shape_cls = { + qn("p:cxnSp"): Connector, + qn("p:grpSp"): GroupShape, + qn("p:sp"): Shape, + qn("p:graphicFrame"): GraphicFrame, + }.get(tag, BaseShape) + + return shape_cls(shape_elm, parent) # pyright: ignore[reportArgumentType] + + +def _LayoutShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a slide layout.""" + if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm: + return LayoutPlaceholder(shape_elm, parent) + return BaseShapeFactory(shape_elm, parent) + + +def _MasterShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a slide master.""" + if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm: + return MasterPlaceholder(shape_elm, parent) + return BaseShapeFactory(shape_elm, parent) + + +def _NotesSlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a notes slide.""" + if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm: + return NotesSlidePlaceholder(shape_elm, parent) + return BaseShapeFactory(shape_elm, parent) + + +def _SlidePlaceholderFactory(shape_elm: ShapeElement, parent: ProvidesPart): + """Return a placeholder shape of the appropriate type for `shape_elm`.""" + tag = shape_elm.tag + if tag == qn("p:sp"): + Constructor = { + PP_PLACEHOLDER.BITMAP: PicturePlaceholder, + PP_PLACEHOLDER.CHART: ChartPlaceholder, + PP_PLACEHOLDER.PICTURE: PicturePlaceholder, + PP_PLACEHOLDER.TABLE: TablePlaceholder, + }.get(shape_elm.ph_type, SlidePlaceholder) + elif tag == qn("p:graphicFrame"): + Constructor = PlaceholderGraphicFrame + elif tag == qn("p:pic"): + Constructor = PlaceholderPicture + else: + Constructor = BaseShapeFactory + return Constructor(shape_elm, parent) # pyright: ignore[reportArgumentType] + + +def SlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a slide.""" + if shape_elm.has_ph_elm: + return _SlidePlaceholderFactory(shape_elm, parent) + return BaseShapeFactory(shape_elm, parent) + + +class _MoviePicElementCreator(object): + """Functional service object for creating a new movie p:pic element. + + It's entire external interface is its :meth:`new_movie_pic` class method that returns a new + `p:pic` element containing the specified video. This class is not intended to be constructed + or an instance of it retained by the caller; it is a "one-shot" object, really a function + wrapped in a object such that its helper methods can be organized here. + """ + + def __init__( + self, + shapes: SlideShapes, + shape_id: int, + movie_file: str | IO[bytes], + x: Length, + y: Length, + cx: Length, + cy: Length, + poster_frame_file: str | IO[bytes] | None, + mime_type: str | None, + ): + super(_MoviePicElementCreator, self).__init__() + self._shapes = shapes + self._shape_id = shape_id + self._movie_file = movie_file + self._x, self._y, self._cx, self._cy = x, y, cx, cy + self._poster_frame_file = poster_frame_file + self._mime_type = mime_type + + @classmethod + def new_movie_pic( + cls, + shapes: SlideShapes, + shape_id: int, + movie_file: str | IO[bytes], + x: Length, + y: Length, + cx: Length, + cy: Length, + poster_frame_image: str | IO[bytes] | None, + mime_type: str | None, + ) -> CT_Picture: + """Return a new `p:pic` element containing video in `movie_file`. + + If `mime_type` is None, 'video/unknown' is used. If `poster_frame_file` is None, the + default "media loudspeaker" image is used. + """ + return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic + + @property + def _media_rId(self) -> str: + """Return the rId of RT.MEDIA relationship to video part. + + For historical reasons, there are two relationships to the same part; one is the video rId + and the other is the media rId. + """ + return self._video_part_rIds[0] + + @lazyproperty + def _pic(self) -> CT_Picture: + """Return the new `p:pic` element referencing the video.""" + return CT_Picture.new_video_pic( + self._shape_id, + self._shape_name, + self._video_rId, + self._media_rId, + self._poster_frame_rId, + self._x, + self._y, + self._cx, + self._cy, + ) + + @lazyproperty + def _poster_frame_image_file(self) -> str | IO[bytes]: + """Return the image file for video placeholder image. + + If no poster frame file is provided, the default "media loudspeaker" image is used. + """ + poster_frame_file = self._poster_frame_file + if poster_frame_file is None: + return io.BytesIO(SPEAKER_IMAGE_BYTES) + return poster_frame_file + + @lazyproperty + def _poster_frame_rId(self) -> str: + """Return the rId of relationship to poster frame image. + + The poster frame is the image used to represent the video before it's played. + """ + _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file) + return poster_frame_rId + + @property + def _shape_name(self) -> str: + """Return the appropriate shape name for the p:pic shape. + + A movie shape is named with the base filename of the video. + """ + return self._video.filename + + @property + def _slide_part(self) -> SlidePart: + """Return SlidePart object for slide containing this movie.""" + return self._shapes.part + + @lazyproperty + def _video(self) -> Video: + """Return a |Video| object containing the movie file.""" + return Video.from_path_or_file_like(self._movie_file, self._mime_type) + + @lazyproperty + def _video_part_rIds(self) -> tuple[str, str]: + """Return the rIds for relationships to media part for video. + + This is where the media part and its relationships to the slide are actually created. + """ + media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video) + return media_rId, video_rId + + @property + def _video_rId(self) -> str: + """Return the rId of RT.VIDEO relationship to video part. + + For historical reasons, there are two relationships to the same part; one is the video rId + and the other is the media rId. + """ + return self._video_part_rIds[1] + + +class _OleObjectElementCreator(object): + """Functional service object for creating a new OLE-object p:graphicFrame element. + + It's entire external interface is its :meth:`graphicFrame` class method that returns a new + `p:graphicFrame` element containing the specified embedded OLE-object shape. This class is not + intended to be constructed or an instance of it retained by the caller; it is a "one-shot" + object, really a function wrapped in a object such that its helper methods can be organized + here. + """ + + def __init__( + self, + shapes: _BaseGroupShapes, + shape_id: int, + ole_object_file: str | IO[bytes], + prog_id: PROG_ID | str, + x: Length, + y: Length, + cx: Length | None, + cy: Length | None, + icon_file: str | IO[bytes] | None, + icon_width: Length | None, + icon_height: Length | None, + ): + self._shapes = shapes + self._shape_id = shape_id + self._ole_object_file = ole_object_file + self._prog_id_arg = prog_id + self._x = x + self._y = y + self._cx_arg = cx + self._cy_arg = cy + self._icon_file_arg = icon_file + self._icon_width_arg = icon_width + self._icon_height_arg = icon_height + + @classmethod + def graphicFrame( + cls, + shapes: _BaseGroupShapes, + shape_id: int, + ole_object_file: str | IO[bytes], + prog_id: PROG_ID | str, + x: Length, + y: Length, + cx: Length | None, + cy: Length | None, + icon_file: str | IO[bytes] | None, + icon_width: Length | None, + icon_height: Length | None, + ) -> CT_GraphicalObjectFrame: + """Return new `p:graphicFrame` element containing embedded `ole_object_file`.""" + return cls( + shapes, + shape_id, + ole_object_file, + prog_id, + x, + y, + cx, + cy, + icon_file, + icon_width, + icon_height, + )._graphicFrame + + @lazyproperty + def _graphicFrame(self) -> CT_GraphicalObjectFrame: + """Newly-created `p:graphicFrame` element referencing embedded OLE-object.""" + return CT_GraphicalObjectFrame.new_ole_object_graphicFrame( + self._shape_id, + self._shape_name, + self._ole_object_rId, + self._progId, + self._icon_rId, + self._x, + self._y, + self._cx, + self._cy, + self._icon_width, + self._icon_height, + ) + + @lazyproperty + def _cx(self) -> Length: + """Emu object specifying width of "show-as-icon" image for OLE shape.""" + # --- a user-specified width overrides any default --- + if self._cx_arg is not None: + return self._cx_arg + + # --- the default width is specified by the PROG_ID member if prog_id is one, + # --- otherwise it gets the default icon width. + return ( + Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200) + ) + + @lazyproperty + def _cy(self) -> Length: + """Emu object specifying height of "show-as-icon" image for OLE shape.""" + # --- a user-specified width overrides any default --- + if self._cy_arg is not None: + return self._cy_arg + + # --- the default height is specified by the PROG_ID member if prog_id is one, + # --- otherwise it gets the default icon height. + return ( + Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600) + ) + + @lazyproperty + def _icon_height(self) -> Length: + """Vertical size of enclosed EMF icon within the OLE graphic-frame. + + This must be specified when a custom icon is used, to avoid stretching of the image and + possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it. + + The correct size can be determined by creating an example PPTX using PowerPoint and then + inspecting the XML of the OLE graphics-frame (p:oleObj.imgH). + """ + return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600) + + @lazyproperty + def _icon_image_file(self) -> str | IO[bytes]: + """Reference to image file containing icon to show in lieu of this object. + + This can be either a str path or a file-like object (io.BytesIO typically). + """ + # --- a user-specified icon overrides any default --- + if self._icon_file_arg is not None: + return self._icon_file_arg + + # --- A prog_id belonging to PROG_ID gets its icon filename from there. A + # --- user-specified (str) prog_id gets the default icon. + icon_filename = ( + self._prog_id_arg.icon_filename + if isinstance(self._prog_id_arg, PROG_ID) + else "generic-icon.emf" + ) + + _thisdir = os.path.split(__file__)[0] + return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename)) + + @lazyproperty + def _icon_rId(self) -> str: + """str rId like "rId7" of rel to icon (image) representing OLE-object part.""" + _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file) + return rId + + @lazyproperty + def _icon_width(self) -> Length: + """Width of enclosed EMF icon within the OLE graphic-frame. + + This must be specified when a custom icon is used, to avoid stretching of the image and + possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it. + """ + return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200) + + @lazyproperty + def _ole_object_rId(self) -> str: + """str rId like "rId6" of relationship to embedded ole_object part. + + This is where the ole_object part and its relationship to the slide are actually created. + """ + return self._slide_part.add_embedded_ole_object_part( + self._prog_id_arg, self._ole_object_file + ) + + @lazyproperty + def _progId(self) -> str: + """str like "Excel.Sheet.12" identifying program used to open object. + + This value appears in the `progId` attribute of the `p:oleObj` element for the object. + """ + prog_id_arg = self._prog_id_arg + + # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller + # --- has specified it explicitly (as str) + return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg + + @lazyproperty + def _shape_name(self) -> str: + """str name like "Object 1" for the embedded ole_object shape. + + The name is formed from the prefix "Object " and the shape-id decremented by 1. + """ + return "Object %d" % (self._shape_id - 1) + + @lazyproperty + def _slide_part(self) -> SlidePart: + """SlidePart object for this slide.""" + return self._shapes.part diff --git a/src/pptx/shared.py b/src/pptx/shared.py new file mode 100644 index 000000000..da2a17182 --- /dev/null +++ b/src/pptx/shared.py @@ -0,0 +1,82 @@ +"""Objects shared by pptx modules.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.oxml.xmlchemy import BaseOxmlElement + from pptx.types import ProvidesPart + + +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-pptx other than custom element (oxml) classes. + """ + + def __init__(self, element: BaseOxmlElement): + self._element = element + + def __eq__(self, other: object) -> bool: + """Return |True| if this proxy object refers to the same oxml element as does *other*. + + ElementProxy objects are value objects and should maintain no mutable local state. + Equality for proxy objects is defined as referring to the same XML element, whether or not + they are the same proxy object instance. + """ + if not isinstance(other, ElementProxy): + return False + return self._element is other._element + + def __ne__(self, other: object) -> bool: + if not isinstance(other, ElementProxy): + return True + return self._element is not other._element + + @property + def element(self): + """The lxml element proxied by this object.""" + return self._element + + +class ParentedElementProxy(ElementProxy): + """Provides access to ancestor objects and part. + + An ancestor may occasionally be required to provide a service, such as add or drop a + relationship. Provides the :attr:`_parent` attribute to subclasses and the public + :attr:`parent` read-only property. + """ + + def __init__(self, element: BaseOxmlElement, parent: ProvidesPart): + super(ParentedElementProxy, self).__init__(element) + self._parent = parent + + @property + def parent(self): + """The ancestor proxy object to this one. + + For example, the parent of a shape is generally the |SlideShapes| object that contains it. + """ + return self._parent + + @property + def part(self) -> XmlPart: + """The package part containing this object.""" + return self._parent.part + + +class PartElementProxy(ElementProxy): + """Provides common members for proxy-objects that wrap a part's root element, e.g. `p:sld`.""" + + def __init__(self, element: BaseOxmlElement, part: XmlPart): + super(PartElementProxy, self).__init__(element) + self._part = part + + @property + def part(self) -> XmlPart: + """The package part containing this object.""" + return self._part diff --git a/pptx/slide.py b/src/pptx/slide.py similarity index 52% rename from pptx/slide.py rename to src/pptx/slide.py index ea0a3c246..3b1b65d8e 100644 --- a/pptx/slide.py +++ b/src/pptx/slide.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Slide-related objects, including masters, layouts, and notes.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator, cast from pptx.dml.fill import FillFormat from pptx.enum.shapes import PP_PLACEHOLDER @@ -19,14 +19,30 @@ from pptx.shared import ElementProxy, ParentedElementProxy, PartElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList + from pptx.oxml.slide import ( + CT_CommonSlideData, + CT_NotesSlide, + CT_Slide, + CT_SlideLayoutIdList, + CT_SlideMaster, + ) + from pptx.parts.presentation import PresentationPart + from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart + from pptx.presentation import Presentation + from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder + from pptx.shapes.shapetree import NotesSlidePlaceholder + from pptx.text.text import TextFrame + class _BaseSlide(PartElementProxy): """Base class for slide objects, including masters, layouts and notes.""" - __slots__ = ("_background",) + _element: CT_Slide @lazyproperty - def background(self): + def background(self) -> _Background: """|_Background| object providing slide background properties. This property returns a |_Background| object whether or not the @@ -38,33 +54,31 @@ def background(self): return _Background(self._element.cSld) @property - def name(self): - """ - String representing the internal name of this slide. Returns an empty - string (`''`) if no name is assigned. Assigning an empty string or - |None| to this property causes any name to be removed. + def name(self) -> str: + """String representing the internal name of this slide. + + Returns an empty string (`''`) if no name is assigned. Assigning an empty string or |None| + to this property causes any name to be removed. """ return self._element.cSld.name @name.setter - def name(self, value): + def name(self, value: str | None): new_value = "" if value is None else value self._element.cSld.name = new_value class _BaseMaster(_BaseSlide): - """ - Base class for master objects such as |SlideMaster| and |NotesMaster|. + """Base class for master objects such as |SlideMaster| and |NotesMaster|. + Provides access to placeholders and regular shapes. """ - __slots__ = ("_placeholders", "_shapes") - @lazyproperty - def placeholders(self): - """ - Instance of |MasterPlaceholders| containing sequence of placeholder - shapes in this master, sorted in *idx* order. + def placeholders(self) -> MasterPlaceholders: + """|MasterPlaceholders| collection of placeholder shapes in this master. + + Sequence sorted in `idx` order. """ return MasterPlaceholders(self._element.spTree, self) @@ -78,35 +92,34 @@ def shapes(self): class NotesMaster(_BaseMaster): - """ - Proxy for the notes master XML document. Provides access to shapes, the - most commonly used of which are placeholders. - """ + """Proxy for the notes master XML document. - __slots__ = () + Provides access to shapes, the most commonly used of which are placeholders. + """ class NotesSlide(_BaseSlide): - """ - Notes slide object. Provides access to slide notes placeholder and other - shapes on the notes handout page. + """Notes slide object. + + Provides access to slide notes placeholder and other shapes on the notes handout + page. """ - __slots__ = ("_placeholders", "_shapes") + element: CT_NotesSlide # pyright: ignore[reportIncompatibleMethodOverride] - def clone_master_placeholders(self, notes_master): - """ - Selectively add placeholder shape elements from *notes_master* to the - shapes collection of this notes slide. Z-order of placeholders is - preserved. Certain placeholders (header, date, footer) are not - cloned. + def clone_master_placeholders(self, notes_master: NotesMaster) -> None: + """Selectively add placeholder shape elements from `notes_master`. + + Selected placeholder shape elements from `notes_master` are added to the shapes + collection of this notes slide. Z-order of placeholders is preserved. Certain + placeholders (header, date, footer) are not cloned. """ - def iter_cloneable_placeholders(notes_master): - """ - Generate a reference to each placeholder in *notes_master* that - should be cloned to a notes slide when the a new notes slide is - created. + def iter_cloneable_placeholders() -> Iterator[MasterPlaceholder]: + """Generate a reference to each cloneable placeholder in `notes_master`. + + These are the placeholders that should be cloned to a notes slide when the a new notes + slide is created. """ cloneable = ( PP_PLACEHOLDER.SLIDE_IMAGE, @@ -118,17 +131,16 @@ def iter_cloneable_placeholders(notes_master): yield placeholder shapes = self.shapes - for placeholder in iter_cloneable_placeholders(notes_master): - shapes.clone_placeholder(placeholder) + for placeholder in iter_cloneable_placeholders(): + shapes.clone_placeholder(cast("LayoutPlaceholder", placeholder)) @property - def notes_placeholder(self): - """ - Return the notes placeholder on this notes slide, the shape that - contains the actual notes text. Return |None| if no notes placeholder - is present; while this is probably uncommon, it can happen if the - notes master does not have a body placeholder, or if the notes - placeholder has been deleted from the notes slide. + def notes_placeholder(self) -> NotesSlidePlaceholder | None: + """the notes placeholder on this notes slide, the shape that contains the actual notes text. + + Return |None| if no notes placeholder is present; while this is probably uncommon, it can + happen if the notes master does not have a body placeholder, or if the notes placeholder + has been deleted from the notes slide. """ for placeholder in self.placeholders: if placeholder.placeholder_format.type == PP_PLACEHOLDER.BODY: @@ -136,12 +148,11 @@ def notes_placeholder(self): return None @property - def notes_text_frame(self): - """ - Return the text frame of the notes placeholder on this notes slide, - or |None| if there is no notes placeholder. This is a shortcut to - accommodate the common case of simply adding "notes" text to the - notes "page". + def notes_text_frame(self) -> TextFrame | None: + """The text frame of the notes placeholder on this notes slide. + + |None| if there is no notes placeholder. This is a shortcut to accommodate the common case + of simply adding "notes" text to the notes "page". """ notes_placeholder = self.notes_placeholder if notes_placeholder is None: @@ -149,40 +160,23 @@ def notes_text_frame(self): return notes_placeholder.text_frame @lazyproperty - def placeholders(self): - """ - An instance of |NotesSlidePlaceholders| containing the sequence of - placeholder shapes in this notes slide. + def placeholders(self) -> NotesSlidePlaceholders: + """Instance of |NotesSlidePlaceholders| for this notes-slide. + + Contains the sequence of placeholder shapes in this notes slide. """ return NotesSlidePlaceholders(self.element.spTree, self) @lazyproperty - def shapes(self): - """ - An instance of |NotesSlideShapes| containing the sequence of shape - objects appearing on this notes slide. - """ + def shapes(self) -> NotesSlideShapes: + """Sequence of shape objects appearing on this notes slide.""" return NotesSlideShapes(self._element.spTree, self) class Slide(_BaseSlide): """Slide object. Provides access to shapes and slide-level properties.""" - __slots__ = ("_placeholders", "_shapes") - - @property - def background(self): - """|_Background| object providing slide background properties. - - This property returns a |_Background| object whether or not the slide - overrides the default background or inherits it. Determining which of - those conditions applies for this slide is accomplished using the - :attr:`follow_master_background` property. - - The same |_Background| object is returned on every call for the same - slide object. - """ - return super(Slide, self).background + part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride] @property def follow_master_background(self): @@ -199,115 +193,99 @@ def follow_master_background(self): return self._element.bg is None @property - def has_notes_slide(self): - """ - Return True if this slide has a notes slide, False otherwise. A notes - slide is created by :attr:`.notes_slide` when one doesn't exist; use - this property to test for a notes slide without the possible side - effect of creating one. + def has_notes_slide(self) -> bool: + """`True` if this slide has a notes slide, `False` otherwise. + + A notes slide is created by :attr:`.notes_slide` when one doesn't exist; use this property + to test for a notes slide without the possible side effect of creating one. """ return self.part.has_notes_slide @property - def notes_slide(self): - """ - Return the |NotesSlide| instance for this slide. If the slide does - not have a notes slide, one is created. The same single instance is + def notes_slide(self) -> NotesSlide: + """The |NotesSlide| instance for this slide. + + If the slide does not have a notes slide, one is created. The same single instance is returned on each call. """ return self.part.notes_slide @lazyproperty - def placeholders(self): - """ - Instance of |SlidePlaceholders| containing sequence of placeholder - shapes in this slide. - """ + def placeholders(self) -> SlidePlaceholders: + """Sequence of placeholder shapes in this slide.""" return SlidePlaceholders(self._element.spTree, self) @lazyproperty - def shapes(self): - """ - Instance of |SlideShapes| containing sequence of shape objects - appearing on this slide. - """ + def shapes(self) -> SlideShapes: + """Sequence of shape objects appearing on this slide.""" return SlideShapes(self._element.spTree, self) @property - def slide_id(self): - """ - The integer value that uniquely identifies this slide within this - presentation. The slide id does not change if the position of this - slide in the slide sequence is changed by adding, rearranging, or - deleting slides. + def slide_id(self) -> int: + """Integer value that uniquely identifies this slide within this presentation. + + The slide id does not change if the position of this slide in the slide sequence is changed + by adding, rearranging, or deleting slides. """ return self.part.slide_id @property - def slide_layout(self): - """ - |SlideLayout| object this slide inherits appearance from. - """ + def slide_layout(self) -> SlideLayout: + """|SlideLayout| object this slide inherits appearance from.""" return self.part.slide_layout class Slides(ParentedElementProxy): + """Sequence of slides belonging to an instance of |Presentation|. + + Has list semantics for access to individual slides. Supports indexed access, len(), and + iteration. """ - Sequence of slides belonging to an instance of |Presentation|, having - list semantics for access to individual slides. Supports indexed access, - len(), and iteration. - """ - def __init__(self, sldIdLst, prs): + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + + def __init__(self, sldIdLst: CT_SlideIdList, prs: Presentation): super(Slides, self).__init__(sldIdLst, prs) self._sldIdLst = sldIdLst - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. 'slides[0]'). - """ + def __getitem__(self, idx: int) -> Slide: + """Provide indexed access, (e.g. 'slides[0]').""" try: - sldId = self._sldIdLst[idx] + sldId = self._sldIdLst.sldId_lst[idx] except IndexError: raise IndexError("slide index out of range") return self.part.related_slide(sldId.rId) - def __iter__(self): - """ - Support iteration (e.g. 'for slide in slides:'). - """ - for sldId in self._sldIdLst: + def __iter__(self) -> Iterator[Slide]: + """Support iteration, e.g. `for slide in slides:`.""" + for sldId in self._sldIdLst.sldId_lst: yield self.part.related_slide(sldId.rId) - def __len__(self): - """ - Support len() built-in function (e.g. 'len(slides) == 4'). - """ + def __len__(self) -> int: + """Support len() built-in function, e.g. `len(slides) == 4`.""" return len(self._sldIdLst) - def add_slide(self, slide_layout): - """ - Return a newly added slide that inherits layout from *slide_layout*. - """ + def add_slide(self, slide_layout: SlideLayout) -> Slide: + """Return a newly added slide that inherits layout from `slide_layout`.""" rId, slide = self.part.add_slide(slide_layout) slide.shapes.clone_layout_placeholders(slide_layout) self._sldIdLst.add_sldId(rId) return slide - def get(self, slide_id, default=None): - """ - Return the slide identified by integer *slide_id* in this - presentation, or *default* if not found. + def get(self, slide_id: int, default: Slide | None = None) -> Slide | None: + """Return the slide identified by int `slide_id` in this presentation. + + Returns `default` if not found. """ slide = self.part.get_slide(slide_id) if slide is None: return default return slide - def index(self, slide): - """ - Map *slide* to an integer representing its zero-based position in - this slide collection. Raises |ValueError| on *slide* not present. + def index(self, slide: Slide) -> int: + """Map `slide` to its zero-based position in this slide sequence. + + Raises |ValueError| on *slide* not present. """ for idx, this_slide in enumerate(self): if this_slide == slide: @@ -316,18 +294,17 @@ def index(self, slide): class SlideLayout(_BaseSlide): - """ - Slide layout object. Provides access to placeholders, regular shapes, and - slide layout-level properties. + """Slide layout object. + + Provides access to placeholders, regular shapes, and slide layout-level properties. """ - __slots__ = ("_placeholders", "_shapes") + part: SlideLayoutPart # pyright: ignore[reportIncompatibleMethodOverride] - def iter_cloneable_placeholders(self): - """ - Generate a reference to each layout placeholder on this slide layout - that should be cloned to a slide when the layout is applied to that - slide. + def iter_cloneable_placeholders(self) -> Iterator[LayoutPlaceholder]: + """Generate layout-placeholders on this slide-layout that should be cloned to a new slide. + + Used when creating a new slide from this slide-layout. """ latent_ph_types = ( PP_PLACEHOLDER.DATE, @@ -339,26 +316,21 @@ def iter_cloneable_placeholders(self): yield ph @lazyproperty - def placeholders(self): - """ - Instance of |LayoutPlaceholders| containing sequence of placeholder - shapes in this slide layout, sorted in *idx* order. + def placeholders(self) -> LayoutPlaceholders: + """Sequence of placeholder shapes in this slide layout. + + Placeholders appear in `idx` order. """ return LayoutPlaceholders(self._element.spTree, self) @lazyproperty - def shapes(self): - """ - Instance of |LayoutShapes| containing the sequence of shapes - appearing on this slide layout. - """ + def shapes(self) -> LayoutShapes: + """Sequence of shapes appearing on this slide layout.""" return LayoutShapes(self._element.spTree, self) @property - def slide_master(self): - """ - Slide master from which this slide layout inherits properties. - """ + def slide_master(self) -> SlideMaster: + """Slide master from which this slide-layout inherits properties.""" return self.part.slide_master @property @@ -375,58 +347,51 @@ class SlideLayouts(ParentedElementProxy): Supports indexed access, len(), iteration, index() and remove(). """ - __slots__ = ("_sldLayoutIdLst",) + part: SlideMasterPart # pyright: ignore[reportIncompatibleMethodOverride] - def __init__(self, sldLayoutIdLst, parent): + def __init__(self, sldLayoutIdLst: CT_SlideLayoutIdList, parent: SlideMaster): super(SlideLayouts, self).__init__(sldLayoutIdLst, parent) self._sldLayoutIdLst = sldLayoutIdLst - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. ``slide_layouts[2]``). - """ + def __getitem__(self, idx: int) -> SlideLayout: + """Provides indexed access, e.g. `slide_layouts[2]`.""" try: - sldLayoutId = self._sldLayoutIdLst[idx] + sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[idx] except IndexError: raise IndexError("slide layout index out of range") return self.part.related_slide_layout(sldLayoutId.rId) - def __iter__(self): - """ - Generate a reference to each of the |SlideLayout| instances in the - collection, in sequence. - """ - for sldLayoutId in self._sldLayoutIdLst: + def __iter__(self) -> Iterator[SlideLayout]: + """Generate each |SlideLayout| in the collection, in sequence.""" + for sldLayoutId in self._sldLayoutIdLst.sldLayoutId_lst: yield self.part.related_slide_layout(sldLayoutId.rId) - def __len__(self): - """ - Support len() built-in function (e.g. 'len(slides) == 4'). - """ + def __len__(self) -> int: + """Support len() built-in function, e.g. `len(slides) == 4`.""" return len(self._sldLayoutIdLst) - def get_by_name(self, name, default=None): - """Return SlideLayout object having *name* or *default* if not found.""" + def get_by_name(self, name: str, default: SlideLayout | None = None) -> SlideLayout | None: + """Return SlideLayout object having `name`, or `default` if not found.""" for slide_layout in self: if slide_layout.name == name: return slide_layout return default - def index(self, slide_layout): - """Return zero-based index of *slide_layout* in this collection. + def index(self, slide_layout: SlideLayout) -> int: + """Return zero-based index of `slide_layout` in this collection. - Raises ValueError if *slide_layout* is not present in this collection. + Raises `ValueError` if `slide_layout` is not present in this collection. """ for idx, this_layout in enumerate(self): if slide_layout == this_layout: return idx raise ValueError("layout not in this SlideLayouts collection") - def remove(self, slide_layout): - """Remove *slide_layout* from the collection. + def remove(self, slide_layout: SlideLayout) -> None: + """Remove `slide_layout` from the collection. - Raises ValueError when *slide_layout* is in use; a slide layout which is the - basis for one or more slides cannot be removed. + Raises ValueError when `slide_layout` is in use; a slide layout which is the basis for one + or more slides cannot be removed. """ # ---raise if layout is in use--- if slide_layout.used_by_slides: @@ -447,16 +412,16 @@ def remove(self, slide_layout): class SlideMaster(_BaseMaster): - """ - Slide master object. Provides access to slide layouts. Access to - placeholders, regular shapes, and slide master-level properties is - inherited from |_BaseMaster|. + """Slide master object. + + Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level + properties is inherited from |_BaseMaster|. """ - __slots__ = ("_slide_layouts",) + _element: CT_SlideMaster # pyright: ignore[reportIncompatibleVariableOverride] @lazyproperty - def slide_layouts(self): + def slide_layouts(self) -> SlideLayouts: """|SlideLayouts| object providing access to this slide-master's layouts.""" return SlideLayouts(self._element.get_or_add_sldLayoutIdLst(), self) @@ -467,34 +432,27 @@ class SlideMasters(ParentedElementProxy): Has list access semantics, supporting indexed access, len(), and iteration. """ - __slots__ = ("_sldMasterIdLst",) + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] - def __init__(self, sldMasterIdLst, parent): + def __init__(self, sldMasterIdLst: CT_SlideMasterIdList, parent: Presentation): super(SlideMasters, self).__init__(sldMasterIdLst, parent) self._sldMasterIdLst = sldMasterIdLst - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. ``slide_masters[2]``). - """ + def __getitem__(self, idx: int) -> SlideMaster: + """Provides indexed access, e.g. `slide_masters[2]`.""" try: - sldMasterId = self._sldMasterIdLst[idx] + sldMasterId = self._sldMasterIdLst.sldMasterId_lst[idx] except IndexError: raise IndexError("slide master index out of range") return self.part.related_slide_master(sldMasterId.rId) def __iter__(self): - """ - Generate a reference to each of the |SlideMaster| instances in the - collection, in sequence. - """ - for smi in self._sldMasterIdLst: + """Generate each |SlideMaster| instance in the collection, in sequence.""" + for smi in self._sldMasterIdLst.sldMasterId_lst: yield self.part.related_slide_master(smi.rId) def __len__(self): - """ - Support len() built-in function (e.g. 'len(slide_masters) == 4'). - """ + """Support len() built-in function, e.g. `len(slide_masters) == 4`.""" return len(self._sldMasterIdLst) @@ -506,9 +464,7 @@ class _Background(ElementProxy): has a |_Background| object. """ - __slots__ = ("_cSld", "_fill") - - def __init__(self, cSld): + def __init__(self, cSld: CT_CommonSlideData): super(_Background, self).__init__(cSld) self._cSld = cSld diff --git a/pptx/spec.py b/src/pptx/spec.py similarity index 98% rename from pptx/spec.py rename to src/pptx/spec.py index 6c5607b00..e9d3b7d58 100644 --- a/pptx/spec.py +++ b/src/pptx/spec.py @@ -1,23 +1,34 @@ -# encoding: utf-8 - """Mappings from the ISO/IEC 29500 spec. Some of these are inferred from PowerPoint application behavior """ -from pptx.enum.shapes import MSO_SHAPE +from __future__ import annotations +from typing import TYPE_CHECKING, TypedDict + +from pptx.enum.shapes import MSO_SHAPE GRAPHIC_DATA_URI_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart" GRAPHIC_DATA_URI_OLEOBJ = "http://schemas.openxmlformats.org/presentationml/2006/ole" GRAPHIC_DATA_URI_TABLE = "http://schemas.openxmlformats.org/drawingml/2006/table" +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +AdjustmentValue: TypeAlias = "tuple[str, int]" + + +class ShapeSpec(TypedDict): + basename: str + avLst: tuple[AdjustmentValue, ...] + # ============================================================================ # AutoShape type specs # ============================================================================ -autoshape_types = { +autoshape_types: dict[MSO_SHAPE, ShapeSpec] = { MSO_SHAPE.ACTION_BUTTON_BACK_OR_PREVIOUS: { "basename": "Action Button: Back or Previous", "avLst": (), @@ -468,7 +479,7 @@ "basename": "Notched Right Arrow", "avLst": (("adj1", 50000), ("adj2", 50000)), }, - MSO_SHAPE.NO_SYMBOL: {"basename": '"No" symbol', "avLst": (("adj", 18750),)}, + MSO_SHAPE.NO_SYMBOL: {"basename": '"No" Symbol', "avLst": (("adj", 18750),)}, MSO_SHAPE.OCTAGON: {"basename": "Octagon", "avLst": (("adj", 29289),)}, MSO_SHAPE.OVAL: {"basename": "Oval", "avLst": ()}, MSO_SHAPE.OVAL_CALLOUT: { diff --git a/src/pptx/table.py b/src/pptx/table.py new file mode 100644 index 000000000..3bdf54ba6 --- /dev/null +++ b/src/pptx/table.py @@ -0,0 +1,496 @@ +"""Table-related objects such as Table and Cell.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator + +from pptx.dml.fill import FillFormat +from pptx.oxml.table import TcRange +from pptx.shapes import Subshape +from pptx.text.text import TextFrame +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from pptx.enum.text import MSO_VERTICAL_ANCHOR + from pptx.oxml.table import CT_Table, CT_TableCell, CT_TableCol, CT_TableRow + from pptx.parts.slide import BaseSlidePart + from pptx.shapes.graphfrm import GraphicFrame + from pptx.types import ProvidesPart + from pptx.util import Length + + +class Table(object): + """A DrawingML table object. + + Not intended to be constructed directly, use + :meth:`.Slide.shapes.add_table` to add a table to a slide. + """ + + def __init__(self, tbl: CT_Table, graphic_frame: GraphicFrame): + super(Table, self).__init__() + self._tbl = tbl + self._graphic_frame = graphic_frame + + def cell(self, row_idx: int, col_idx: int) -> _Cell: + """Return cell at `row_idx`, `col_idx`. + + Return value is an instance of |_Cell|. `row_idx` and `col_idx` are zero-based, e.g. + cell(0, 0) is the top, left cell in the table. + """ + return _Cell(self._tbl.tc(row_idx, col_idx), self) + + @lazyproperty + def columns(self) -> _ColumnCollection: + """|_ColumnCollection| instance for this table. + + Provides access to |_Column| objects representing the table's columns. |_Column| objects + are accessed using list notation, e.g. `col = tbl.columns[0]`. + """ + return _ColumnCollection(self._tbl, self) + + @property + def first_col(self) -> bool: + """When `True`, indicates first column should have distinct formatting. + + Read/write. Distinct formatting is used, for example, when the first column contains row + headings (is a side-heading column). + """ + return self._tbl.firstCol + + @first_col.setter + def first_col(self, value: bool): + self._tbl.firstCol = value + + @property + def first_row(self) -> bool: + """When `True`, indicates first row should have distinct formatting. + + Read/write. Distinct formatting is used, for example, when the first row contains column + headings. + """ + return self._tbl.firstRow + + @first_row.setter + def first_row(self, value: bool): + self._tbl.firstRow = value + + @property + def horz_banding(self) -> bool: + """When `True`, indicates rows should have alternating shading. + + Read/write. Used to allow rows to be traversed more easily without losing track of which + row is being read. + """ + return self._tbl.bandRow + + @horz_banding.setter + def horz_banding(self, value: bool): + self._tbl.bandRow = value + + def iter_cells(self) -> Iterator[_Cell]: + """Generate _Cell object for each cell in this table. + + Each grid cell is generated in left-to-right, top-to-bottom order. + """ + return (_Cell(tc, self) for tc in self._tbl.iter_tcs()) + + @property + def last_col(self) -> bool: + """When `True`, indicates the rightmost column should have distinct formatting. + + Read/write. Used, for example, when a row totals column appears at the far right of the + table. + """ + return self._tbl.lastCol + + @last_col.setter + def last_col(self, value: bool): + self._tbl.lastCol = value + + @property + def last_row(self) -> bool: + """When `True`, indicates the bottom row should have distinct formatting. + + Read/write. Used, for example, when a totals row appears as the bottom row. + """ + return self._tbl.lastRow + + @last_row.setter + def last_row(self, value: bool): + self._tbl.lastRow = value + + def notify_height_changed(self) -> None: + """Called by a row when its height changes. + + Triggers the graphic frame to recalculate its total height (as the sum of the row + heights). + """ + new_table_height = Emu(sum([row.height for row in self.rows])) + self._graphic_frame.height = new_table_height + + def notify_width_changed(self) -> None: + """Called by a column when its width changes. + + Triggers the graphic frame to recalculate its total width (as the sum of the column + widths). + """ + new_table_width = Emu(sum([col.width for col in self.columns])) + self._graphic_frame.width = new_table_width + + @property + def part(self) -> BaseSlidePart: + """The package part containing this table.""" + return self._graphic_frame.part + + @lazyproperty + def rows(self): + """|_RowCollection| instance for this table. + + Provides access to |_Row| objects representing the table's rows. |_Row| objects are + accessed using list notation, e.g. `col = tbl.rows[0]`. + """ + return _RowCollection(self._tbl, self) + + @property + def vert_banding(self) -> bool: + """When `True`, indicates columns should have alternating shading. + + Read/write. Used to allow columns to be traversed more easily without losing track of + which column is being read. + """ + return self._tbl.bandCol + + @vert_banding.setter + def vert_banding(self, value: bool): + self._tbl.bandCol = value + + +class _Cell(Subshape): + """Table cell""" + + def __init__(self, tc: CT_TableCell, parent: ProvidesPart): + super(_Cell, self).__init__(parent) + self._tc = tc + + def __eq__(self, other: object) -> bool: + """|True| if this object proxies the same element as `other`. + + 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, type(self)): + return False + return self._tc is other._tc + + def __ne__(self, other: object) -> bool: + if not isinstance(other, type(self)): + return True + return self._tc is not other._tc + + @lazyproperty + def fill(self) -> FillFormat: + """|FillFormat| instance for this cell. + + Provides access to fill properties such as foreground color. + """ + tcPr = self._tc.get_or_add_tcPr() + return FillFormat.from_fill_parent(tcPr) + + @property + def is_merge_origin(self) -> bool: + """True if this cell is the top-left grid cell in a merged cell.""" + return self._tc.is_merge_origin + + @property + def is_spanned(self) -> bool: + """True if this cell is spanned by a merge-origin cell. + + A merge-origin cell "spans" the other grid cells in its merge range, consuming their area + and "shadowing" the spanned grid cells. + + Note this value is |False| for a merge-origin cell. A merge-origin cell spans other grid + cells, but is not itself a spanned cell. + """ + return self._tc.is_spanned + + @property + def margin_left(self) -> Length: + """Left margin of cells. + + Read/write. If assigned |None|, the default value is used, 0.1 inches for left and right + margins and 0.05 inches for top and bottom. + """ + return self._tc.marL + + @margin_left.setter + def margin_left(self, margin_left: Length | None): + self._validate_margin_value(margin_left) + self._tc.marL = margin_left + + @property + def margin_right(self) -> Length: + """Right margin of cell.""" + return self._tc.marR + + @margin_right.setter + def margin_right(self, margin_right: Length | None): + self._validate_margin_value(margin_right) + self._tc.marR = margin_right + + @property + def margin_top(self) -> Length: + """Top margin of cell.""" + return self._tc.marT + + @margin_top.setter + def margin_top(self, margin_top: Length | None): + self._validate_margin_value(margin_top) + self._tc.marT = margin_top + + @property + def margin_bottom(self) -> Length: + """Bottom margin of cell.""" + return self._tc.marB + + @margin_bottom.setter + def margin_bottom(self, margin_bottom: Length | None): + self._validate_margin_value(margin_bottom) + self._tc.marB = margin_bottom + + def merge(self, other_cell: _Cell) -> None: + """Create merged cell from this cell to `other_cell`. + + This cell and `other_cell` specify opposite corners of the merged cell range. Either + diagonal of the cell region may be specified in either order, e.g. self=bottom-right, + other_cell=top-left, etc. + + Raises |ValueError| if the specified range already contains merged cells anywhere within + its extents or if `other_cell` is not in the same table as `self`. + """ + tc_range = TcRange(self._tc, other_cell._tc) + + if not tc_range.in_same_table: + raise ValueError("other_cell from different table") + if tc_range.contains_merged_cell: + raise ValueError("range contains one or more merged cells") + + tc_range.move_content_to_origin() + + row_count, col_count = tc_range.dimensions + + for tc in tc_range.iter_top_row_tcs(): + tc.rowSpan = row_count + for tc in tc_range.iter_left_col_tcs(): + tc.gridSpan = col_count + for tc in tc_range.iter_except_left_col_tcs(): + tc.hMerge = True + for tc in tc_range.iter_except_top_row_tcs(): + tc.vMerge = True + + @property + def span_height(self) -> int: + """int count of rows spanned by this cell. + + The value of this property may be misleading (often 1) on cells where `.is_merge_origin` + is not |True|, since only a merge-origin cell contains complete span information. This + property is only intended for use on cells known to be a merge origin by testing + `.is_merge_origin`. + """ + return self._tc.rowSpan + + @property + def span_width(self) -> int: + """int count of columns spanned by this cell. + + The value of this property may be misleading (often 1) on cells where `.is_merge_origin` + is not |True|, since only a merge-origin cell contains complete span information. This + property is only intended for use on cells known to be a merge origin by testing + `.is_merge_origin`. + """ + return self._tc.gridSpan + + def split(self) -> None: + """Remove merge from this (merge-origin) cell. + + The merged cell represented by this object will be "unmerged", yielding a separate + unmerged cell for each grid cell previously spanned by this merge. + + Raises |ValueError| when this cell is not a merge-origin cell. Test with + `.is_merge_origin` before calling. + """ + if not self.is_merge_origin: + raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit") + + tc_range = TcRange.from_merge_origin(self._tc) + + for tc in tc_range.iter_tcs(): + tc.rowSpan = tc.gridSpan = 1 + tc.hMerge = tc.vMerge = False + + @property + def text(self) -> str: + """Textual content of cell as a single string. + + The returned string will contain a newline character (`"\\n"`) separating each paragraph + and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the + cell's text. + + Assignment to `text` replaces all text currently contained in the cell. A newline + character (`"\\n"`) in the assigned text causes a new paragraph to be started. A + vertical-tab (`"\\v"`) character in the assigned text causes a line-break (soft + carriage-return) to be inserted. (The vertical-tab character appears in clipboard text + copied from PowerPoint as its encoding of line-breaks.) + """ + return self.text_frame.text + + @text.setter + def text(self, text: str): + self.text_frame.text = text + + @property + def text_frame(self) -> TextFrame: + """|TextFrame| containing the text that appears in the cell.""" + txBody = self._tc.get_or_add_txBody() + return TextFrame(txBody, self) + + @property + def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None: + """Vertical alignment of this cell. + + This value is a member of the :ref:`MsoVerticalAnchor` enumeration or |None|. A value of + |None| indicates the cell has no explicitly applied vertical anchor setting and its + effective value is inherited from its style-hierarchy ancestors. + + Assigning |None| to this property causes any explicitly applied vertical anchor setting to + be cleared and inheritance of its effective value to be restored. + """ + return self._tc.anchor + + @vertical_anchor.setter + def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None): + self._tc.anchor = mso_anchor_idx + + @staticmethod + def _validate_margin_value(margin_value: Length | None) -> None: + """Raise ValueError if `margin_value` is not a positive integer value or |None|.""" + if not isinstance(margin_value, int) and margin_value is not None: + tmpl = "margin value must be integer or None, got '%s'" + raise TypeError(tmpl % margin_value) + + +class _Column(Subshape): + """Table column""" + + def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection): + super(_Column, self).__init__(parent) + self._parent = parent + self._gridCol = gridCol + + @property + def width(self) -> Length: + """Width of column in EMU.""" + return self._gridCol.w + + @width.setter + def width(self, width: Length): + self._gridCol.w = width + self._parent.notify_width_changed() + + +class _Row(Subshape): + """Table row""" + + def __init__(self, tr: CT_TableRow, parent: _RowCollection): + super(_Row, self).__init__(parent) + self._parent = parent + self._tr = tr + + @property + def cells(self): + """Read-only reference to collection of cells in row. + + An individual cell is referenced using list notation, e.g. `cell = row.cells[0]`. + """ + return _CellCollection(self._tr, self) + + @property + def height(self) -> Length: + """Height of row in EMU.""" + return self._tr.h + + @height.setter + def height(self, height: Length): + self._tr.h = height + self._parent.notify_height_changed() + + +class _CellCollection(Subshape): + """Horizontal sequence of row cells""" + + def __init__(self, tr: CT_TableRow, parent: _Row): + super(_CellCollection, self).__init__(parent) + self._parent = parent + self._tr = tr + + def __getitem__(self, idx: int) -> _Cell: + """Provides indexed access, (e.g. 'cells[0]').""" + if idx < 0 or idx >= len(self._tr.tc_lst): + msg = "cell index [%d] out of range" % idx + raise IndexError(msg) + return _Cell(self._tr.tc_lst[idx], self) + + def __iter__(self) -> Iterator[_Cell]: + """Provides iterability.""" + return (_Cell(tc, self) for tc in self._tr.tc_lst) + + def __len__(self) -> int: + """Supports len() function (e.g. 'len(cells) == 1').""" + return len(self._tr.tc_lst) + + +class _ColumnCollection(Subshape): + """Sequence of table columns.""" + + def __init__(self, tbl: CT_Table, parent: Table): + super(_ColumnCollection, self).__init__(parent) + self._parent = parent + self._tbl = tbl + + def __getitem__(self, idx: int): + """Provides indexed access, (e.g. 'columns[0]').""" + if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst): + msg = "column index [%d] out of range" % idx + raise IndexError(msg) + return _Column(self._tbl.tblGrid.gridCol_lst[idx], self) + + def __len__(self): + """Supports len() function (e.g. 'len(columns) == 1').""" + return len(self._tbl.tblGrid.gridCol_lst) + + def notify_width_changed(self): + """Called by a column when its width changes. Pass along to parent.""" + self._parent.notify_width_changed() + + +class _RowCollection(Subshape): + """Sequence of table rows""" + + def __init__(self, tbl: CT_Table, parent: Table): + super(_RowCollection, self).__init__(parent) + self._parent = parent + self._tbl = tbl + + def __getitem__(self, idx: int) -> _Row: + """Provides indexed access, (e.g. 'rows[0]').""" + if idx < 0 or idx >= len(self): + msg = "row index [%d] out of range" % idx + raise IndexError(msg) + return _Row(self._tbl.tr_lst[idx], self) + + def __len__(self): + """Supports len() function (e.g. 'len(rows) == 1').""" + return len(self._tbl.tr_lst) + + def notify_height_changed(self): + """Called by a row when its height changes. Pass along to parent.""" + self._parent.notify_height_changed() diff --git a/pptx/templates/default.pptx b/src/pptx/templates/default.pptx similarity index 100% rename from pptx/templates/default.pptx rename to src/pptx/templates/default.pptx diff --git a/pptx/templates/docx-icon.emf b/src/pptx/templates/docx-icon.emf similarity index 100% rename from pptx/templates/docx-icon.emf rename to src/pptx/templates/docx-icon.emf diff --git a/pptx/templates/generic-icon.emf b/src/pptx/templates/generic-icon.emf similarity index 100% rename from pptx/templates/generic-icon.emf rename to src/pptx/templates/generic-icon.emf diff --git a/pptx/templates/notes.xml b/src/pptx/templates/notes.xml similarity index 100% rename from pptx/templates/notes.xml rename to src/pptx/templates/notes.xml diff --git a/pptx/templates/notesMaster.xml b/src/pptx/templates/notesMaster.xml similarity index 100% rename from pptx/templates/notesMaster.xml rename to src/pptx/templates/notesMaster.xml diff --git a/pptx/templates/pptx-icon.emf b/src/pptx/templates/pptx-icon.emf similarity index 100% rename from pptx/templates/pptx-icon.emf rename to src/pptx/templates/pptx-icon.emf diff --git a/pptx/templates/theme.xml b/src/pptx/templates/theme.xml similarity index 100% rename from pptx/templates/theme.xml rename to src/pptx/templates/theme.xml diff --git a/pptx/templates/xlsx-icon.emf b/src/pptx/templates/xlsx-icon.emf similarity index 100% rename from pptx/templates/xlsx-icon.emf rename to src/pptx/templates/xlsx-icon.emf diff --git a/pptx/text/__init__.py b/src/pptx/text/__init__.py similarity index 100% rename from pptx/text/__init__.py rename to src/pptx/text/__init__.py diff --git a/pptx/text/fonts.py b/src/pptx/text/fonts.py similarity index 87% rename from pptx/text/fonts.py rename to src/pptx/text/fonts.py index ceb070cc8..5ae054a83 100644 --- a/pptx/text/fonts.py +++ b/src/pptx/text/fonts.py @@ -1,31 +1,24 @@ -# encoding: utf-8 +"""Objects related to system font file lookup.""" -""" -Objects related to system font file lookup. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import os import sys - from struct import calcsize, unpack_from -from ..util import lazyproperty +from pptx.util import lazyproperty class FontFiles(object): - """ - A class-based singleton serving as a lazy cache for system font details. - """ + """A class-based singleton serving as a lazy cache for system font details.""" _font_files = None @classmethod - def find(cls, family_name, is_bold, is_italic): - """ - Return the absolute path to the installed OpenType font having - *family_name* and the styles *is_bold* and *is_italic*. + def find(cls, family_name: str, is_bold: bool, is_italic: bool) -> str: + """Return the absolute path to an installed OpenType font. + + File is matched by `family_name` and the styles `is_bold` and `is_italic`. """ if cls._font_files is None: cls._font_files = cls._installed_fonts() @@ -156,9 +149,9 @@ def family_name(self): @lazyproperty def _fields(self): - """ - A 5-tuple containing the fields read from the font file header, also - known as the offset table. + """5-tuple containing the fields read from the font file header. + + Also known as the offset table. """ # sfnt_version, tbl_count, search_range, entry_selector, range_shift return self._stream.read_fields(">4sHHHH", 0) @@ -196,20 +189,14 @@ def _table_count(self): class _Stream(object): - """ - A thin wrapper around a file that facilitates reading C-struct values - from a binary file. - """ + """A thin wrapper around a binary file that facilitates reading C-struct values.""" def __init__(self, file): self._file = file @classmethod def open(cls, path): - """ - Return a |_Stream| providing binary access to the contents of the - file at *path*. - """ + """Return |_Stream| providing binary access to contents of file at `path`.""" return cls(open(path, "rb")) def close(self): @@ -328,18 +315,16 @@ def _decode_name(raw_name, platform_id, encoding_id): return None def _iter_names(self): - """ - Generate a key/value pair for each name in this table. The key is a - (platform_id, name_id) 2-tuple and the value is the unicode text + """Generate a key/value pair for each name in this table. + + The key is a (platform_id, name_id) 2-tuple and the value is the unicode text corresponding to that key. """ table_format, count, strings_offset = self._table_header table_bytes = self._table_bytes for idx in range(count): - platform_id, name_id, name = self._read_name( - table_bytes, idx, strings_offset - ) + platform_id, name_id, name = self._read_name(table_bytes, idx, strings_offset) if name is None: continue yield ((platform_id, name_id), name) @@ -364,18 +349,14 @@ def _raw_name_string(bufr, strings_offset, str_offset, length): return unpack_from(tmpl, bufr, offset)[0] def _read_name(self, bufr, idx, strings_offset): + """Return a (platform_id, name_id, name) 3-tuple for name at `idx` in `bufr`. + + The triple looks like (0, 1, 'Arial'). `strings_offset` is the for the name at + `idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual + name strings begin. The returned name is a unicode string. """ - Return a (platform_id, name_id, name) 3-tuple like (0, 1, 'Arial') - for the name at *idx* position in *bufr*. *strings_offset* is the - index into *bufr* where actual name strings begin. The returned name - is a unicode string. - """ - platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header( - bufr, idx - ) - name = self._read_name_text( - bufr, platform_id, enc_id, strings_offset, str_offset, length - ) + platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(bufr, idx) + name = self._read_name_text(bufr, platform_id, enc_id, strings_offset, str_offset, length) return platform_id, name_id, name def _read_name_text( @@ -405,10 +386,7 @@ def _table_header(self): @lazyproperty def _names(self): - """ - A mapping of (platform_id, name_id) keys to string names for this - font. - """ + """A mapping of (platform_id, name_id) keys to string names for this font.""" return dict(self._iter_names()) diff --git a/pptx/text/layout.py b/src/pptx/text/layout.py similarity index 86% rename from pptx/text/layout.py rename to src/pptx/text/layout.py index 290b83a5a..d2b439939 100644 --- a/pptx/text/layout.py +++ b/src/pptx/text/layout.py @@ -1,29 +1,31 @@ -# encoding: utf-8 +"""Objects related to layout of rendered text, such as TextFitter.""" -""" -Objects related to layout of rendered text, such as TextFitter. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function +from typing import TYPE_CHECKING from PIL import ImageFont +if TYPE_CHECKING: + from pptx.util import Length + class TextFitter(tuple): - """ - Value object that knows how to fit text into given rectangular extents. - """ + """Value object that knows how to fit text into given rectangular extents.""" def __new__(cls, line_source, extents, font_file): width, height = extents return tuple.__new__(cls, (line_source, width, height, font_file)) @classmethod - def best_fit_font_size(cls, text, extents, max_size, font_file): - """ - Return the largest whole-number point size less than or equal to - *max_size* that allows *text* to fit completely within *extents* when - rendered using font defined in *font_file*. + def best_fit_font_size( + cls, text: str, extents: tuple[Length, Length], max_size: int, font_file: str + ) -> int: + """Return whole-number best fit point size less than or equal to `max_size`. + + The return value is the largest whole-number point size less than or equal to + `max_size` that allows `text` to fit completely within `extents` when rendered + using font defined in `font_file`. """ line_source = _LineSource(text) text_fitter = cls(line_source, extents, font_file) @@ -67,17 +69,17 @@ def predicate(line): @property def _fits_inside_predicate(self): - """ - Return a function taking an integer point size argument that returns - |True| if the text in this fitter can be wrapped to fit entirely - within its extents when rendered at that point size. + """Return function taking an integer point size argument. + + The function returns |True| if the text in this fitter can be wrapped to fit + entirely within its extents when rendered at that point size. """ def predicate(point_size): - """ - Return |True| if the text in *line_source* can be wrapped to fit - entirely within *extents* when rendered at *point_size* using the - font defined in *font_file*. + """Return |True| when text in `line_source` can be wrapped to fit. + + Fit means text can be broken into lines that fit entirely within `extents` + when rendered at `point_size` using the font defined in `font_file`. """ text_lines = self._wrap_lines(self._line_source, point_size) cy = _rendered_size("Ty", point_size, self._font_file)[1] @@ -297,9 +299,7 @@ class _Fonts(object): @classmethod def font(cls, font_path, point_size): if (font_path, point_size) not in cls.fonts: - cls.fonts[(font_path, point_size)] = ImageFont.truetype( - font_path, point_size - ) + cls.fonts[(font_path, point_size)] = ImageFont.truetype(font_path, point_size) return cls.fonts[(font_path, point_size)] @@ -313,7 +313,11 @@ def _rendered_size(text, point_size, font_file): px_per_inch = 72.0 font = _Fonts.font(font_file, point_size) - px_width, px_height = font.getsize(text) + try: + px_width, px_height = font.getsize(text) + except AttributeError: + left, top, right, bottom = font.getbbox(text) + px_width, px_height = right - left, bottom - top emu_width = int(px_width / px_per_inch * emu_per_inch) emu_height = int(px_height / px_per_inch * emu_per_inch) diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py new file mode 100644 index 000000000..e139410c2 --- /dev/null +++ b/src/pptx/text/text.py @@ -0,0 +1,681 @@ +"""Text-related objects such as TextFrame and Paragraph.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator, cast + +from pptx.dml.fill import FillFormat +from pptx.enum.dml import MSO_FILL +from pptx.enum.lang import MSO_LANGUAGE_ID +from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.oxml.simpletypes import ST_TextWrappingType +from pptx.shapes import Subshape +from pptx.text.fonts import FontFiles +from pptx.text.layout import TextFitter +from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty + +if TYPE_CHECKING: + from pptx.dml.color import ColorFormat + from pptx.enum.text import ( + MSO_TEXT_UNDERLINE_TYPE, + MSO_VERTICAL_ANCHOR, + PP_PARAGRAPH_ALIGNMENT, + ) + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.text import ( + CT_RegularTextRun, + CT_TextBody, + CT_TextCharacterProperties, + CT_TextParagraph, + CT_TextParagraphProperties, + ) + from pptx.types import ProvidesExtents, ProvidesPart + + +class TextFrame(Subshape): + """The part of a shape that contains its text. + + Not all shapes have a text frame. Corresponds to the `p:txBody` element that can + appear as a child element of `p:sp`. Not intended to be constructed directly. + """ + + def __init__(self, txBody: CT_TextBody, parent: ProvidesPart): + super(TextFrame, self).__init__(parent) + self._element = self._txBody = txBody + self._parent = parent + + def add_paragraph(self): + """ + Return new |_Paragraph| instance appended to the sequence of + paragraphs contained in this text frame. + """ + p = self._txBody.add_p() + return _Paragraph(p, self) + + @property + def auto_size(self) -> MSO_AUTO_SIZE | None: + """Resizing strategy used to fit text within this shape. + + Determins the type of automatic resizing used to fit the text of this shape within its + bounding box when the text would otherwise extend beyond the shape boundaries. May be + |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or + `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`. + """ + return self._bodyPr.autofit + + @auto_size.setter + def auto_size(self, value: MSO_AUTO_SIZE | None): + self._bodyPr.autofit = value + + def clear(self): + """Remove all paragraphs except one empty one.""" + for p in self._txBody.p_lst[1:]: + self._txBody.remove(p) + p = self.paragraphs[0] + p.clear() + + def fit_text( + self, + font_family: str = "Calibri", + max_size: int = 18, + bold: bool = False, + italic: bool = False, + font_file: str | None = None, + ): + """Fit text-frame text entirely within bounds of its shape. + + Make the text in this text frame fit entirely within the bounds of its shape by setting + word wrap on and applying the "best-fit" font size to all the text it contains. + + :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not + be set larger than `max_size` points. If the path to a matching TrueType font is provided + as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|, + best efforts are made to locate a font file with matchhing `font_family`, `bold`, and + `italic` installed on the current system (usually succeeds if the font is installed). + """ + # ---no-op when empty as fit behavior not defined for that case--- + if self.text == "": + return # pragma: no cover + + font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file) + self._apply_fit(font_family, font_size, bold, italic) + + @property + def margin_bottom(self) -> Length: + """|Length| value representing the inset of text from the bottom text frame border. + + :meth:`pptx.util.Inches` provides a convenient way of setting the value, e.g. + `text_frame.margin_bottom = Inches(0.05)`. + """ + return self._bodyPr.bIns + + @margin_bottom.setter + def margin_bottom(self, emu: Length): + self._bodyPr.bIns = emu + + @property + def margin_left(self) -> Length: + """Inset of text from left text frame border as |Length| value.""" + return self._bodyPr.lIns + + @margin_left.setter + def margin_left(self, emu: Length): + self._bodyPr.lIns = emu + + @property + def margin_right(self) -> Length: + """Inset of text from right text frame border as |Length| value.""" + return self._bodyPr.rIns + + @margin_right.setter + def margin_right(self, emu: Length): + self._bodyPr.rIns = emu + + @property + def margin_top(self) -> Length: + """Inset of text from top text frame border as |Length| value.""" + return self._bodyPr.tIns + + @margin_top.setter + def margin_top(self, emu: Length): + self._bodyPr.tIns = emu + + @property + def paragraphs(self) -> tuple[_Paragraph, ...]: + """Sequence of paragraphs in this text frame. + + A text frame always contains at least one paragraph. + """ + return tuple([_Paragraph(p, self) for p in self._txBody.p_lst]) + + @property + def text(self) -> str: + """All text in this text-frame as a single string. + + Read/write. The return value contains all text in this text-frame. A line-feed character + (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears + for each line break (aka. soft carriage-return) encountered. + + The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard + text, which is why that encoding was chosen. + + Assignment replaces all text in the text frame. A new paragraph is added for each line-feed + character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each + vertical-tab character (`"\\v"`) encountered. + + Any control character other than newline, tab, or vertical-tab are escaped as plain-text + like "_x001B_" (for ESC (ASCII 32) in this example). + """ + return "\n".join(paragraph.text for paragraph in self.paragraphs) + + @text.setter + def text(self, text: str): + txBody = self._txBody + txBody.clear_content() + for p_text in text.split("\n"): + p = txBody.add_p() + p.append_text(p_text) + + @property + def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None: + """Represents the vertical alignment of text in this text frame. + + |None| indicates the effective value should be inherited from this object's style hierarchy. + """ + return self._txBody.bodyPr.anchor + + @vertical_anchor.setter + def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None): + bodyPr = self._txBody.bodyPr + bodyPr.anchor = value + + @property + def word_wrap(self) -> bool | None: + """`True` when lines of text in this shape are wrapped to fit within the shape's width. + + Read-write. Valid values are True, False, or None. True and False turn word wrap on and + off, respectively. Assigning None to word wrap causes any word wrap setting to be removed + from the text frame, causing it to inherit this setting from its style hierarchy. + """ + return { + ST_TextWrappingType.SQUARE: True, + ST_TextWrappingType.NONE: False, + None: None, + }[self._txBody.bodyPr.wrap] + + @word_wrap.setter + def word_wrap(self, value: bool | None): + if value not in (True, False, None): + raise ValueError( # pragma: no cover + "assigned value must be True, False, or None, got %s" % value + ) + self._txBody.bodyPr.wrap = { + True: ST_TextWrappingType.SQUARE, + False: ST_TextWrappingType.NONE, + None: None, + }[value] + + def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool): + """Arrange text in this text frame to fit inside its extents. + + This is accomplished by setting auto size off, wrap on, and setting the font of + all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`. + """ + self.auto_size = MSO_AUTO_SIZE.NONE + self.word_wrap = True + self._set_font(font_family, font_size, is_bold, is_italic) + + def _best_fit_font_size( + self, family: str, max_size: int, bold: bool, italic: bool, font_file: str | None + ) -> int: + """Return font-size in points that best fits text in this text-frame. + + The best-fit font size is the largest integer point size not greater than `max_size` that + allows all the text in this text frame to fit inside its extents when rendered using the + font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used + to calculate the fit, whether or not it matches `family`, `bold`, and `italic`. + """ + if font_file is None: + font_file = FontFiles.find(family, bold, italic) + return TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file) + + @property + def _bodyPr(self): + return self._txBody.bodyPr + + @property + def _extents(self) -> tuple[Length, Length]: + """(cx, cy) 2-tuple representing the effective rendering area of this text-frame. + + Margins are taken into account. + """ + parent = cast("ProvidesExtents", self._parent) + return ( + Length(parent.width - self.margin_left - self.margin_right), + Length(parent.height - self.margin_top - self.margin_bottom), + ) + + def _set_font(self, family: str, size: int, bold: bool, italic: bool): + """Set the font properties of all the text in this text frame.""" + + def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]: + for p in txBody.p_lst: + for elm in p.content_children: + yield elm.get_or_add_rPr() + # generate a:endParaRPr for each element + yield p.get_or_add_endParaRPr() + + def set_rPr_font( + rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool + ): + f = Font(rPr) + f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic + + txBody = self._element + for rPr in iter_rPrs(txBody): + set_rPr_font(rPr, family, size, bold, italic) + + +class Font(object): + """Character properties object, providing font size, font name, bold, italic, etc. + + Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and + `a:endParaRPr` in paragraph and `a:defRPr` in list style elements. + """ + + def __init__(self, rPr: CT_TextCharacterProperties): + super(Font, self).__init__() + self._element = self._rPr = rPr + + @property + def bold(self) -> bool | None: + """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`. + + If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's + setting, or a setting in a style or master. Returns None if no bold attribute is present, + meaning the effective bold value is inherited from a master or the theme. + """ + return self._rPr.b + + @bold.setter + def bold(self, value: bool | None): + self._rPr.b = value + + @lazyproperty + def color(self) -> ColorFormat: + """The |ColorFormat| instance that provides access to the color settings for this font.""" + if self.fill.type != MSO_FILL.SOLID: + self.fill.solid() + return self.fill.fore_color + + @lazyproperty + def fill(self) -> FillFormat: + """|FillFormat| instance for this font. + + Provides access to fill properties such as fill color. + """ + return FillFormat.from_fill_parent(self._rPr) + + @property + def italic(self) -> bool | None: + """Get or set boolean italic value of |Font| instance. + + Has the same behaviors as bold with respect to None values. + """ + return self._rPr.i + + @italic.setter + def italic(self, value: bool | None): + self._rPr.i = value + + @property + def language_id(self) -> MSO_LANGUAGE_ID | None: + """Get or set the language id of this |Font| instance. + + The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None| + removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`. + """ + lang = self._rPr.lang + if lang is None: + return MSO_LANGUAGE_ID.NONE + return self._rPr.lang + + @language_id.setter + def language_id(self, value: MSO_LANGUAGE_ID | None): + if value == MSO_LANGUAGE_ID.NONE: + value = None + self._rPr.lang = value + + @property + def name(self) -> str | None: + """Get or set the typeface name for this |Font| instance. + + Causes the text it controls to appear in the named font, if a matching font is found. + Returns |None| if the typeface is currently inherited from the theme. Setting it to |None| + removes any override of the theme typeface. + """ + latin = self._rPr.latin + if latin is None: + return None + return latin.typeface + + @name.setter + def name(self, value: str | None): + if value is None: + self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage] + else: + latin = self._rPr.get_or_add_latin() + latin.typeface = value + + @property + def size(self) -> Length | None: + """Indicates the font height in English Metric Units (EMU). + + Read/write. |None| indicates the font size should be inherited from its style hierarchy, + such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int| + having properties for convenient conversion into points or other length units. Likewise, + the :class:`pptx.util.Pt` class allows convenient specification of point values:: + + >>> font.size = Pt(24) + >>> font.size + 304800 + >>> font.size.pt + 24.0 + """ + sz = self._rPr.sz + if sz is None: + return None + return Centipoints(sz) + + @size.setter + def size(self, emu: Length | None): + if emu is None: + self._rPr.sz = None + else: + sz = Emu(emu).centipoints + self._rPr.sz = sz + + @property + def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None: + """Indicaties the underline setting for this font. + + Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType` + enumeration. |None| is the default and indicates the underline setting should be inherited + from the style hierarchy, such as from a placeholder. |True| indicates single underline. + |False| indicates no underline. Other settings such as double and wavy underlining are + indicated with members of the :ref:`MsoTextUnderlineType` enumeration. + """ + u = self._rPr.u + if u is MSO_UNDERLINE.NONE: + return False + if u is MSO_UNDERLINE.SINGLE_LINE: + return True + return u + + @underline.setter + def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None): + if value is True: + value = MSO_UNDERLINE.SINGLE_LINE + elif value is False: + value = MSO_UNDERLINE.NONE + self._element.u = value + + +class _Hyperlink(Subshape): + """Text run hyperlink object. + + Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`). + """ + + def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart): + super(_Hyperlink, self).__init__(parent) + self._rPr = rPr + + @property + def address(self) -> str | None: + """The URL of the hyperlink. + + Read/write. URL can be on http, https, mailto, or file scheme; others may work. + """ + if self._hlinkClick is None: + return None + return self.part.target_ref(self._hlinkClick.rId) + + @address.setter + def address(self, url: str | None): + # implements all three of add, change, and remove hyperlink + if self._hlinkClick is not None: + self._remove_hlinkClick() + if url: + self._add_hlinkClick(url) + + def _add_hlinkClick(self, url: str): + rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True) + self._rPr.add_hlinkClick(rId) + + @property + def _hlinkClick(self) -> CT_Hyperlink | None: + return self._rPr.hlinkClick + + def _remove_hlinkClick(self): + assert self._hlinkClick is not None + self.part.drop_rel(self._hlinkClick.rId) + self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage] + + +class _Paragraph(Subshape): + """Paragraph object. Not intended to be constructed directly.""" + + def __init__(self, p: CT_TextParagraph, parent: ProvidesPart): + super(_Paragraph, self).__init__(parent) + self._element = self._p = p + + def add_line_break(self): + """Add line break at end of this paragraph.""" + self._p.add_br() + + def add_run(self) -> _Run: + """Return a new run appended to the runs in this paragraph.""" + r = self._p.add_r() + return _Run(r, self) + + @property + def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None: + """Horizontal alignment of this paragraph. + + The value |None| indicates the paragraph should 'inherit' its effective value from its + style hierarchy. Assigning |None| removes any explicit setting, causing its inherited + value to be used. + """ + return self._pPr.algn + + @alignment.setter + def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None): + self._pPr.algn = value + + def clear(self): + """Remove all content from this paragraph. + + Paragraph properties are preserved. Content includes runs, line breaks, and fields. + """ + for elm in self._element.content_children: + self._element.remove(elm) + return self + + @property + def font(self) -> Font: + """|Font| object containing default character properties for the runs in this paragraph. + + These character properties override default properties inherited from parent objects such + as the text frame the paragraph is contained in and they may be overridden by character + properties set at the run level. + """ + return Font(self._defRPr) + + @property + def level(self) -> int: + """Indentation level of this paragraph. + + Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the + default value. Indentation level is most commonly encountered in a bulleted list, as is + found on a word bullet slide. + """ + return self._pPr.lvl + + @level.setter + def level(self, level: int): + self._pPr.lvl = level + + @property + def line_spacing(self) -> int | float | Length | None: + """The space between baselines in successive lines of this paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`, + 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. + """ + pPr = self._p.pPr + if pPr is None: + return None + return pPr.line_spacing + + @line_spacing.setter + def line_spacing(self, value: int | float | Length | None): + pPr = self._p.get_or_add_pPr() + pPr.line_spacing = value + + @property + def runs(self) -> tuple[_Run, ...]: + """Sequence of runs in this paragraph.""" + return tuple(_Run(r, self) for r in self._element.r_lst) + + @property + def space_after(self) -> Length | None: + """The spacing to appear between this paragraph and the subsequent paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. |Length| objects provide convenience + properties, such as `.pt` and `.inches`, that allow easy conversion to various length + units. + """ + pPr = self._p.pPr + if pPr is None: + return None + return pPr.space_after + + @space_after.setter + def space_after(self, value: Length | None): + pPr = self._p.get_or_add_pPr() + pPr.space_after = value + + @property + def space_before(self) -> Length | None: + """The spacing to appear between this paragraph and the prior paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. |Length| objects provide convenience + properties, such as `.pt` and `.cm`, that allow easy conversion to various length units. + """ + pPr = self._p.pPr + if pPr is None: + return None + return pPr.space_before + + @space_before.setter + def space_before(self, value: Length | None): + pPr = self._p.get_or_add_pPr() + pPr.space_before = value + + @property + def text(self) -> str: + """Text of paragraph as a single string. + + Read/write. This value is formed by concatenating the text in each run and field making up + the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element + (``, soft carriage-return) encountered. + + While the encoding of line-breaks as a vertical tab might be surprising at first, doing so + is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be + distinguished from a paragraph boundary within the str return value. + + Assignment causes all content in the paragraph to be replaced. Each vertical-tab character + (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed + character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter. + If line-feed characters are intended to produce new paragraphs, use `TextFrame.text` + instead. Any other control characters in the assigned string are escaped as a hex + representation like "_x001B_" (for ESC (ASCII 27) in this example). + """ + return "".join(elm.text for elm in self._element.content_children) + + @text.setter + def text(self, text: str): + self.clear() + self._element.append_text(text) + + @property + def _defRPr(self) -> CT_TextCharacterProperties: + """The element that defines the default run properties for runs in this paragraph. + + Causes the element to be added if not present. + """ + return self._pPr.get_or_add_defRPr() + + @property + def _pPr(self) -> CT_TextParagraphProperties: + """Contains the properties for this paragraph. + + Causes the element to be added if not present. + """ + return self._p.get_or_add_pPr() + + +class _Run(Subshape): + """Text run object. Corresponds to `a:r` child element in a paragraph.""" + + def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart): + super(_Run, self).__init__(parent) + self._r = r + + @property + def font(self): + """|Font| instance containing run-level character properties for the text in this run. + + Character properties can be and perhaps most often are inherited from parent objects such + as the paragraph and slide layout the run is contained in. Only those specifically + overridden at the run level are contained in the font object. + """ + rPr = self._r.get_or_add_rPr() + return Font(rPr) + + @lazyproperty + def hyperlink(self) -> _Hyperlink: + """Proxy for any `a:hlinkClick` element under the run properties element. + + Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is + present or not, and creates or deletes that element as appropriate in response to actions + on its methods and attributes. + """ + rPr = self._r.get_or_add_rPr() + return _Hyperlink(rPr, self) + + @property + def text(self): + """Read/write. A unicode string containing the text in this run. + + Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII + string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to + unicode assuming UTF-8 encoding. + + Any other control characters in the assigned string other than tab or newline + are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as + "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with + respect to line-feed and vertical-tab characters. + """ + return self._r.text + + @text.setter + def text(self, text: str): + self._r.text = text diff --git a/src/pptx/types.py b/src/pptx/types.py new file mode 100644 index 000000000..46d86661b --- /dev/null +++ b/src/pptx/types.py @@ -0,0 +1,36 @@ +"""Abstract types used by `python-pptx`.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from typing_extensions import Protocol + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.util import Length + + +class ProvidesExtents(Protocol): + """An object that has width and height.""" + + @property + def height(self) -> Length: + """Distance between top and bottom extents of shape in EMUs.""" + ... + + @property + def width(self) -> Length: + """Distance between left and right extents of shape in EMUs.""" + ... + + +class ProvidesPart(Protocol): + """An object that provides access to its XmlPart. + + This type is for objects that need access to their part, possibly because they need access to + the package or related parts. + """ + + @property + def part(self) -> XmlPart: ... diff --git a/src/pptx/util.py b/src/pptx/util.py new file mode 100644 index 000000000..fdec79298 --- /dev/null +++ b/src/pptx/util.py @@ -0,0 +1,214 @@ +"""Utility functions and classes.""" + +from __future__ import annotations + +import functools +from typing import Any, Callable, Generic, TypeVar, cast + + +class Length(int): + """Base class for length classes Inches, Emu, Cm, Mm, and Pt. + + Provides properties for converting length values to convenient units. + """ + + _EMUS_PER_INCH = 914400 + _EMUS_PER_CENTIPOINT = 127 + _EMUS_PER_CM = 360000 + _EMUS_PER_MM = 36000 + _EMUS_PER_PT = 12700 + + def __new__(cls, emu: int): + return int.__new__(cls, emu) + + @property + def inches(self) -> float: + """Floating point length in inches.""" + return self / float(self._EMUS_PER_INCH) + + @property + def centipoints(self) -> int: + """Integer length in hundredths of a point (1/7200 inch). + + Used internally because PowerPoint stores font size in centipoints. + """ + return self // self._EMUS_PER_CENTIPOINT + + @property + def cm(self) -> float: + """Floating point length in centimeters.""" + return self / float(self._EMUS_PER_CM) + + @property + def emu(self) -> int: + """Integer length in English Metric Units.""" + return self + + @property + def mm(self) -> float: + """Floating point length in millimeters.""" + return self / float(self._EMUS_PER_MM) + + @property + def pt(self) -> float: + """Floating point length in points.""" + return self / float(self._EMUS_PER_PT) + + +class Inches(Length): + """Convenience constructor for length in inches.""" + + def __new__(cls, inches: float): + emu = int(inches * Length._EMUS_PER_INCH) + return Length.__new__(cls, emu) + + +class Centipoints(Length): + """Convenience constructor for length in hundredths of a point.""" + + def __new__(cls, centipoints: int): + emu = int(centipoints * Length._EMUS_PER_CENTIPOINT) + return Length.__new__(cls, emu) + + +class Cm(Length): + """Convenience constructor for length in centimeters.""" + + def __new__(cls, cm: float): + emu = int(cm * Length._EMUS_PER_CM) + return Length.__new__(cls, emu) + + +class Emu(Length): + """Convenience constructor for length in english metric units.""" + + def __new__(cls, emu: int): + return Length.__new__(cls, int(emu)) + + +class Mm(Length): + """Convenience constructor for length in millimeters.""" + + def __new__(cls, mm: float): + emu = int(mm * Length._EMUS_PER_MM) + return Length.__new__(cls, emu) + + +class Pt(Length): + """Convenience value class for specifying a length in points.""" + + def __new__(cls, points: float): + emu = int(points * Length._EMUS_PER_PT) + return Length.__new__(cls, emu) + + +_T = TypeVar("_T") + + +class lazyproperty(Generic[_T]): + """Decorator like @property, but evaluated only on first access. + + Like @property, this can only be used to decorate methods having only a `self` parameter, and + is accessed like an attribute on an instance, i.e. trailing parentheses are not used. Unlike + @property, the decorated method is only evaluated on first access; the resulting value is + cached and that same value returned on second and later access without re-evaluation of the + method. + + Like @property, this class produces a *data descriptor* object, which is stored in the __dict__ + of the *class* under the name of the decorated method ('fget' nominally). The cached value is + stored in the __dict__ of the *instance* under that same name. + + Because it is a data descriptor (as opposed to a *non-data descriptor*), its `__get__()` method + is executed on each access of the decorated attribute; the __dict__ item of the same name is + "shadowed" by the descriptor. + + While this may represent a performance improvement over a property, its greater benefit may be + its other characteristics. One common use is to construct collaborator objects, removing that + "real work" from the constructor, while still only executing once. It also de-couples client + code from any sequencing considerations; if it's accessed from more than one location, it's + assured it will be ready whenever needed. + + Loosely based on: https://stackoverflow.com/a/6849299/1902513. + + A lazyproperty is read-only. There is no counterpart to the optional "setter" (or deleter) + behavior of an @property. This is critically important to maintaining its immutability and + idempotence guarantees. Attempting to assign to a lazyproperty raises AttributeError + unconditionally. + + The parameter names in the methods below correspond to this usage example:: + + class Obj(object) + + @lazyproperty + def fget(self): + return 'some result' + + obj = Obj() + + Not suitable for wrapping a function (as opposed to a method) because it is not callable. + """ + + def __init__(self, fget: Callable[..., _T]) -> None: + """*fget* is the decorated method (a "getter" function). + + A lazyproperty is read-only, so there is only an *fget* function (a regular + @property can also have an fset and fdel function). This name was chosen for + consistency with Python's `property` class which uses this name for the + corresponding parameter. + """ + # --- maintain a reference to the wrapped getter method + self._fget = fget + # --- and store the name of that decorated method + self._name = fget.__name__ + # --- adopt fget's __name__, __doc__, and other attributes + functools.update_wrapper(self, fget) # pyright: ignore + + def __get__(self, obj: Any, type: Any = None) -> _T: + """Called on each access of 'fget' attribute on class or instance. + + *self* is this instance of a lazyproperty descriptor "wrapping" the property + method it decorates (`fget`, nominally). + + *obj* is the "host" object instance when the attribute is accessed from an + object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on + the class, e.g. `Obj.fget`. + + *type* is the class hosting the decorated getter method (`fget`) on both class + and instance attribute access. + """ + # --- when accessed on class, e.g. Obj.fget, just return this descriptor + # --- instance (patched above to look like fget). + if obj is None: + return self # type: ignore + + # --- when accessed on instance, start by checking instance __dict__ for + # --- item with key matching the wrapped function's name + value = obj.__dict__.get(self._name) + if value is None: + # --- on first access, the __dict__ item will be absent. Evaluate fget() + # --- and store that value in the (otherwise unused) host-object + # --- __dict__ value of same name ('fget' nominally) + value = self._fget(obj) + obj.__dict__[self._name] = value + return cast(_T, value) + + def __set__(self, obj: Any, value: Any) -> None: + """Raises unconditionally, to preserve read-only behavior. + + This decorator is intended to implement immutable (and idempotent) object + attributes. For that reason, assignment to this property must be explicitly + prevented. + + If this __set__ method was not present, this descriptor would become a + *non-data descriptor*. That would be nice because the cached value would be + accessed directly once set (__dict__ attrs have precedence over non-data + descriptors on instance attribute lookup). The problem is, there would be + nothing to stop assignment to the cached value, which would overwrite the result + of `fget()` and break both the immutability and idempotence guarantees of this + decorator. + + The performance with this __set__() method in place was roughly 0.4 usec per + access when measured on a 2.8GHz development machine; so quite snappy and + probably not a rich target for optimization efforts. + """ + raise AttributeError("can't set attribute") diff --git a/tests/chart/test_axis.py b/tests/chart/test_axis.py index 47e60f9c1..9dbb50f51 100644 --- a/tests/chart/test_axis.py +++ b/tests/chart/test_axis.py @@ -1,29 +1,29 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart module -""" +"""Unit-test suite for `pptx.chart.axis` module.""" -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest from pptx.chart.axis import ( AxisTitle, - _BaseAxis, CategoryAxis, DateAxis, MajorGridlines, TickLabels, ValueAxis, + _BaseAxis, ) from pptx.dml.chtfmt import ChartFormat from pptx.enum.chart import ( XL_AXIS_CROSSES, XL_CATEGORY_TYPE, - XL_TICK_LABEL_POSITION as XL_TICK_LBL_POS, XL_TICK_MARK, ) +from pptx.enum.chart import ( + XL_TICK_LABEL_POSITION as XL_TICK_LBL_POS, +) from pptx.text.text import Font from ..unitutil.cxml import element, xml @@ -31,6 +31,20 @@ class Describe_BaseAxis(object): + """Unit-test suite for `pptx.chart.axis._BaseAxis` objects.""" + + def it_provides_access_to_its_title(self, title_fixture): + axis, AxisTitle_, axis_title_ = title_fixture + axis_title = axis.axis_title + AxisTitle_.assert_called_once_with(axis._element.title) + assert axis_title is axis_title_ + + def it_provides_access_to_its_format(self, format_fixture): + axis, ChartFormat_, format_ = format_fixture + format = axis.format + ChartFormat_.assert_called_once_with(axis._xAx) + assert format is format_ + def it_knows_whether_it_has_major_gridlines(self, major_gridlines_get_fixture): base_axis, expected_value = major_gridlines_get_fixture assert base_axis.has_major_gridlines is expected_value @@ -58,47 +72,41 @@ def it_can_change_whether_it_has_a_title(self, has_title_set_fixture): axis.has_title = new_value assert axis._element.xml == expected_xml - def it_knows_whether_it_is_visible(self, visible_get_fixture): - axis, expected_bool_value = visible_get_fixture - assert axis.visible is expected_bool_value + def it_provides_access_to_its_major_gridlines(self, maj_grdlns_fixture): + axis, MajorGridlines_, xAx, major_gridlines_ = maj_grdlns_fixture - def it_can_change_whether_it_is_visible(self, visible_set_fixture): - axis, new_value, expected_xml = visible_set_fixture - axis.visible = new_value - assert axis._element.xml == expected_xml + major_gridlines = axis.major_gridlines - def it_raises_on_assign_non_bool_to_visible(self): - axis = _BaseAxis(None) - with pytest.raises(ValueError): - axis.visible = "foobar" + MajorGridlines_.assert_called_once_with(xAx) + assert major_gridlines is major_gridlines_ + + def it_knows_its_major_tick_setting(self, major_tick_get_fixture): + axis, expected_value = major_tick_get_fixture + assert axis.major_tick_mark == expected_value - def it_knows_the_scale_maximum(self, maximum_scale_get_fixture): + def it_can_change_its_major_tick_mark(self, major_tick_set_fixture): + axis, new_value, expected_xml = major_tick_set_fixture + axis.major_tick_mark = new_value + assert axis._element.xml == expected_xml + + def it_knows_its_maximum_scale(self, maximum_scale_get_fixture): axis, expected_value = maximum_scale_get_fixture assert axis.maximum_scale == expected_value - def it_can_change_the_scale_maximum(self, maximum_scale_set_fixture): + def it_can_change_its_maximum_scale(self, maximum_scale_set_fixture): axis, new_value, expected_xml = maximum_scale_set_fixture axis.maximum_scale = new_value assert axis._element.xml == expected_xml - def it_knows_the_scale_minimum(self, minimum_scale_get_fixture): + def it_knows_its_minimum_scale(self, minimum_scale_get_fixture): axis, expected_value = minimum_scale_get_fixture assert axis.minimum_scale == expected_value - def it_can_change_the_scale_minimum(self, minimum_scale_set_fixture): + def it_can_change_its_minimum_scale(self, minimum_scale_set_fixture): axis, new_value, expected_xml = minimum_scale_set_fixture axis.minimum_scale = new_value assert axis._element.xml == expected_xml - def it_knows_its_major_tick_setting(self, major_tick_get_fixture): - axis, expected_value = major_tick_get_fixture - assert axis.major_tick_mark == expected_value - - def it_can_change_its_major_tick_mark(self, major_tick_set_fixture): - axis, new_value, expected_xml = major_tick_set_fixture - axis.major_tick_mark = new_value - assert axis._element.xml == expected_xml - def it_knows_its_minor_tick_setting(self, minor_tick_get_fixture): axis, expected_value = minor_tick_get_fixture assert axis.minor_tick_mark == expected_value @@ -108,6 +116,18 @@ def it_can_change_its_minor_tick_mark(self, minor_tick_set_fixture): axis.minor_tick_mark = new_value assert axis._element.xml == expected_xml + def it_knows_whether_it_renders_in_reverse_order(self, reverse_order_get_fixture): + xAx, expected_value = reverse_order_get_fixture + assert _BaseAxis(xAx).reverse_order == expected_value + + def it_can_change_whether_it_renders_in_reverse_order(self, reverse_order_set_fixture): + xAx, new_value, expected_xml = reverse_order_set_fixture + axis = _BaseAxis(xAx) + + axis.reverse_order = new_value + + assert axis._element.xml == expected_xml + def it_knows_its_tick_label_position(self, tick_lbl_pos_get_fixture): axis, expected_value = tick_lbl_pos_get_fixture assert axis.tick_label_position == expected_value @@ -117,30 +137,26 @@ def it_can_change_its_tick_label_position(self, tick_lbl_pos_set_fixture): axis.tick_label_position = new_value assert axis._element.xml == expected_xml - def it_provides_access_to_its_title(self, title_fixture): - axis, AxisTitle_, axis_title_ = title_fixture - axis_title = axis.axis_title - AxisTitle_.assert_called_once_with(axis._element.title) - assert axis_title is axis_title_ - - def it_provides_access_to_its_format(self, format_fixture): - axis, ChartFormat_, format_ = format_fixture - format = axis.format - ChartFormat_.assert_called_once_with(axis._xAx) - assert format is format_ - - def it_provides_access_to_its_major_gridlines(self, maj_grdlns_fixture): - axis, MajorGridlines_, xAx, major_gridlines_ = maj_grdlns_fixture - major_gridlines = axis.major_gridlines - MajorGridlines_.assert_called_once_with(xAx) - assert major_gridlines is major_gridlines_ - def it_provides_access_to_the_tick_labels(self, tick_labels_fixture): axis, tick_labels_, TickLabels_, xAx = tick_labels_fixture tick_labels = axis.tick_labels TickLabels_.assert_called_once_with(xAx) assert tick_labels is tick_labels_ + def it_knows_whether_it_is_visible(self, visible_get_fixture): + axis, expected_bool_value = visible_get_fixture + assert axis.visible is expected_bool_value + + def it_can_change_whether_it_is_visible(self, visible_set_fixture): + axis, new_value, expected_xml = visible_set_fixture + axis.visible = new_value + assert axis._element.xml == expected_xml + + def but_it_raises_on_assign_non_bool_to_visible(self): + axis = _BaseAxis(None) + with pytest.raises(ValueError): + axis.visible = "foobar" + # fixtures ------------------------------------------------------- @pytest.fixture(params=["c:catAx", "c:dateAx", "c:valAx"]) @@ -475,6 +491,55 @@ def minor_tick_set_fixture(self, request): expected_xml = xml(expected_xAx_cxml) return axis, new_value, expected_xml + @pytest.fixture( + params=[ + ("c:catAx/c:scaling", False), + ("c:valAx/c:scaling/c:orientation", False), + ("c:catAx/c:scaling/c:orientation{val=minMax}", False), + ("c:valAx/c:scaling/c:orientation{val=maxMin}", True), + ] + ) + def reverse_order_get_fixture(self, request): + xAx_cxml, expected_value = request.param + return element(xAx_cxml), expected_value + + @pytest.fixture( + params=[ + ("c:catAx/c:scaling", False, "c:catAx/c:scaling"), + ("c:catAx/c:scaling", True, "c:catAx/c:scaling/c:orientation{val=maxMin}"), + ("c:valAx/c:scaling/c:orientation", False, "c:valAx/c:scaling"), + ( + "c:valAx/c:scaling/c:orientation", + True, + "c:valAx/c:scaling/c:orientation{val=maxMin}", + ), + ( + "c:dateAx/c:scaling/c:orientation{val=minMax}", + False, + "c:dateAx/c:scaling", + ), + ( + "c:dateAx/c:scaling/c:orientation{val=minMax}", + True, + "c:dateAx/c:scaling/c:orientation{val=maxMin}", + ), + ( + "c:catAx/c:scaling/c:orientation{val=maxMin}", + False, + "c:catAx/c:scaling", + ), + ( + "c:catAx/c:scaling/c:orientation{val=maxMin}", + True, + "c:catAx/c:scaling/c:orientation{val=maxMin}", + ), + ] + ) + def reverse_order_set_fixture(self, request): + xAx_cxml, new_value, expected_xAx_cxml = request.param + xAx, expected_xml = element(xAx_cxml), xml(expected_xAx_cxml) + return xAx, new_value, expected_xml + @pytest.fixture(params=["c:catAx", "c:dateAx", "c:valAx"]) def tick_labels_fixture(self, request, TickLabels_, tick_labels_): xAx_cxml = request.param @@ -592,9 +657,7 @@ def visible_set_fixture(self, request): @pytest.fixture def AxisTitle_(self, request, axis_title_): - return class_mock( - request, "pptx.chart.axis.AxisTitle", return_value=axis_title_ - ) + return class_mock(request, "pptx.chart.axis.AxisTitle", return_value=axis_title_) @pytest.fixture def axis_title_(self, request): @@ -610,9 +673,7 @@ def format_(self, request): @pytest.fixture def MajorGridlines_(self, request, major_gridlines_): - return class_mock( - request, "pptx.chart.axis.MajorGridlines", return_value=major_gridlines_ - ) + return class_mock(request, "pptx.chart.axis.MajorGridlines", return_value=major_gridlines_) @pytest.fixture def major_gridlines_(self, request): @@ -620,9 +681,7 @@ def major_gridlines_(self, request): @pytest.fixture def TickLabels_(self, request, tick_labels_): - return class_mock( - request, "pptx.chart.axis.TickLabels", return_value=tick_labels_ - ) + return class_mock(request, "pptx.chart.axis.TickLabels", return_value=tick_labels_) @pytest.fixture def tick_labels_(self, request): @@ -677,20 +736,17 @@ def has_tf_get_fixture(self, request): ( "c:title{a:b=c}", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx/c:strRef", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ("c:title/c:tx/c:rich", True, "c:title/c:tx/c:rich"), ("c:title", False, "c:title"), @@ -759,9 +815,7 @@ def it_provides_access_to_its_format(self, format_fixture): gridlines, expected_xml, ChartFormat_, format_ = format_fixture format = gridlines.format assert gridlines._xAx.xml == expected_xml - ChartFormat_.assert_called_once_with( - gridlines._xAx.xpath("c:majorGridlines")[0] - ) + ChartFormat_.assert_called_once_with(gridlines._xAx.xpath("c:majorGridlines")[0]) assert format is format_ # fixtures ------------------------------------------------------- @@ -810,9 +864,7 @@ def it_can_change_its_number_format(self, number_format_set_fixture): tick_labels.number_format = new_value assert tick_labels._element.xml == expected_xml - def it_knows_whether_its_number_format_is_linked( - self, number_format_is_linked_get_fixture - ): + def it_knows_whether_its_number_format_is_linked(self, number_format_is_linked_get_fixture): tick_labels, expected_value = number_format_is_linked_get_fixture assert tick_labels.number_format_is_linked is expected_value diff --git a/tests/chart/test_category.py b/tests/chart/test_category.py index 28bbeb096..9319d664b 100644 --- a/tests/chart/test_category.py +++ b/tests/chart/test_category.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.chart.category` module.""" -""" -Unit test suite for the pptx.chart.category module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -28,7 +24,12 @@ def it_supports_indexed_access(self, getitem_fixture): assert category is category_ def it_can_iterate_over_the_categories_it_contains(self, iter_fixture): - categories, expected_categories, Category_, calls, = iter_fixture + ( + categories, + expected_categories, + Category_, + calls, + ) = iter_fixture assert [c for c in categories] == expected_categories assert Category_.call_args_list == calls @@ -117,9 +118,7 @@ def iter_fixture(self, Category_, category_): calls = [call(None, 0), call(pt, 1)] return categories, expected_categories, Category_, calls - @pytest.fixture( - params=[("c:barChart", 0), ("c:barChart/c:ser/c:cat/c:ptCount{val=4}", 4)] - ) + @pytest.fixture(params=[("c:barChart", 0), ("c:barChart/c:ser/c:cat/c:ptCount{val=4}", 4)]) def len_fixture(self, request): xChart_cxml, expected_len = request.param categories = Categories(element(xChart_cxml)) @@ -147,9 +146,7 @@ def levels_fixture(self, request, CategoryLevel_, category_level_): @pytest.fixture def Category_(self, request, category_): - return class_mock( - request, "pptx.chart.category.Category", return_value=category_ - ) + return class_mock(request, "pptx.chart.category.Category", return_value=category_) @pytest.fixture def category_(self, request): @@ -245,9 +242,7 @@ def len_fixture(self, request): @pytest.fixture def Category_(self, request, category_): - return class_mock( - request, "pptx.chart.category.Category", return_value=category_ - ) + return class_mock(request, "pptx.chart.category.Category", return_value=category_) @pytest.fixture def category_(self, request): diff --git a/tests/chart/test_chart.py b/tests/chart/test_chart.py index aa5e4cc93..667253347 100644 --- a/tests/chart/test_chart.py +++ b/tests/chart/test_chart.py @@ -1,8 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.chart.chart module""" +"""Unit-test suite for `pptx.chart.chart` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -28,6 +28,8 @@ class DescribeChart(object): + """Unit-test suite for `pptx.chart.chart.Chart` objects.""" + def it_provides_access_to_its_font(self, font_fixture, Font_, font_): chartSpace, expected_xml = font_fixture Font_.return_value = font_ @@ -36,9 +38,7 @@ def it_provides_access_to_its_font(self, font_fixture, Font_, font_): font = chart.font assert chartSpace.xml == expected_xml - Font_.assert_called_once_with( - chartSpace.xpath("./c:txPr/a:p/a:pPr/a:defRPr")[0] - ) + Font_.assert_called_once_with(chartSpace.xpath("./c:txPr/a:p/a:pPr/a:defRPr")[0]) assert font is font_ def it_knows_whether_it_has_a_title(self, has_title_get_fixture): @@ -108,11 +108,15 @@ def it_provides_access_to_its_legend(self, legend_fixture): assert Legend_.call_args_list == expected_calls assert legend is expected_value - def it_knows_its_chart_type(self, chart_type_fixture): - chart, PlotTypeInspector_, plot_, chart_type = chart_type_fixture - _chart_type = chart.chart_type + def it_knows_its_chart_type(self, request, PlotTypeInspector_, plot_): + property_mock(request, Chart, "plots", return_value=[plot_]) + PlotTypeInspector_.chart_type.return_value = XL_CHART_TYPE.PIE + chart = Chart(None, None) + + chart_type = chart.chart_type + PlotTypeInspector_.chart_type.assert_called_once_with(plot_) - assert _chart_type is chart_type + assert chart_type == XL_CHART_TYPE.PIE def it_knows_its_style(self, style_get_fixture): chart, expected_value = style_get_fixture @@ -163,20 +167,11 @@ def cat_ax_raise_fixture(self): chart = Chart(element("c:chartSpace/c:chart/c:plotArea"), None) return chart - @pytest.fixture - def chart_type_fixture(self, PlotTypeInspector_, plot_): - chart = Chart(None, None) - chart._plots = [plot_] - chart_type = XL_CHART_TYPE.PIE - PlotTypeInspector_.chart_type.return_value = chart_type - return chart, PlotTypeInspector_, plot_, chart_type - @pytest.fixture( params=[ ( "c:chartSpace{a:b=c}", - "c:chartSpace{a:b=c}/c:txPr/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:chartSpace{a:b=c}/c:txPr/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ("c:chartSpace/c:txPr/a:p", "c:chartSpace/c:txPr/a:p/a:pPr/a:defRPr"), ( @@ -202,9 +197,7 @@ def has_legend_get_fixture(self, request): chart = Chart(element(chartSpace_cxml), None) return chart, expected_value - @pytest.fixture( - params=[("c:chartSpace/c:chart", True, "c:chartSpace/c:chart/c:legend")] - ) + @pytest.fixture(params=[("c:chartSpace/c:chart", True, "c:chartSpace/c:chart/c:legend")]) def has_legend_set_fixture(self, request): chartSpace_cxml, new_value, expected_chartSpace_cxml = request.param chart = Chart(element(chartSpace_cxml), None) @@ -289,9 +282,7 @@ def series_fixture(self, SeriesCollection_, series_collection_): chart = Chart(chartSpace, None) return chart, SeriesCollection_, plotArea, series_collection_ - @pytest.fixture( - params=[("c:chartSpace/c:style{val=42}", 42), ("c:chartSpace", None)] - ) + @pytest.fixture(params=[("c:chartSpace/c:style{val=42}", 42), ("c:chartSpace", None)]) def style_get_fixture(self, request): chartSpace_cxml, expected_value = request.param chart = Chart(element(chartSpace_cxml), None) @@ -345,9 +336,7 @@ def val_ax_raise_fixture(self): @pytest.fixture def CategoryAxis_(self, request, category_axis_): - return class_mock( - request, "pptx.chart.chart.CategoryAxis", return_value=category_axis_ - ) + return class_mock(request, "pptx.chart.chart.CategoryAxis", return_value=category_axis_) @pytest.fixture def category_axis_(self, request): @@ -359,9 +348,7 @@ def chart_data_(self, request): @pytest.fixture def ChartTitle_(self, request, chart_title_): - return class_mock( - request, "pptx.chart.chart.ChartTitle", return_value=chart_title_ - ) + return class_mock(request, "pptx.chart.chart.ChartTitle", return_value=chart_title_) @pytest.fixture def chart_title_(self, request): @@ -434,9 +421,7 @@ def series_rewriter_(self, request): @pytest.fixture def ValueAxis_(self, request, value_axis_): - return class_mock( - request, "pptx.chart.chart.ValueAxis", return_value=value_axis_ - ) + return class_mock(request, "pptx.chart.chart.ValueAxis", return_value=value_axis_) @pytest.fixture def value_axis_(self, request): @@ -452,6 +437,8 @@ def workbook_prop_(self, request, workbook_): class DescribeChartTitle(object): + """Unit-test suite for `pptx.chart.chart.ChartTitle` objects.""" + def it_provides_access_to_its_format(self, format_fixture): chart_title, ChartFormat_, format_ = format_fixture format = chart_title.format @@ -499,20 +486,17 @@ def has_tf_get_fixture(self, request): ( "c:title{a:b=c}", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx/c:strRef", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ("c:title/c:tx/c:rich", True, "c:title/c:tx/c:rich"), ("c:title", False, "c:title"), @@ -549,6 +533,8 @@ def TextFrame_(self, request): class Describe_Plots(object): + """Unit-test suite for `pptx.chart.chart._Plots` objects.""" + def it_supports_indexed_access(self, getitem_fixture): plots, idx, PlotFactory_, plot_elm, chart_, plot_ = getitem_fixture plot = plots[idx] @@ -594,9 +580,7 @@ def chart_(self, request): @pytest.fixture def PlotFactory_(self, request, plot_): - return function_mock( - request, "pptx.chart.chart.PlotFactory", return_value=plot_ - ) + return function_mock(request, "pptx.chart.chart.PlotFactory", return_value=plot_) @pytest.fixture def plot_(self, request): diff --git a/tests/chart/test_data.py b/tests/chart/test_data.py index 537875569..9b6097020 100644 --- a/tests/chart/test_data.py +++ b/tests/chart/test_data.py @@ -1,19 +1,14 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.data module -""" +"""Test suite for `pptx.chart.data` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from datetime import date, datetime import pytest from pptx.chart.data import ( - _BaseChartData, - _BaseDataPoint, - _BaseSeriesData, BubbleChartData, BubbleDataPoint, BubbleSeriesData, @@ -26,11 +21,14 @@ XyChartData, XyDataPoint, XySeriesData, + _BaseChartData, + _BaseDataPoint, + _BaseSeriesData, ) from pptx.chart.xlsx import CategoryWorkbookWriter -from pptx.enum.base import EnumValue +from pptx.enum.chart import XL_CHART_TYPE -from ..unitutil.mock import call, class_mock, instance_mock, property_mock +from ..unitutil.mock import Mock, call, class_mock, instance_mock, property_mock class DescribeChartData(object): @@ -39,12 +37,16 @@ def it_is_a_CategoryChartData_object(self): class Describe_BaseChartData(object): - def it_can_generate_chart_part_XML_for_its_data(self, xml_bytes_fixture): - chart_data, chart_type_, ChartXmlWriter_, expected_bytes = xml_bytes_fixture - xml_bytes = chart_data.xml_bytes(chart_type_) + """Unit-test suite for `pptx.chart.data._BaseChartData`.""" + + def it_can_generate_chart_part_XML_for_its_data(self, ChartXmlWriter_: Mock): + ChartXmlWriter_.return_value.xml = "ƒøØßår" + chart_data = _BaseChartData() - ChartXmlWriter_.assert_called_once_with(chart_type_, chart_data) - assert xml_bytes == expected_bytes + xml_bytes = chart_data.xml_bytes(XL_CHART_TYPE.PIE) + + ChartXmlWriter_.assert_called_once_with(XL_CHART_TYPE.PIE, chart_data) + assert xml_bytes == "ƒøØßår".encode("utf-8") def it_knows_its_number_format(self, number_format_fixture): chart_data, expected_value = number_format_fixture @@ -59,12 +61,6 @@ def number_format_fixture(self, request): chart_data = _BaseChartData(*argv) return chart_data, expected_value - @pytest.fixture - def xml_bytes_fixture(self, chart_type_, ChartXmlWriter_): - chart_data = _BaseChartData() - expected_bytes = "ƒøØßår".encode("utf-8") - return chart_data, chart_type_, ChartXmlWriter_, expected_bytes - # fixture components --------------------------------------------- @pytest.fixture @@ -73,10 +69,6 @@ def ChartXmlWriter_(self, request): ChartXmlWriter_.return_value.xml = "ƒøØßår" return ChartXmlWriter_ - @pytest.fixture - def chart_type_(self, request): - return instance_mock(request, EnumValue) - class Describe_BaseSeriesData(object): def it_knows_its_name(self, name_fixture): @@ -229,15 +221,11 @@ def values_ref_fixture(self, _workbook_writer_prop_, workbook_writer_, series_): @pytest.fixture def Categories_(self, request, categories_): - return class_mock( - request, "pptx.chart.data.Categories", return_value=categories_ - ) + return class_mock(request, "pptx.chart.data.Categories", return_value=categories_) @pytest.fixture def CategorySeriesData_(self, request, series_): - return class_mock( - request, "pptx.chart.data.CategorySeriesData", return_value=series_ - ) + return class_mock(request, "pptx.chart.data.CategorySeriesData", return_value=series_) @pytest.fixture def categories_(self, request): @@ -245,9 +233,7 @@ def categories_(self, request): @pytest.fixture def categories_prop_(self, request, categories_): - return property_mock( - request, CategoryChartData, "categories", return_value=categories_ - ) + return property_mock(request, CategoryChartData, "categories", return_value=categories_) @pytest.fixture def category_(self, request): @@ -357,9 +343,7 @@ def are_numeric_fixture(self, request): categories.add_category(label) return categories, expected_value - @pytest.fixture( - params=[((), 0), ((1,), 1), ((3,), 3), ((1, 1, 1), 1), ((3, 3, 3), 3)] - ) + @pytest.fixture(params=[((), 0), ((1,), 1), ((3,), 3), ((1, 1, 1), 1), ((3, 3, 3), 3)]) def depth_fixture(self, request): depths, expected_value = request.param categories = Categories() @@ -392,9 +376,7 @@ def leaf_fixture(self, request): leaf_counts, expected_value = request.param categories = Categories() for leaf_count in leaf_counts: - categories._categories.append( - instance_mock(request, Category, leaf_count=leaf_count) - ) + categories._categories.append(instance_mock(request, Category, leaf_count=leaf_count)) return categories, expected_value @pytest.fixture( @@ -525,9 +507,7 @@ def it_calculates_an_excel_date_number_to_help(self, excel_date_fixture): def add_sub_fixture(self, request, category_): category = Category(None, None) name = "foobar" - Category_ = class_mock( - request, "pptx.chart.data.Category", return_value=category_ - ) + Category_ = class_mock(request, "pptx.chart.data.Category", return_value=category_) return category, name, Category_, category_ @pytest.fixture(params=[((), 1), ((1,), 2), ((1, 1, 1), 2), ((2, 2, 2), 3)]) @@ -535,18 +515,14 @@ def depth_fixture(self, request): depths, expected_value = request.param category = Category(None, None) for depth in depths: - category._sub_categories.append( - instance_mock(request, Category, depth=depth) - ) + category._sub_categories.append(instance_mock(request, Category, depth=depth)) return category, expected_value @pytest.fixture def depth_raises_fixture(self, request): category = Category(None, None) for depth in (1, 2, 1): - category._sub_categories.append( - instance_mock(request, Category, depth=depth) - ) + category._sub_categories.append(instance_mock(request, Category, depth=depth)) return category @pytest.fixture( @@ -594,9 +570,7 @@ def leaf_fixture(self, request): leaf_counts, expected_value = request.param category = Category(None, None) for leaf_count in leaf_counts: - category._sub_categories.append( - instance_mock(request, Category, leaf_count=leaf_count) - ) + category._sub_categories.append(instance_mock(request, Category, leaf_count=leaf_count)) return category, expected_value @pytest.fixture( @@ -683,9 +657,7 @@ def values_fixture(self, request): series_data = CategorySeriesData(None, None, None) expected_values = [1, 2, 3] for value in expected_values: - series_data._data_points.append( - instance_mock(request, CategoryDataPoint, value=value) - ) + series_data._data_points.append(instance_mock(request, CategoryDataPoint, value=value)) return series_data, expected_values @pytest.fixture @@ -699,9 +671,7 @@ def values_ref_fixture(self, chart_data_): @pytest.fixture def CategoryDataPoint_(self, request, data_point_): - return class_mock( - request, "pptx.chart.data.CategoryDataPoint", return_value=data_point_ - ) + return class_mock(request, "pptx.chart.data.CategoryDataPoint", return_value=data_point_) @pytest.fixture def categories_(self, request): @@ -736,9 +706,7 @@ def add_series_fixture(self, request, BubbleSeriesData_, series_data_): @pytest.fixture def BubbleSeriesData_(self, request, series_data_): - return class_mock( - request, "pptx.chart.data.BubbleSeriesData", return_value=series_data_ - ) + return class_mock(request, "pptx.chart.data.BubbleSeriesData", return_value=series_data_) @pytest.fixture def series_data_(self, request): @@ -768,9 +736,7 @@ def add_series_fixture(self, request, XySeriesData_, series_data_): @pytest.fixture def XySeriesData_(self, request, series_data_): - return class_mock( - request, "pptx.chart.data.XySeriesData", return_value=series_data_ - ) + return class_mock(request, "pptx.chart.data.XySeriesData", return_value=series_data_) @pytest.fixture def series_data_(self, request): @@ -797,9 +763,7 @@ def add_data_point_fixture(self, request, BubbleDataPoint_, data_point_): @pytest.fixture def BubbleDataPoint_(self, request, data_point_): - return class_mock( - request, "pptx.chart.data.BubbleDataPoint", return_value=data_point_ - ) + return class_mock(request, "pptx.chart.data.BubbleDataPoint", return_value=data_point_) @pytest.fixture def data_point_(self, request): @@ -842,9 +806,7 @@ def data_point_(self, request): @pytest.fixture def XyDataPoint_(self, request, data_point_): - return class_mock( - request, "pptx.chart.data.XyDataPoint", return_value=data_point_ - ) + return class_mock(request, "pptx.chart.data.XyDataPoint", return_value=data_point_) class DescribeCategoryDataPoint(object): diff --git a/tests/chart/test_datalabel.py b/tests/chart/test_datalabel.py index 19eddcc6f..ad02efc10 100644 --- a/tests/chart/test_datalabel.py +++ b/tests/chart/test_datalabel.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Unit test suite for the pptx.chart.datalabel module""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -279,9 +277,7 @@ def it_can_change_its_number_format(self, number_format_set_fixture): data_labels.number_format = new_value assert data_labels._element.xml == expected_xml - def it_knows_whether_its_number_format_is_linked( - self, number_format_is_linked_get_fixture - ): + def it_knows_whether_its_number_format_is_linked(self, number_format_is_linked_get_fixture): data_labels, expected_value = number_format_is_linked_get_fixture assert data_labels.number_format_is_linked is expected_value diff --git a/tests/chart/test_legend.py b/tests/chart/test_legend.py index 1624dc6d6..d77cd9f37 100644 --- a/tests/chart/test_legend.py +++ b/tests/chart/test_legend.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.chart.legend` module.""" -""" -Test suite for pptx.chart.legend module -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -32,15 +28,11 @@ def it_can_change_its_horizontal_offset(self, horz_offset_set_fixture): legend.horz_offset = new_value assert legend._element.xml == expected_xml - def it_knows_whether_it_should_overlap_the_chart( - self, include_in_layout_get_fixture - ): + def it_knows_whether_it_should_overlap_the_chart(self, include_in_layout_get_fixture): legend, expected_value = include_in_layout_get_fixture assert legend.include_in_layout == expected_value - def it_can_change_whether_it_overlaps_the_chart( - self, include_in_layout_set_fixture - ): + def it_can_change_whether_it_overlaps_the_chart(self, include_in_layout_set_fixture): legend, new_value, expected_xml = include_in_layout_set_fixture legend.include_in_layout = new_value assert legend._element.xml == expected_xml @@ -80,8 +72,7 @@ def font_fixture(self, request): ("c:legend/c:layout/c:manualLayout/c:xMode{val=factor}", 0.0), ("c:legend/c:layout/c:manualLayout/(c:xMode,c:x{val=0.42})", 0.42), ( - "c:legend/c:layout/c:manualLayout/(c:xMode{val=factor},c:x{val=0.42" - "})", + "c:legend/c:layout/c:manualLayout/(c:xMode{val=factor},c:x{val=0.42" "})", 0.42, ), ] diff --git a/tests/chart/test_marker.py b/tests/chart/test_marker.py index 4bbe22cb4..b9a8f3c5d 100644 --- a/tests/chart/test_marker.py +++ b/tests/chart/test_marker.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.chart.marker` module.""" -""" -Unit test suite for the pptx.chart.marker module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -131,9 +127,7 @@ def style_set_fixture(self, request): @pytest.fixture def ChartFormat_(self, request, chart_format_): - return class_mock( - request, "pptx.chart.marker.ChartFormat", return_value=chart_format_ - ) + return class_mock(request, "pptx.chart.marker.ChartFormat", return_value=chart_format_) @pytest.fixture def chart_format_(self, request): diff --git a/tests/chart/test_plot.py b/tests/chart/test_plot.py index 3a9e9f136..7e0f75e2d 100644 --- a/tests/chart/test_plot.py +++ b/tests/chart/test_plot.py @@ -1,19 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.plot module -""" +"""Unit-test suite for `pptx.chart.plot` module.""" -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest from pptx.chart.category import Categories from pptx.chart.chart import Chart from pptx.chart.plot import ( - _BasePlot, - AreaPlot, Area3DPlot, + AreaPlot, BarPlot, BubblePlot, DataLabels, @@ -24,6 +21,7 @@ PlotTypeInspector, RadarPlot, XyPlot, + _BasePlot, ) from pptx.chart.series import SeriesCollection from pptx.enum.chart import XL_CHART_TYPE as XL @@ -46,15 +44,11 @@ def it_can_change_whether_it_has_data_labels(self, has_data_labels_set_fixture): plot.has_data_labels = new_value assert plot._element.xml == expected_xml - def it_knows_whether_it_varies_color_by_category( - self, vary_by_categories_get_fixture - ): + def it_knows_whether_it_varies_color_by_category(self, vary_by_categories_get_fixture): plot, expected_value = vary_by_categories_get_fixture assert plot.vary_by_categories == expected_value - def it_can_change_whether_it_varies_color_by_category( - self, vary_by_categories_set_fixture - ): + def it_can_change_whether_it_varies_color_by_category(self, vary_by_categories_set_fixture): plot, new_value, expected_xml = vary_by_categories_set_fixture plot.vary_by_categories = new_value assert plot._element.xml == expected_xml @@ -176,9 +170,7 @@ def vary_by_categories_set_fixture(self, request): @pytest.fixture def Categories_(self, request, categories_): - return class_mock( - request, "pptx.chart.plot.Categories", return_value=categories_ - ) + return class_mock(request, "pptx.chart.plot.Categories", return_value=categories_) @pytest.fixture def categories_(self, request): @@ -190,9 +182,7 @@ def chart_(self, request): @pytest.fixture def DataLabels_(self, request, data_labels_): - return class_mock( - request, "pptx.chart.plot.DataLabels", return_value=data_labels_ - ) + return class_mock(request, "pptx.chart.plot.DataLabels", return_value=data_labels_) @pytest.fixture def data_labels_(self, request): @@ -430,21 +420,18 @@ def it_can_determine_the_chart_type_of_a_plot(self, chart_type_fixture): ("c:lineChart/c:grouping{val=percentStacked}", XL.LINE_MARKERS_STACKED_100), ("c:lineChart/c:ser/c:marker/c:symbol{val=none}", XL.LINE), ( - "c:lineChart/(c:grouping{val=stacked},c:ser/c:marker/c:symbol{val=n" - "one})", + "c:lineChart/(c:grouping{val=stacked},c:ser/c:marker/c:symbol{val=n" "one})", XL.LINE_STACKED, ), ( - "c:lineChart/(c:grouping{val=percentStacked},c:ser/c:marker/c:symbo" - "l{val=none})", + "c:lineChart/(c:grouping{val=percentStacked},c:ser/c:marker/c:symbo" "l{val=none})", XL.LINE_STACKED_100, ), ("c:pieChart", XL.PIE), ("c:pieChart/c:ser/c:explosion{val=25}", XL.PIE_EXPLODED), ("c:scatterChart/c:scatterStyle", XL.XY_SCATTER), ( - "c:scatterChart/(c:scatterStyle{val=lineMarker},c:ser/c:spPr/a:ln/a" - ":noFill)", + "c:scatterChart/(c:scatterStyle{val=lineMarker},c:ser/c:spPr/a:ln/a" ":noFill)", XL.XY_SCATTER, ), ("c:scatterChart/c:scatterStyle{val=lineMarker}", XL.XY_SCATTER_LINES), @@ -473,8 +460,7 @@ def it_can_determine_the_chart_type_of_a_plot(self, chart_type_fixture): ("c:radarChart/c:radarStyle{val=marker}", XL.RADAR_MARKERS), ("c:radarChart/c:radarStyle{val=filled}", XL.RADAR_FILLED), ( - "c:radarChart/(c:radarStyle{val=marker},c:ser/c:marker/c:symbol{val" - "=none})", + "c:radarChart/(c:radarStyle{val=marker},c:ser/c:marker/c:symbol{val" "=none})", XL.RADAR, ), ] diff --git a/tests/chart/test_point.py b/tests/chart/test_point.py index cba2eb0bc..8e00d9675 100644 --- a/tests/chart/test_point.py +++ b/tests/chart/test_point.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.chart.point` module.""" -""" -Unit test suite for the pptx.chart.point module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -166,9 +162,7 @@ def marker_fixture(self, Marker_, marker_): @pytest.fixture def ChartFormat_(self, request, chart_format_): - return class_mock( - request, "pptx.chart.point.ChartFormat", return_value=chart_format_ - ) + return class_mock(request, "pptx.chart.point.ChartFormat", return_value=chart_format_) @pytest.fixture def chart_format_(self, request): @@ -176,9 +170,7 @@ def chart_format_(self, request): @pytest.fixture def DataLabel_(self, request, data_label_): - return class_mock( - request, "pptx.chart.point.DataLabel", return_value=data_label_ - ) + return class_mock(request, "pptx.chart.point.DataLabel", return_value=data_label_) @pytest.fixture def data_label_(self, request): diff --git a/tests/chart/test_series.py b/tests/chart/test_series.py index 35fa2425d..9a60351e1 100644 --- a/tests/chart/test_series.py +++ b/tests/chart/test_series.py @@ -1,8 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.chart.series module.""" +"""Unit-test suite for `pptx.chart.series` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -12,16 +12,16 @@ from pptx.chart.series import ( AreaSeries, BarSeries, - _BaseCategorySeries, - _BaseSeries, BubbleSeries, LineSeries, - _MarkerMixin, PieSeries, RadarSeries, SeriesCollection, - _SeriesFactory, XySeries, + _BaseCategorySeries, + _BaseSeries, + _MarkerMixin, + _SeriesFactory, ) from pptx.dml.chtfmt import ChartFormat @@ -73,9 +73,7 @@ def name_fixture(self, request): @pytest.fixture def ChartFormat_(self, request, chart_format_): - return class_mock( - request, "pptx.chart.series.ChartFormat", return_value=chart_format_ - ) + return class_mock(request, "pptx.chart.series.ChartFormat", return_value=chart_format_) @pytest.fixture def chart_format_(self, request): @@ -87,9 +85,7 @@ def it_is_a_BaseSeries_subclass(self, subclass_fixture): base_category_series = subclass_fixture assert isinstance(base_category_series, _BaseSeries) - def it_provides_access_to_its_data_labels( - self, data_labels_fixture, DataLabels_, data_labels_ - ): + def it_provides_access_to_its_data_labels(self, data_labels_fixture, DataLabels_, data_labels_): ser, expected_dLbls_xml = data_labels_fixture DataLabels_.return_value = data_labels_ series = _BaseCategorySeries(ser) @@ -148,8 +144,7 @@ def subclass_fixture(self): ("c:ser/c:val/c:numRef/c:numCache", ()), ("c:ser/c:val/c:numRef/c:numCache/c:ptCount{val=0}", ()), ( - 'c:ser/c:val/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v"' - '1.1")', + 'c:ser/c:val/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v"' '1.1")', (1.1,), ), ( @@ -178,9 +173,7 @@ def values_get_fixture(self, request): @pytest.fixture def CategoryPoints_(self, request, points_): - return class_mock( - request, "pptx.chart.series.CategoryPoints", return_value=points_ - ) + return class_mock(request, "pptx.chart.series.CategoryPoints", return_value=points_) @pytest.fixture def DataLabels_(self, request): @@ -238,15 +231,11 @@ def it_is_a_BaseCategorySeries_subclass(self, subclass_fixture): bar_series = subclass_fixture assert isinstance(bar_series, _BaseCategorySeries) - def it_knows_whether_it_should_invert_if_negative( - self, invert_if_negative_get_fixture - ): + def it_knows_whether_it_should_invert_if_negative(self, invert_if_negative_get_fixture): bar_series, expected_value = invert_if_negative_get_fixture assert bar_series.invert_if_negative == expected_value - def it_can_change_whether_it_inverts_if_negative( - self, invert_if_negative_set_fixture - ): + def it_can_change_whether_it_inverts_if_negative(self, invert_if_negative_set_fixture): bar_series, new_value, expected_xml = invert_if_negative_set_fixture bar_series.invert_if_negative = new_value assert bar_series._element.xml == expected_xml @@ -312,9 +301,7 @@ def points_fixture(self, BubblePoints_, points_): @pytest.fixture def BubblePoints_(self, request, points_): - return class_mock( - request, "pptx.chart.series.BubblePoints", return_value=points_ - ) + return class_mock(request, "pptx.chart.series.BubblePoints", return_value=points_) @pytest.fixture def points_(self, request): @@ -433,8 +420,7 @@ def subclass_fixture(self): ("c:ser/c:yVal/c:numRef", ()), ("c:ser/c:val/c:numRef/c:numCache", ()), ( - "c:ser/c:yVal/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v" - '"1.1")', + "c:ser/c:yVal/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v" '"1.1")', (1.1,), ), ( @@ -483,8 +469,7 @@ def it_supports_len(self, len_fixture): params=[ ("c:barChart/c:ser/c:order{val=42}", 0, 0), ( - "c:barChart/(c:ser/c:order{val=9},c:ser/c:order{val=6},c:ser/c:orde" - "r{val=3})", + "c:barChart/(c:ser/c:order{val=9},c:ser/c:order{val=6},c:ser/c:orde" "r{val=3})", 2, 0, ), @@ -509,8 +494,7 @@ def getitem_fixture(self, request, _SeriesFactory_, series_): ("c:barChart", 0), ("c:barChart/c:ser/c:order{val=4}", 1), ( - "c:barChart/(c:ser/c:order{val=4},c:ser/c:order{val=1},c:ser/c:orde" - "r{val=6})", + "c:barChart/(c:ser/c:order{val=4},c:ser/c:order{val=1},c:ser/c:orde" "r{val=6})", 3, ), ("c:plotArea/c:barChart", 0), @@ -531,9 +515,7 @@ def len_fixture(self, request): @pytest.fixture def _SeriesFactory_(self, request, series_): - return function_mock( - request, "pptx.chart.series._SeriesFactory", return_value=series_ - ) + return function_mock(request, "pptx.chart.series._SeriesFactory", return_value=series_) @pytest.fixture def series_(self, request): diff --git a/tests/chart/test_xlsx.py b/tests/chart/test_xlsx.py index 9f26d2c4b..dde9d4d53 100644 --- a/tests/chart/test_xlsx.py +++ b/tests/chart/test_xlsx.py @@ -1,13 +1,12 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.xlsx module -""" +"""Unit-test suite for `pptx.chart.xlsx` module.""" -from __future__ import absolute_import, print_function +from __future__ import annotations -import pytest +import io +import pytest from xlsxwriter import Workbook from xlsxwriter.worksheet import Worksheet @@ -19,28 +18,34 @@ XyChartData, ) from pptx.chart.xlsx import ( - _BaseWorkbookWriter, BubbleWorkbookWriter, CategoryWorkbookWriter, XyWorkbookWriter, + _BaseWorkbookWriter, ) -from pptx.compat import BytesIO from ..unitutil.mock import ANY, call, class_mock, instance_mock, method_mock class Describe_BaseWorkbookWriter(object): - def it_can_generate_a_chart_data_Excel_blob(self, xlsx_blob_fixture): - workbook_writer, xlsx_file_, workbook_, worksheet_, xlsx_blob = ( - xlsx_blob_fixture - ) - _xlsx_blob = workbook_writer.xlsx_blob + """Unit-test suite for `pptx.chart.xlsx._BaseWorkbookWriter` objects.""" - workbook_writer._open_worksheet.assert_called_once_with(xlsx_file_) - workbook_writer._populate_worksheet.assert_called_once_with( - workbook_writer, workbook_, worksheet_ - ) - assert _xlsx_blob is xlsx_blob + def it_can_generate_a_chart_data_Excel_blob( + self, request, xlsx_file_, workbook_, worksheet_, BytesIO_ + ): + _populate_worksheet_ = method_mock(request, _BaseWorkbookWriter, "_populate_worksheet") + _open_worksheet_ = method_mock(request, _BaseWorkbookWriter, "_open_worksheet") + # --- to make context manager behavior work --- + _open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) + BytesIO_.return_value = xlsx_file_ + xlsx_file_.getvalue.return_value = b"xlsx-blob" + workbook_writer = _BaseWorkbookWriter(None) + + xlsx_blob = workbook_writer.xlsx_blob + + _open_worksheet_.assert_called_once_with(workbook_writer, xlsx_file_) + _populate_worksheet_.assert_called_once_with(workbook_writer, workbook_, worksheet_) + assert xlsx_blob == b"xlsx-blob" def it_can_open_a_worksheet_in_a_context(self, open_fixture): wb_writer, xlsx_file_, workbook_, worksheet_, Workbook_ = open_fixture @@ -70,40 +75,11 @@ def populate_fixture(self): workbook_writer = _BaseWorkbookWriter(None) return workbook_writer - @pytest.fixture - def xlsx_blob_fixture( - self, - request, - xlsx_file_, - workbook_, - worksheet_, - _populate_worksheet_, - _open_worksheet_, - BytesIO_, - ): - workbook_writer = _BaseWorkbookWriter(None) - xlsx_blob = "fooblob" - BytesIO_.return_value = xlsx_file_ - # to make context manager behavior work - _open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) - xlsx_file_.getvalue.return_value = xlsx_blob - return (workbook_writer, xlsx_file_, workbook_, worksheet_, xlsx_blob) - # fixture components --------------------------------------------- @pytest.fixture def BytesIO_(self, request): - return class_mock(request, "pptx.chart.xlsx.BytesIO") - - @pytest.fixture - def _open_worksheet_(self, request): - return method_mock(request, _BaseWorkbookWriter, "_open_worksheet") - - @pytest.fixture - def _populate_worksheet_(self, request): - return method_mock( - request, _BaseWorkbookWriter, "_populate_worksheet", autospec=True - ) + return class_mock(request, "pptx.chart.xlsx.io.BytesIO") @pytest.fixture def Workbook_(self, request, workbook_): @@ -119,10 +95,12 @@ def worksheet_(self, request): @pytest.fixture def xlsx_file_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) class DescribeCategoryWorkbookWriter(object): + """Unit-test suite for `pptx.chart.xlsx.CategoryWorkbookWriter` objects.""" + def it_knows_the_categories_range_ref(self, categories_ref_fixture): workbook_writer, expected_value = categories_ref_fixture assert workbook_writer.categories_ref == expected_value @@ -227,9 +205,7 @@ def col_ref_fixture(self, request): return column_number, expected_value @pytest.fixture - def populate_fixture( - self, workbook_, worksheet_, _write_categories_, _write_series_ - ): + def populate_fixture(self, workbook_, worksheet_, _write_categories_, _write_series_): workbook_writer = CategoryWorkbookWriter(None) return workbook_writer, workbook_, worksheet_ @@ -313,9 +289,7 @@ def write_cats_fixture( return workbook_writer, workbook_, worksheet_, number_format, calls @pytest.fixture - def write_sers_fixture( - self, request, chart_data_, workbook_, worksheet_, categories_ - ): + def write_sers_fixture(self, request, chart_data_, workbook_, worksheet_, categories_): workbook_writer = CategoryWorkbookWriter(chart_data_) num_format = workbook_.add_format.return_value calls = [call.write(0, 1, "S1"), call.write_column(1, 1, (42, 24), num_format)] @@ -350,24 +324,20 @@ def worksheet_(self, request): @pytest.fixture def _write_cat_column_(self, request): - return method_mock( - request, CategoryWorkbookWriter, "_write_cat_column", autospec=True - ) + return method_mock(request, CategoryWorkbookWriter, "_write_cat_column", autospec=True) @pytest.fixture def _write_categories_(self, request): - return method_mock( - request, CategoryWorkbookWriter, "_write_categories", autospec=True - ) + return method_mock(request, CategoryWorkbookWriter, "_write_categories", autospec=True) @pytest.fixture def _write_series_(self, request): - return method_mock( - request, CategoryWorkbookWriter, "_write_series", autospec=True - ) + return method_mock(request, CategoryWorkbookWriter, "_write_series", autospec=True) class DescribeBubbleWorkbookWriter(object): + """Unit-test suite for `pptx.chart.xlsx.BubbleWorkbookWriter` objects.""" + def it_can_populate_a_worksheet_with_chart_data(self, populate_fixture): workbook_writer, workbook_, worksheet_, expected_calls = populate_fixture workbook_writer._populate_worksheet(workbook_, worksheet_) @@ -413,16 +383,7 @@ def worksheet_(self, request): class DescribeXyWorkbookWriter(object): - def it_can_generate_a_chart_data_Excel_blob(self, xlsx_blob_fixture): - workbook_writer, _open_worksheet_, xlsx_file_ = xlsx_blob_fixture[:3] - _populate_worksheet_, workbook_, worksheet_ = xlsx_blob_fixture[3:6] - xlsx_blob_ = xlsx_blob_fixture[6] - - xlsx_blob = workbook_writer.xlsx_blob - - _open_worksheet_.assert_called_once_with(xlsx_file_) - _populate_worksheet_.assert_called_once_with(workbook_, worksheet_) - assert xlsx_blob is xlsx_blob_ + """Unit-test suite for `pptx.chart.xlsx.XyWorkbookWriter` objects.""" def it_can_populate_a_worksheet_with_chart_data(self, populate_fixture): workbook_writer, workbook_, worksheet_, expected_calls = populate_fixture @@ -453,46 +414,8 @@ def populate_fixture(self, workbook_, worksheet_): ] return workbook_writer, workbook_, worksheet_, expected_calls - @pytest.fixture - def xlsx_blob_fixture( - self, - request, - xlsx_file_, - BytesIO_, - _open_worksheet_, - workbook_, - worksheet_, - _populate_worksheet_, - xlsx_blob_, - ): - workbook_writer = XyWorkbookWriter(None) - return ( - workbook_writer, - _open_worksheet_, - xlsx_file_, - _populate_worksheet_, - workbook_, - worksheet_, - xlsx_blob_, - ) - # fixture components --------------------------------------------- - @pytest.fixture - def BytesIO_(self, request, xlsx_file_): - return class_mock(request, "pptx.chart.xlsx.BytesIO", return_value=xlsx_file_) - - @pytest.fixture - def _open_worksheet_(self, request, workbook_, worksheet_): - open_worksheet_ = method_mock(request, XyWorkbookWriter, "_open_worksheet") - # to make context manager behavior work - open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) - return open_worksheet_ - - @pytest.fixture - def _populate_worksheet_(self, request): - return method_mock(request, XyWorkbookWriter, "_populate_worksheet") - @pytest.fixture def workbook_(self, request): return instance_mock(request, Workbook) @@ -500,13 +423,3 @@ def workbook_(self, request): @pytest.fixture def worksheet_(self, request): return instance_mock(request, Worksheet) - - @pytest.fixture - def xlsx_blob_(self, request): - return instance_mock(request, bytes) - - @pytest.fixture - def xlsx_file_(self, request, xlsx_blob_): - xlsx_file_ = instance_mock(request, BytesIO) - xlsx_file_.getvalue.return_value = xlsx_blob_ - return xlsx_file_ diff --git a/tests/chart/test_xmlwriter.py b/tests/chart/test_xmlwriter.py index 19e7e6473..bb7354983 100644 --- a/tests/chart/test_xmlwriter.py +++ b/tests/chart/test_xmlwriter.py @@ -1,10 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.xmlwriter module -""" +"""Unit-test suite for `pptx.chart.xmlwriter` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from datetime import date from itertools import islice @@ -12,14 +10,16 @@ import pytest from pptx.chart.data import ( - _BaseChartData, - _BaseSeriesData, BubbleChartData, CategoryChartData, CategorySeriesData, XyChartData, + _BaseChartData, + _BaseSeriesData, ) from pptx.chart.xmlwriter import ( + ChartXmlWriter, + SeriesXmlRewriterFactory, _AreaChartXmlWriter, _BarChartXmlWriter, _BaseSeriesXmlRewriter, @@ -28,12 +28,10 @@ _BubbleSeriesXmlWriter, _CategorySeriesXmlRewriter, _CategorySeriesXmlWriter, - ChartXmlWriter, _DoughnutChartXmlWriter, _LineChartXmlWriter, _PieChartXmlWriter, _RadarChartXmlWriter, - SeriesXmlRewriterFactory, _XyChartXmlWriter, _XySeriesXmlRewriter, _XySeriesXmlWriter, @@ -292,9 +290,7 @@ class Describe_PieChartXmlWriter(object): ("PIE_EXPLODED", 3, 1, "3x1-pie-exploded"), ), ) - def it_can_generate_xml_for_a_pie_chart( - self, enum_member, cat_count, ser_count, snippet_name - ): + def it_can_generate_xml_for_a_pie_chart(self, enum_member, cat_count, ser_count, snippet_name): chart_type = getattr(XL_CHART_TYPE, enum_member) chart_data = make_category_chart_data(cat_count, str, ser_count) xml_writer = _PieChartXmlWriter(chart_type, chart_data) @@ -306,9 +302,7 @@ class Describe_RadarChartXmlWriter(object): """Unit-test suite for `pptx.chart.xmlwriter._RadarChartXmlWriter`.""" def it_can_generate_xml_for_a_radar_chart(self): - series_data_seq = make_category_chart_data( - cat_count=5, cat_type=str, ser_count=2 - ) + series_data_seq = make_category_chart_data(cat_count=5, cat_type=str, ser_count=2) xml_writer = _RadarChartXmlWriter(XL_CHART_TYPE.RADAR, series_data_seq) assert xml_writer.xml == snippet_text("2x5-radar") @@ -456,9 +450,7 @@ class Describe_BaseSeriesXmlRewriter(object): def it_can_replace_series_data(self, replace_fixture): rewriter, chartSpace, plotArea, ser_count, calls = replace_fixture rewriter.replace_series_data(chartSpace) - rewriter._adjust_ser_count.assert_called_once_with( - rewriter, plotArea, ser_count - ) + rewriter._adjust_ser_count.assert_called_once_with(rewriter, plotArea, ser_count) assert rewriter._rewrite_ser_data.call_args_list == calls def it_adjusts_the_ser_count_to_help(self, adjust_fixture): @@ -519,9 +511,7 @@ def clone_fixture(self, request): return rewriter, plotArea, count, expected_xml @pytest.fixture - def replace_fixture( - self, request, chart_data_, _adjust_ser_count_, _rewrite_ser_data_ - ): + def replace_fixture(self, request, chart_data_, _adjust_ser_count_, _rewrite_ser_data_): rewriter = _BaseSeriesXmlRewriter(chart_data_) chartSpace = element( "c:chartSpace/c:chart/c:plotArea/c:barChart/(c:ser/c:order{val=0" @@ -572,15 +562,11 @@ def trim_fixture(self, request): @pytest.fixture def _add_cloned_sers_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_add_cloned_sers", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_add_cloned_sers", autospec=True) @pytest.fixture def _adjust_ser_count_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_adjust_ser_count", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_adjust_ser_count", autospec=True) @pytest.fixture def chart_data_(self, request): @@ -588,15 +574,11 @@ def chart_data_(self, request): @pytest.fixture def _rewrite_ser_data_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_rewrite_ser_data", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_rewrite_ser_data", autospec=True) @pytest.fixture def _trim_ser_count_by_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_trim_ser_count_by", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_trim_ser_count_by", autospec=True) class Describe_BubbleSeriesXmlRewriter(object): diff --git a/tests/dml/test_chtfmt.py b/tests/dml/test_chtfmt.py index 42b90f498..f87752180 100644 --- a/tests/dml/test_chtfmt.py +++ b/tests/dml/test_chtfmt.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.dml.chtfmt` module.""" -""" -Unit test suite for the pptx.dml.chtfmt module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index f0c536340..95a1f7c5d 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.text` module.""" -""" -Test suite for pptx.text module. -""" - -from __future__ import absolute_import +from __future__ import annotations import pytest @@ -182,9 +178,7 @@ def set_brightness_fixture_(self, request): "-0.3 to -0.4": (an_srgbClr, 70000, None, -0.4, 60000, None), "-0.4 to 0": (a_sysClr, 60000, None, 0, None, None), } - xClr_bldr_fn, mod_in, off_in, brightness, mod_out, off_out = mapping[ - request.param - ] + xClr_bldr_fn, mod_in, off_in, brightness, mod_out, off_out = mapping[request.param] xClr_bldr = xClr_bldr_fn() if mod_in is not None: @@ -222,10 +216,7 @@ def set_rgb_fixture_(self, request): color_format = ColorFormat.from_colorchoice_parent(solidFill) rgb_color = RGBColor(0x12, 0x34, 0x56) expected_xml = ( - a_solidFill() - .with_nsdecls() - .with_child(an_srgbClr().with_val("123456")) - .xml() + a_solidFill().with_nsdecls().with_child(an_srgbClr().with_val("123456")).xml() ) return color_format, rgb_color, expected_xml @@ -248,10 +239,7 @@ def set_theme_color_fixture_(self, request): color_format = ColorFormat.from_colorchoice_parent(solidFill) theme_color = MSO_THEME_COLOR.ACCENT_6 expected_xml = ( - a_solidFill() - .with_nsdecls() - .with_child(a_schemeClr().with_val("accent6")) - .xml() + a_solidFill().with_nsdecls().with_child(a_schemeClr().with_val("accent6")).xml() ) return color_format, theme_color, expected_xml diff --git a/tests/dml/test_effect.py b/tests/dml/test_effect.py index 53e2106de..1907e561d 100644 --- a/tests/dml/test_effect.py +++ b/tests/dml/test_effect.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.dml.effect` module.""" -"""Test suite for pptx.dml.effect module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/dml/test_fill.py b/tests/dml/test_fill.py index 2e01355a5..defbaf980 100644 --- a/tests/dml/test_fill.py +++ b/tests/dml/test_fill.py @@ -1,16 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.dml.fill module.""" +"""Unit-test suite for `pptx.dml.fill` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest from pptx.dml.color import ColorFormat from pptx.dml.fill import ( + FillFormat, _BlipFill, _Fill, - FillFormat, _GradFill, _GradientStop, _GradientStops, @@ -96,9 +96,7 @@ def it_can_change_the_angle_of_a_linear_gradient(self, grad_fill_, type_prop_): assert grad_fill_.gradient_angle == 42.24 - def it_provides_access_to_the_gradient_stops( - self, type_prop_, grad_fill_, gradient_stops_ - ): + def it_provides_access_to_the_gradient_stops(self, type_prop_, grad_fill_, gradient_stops_): type_prop_.return_value = MSO_FILL.GRADIENT grad_fill_.gradient_stops = gradient_stops_ fill = FillFormat(None, grad_fill_) @@ -492,6 +490,8 @@ def fill_type_fixture(self): class Describe_PattFill(object): + """Unit-test suite for `pptx.dml.fill._PattFill` objects.""" + def it_knows_its_fill_type(self, fill_type_fixture): patt_fill, expected_value = fill_type_fixture fill_type = patt_fill.type @@ -618,7 +618,7 @@ def pattern_set_fixture(self, request): @pytest.fixture def ColorFormat_from_colorchoice_parent_(self, request): - return method_mock(request, ColorFormat, "from_colorchoice_parent") + return method_mock(request, ColorFormat, "from_colorchoice_parent", autospec=False) @pytest.fixture def color_(self, request): @@ -660,7 +660,7 @@ def fore_color_fixture(self, ColorFormat_from_colorchoice_parent_, color_): @pytest.fixture def ColorFormat_from_colorchoice_parent_(self, request): - return method_mock(request, ColorFormat, "from_colorchoice_parent") + return method_mock(request, ColorFormat, "from_colorchoice_parent", autospec=False) @pytest.fixture def color_(self, request): diff --git a/tests/dml/test_line.py b/tests/dml/test_line.py index 9d6cec30a..158e55589 100644 --- a/tests/dml/test_line.py +++ b/tests/dml/test_line.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Test suite for `pptx.dml.line` module.""" -""" -Test suite for pptx.dml.line module -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import pytest @@ -25,10 +21,39 @@ def it_knows_its_dash_style(self, dash_style_get_fixture): line, expected_value = dash_style_get_fixture assert line.dash_style == expected_value - def it_can_change_its_dash_style(self, dash_style_set_fixture): - line, dash_style, spPr, expected_xml = dash_style_set_fixture + @pytest.mark.parametrize( + ("spPr_cxml", "dash_style", "expected_cxml"), + [ + ("p:spPr{a:b=c}", MSO_LINE.DASH, "p:spPr{a:b=c}/a:ln/a:prstDash{val=dash}"), + ("p:spPr/a:ln", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=sysDot}"), + ( + "p:spPr/a:ln/a:prstDash", + MSO_LINE.SOLID, + "p:spPr/a:ln/a:prstDash{val=solid}", + ), + ( + "p:spPr/a:ln/a:custDash", + MSO_LINE.DASH_DOT, + "p:spPr/a:ln/a:prstDash{val=dashDot}", + ), + ( + "p:spPr/a:ln/a:prstDash{val=dash}", + MSO_LINE.LONG_DASH, + "p:spPr/a:ln/a:prstDash{val=lgDash}", + ), + ("p:spPr/a:ln/a:prstDash{val=dash}", None, "p:spPr/a:ln"), + ("p:spPr/a:ln/a:custDash", None, "p:spPr/a:ln"), + ], + ) + def it_can_change_its_dash_style( + self, spPr_cxml: str, dash_style: MSO_LINE, expected_cxml: str + ): + spPr = element(spPr_cxml) + line = LineFormat(spPr) + line.dash_style = dash_style - assert spPr.xml == expected_xml + + assert spPr.xml == xml(expected_cxml) def it_knows_its_width(self, width_get_fixture): line, expected_line_width = width_get_fixture @@ -53,9 +78,7 @@ def it_has_a_color(self, color_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture( - params=[(MSO_FILL.SOLID, False), (MSO_FILL.BACKGROUND, True), (None, True)] - ) + @pytest.fixture(params=[(MSO_FILL.SOLID, False), (MSO_FILL.BACKGROUND, True), (None, True)]) def color_fixture(self, request, line, fill_prop_, fill_, color_): pre_call_fill_type, solid_call_expected = request.param fill_.type = pre_call_fill_type @@ -77,36 +100,6 @@ def dash_style_get_fixture(self, request): line = LineFormat(spPr) return line, expected_value - @pytest.fixture( - params=[ - ("p:spPr{a:b=c}", MSO_LINE.DASH, "p:spPr{a:b=c}/a:ln/a:prstDash{val=dash}"), - ("p:spPr/a:ln", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=dot}"), - ( - "p:spPr/a:ln/a:prstDash", - MSO_LINE.SOLID, - "p:spPr/a:ln/a:prstDash{val=solid}", - ), - ( - "p:spPr/a:ln/a:custDash", - MSO_LINE.DASH_DOT, - "p:spPr/a:ln/a:prstDash{val=dashDot}", - ), - ( - "p:spPr/a:ln/a:prstDash{val=dash}", - MSO_LINE.LONG_DASH, - "p:spPr/a:ln/a:prstDash{val=lgDash}", - ), - ("p:spPr/a:ln/a:prstDash{val=dash}", None, "p:spPr/a:ln"), - ("p:spPr/a:ln/a:custDash", None, "p:spPr/a:ln"), - ] - ) - def dash_style_set_fixture(self, request): - spPr_cxml, dash_style, expected_cxml = request.param - spPr = element(spPr_cxml) - line = LineFormat(spPr) - expected_xml = xml(expected_cxml) - return line, dash_style, spPr, expected_xml - @pytest.fixture def fill_fixture(self, line, FillFormat_, ln_, fill_): return line, FillFormat_, ln_, fill_ @@ -129,9 +122,7 @@ def width_get_fixture(self, request, shape_): ) def width_set_fixture(self, request, shape_): initial_width, width = request.param - shape_.ln = shape_.get_or_add_ln.return_value = self.ln_bldr( - initial_width - ).element + shape_.ln = shape_.get_or_add_ln.return_value = self.ln_bldr(initial_width).element line = LineFormat(shape_) expected_xml = self.ln_bldr(width).xml() return line, width, expected_xml diff --git a/tests/opc/unitdata/__init__.py b/tests/enum/__init__.py similarity index 100% rename from tests/opc/unitdata/__init__.py rename to tests/enum/__init__.py diff --git a/tests/enum/test_base.py b/tests/enum/test_base.py new file mode 100644 index 000000000..3b4e970a7 --- /dev/null +++ b/tests/enum/test_base.py @@ -0,0 +1,73 @@ +"""Unit-test suite for `pptx.enum.base`.""" + +from __future__ import annotations + +import pytest + +from pptx.enum.action import PP_ACTION, PP_ACTION_TYPE +from pptx.enum.dml import MSO_LINE_DASH_STYLE + + +class DescribeBaseEnum: + """Unit-test suite for `pptx.enum.base.BaseEnum`.""" + + def it_produces_members_each_equivalent_to_an_integer_value(self): + assert PP_ACTION_TYPE.END_SHOW == 6 + assert PP_ACTION_TYPE.NONE == 0 + + def but_member_reprs_are_a_str_indicating_the_enum_and_member_name(self): + assert repr(PP_ACTION_TYPE.END_SHOW) == "" + assert repr(PP_ACTION_TYPE.RUN_MACRO) == "" + + def and_member_str_values_are_a_str_indicating_the_member_name(self): + assert str(PP_ACTION_TYPE.FIRST_SLIDE) == "FIRST_SLIDE (3)" + assert str(PP_ACTION_TYPE.HYPERLINK) == "HYPERLINK (7)" + + def it_provides_a_docstring_for_each_member(self): + assert PP_ACTION_TYPE.LAST_SLIDE.__doc__ == "Moves to the last slide." + assert PP_ACTION_TYPE.LAST_SLIDE_VIEWED.__doc__ == "Moves to the last slide viewed." + + def it_can_look_up_a_member_by_its_value(self): + assert PP_ACTION_TYPE(10) == PP_ACTION_TYPE.NAMED_SLIDE_SHOW + assert PP_ACTION_TYPE(101) == PP_ACTION_TYPE.NAMED_SLIDE + + def but_it_raises_when_no_member_has_that_value(self): + with pytest.raises(ValueError, match="42 is not a valid PP_ACTION_TYPE"): + PP_ACTION_TYPE(42) + + def it_knows_its_name(self): + assert PP_ACTION_TYPE.NEXT_SLIDE.name == "NEXT_SLIDE" + assert PP_ACTION_TYPE.NONE.name == "NONE" + + def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): + assert PP_ACTION_TYPE.OPEN_FILE is PP_ACTION.OPEN_FILE + + +class DescribeBaseXmlEnum: + """Unit-test suite for `pptx.enum.base.BaseXmlEnum`.""" + + def it_can_look_up_a_member_by_its_corresponding_XML_attribute_value(self): + assert MSO_LINE_DASH_STYLE.from_xml("dash") == MSO_LINE_DASH_STYLE.DASH + assert MSO_LINE_DASH_STYLE.from_xml("dashDot") == MSO_LINE_DASH_STYLE.DASH_DOT + + def but_it_raises_on_an_attribute_value_that_is_not_regitstered(self): + with pytest.raises(ValueError, match="MSO_LINE_DASH_STYLE has no XML mapping for 'wavy'"): + MSO_LINE_DASH_STYLE.from_xml("wavy") + + def and_the_empty_string_never_maps_to_a_member(self): + with pytest.raises(ValueError, match="MSO_LINE_DASH_STYLE has no XML mapping for ''"): + MSO_LINE_DASH_STYLE.from_xml("") + + def it_knows_the_XML_attribute_value_for_each_member_that_has_one(self): + assert MSO_LINE_DASH_STYLE.to_xml(MSO_LINE_DASH_STYLE.SOLID) == "solid" + + def and_it_looks_up_the_member_by_int_value_before_mapping_when_provided_that_way(self): + assert MSO_LINE_DASH_STYLE.to_xml(3) == "sysDot" + + def but_it_raises_when_no_member_has_the_provided_int_value(self): + with pytest.raises(ValueError, match="42 is not a valid MSO_LINE_DASH_STYLE"): + MSO_LINE_DASH_STYLE.to_xml(42) + + def and_it_raises_when_the_member_has_no_XML_value(self): + with pytest.raises(ValueError, match="MSO_LINE_DASH_STYLE.DASH_STYLE_MIXED has no XML r"): + MSO_LINE_DASH_STYLE.to_xml(-2) diff --git a/tests/enum/test_shapes.py b/tests/enum/test_shapes.py new file mode 100644 index 000000000..5bfac6966 --- /dev/null +++ b/tests/enum/test_shapes.py @@ -0,0 +1,46 @@ +"""Unit-test suite for `pptx.enum.shapes`.""" + +from __future__ import annotations + +import pytest + +from pptx.enum.shapes import PROG_ID + + +class DescribeProgId: + """Unit-test suite for `pptx.enum.shapes.ProgId`.""" + + def it_has_members_for_the_OLE_embeddings_known_to_work_on_Windows(self): + assert PROG_ID.DOCX + assert PROG_ID.PPTX + assert PROG_ID.XLSX + + @pytest.mark.parametrize( + ("member", "expected_value"), + [(PROG_ID.DOCX, 609600), (PROG_ID.PPTX, 609600), (PROG_ID.XLSX, 609600)], + ) + def it_knows_its_height(self, member: PROG_ID, expected_value: int): + assert member.height == expected_value + + def it_knows_its_icon_filename(self): + assert PROG_ID.DOCX.icon_filename == "docx-icon.emf" + + def it_knows_its_progId(self): + assert PROG_ID.PPTX.progId == "PowerPoint.Show.12" + + def it_knows_its_width(self): + assert PROG_ID.XLSX.width == 965200 + + @pytest.mark.parametrize( + ("value", "expected_value"), + [ + # -DELETEME--------------------------------------------------------------- + (PROG_ID.DOCX, True), + (PROG_ID.PPTX, True), + (PROG_ID.XLSX, True), + (17, False), + ("XLSX", False), + ], + ) + def it_knows_each_of_its_members_is_an_instance(self, value: object, expected_value: bool): + assert isinstance(value, PROG_ID) is expected_value diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index 76131f6ea..5dee9408b 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -1,10 +1,8 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.opc.oxml` module.""" -""" -Test suite for opc.oxml module -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals +from typing import cast import pytest @@ -15,129 +13,179 @@ CT_Relationship, CT_Relationships, CT_Types, + nsmap, oxml_tostring, serialize_part_xml, ) +from pptx.opc.packuri import PackURI from pptx.oxml import parse_xml +from pptx.oxml.xmlchemy import BaseOxmlElement + +from ..unitutil.cxml import element -from .unitdata.rels import ( - a_Default, - an_Override, - a_Relationship, - a_Relationships, - a_Types, -) +class DescribeCT_Default: + """Unit-test suite for `pptx.opc.oxml.CT_Default` objects.""" -class DescribeCT_Default(object): def it_provides_read_access_to_xml_values(self): - default = a_Default().element + default = cast(CT_Default, element("ct:Default{Extension=xml,ContentType=application/xml}")) assert default.extension == "xml" assert default.contentType == "application/xml" -class DescribeCT_Override(object): +class DescribeCT_Override: + """Unit-test suite for `pptx.opc.oxml.CT_Override` objects.""" + def it_provides_read_access_to_xml_values(self): - override = an_Override().element + override = cast( + CT_Override, element("ct:Override{PartName=/part/name.xml,ContentType=text/plain}") + ) assert override.partName == "/part/name.xml" - assert override.contentType == "app/vnd.type" + assert override.contentType == "text/plain" + +class DescribeCT_Relationship: + """Unit-test suite for `pptx.opc.oxml.CT_Relationship` objects.""" -class DescribeCT_Relationship(object): def it_provides_read_access_to_xml_values(self): - rel = a_Relationship().element + rel = cast( + CT_Relationship, + element("pr:Relationship{Id=rId9,Type=ReLtYpE,Target=docProps/core.xml}"), + ) assert rel.rId == "rId9" assert rel.reltype == "ReLtYpE" assert rel.target_ref == "docProps/core.xml" assert rel.targetMode == 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), + def it_constructs_an_internal_relationship_when_no_target_mode_is_provided(self): + rel = CT_Relationship.new("rId9", "ReLtYpE", "foo/bar.xml") + + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "foo/bar.xml" + assert rel.targetMode == RTM.INTERNAL + assert rel.xml == ( + f'' ) - for rId, reltype, target, target_mode in cases: - if target_mode is None: - rel = CT_Relationship.new(rId, reltype, target) - else: - rel = CT_Relationship.new(rId, reltype, target, target_mode) - builder = a_Relationship().with_target(target) - if target_mode == RTM.EXTERNAL: - builder = builder.with_target_mode(RTM.EXTERNAL) - expected_rel_xml = builder.xml - assert rel.xml == expected_rel_xml - - -class DescribeCT_Relationships(object): + + def and_it_constructs_an_internal_relationship_when_target_mode_INTERNAL_is_specified(self): + rel = CT_Relationship.new("rId9", "ReLtYpE", "foo/bar.xml", RTM.INTERNAL) + + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "foo/bar.xml" + assert rel.targetMode == RTM.INTERNAL + assert rel.xml == ( + f'' + ) + + def and_it_constructs_an_external_relationship_when_target_mode_EXTERNAL_is_specified(self): + rel = CT_Relationship.new("rId9", "ReLtYpE", "http://some/link", RTM.EXTERNAL) + + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "http://some/link" + assert rel.targetMode == RTM.EXTERNAL + assert rel.xml == ( + f'' + ) + + +class DescribeCT_Relationships: + """Unit-test suite for `pptx.opc.oxml.CT_Relationships` objects.""" + def it_can_construct_a_new_relationships_element(self): rels = CT_Relationships.new() - expected_xml = ( - "\n" - '' + assert rels.xml == ( + '' ) - assert rels.xml.decode("utf-8") == expected_xml 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") - # verify ----------------------- - expected_rels_xml = a_Relationships().xml - actual_xml = oxml_tostring(rels, encoding="unicode", pretty_print=True) - assert actual_xml == expected_rels_xml + + assert oxml_tostring(rels, encoding="unicode", pretty_print=True) == ( + '\n' + ' \n' + ' \n' + ' \n' + "\n" + ) def it_can_generate_rels_file_xml(self): - expected_xml = ( + assert CT_Relationships.new().xml_file_bytes == ( "\n" ''.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 +class DescribeCT_Types: + """Unit-test suite for `pptx.opc.oxml.CT_Types` objects.""" + + def it_provides_access_to_default_child_elements(self, types: CT_Types): assert len(types.default_lst) == 2 for default in types.default_lst: assert isinstance(default, CT_Default) - def it_provides_access_to_override_child_elements(self): - types = a_Types().element + def it_provides_access_to_override_child_elements(self, types: CT_Types): assert len(types.override_lst) == 3 for override in types.override_lst: assert isinstance(override, CT_Override) def it_should_have_empty_list_on_no_matching_elements(self): - types = a_Types().empty().element + types = cast(CT_Types, element("ct:Types")) assert types.default_lst == [] assert types.override_lst == [] def it_can_construct_a_new_types_element(self): types = CT_Types.new() - expected_xml = a_Types().empty().xml - assert types.xml == expected_xml + assert types.xml == ( + '\n' + ) 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") - expected_types_xml = a_Types().xml - assert types.xml == expected_types_xml + types.add_override(PackURI("/docProps/core.xml"), "app/vnd.type1") + types.add_override(PackURI("/ppt/presentation.xml"), "app/vnd.type2") + types.add_override(PackURI("/docProps/thumbnail.jpeg"), "image/jpeg") + assert types.xml == ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + "\n" + ) + + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def types(self) -> CT_Types: + return cast( + CT_Types, + element( + "ct:Types/(ct:Default{Extension=xml,ContentType=application/xml}" + ",ct:Default{Extension=jpeg,ContentType=image/jpeg}" + ",ct:Override{PartName=/docProps/core.xml,ContentType=app/vnd.type1}" + ",ct:Override{PartName=/ppt/presentation.xml,ContentType=app/vnd.type2}" + ",ct:Override{PartName=/docProps/thunbnail.jpeg,ContentType=image/jpeg})" + ), + ) + +class Describe_serialize_part_xml: + """Unit-test suite for `pptx.opc.oxml.serialize_part_xml` function.""" -class DescribeSerializePartXml(object): - def it_produces_properly_formatted_xml_for_an_opc_part( - self, part_elm, expected_part_xml - ): + def it_produces_properly_formatted_xml_for_an_opc_part(self): """ Tested aspects: --------------- @@ -146,27 +194,18 @@ def it_produces_properly_formatted_xml_for_an_opc_part( * [X] it preserves unused namespaces * [X] it returns bytes ready to save to file (not unicode) """ + part_elm = cast( + BaseOxmlElement, + parse_xml( + '\n fØØ' + "bÅr\n\n" + ), + ) xml = serialize_part_xml(part_elm) - assert xml == expected_part_xml # xml contains 134 chars, of which 3 are double-byte; it will have # len of 134 if it's unicode and 137 if it's bytes assert len(xml) == 137 - - # fixtures ----------------------------------- - - @pytest.fixture - def part_elm(self): - return parse_xml( - '\n fØØ' - "bÅr\n\n" - ) - - @pytest.fixture - def expected_part_xml(self): - unicode_xml = ( + assert xml == ( "\n" - 'fØØbÅr<' - "/f:bar>" - ) - xml_bytes = unicode_xml.encode("utf-8") - return xml_bytes + 'fØØbÅr' + ).encode("utf-8") diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index da09d2c2e..8c0e95809 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -1,327 +1,427 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.opc.package` module.""" +from __future__ import annotations + +import collections import io +import itertools +from typing import Any import pytest -from pptx.opc.oxml import CT_Relationships -from pptx.opc.packuri import PACKAGE_URI, PackURI +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.oxml import CT_Relationship, CT_Relationships from pptx.opc.package import ( OpcPackage, Part, PartFactory, - _Relationship, - RelationshipCollection, - Unmarshaller, XmlPart, + _ContentTypeMap, + _PackageLoader, + _RelatableMixin, + _Relationship, + _Relationships, ) -from pptx.opc.pkgreader import PackageReader -from pptx.oxml.xmlchemy import BaseOxmlElement -from pptx.package import Package +from pptx.opc.packuri import PACKAGE_URI, PackURI +from pptx.oxml import parse_xml +from pptx.parts.presentation import PresentationPart from ..unitutil.cxml import element -from ..unitutil.file import absjoin, test_file_dir +from ..unitutil.file import absjoin, snippet_bytes, test_file_dir, testfile_bytes from ..unitutil.mock import ( + ANY, + FixtureRequest, + Mock, call, class_mock, - cls_attr_mock, function_mock, initializer_mock, instance_mock, - loose_mock, method_mock, - Mock, - patch, - PropertyMock, + property_mock, ) -class DescribeOpcPackage(object): - def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): - # mockery ---------------------- - 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_) - assert isinstance(pkg, OpcPackage) - - def it_initializes_its_rels_collection_on_first_reference( - self, RelationshipCollection_ +class Describe_RelatableMixin: + """Unit-test suite for `pptx.opc.package._RelatableMixin`. + + This mixin is used for both OpcPackage and Part because both a package and a part + can have relationships to target parts. + """ + + def it_can_find_a_part_related_by_reltype(self, _rels_prop_, relationships_, part_): + relationships_.part_with_reltype.return_value = part_ + _rels_prop_.return_value = relationships_ + mixin = _RelatableMixin() + + related_part = mixin.part_related_by(RT.CHART) + + relationships_.part_with_reltype.assert_called_once_with(RT.CHART) + assert related_part is part_ + + def it_can_establish_a_relationship_to_another_part(self, _rels_prop_, relationships_, part_): + relationships_.get_or_add.return_value = "rId42" + _rels_prop_.return_value = relationships_ + mixin = _RelatableMixin() + + rId = mixin.relate_to(part_, RT.SLIDE) + + relationships_.get_or_add.assert_called_once_with(RT.SLIDE, part_) + assert rId == "rId42" + + def and_it_can_establish_a_relationship_to_an_external_link( + self, request, _rels_prop_, relationships_ ): - pkg = OpcPackage() - rels = pkg.rels - RelationshipCollection_.assert_called_once_with(PACKAGE_URI.baseURI) - assert rels == RelationshipCollection_.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_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 - - def it_can_provide_a_list_of_the_parts_it_contains(self): - # mockery ---------------------- - parts = [Mock(name="part1"), Mock(name="part2")] - pkg = OpcPackage() - # verify ----------------------- - with patch.object(OpcPackage, "iter_parts", return_value=parts): - assert pkg.parts == [parts[0], parts[1]] - - def it_can_iterate_over_its_parts(self, iter_parts_fixture): - package, expected_parts = iter_parts_fixture - parts = list(package.iter_parts()) - assert parts == expected_parts - - def it_can_iterate_over_its_relationships(self, iter_rels_fixture): - package, expected_rels = iter_rels_fixture - rels = list(package.iter_rels()) - assert rels == expected_rels - - def it_can_find_a_part_related_by_reltype(self, related_part_fixture_): - pkg, reltype, related_part_ = related_part_fixture_ - related_part = pkg.part_related_by(reltype) - pkg.rels.part_with_reltype.assert_called_once_with(reltype) - assert related_part is related_part_ - - def it_can_find_the_next_available_vector_partname(self, next_partname_fixture): - package, partname_template, expected_partname = next_partname_fixture - partname = package.next_partname(partname_template) - assert isinstance(partname, PackURI) - assert partname == expected_partname - - 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_) - - def it_can_be_notified_after_unmarshalling_is_complete(self, pkg): - pkg.after_unmarshal() + relationships_.get_or_add_ext_rel.return_value = "rId24" + _rels_prop_.return_value = relationships_ + mixin = _RelatableMixin() - # fixtures --------------------------------------------- + rId = mixin.relate_to("http://url", RT.HYPERLINK, is_external=True) - @pytest.fixture - def iter_parts_fixture(self, request, rels_fixture): - package, parts, rels = rels_fixture - expected_parts = list(parts) - return package, expected_parts + relationships_.get_or_add_ext_rel.assert_called_once_with(RT.HYPERLINK, "http://url") + assert rId == "rId24" + + def it_can_find_a_related_part_by_rId( + self, request, _rels_prop_, relationships_, relationship_, part_ + ): + _rels_prop_.return_value = relationships_ + relationships_.__getitem__.return_value = relationship_ + relationship_.target_part = part_ + mixin = _RelatableMixin() + + related_part = mixin.related_part("rId17") + + relationships_.__getitem__.assert_called_once_with("rId17") + assert related_part is part_ + + def it_can_find_a_target_ref_URI_by_rId( + self, request, _rels_prop_, relationships_, relationship_ + ): + _rels_prop_.return_value = relationships_ + relationships_.__getitem__.return_value = relationship_ + relationship_.target_ref = "http://url" + mixin = _RelatableMixin() + + target_ref = mixin.target_ref("rId9") + + relationships_.__getitem__.assert_called_once_with("rId9") + assert target_ref == "http://url" + + # fixture components ----------------------------------- @pytest.fixture - def iter_rels_fixture(self, request, rels_fixture): - package, parts, rels = rels_fixture - expected_rels = list(rels) - return package, expected_rels - - @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_numbers, next_partname_number = request.param - package = OpcPackage() - parts = [ - instance_mock( - request, Part, name="part[%d]" % idx, partname="/foo/bar/baz%d.xml" % n - ) - for idx, n in enumerate(existing_partname_numbers) - ] - iter_parts_.return_value = iter(parts) - partname_template = "/foo/bar/baz%d.xml" - expected_partname = PackURI("/foo/bar/baz%d.xml" % next_partname_number) - return package, partname_template, expected_partname + def part_(self, request): + return instance_mock(request, Part) @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 + def relationship_(self, request): + return instance_mock(request, _Relationship) @pytest.fixture - def related_part_fixture_(self, request, rels_, reltype): - 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_ + def relationships_(self, request): + return instance_mock(request, _Relationships) @pytest.fixture - def rels_fixture(self, request, part_1_, part_2_): + def _rels_prop_(self, request): + return property_mock(request, _RelatableMixin, "_rels") + + +class DescribeOpcPackage: + """Unit-test suite for `pptx.opc.package.OpcPackage` objects.""" + + def it_can_open_a_pkg_file(self, request): + package_ = instance_mock(request, OpcPackage) + _init_ = initializer_mock(request, OpcPackage) + _load_ = method_mock(request, OpcPackage, "_load", return_value=package_) + + package = OpcPackage.open("package.pptx") + + _init_.assert_called_once_with(ANY, "package.pptx") + _load_.assert_called_once_with(ANY) + assert package is package_ + + def it_can_drop_a_relationship(self, _rels_prop_, relationships_): + _rels_prop_.return_value = relationships_ + + OpcPackage(None).drop_rel("rId42") + + relationships_.pop.assert_called_once_with("rId42") + + def it_can_iterate_over_its_parts(self, request): + part_, part_2_ = [instance_mock(request, Part, name="part_%d" % i) for i in range(2)] + rels_iter = ( + instance_mock(request, _Relationship, is_external=is_external, target_part=target) + for is_external, target in ( + (True, "http://some/url/"), + (False, part_), + (False, part_), + (False, part_2_), + (False, part_), + (False, part_2_), + ) + ) + method_mock(request, OpcPackage, "iter_rels", return_value=rels_iter) + package = OpcPackage(None) + + assert list(package.iter_parts()) == [part_, part_2_] + + def it_can_iterate_over_its_relationships(self, request, _rels_prop_): """ +----------+ +--------+ - | pkg_rels |-- r1 --> | part_1 | + | pkg_rels |-- r0 --> | part_0 | +----------+ +--------+ | | | ^ - r5 | | r4 r2 | | r3 + r2 | | r1 r3 | | r4 | | | | v | v | external | +--------+ - +--------> | part_2 | + +--------> | part_1 | +--------+ """ - r1 = self.rel(request, False, part_1_, "r1") - r2 = self.rel(request, False, part_2_, "r2") - r3 = self.rel(request, False, part_1_, "r3") - r4 = self.rel(request, False, part_2_, "r4") - r5 = self.rel(request, True, None, "r5") + part_0_, part_1_ = [instance_mock(request, Part, name="part_%d" % i) for i in range(2)] + all_rels = tuple( + instance_mock( + request, + _Relationship, + name="r%d" % i, + is_external=ext, + target_part=part, + ) + for i, (ext, part) in enumerate( + ( + (False, part_0_), + (False, part_1_), + (True, None), + (False, part_1_), + (False, part_0_), + ) + ) + ) + _rels_prop_.return_value = {r.rId: r for r in all_rels[:3]} + part_0_.rels = {r.rId: r for r in all_rels[3:4]} + part_1_.rels = {r.rId: r for r in all_rels[4:]} + package = OpcPackage(None) - package = OpcPackage() + rels = set(package.iter_rels()) - package._rels = self.rels(request, (r1, r4, r5)) - part_1_.rels = self.rels(request, (r2,)) - part_2_.rels = self.rels(request, (r3,)) + # -- sequence is not guaranteed, but count (len) and uniqueness are -- + assert rels == set(all_rels) - return package, (part_1_, part_2_), (r1, r2, r3, r4, r5) + def it_provides_access_to_the_main_document_part(self, request): + presentation_part_ = instance_mock(request, PresentationPart) + part_related_by_ = method_mock( + request, OpcPackage, "part_related_by", return_value=presentation_part_ + ) + package = OpcPackage(None) - # fixture components ----------------------------------- + presentation_part = package.main_document_part - @pytest.fixture - def iter_parts_(self, request): - return method_mock(request, OpcPackage, "iter_parts") + part_related_by_.assert_called_once_with(package, RT.OFFICE_DOCUMENT) + assert presentation_part is presentation_part_ - @pytest.fixture - def PackageReader_(self, request): - return class_mock(request, "pptx.opc.package.PackageReader") + @pytest.mark.parametrize( + "ns, expected_n", (((), 1), ((1,), 2), ((1, 2), 3), ((2, 4), 3), ((1, 4), 3)) + ) + def it_can_find_the_next_available_partname(self, request, ns, expected_n): + tmpl = "/x%d.xml" + method_mock( + request, + OpcPackage, + "iter_parts", + return_value=(instance_mock(request, Part, partname=tmpl % n) for n in ns), + ) + next_partname = tmpl % expected_n + PackURI_ = class_mock( + request, "pptx.opc.package.PackURI", return_value=PackURI(next_partname) + ) + package = OpcPackage(None) - @pytest.fixture - def PackageWriter_(self, request): - return class_mock(request, "pptx.opc.package.PackageWriter") + partname = package.next_partname(tmpl) - @pytest.fixture - def PartFactory_(self, request): - return class_mock(request, "pptx.opc.package.PartFactory") + PackURI_.assert_called_once_with(next_partname) + assert partname == next_partname - @pytest.fixture - def part_1_(self, request): - return instance_mock(request, Part) + def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): + _rels_prop_.return_value = relationships_ + parts_ = tuple(instance_mock(request, Part) for _ in range(3)) + method_mock(request, OpcPackage, "iter_parts", return_value=iter(parts_)) + PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") + package = OpcPackage(None) - @pytest.fixture - def part_2_(self, request): - return instance_mock(request, Part) + package.save("prs.pptx") - @pytest.fixture - def parts(self, request, parts_): - """ - Return a mock patching property OpcPackage.parts, reversing the - patch after each use. - """ - _patch = patch.object( - OpcPackage, "parts", new_callable=PropertyMock, return_value=parts_ + PackageWriter_.write.assert_called_once_with("prs.pptx", relationships_, parts_) + + def it_loads_the_pkg_file_to_help(self, request, _rels_prop_, relationships_): + _PackageLoader_ = class_mock(request, "pptx.opc.package._PackageLoader") + _PackageLoader_.load.return_value = "pkg-rels-xml", {"partname": "part"} + _rels_prop_.return_value = relationships_ + package = OpcPackage("prs.pptx") + + return_value = package._load() + + _PackageLoader_.load.assert_called_once_with("prs.pptx", package) + relationships_.load_from_xml.assert_called_once_with( + PACKAGE_URI, "pkg-rels-xml", {"partname": "part"} ) - request.addfinalizer(_patch.stop) - return _patch.start() + assert return_value is package - @pytest.fixture - def parts_(self, request): - part_ = instance_mock(request, Part, name="part_") - part_2_ = instance_mock(request, Part, name="part_2_") - return [part_, part_2_] + def it_constructs_its_relationships_object_to_help(self, request, relationships_): + _Relationships_ = class_mock( + request, "pptx.opc.package._Relationships", return_value=relationships_ + ) + package = OpcPackage(None) - @pytest.fixture - def pkg(self, request): - return OpcPackage() + rels = package._rels - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) + _Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) + assert rels is relationships_ - @pytest.fixture - def pkg_with_rels_(self, request, rels_): - pkg = OpcPackage() - pkg._rels = rels_ - return pkg + # fixture components ----------------------------------- @pytest.fixture - def RelationshipCollection_(self, request): - return class_mock(request, "pptx.opc.package.RelationshipCollection") + def relationships_(self, request): + return instance_mock(request, _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, is_external, target_part, name): - return instance_mock( + def _rels_prop_(self, request): + return property_mock(request, OpcPackage, "_rels") + + +class Describe_PackageLoader: + """Unit-test suite for `pptx.opc.package._PackageLoader` objects.""" + + def it_provides_a_load_interface_classmethod(self, request, package_): + _init_ = initializer_mock(request, _PackageLoader) + pkg_xml_rels_ = element("r:Relationships") + _load_ = method_mock( request, - _Relationship, - is_external=is_external, - target_part=target_part, - name=name, + _PackageLoader, + "_load", + return_value=(pkg_xml_rels_, {"partname": "part"}), + ) + + pkg_xml_rels, parts = _PackageLoader.load("prs.pptx", package_) + + _init_.assert_called_once_with(ANY, "prs.pptx", package_) + _load_.assert_called_once_with(ANY) + assert pkg_xml_rels is pkg_xml_rels_ + assert parts == {"partname": "part"} + + def it_loads_the_package_to_help(self, request, _xml_rels_prop_): + parts_ = { + "partname_%d" % n: instance_mock(request, Part, partname="partname_%d" % n) + for n in range(1, 4) + } + property_mock(request, _PackageLoader, "_parts", return_value=parts_) + rels_ = dict( + itertools.chain( + (("/", instance_mock(request, _Relationships)),), + (("partname_%d" % n, instance_mock(request, _Relationships)) for n in range(1, 4)), + ) ) + _xml_rels_prop_.return_value = rels_ + package_loader = _PackageLoader(None, None) + + pkg_xml_rels, parts = package_loader._load() + + for part_ in parts_.values(): + part_.load_rels_from_xml.assert_called_once_with(rels_[part_.partname], parts_) + assert pkg_xml_rels is rels_["/"] + assert parts is parts_ + + def it_loads_the_xml_relationships_from_the_package_to_help(self, request): + pkg_xml_rels = parse_xml(snippet_bytes("package-rels-xml")) + prs_xml_rels = parse_xml(snippet_bytes("presentation-rels-xml")) + slide_xml_rels = CT_Relationships.new() + thumbnail_xml_rels = CT_Relationships.new() + core_xml_rels = CT_Relationships.new() + _xml_rels_for_ = method_mock( + request, + _PackageLoader, + "_xml_rels_for", + side_effect=iter( + ( + pkg_xml_rels, + prs_xml_rels, + slide_xml_rels, + thumbnail_xml_rels, + core_xml_rels, + ) + ), + ) + package_loader = _PackageLoader(None, None) - def rels(self, request, values): - rels = instance_mock(request, RelationshipCollection) - rels.values.return_value = values - return rels + xml_rels = package_loader._xml_rels - @pytest.fixture - def rels_(self, request): - return instance_mock(request, RelationshipCollection) + # print(f"{_xml_rels_for_.call_args_list=}") + assert _xml_rels_for_.call_args_list == [ + call(package_loader, "/"), + call(package_loader, "/ppt/presentation.xml"), + call(package_loader, "/ppt/slides/slide1.xml"), + call(package_loader, "/docProps/thumbnail.jpeg"), + call(package_loader, "/docProps/core.xml"), + ] + assert xml_rels == { + "/": pkg_xml_rels, + "/ppt/presentation.xml": prs_xml_rels, + "/ppt/slides/slide1.xml": slide_xml_rels, + "/docProps/thumbnail.jpeg": thumbnail_xml_rels, + "/docProps/core.xml": core_xml_rels, + } + + # fixture components ----------------------------------- @pytest.fixture - def reltype(self, request): - return "http://rel/type" + def package_(self, request): + return instance_mock(request, OpcPackage) @pytest.fixture - def Unmarshaller_(self, request): - return class_mock(request, "pptx.opc.package.Unmarshaller") + def _xml_rels_prop_(self, request): + return property_mock(request, _PackageLoader, "_xml_rels") -class DescribePart(object): +class DescribePart: """Unit-test suite for `pptx.opc.package.Part` objects.""" - def it_can_be_constructed_by_PartFactory(self, load_fixture): - partname_, content_type_, blob_, package_, __init_ = load_fixture - part = Part.load(partname_, content_type_, blob_, package_) - __init_.assert_called_once_with(partname_, content_type_, blob_, package_) - assert isinstance(part, Part) + def it_can_be_constructed_by_PartFactory(self, request, package_): + partname_ = instance_mock(request, PackURI) + _init_ = initializer_mock(request, Part) - def it_knows_its_partname(self, partname_get_fixture): - part, expected_partname = partname_get_fixture - assert part.partname == expected_partname + part = Part.load(partname_, CT.PML_SLIDE, package_, b"blob") - 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 + _init_.assert_called_once_with(part, partname_, CT.PML_SLIDE, package_, b"blob") + assert isinstance(part, Part) - 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_uses_the_load_blob_as_its_blob(self): + assert Part(None, None, None, b"blob").blob == b"blob" - 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_can_change_its_blob(self): + part = Part(None, None, None, b"old-blob") + part.blob = b"new-blob" + assert part.blob == b"new-blob" - def it_can_be_notified_after_unmarshalling_is_complete(self, part): - part.after_unmarshal() + def it_knows_its_content_type(self): + assert Part(None, CT.PML_SLIDE, None).content_type == CT.PML_SLIDE - def it_can_be_notified_before_marshalling_is_started(self, part): - part.before_marshal() + def it_knows_the_package_it_belongs_to(self, package_): + assert Part(None, None, package_).package is package_ - 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_knows_its_partname(self): + assert Part(PackURI("/part/name"), None, None).partname == PackURI("/part/name") - def it_can_change_its_blob(self): - part, new_blob = Part(None, None, "xyz", None), "foobar" - part.blob = new_blob - assert part.blob == new_blob + def it_can_change_its_partname(self): + part = Part(PackURI("/old/part/name"), None, None) + part.partname = PackURI("/new/part/name") + assert part.partname == PackURI("/new/part/name") + + def it_provides_access_to_its_relationships_for_traversal(self, request, relationships_): + property_mock(request, Part, "_rels", return_value=relationships_) + assert Part(None, None, None).rels is relationships_ def it_can_load_a_blob_from_a_file_path_to_help(self): path = absjoin(test_file_dir, "minimal.pptx") @@ -335,697 +435,597 @@ def it_can_load_a_blob_from_a_file_like_object_to_help(self): part = Part(None, None, None, None) assert part._blob_from_file(io.BytesIO(b"012345")) == b"012345" - # fixtures --------------------------------------------- + def it_constructs_its_relationships_object_to_help(self, request, relationships_): + _Relationships_ = class_mock( + request, "pptx.opc.package._Relationships", return_value=relationships_ + ) + part = Part(PackURI("/ppt/slides/slide1.xml"), None, None) - @pytest.fixture - def blob_fixture(self, blob_): - part = Part(None, None, blob_, None) - return part, blob_ + rels = part._rels - @pytest.fixture - def content_type_fixture(self): - content_type = "content/type" - part = Part(None, content_type, None, None) - return part, content_type + _Relationships_.assert_called_once_with("/ppt/slides") + assert rels is relationships_ - @pytest.fixture - def load_fixture(self, request, partname_, content_type_, blob_, package_, __init_): - return (partname_, content_type_, blob_, package_, __init_) + # fixture components --------------------------------------------- @pytest.fixture - def package_get_fixture(self, package_): - part = Part(None, None, None, package_) - return part, package_ + def package_(self, request): + return instance_mock(request, OpcPackage) @pytest.fixture - def part(self): - part = Part(None, None) - return part + def relationships_(self, request): + return instance_mock(request, _Relationships) - @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 +class DescribeXmlPart: + """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" - # fixture components --------------------------------------------- + def it_can_be_constructed_by_PartFactory(self, request): + partname = PackURI("/ppt/slides/slide1.xml") + element_ = element("p:sld") + package_ = instance_mock(request, OpcPackage) + parse_xml_ = function_mock(request, "pptx.opc.package.parse_xml", return_value=element_) + _init_ = initializer_mock(request, XmlPart) - @pytest.fixture - def blob_(self, request): - return instance_mock(request, bytes) + part = XmlPart.load(partname, CT.PML_SLIDE, package_, b"blob") - @pytest.fixture - def content_type_(self, request): - return instance_mock(request, str) + parse_xml_.assert_called_once_with(b"blob") + _init_.assert_called_once_with(part, partname, CT.PML_SLIDE, package_, element_) + assert isinstance(part, XmlPart) - @pytest.fixture - def __init_(self, request): - return initializer_mock(request, Part) + def it_can_serialize_to_xml(self, request): + element_ = element("p:sld") + serialize_part_xml_ = function_mock(request, "pptx.opc.package.serialize_part_xml") + xml_part = XmlPart(None, None, None, element_) - @pytest.fixture - def package_(self, request): - return instance_mock(request, OpcPackage) + blob = xml_part.blob - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - - -class DescribePartRelationshipManagementInterface(object): - def it_provides_access_to_its_relationships(self, rels_fixture): - part, Relationships_, partname_, rels_ = rels_fixture - 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_ + serialize_part_xml_.assert_called_once_with(element_) + assert blob is serialize_part_xml_.return_value - # fixtures --------------------------------------------- + @pytest.mark.parametrize(("ref_count", "calls"), [(2, []), (1, [call("rId42")])]) + def it_can_drop_a_relationship( + self, request: FixtureRequest, relationships_: Mock, ref_count: int, calls: list[Any] + ): + _rel_ref_count_ = method_mock(request, XmlPart, "_rel_ref_count", return_value=ref_count) + property_mock(request, XmlPart, "_rels", return_value=relationships_) + part = XmlPart(None, None, None, None) - @pytest.fixture( - params=[ - ("p:sp", True), - ("p:sp/r:a{r:id=rId42}", True), - ("p:sp/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 + part.drop_rel("rId42") - @pytest.fixture - def load_rel_fixture(self, part, rels_, reltype_, part_, rId_): - part._rels = rels_ - return part, rels_, reltype_, part_, rId_ + _rel_ref_count_.assert_called_once_with(part, "rId42") + assert relationships_.pop.call_args_list == calls - @pytest.fixture - def relate_to_part_fixture(self, request, part, reltype_, part_, rels_, rId_): - part._rels = rels_ - target_ = part_ - return part, target_, reltype_, rId_ + def it_knows_it_is_the_part_for_its_child_objects(self): + xml_part = XmlPart(None, None, None, None) + assert xml_part.part is xml_part - @pytest.fixture - def relate_to_url_fixture(self, request, part, rels_, url_, reltype_, rId_): - part._rels = rels_ - return part, url_, reltype_, rId_ + # -- fixtures ---------------------------------------------------- @pytest.fixture - def related_part_fixture(self, request, part, rels_, reltype_, part_): - part._rels = rels_ - return part, reltype_, part_ + def relationships_(self, request): + return instance_mock(request, _Relationships) - @pytest.fixture - def related_parts_fixture(self, request, part, rels_, related_parts_): - part._rels = rels_ - return part, related_parts_ - @pytest.fixture - def rels_fixture(self, Relationships_, partname_, rels_): - part = Part(partname_, None) - return part, Relationships_, partname_, rels_ +class DescribePartFactory: + """Unit-test suite for `pptx.opc.package.PartFactory` objects.""" - @pytest.fixture - def target_ref_fixture(self, request, part, rId_, rel_, url_): - part._rels = {rId_: rel_} - return part, rId_, url_ + def it_constructs_custom_part_type_for_registered_content_types(self, request, package_, part_): + SlidePart_ = class_mock(request, "pptx.opc.package.XmlPart") + SlidePart_.load.return_value = part_ + partname = PackURI("/ppt/slides/slide7.xml") + PartFactory.part_type_for[CT.PML_SLIDE] = SlidePart_ - # fixture components --------------------------------------------- + part = PartFactory(partname, CT.PML_SLIDE, package_, b"blob") + + SlidePart_.load.assert_called_once_with(partname, CT.PML_SLIDE, package_, b"blob") + assert part is part_ + + def it_constructs_part_using_default_class_when_no_custom_registered( + self, request, package_, part_ + ): + Part_ = class_mock(request, "pptx.opc.package.Part") + Part_.load.return_value = part_ + partname = PackURI("/bar/foo.xml") + + part = PartFactory(partname, CT.OFC_VML_DRAWING, package_, b"blob") + + Part_.load.assert_called_once_with(partname, CT.OFC_VML_DRAWING, package_, b"blob") + assert part is part_ + + # fixtures components ---------------------------------- @pytest.fixture - def part(self): - return Part(None, None) + def package_(self, request): + return instance_mock(request, OpcPackage) @pytest.fixture def part_(self, request): return instance_mock(request, Part) - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - @pytest.fixture - def Relationships_(self, request, rels_): - return class_mock( - request, "pptx.opc.package.RelationshipCollection", return_value=rels_ +class Describe_ContentTypeMap: + """Unit-test suite for `pptx.opc.package._ContentTypeMap` objects.""" + + def it_can_construct_from_content_types_xml(self, request): + _init_ = initializer_mock(request, _ContentTypeMap) + content_types_xml = ( + '\n' + ' \n' + ' \n' + ' \n" + "\n" ) - @pytest.fixture - def rel_(self, request, rId_, url_): - return instance_mock(request, _Relationship, rId=rId_, target_ref=url_) + ct_map = _ContentTypeMap.from_xml(content_types_xml) - @pytest.fixture - def rels_(self, request, part_, rel_, rId_, related_parts_): - rels_ = instance_mock(request, RelationshipCollection) - 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_ - - @pytest.fixture - def related_parts_(self, request): - return instance_mock(request, dict) + _init_.assert_called_once_with( + ct_map, + {"/ppt/presentation.xml": CT.PML_PRESENTATION_MAIN}, + {"png": CT.PNG, "xml": CT.XML}, + ) - @pytest.fixture - def reltype_(self, request): - return instance_mock(request, str) + @pytest.mark.parametrize( + "partname, expected_value", + ( + ("/docProps/core.xml", CT.OPC_CORE_PROPERTIES), + ("/ppt/presentation.xml", CT.PML_PRESENTATION_MAIN), + ("/PPT/Presentation.XML", CT.PML_PRESENTATION_MAIN), + ("/ppt/viewprops.xml", CT.PML_VIEW_PROPS), + ), + ) + def it_matches_an_override_on_case_insensitive_partname( + self, content_type_map, partname, expected_value + ): + assert content_type_map[PackURI(partname)] == expected_value + + @pytest.mark.parametrize( + "partname, expected_value", + ( + ("/foo/bar.xml", CT.XML), + ("/FOO/BAR.Rels", CT.OPC_RELATIONSHIPS), + ("/foo/bar.jpeg", CT.JPEG), + ), + ) + def it_falls_back_to_case_insensitive_extension_default_match( + self, content_type_map, partname, expected_value + ): + assert content_type_map[PackURI(partname)] == expected_value - @pytest.fixture - def rId_(self, request): - return instance_mock(request, str) + def it_raises_KeyError_on_partname_not_found(self, content_type_map): + with pytest.raises(KeyError) as e: + content_type_map[PackURI("/!blat/rhumba.1x&")] + assert str(e.value) == ( + "\"no content-type for partname '/!blat/rhumba.1x&' " 'in [Content_Types].xml"' + ) - @pytest.fixture - def url_(self, request): - return instance_mock(request, str) - - -class DescribeXmlPart(object): - def it_can_be_constructed_by_PartFactory(self, load_fixture): - partname_, content_type_, blob_, package_ = load_fixture[:4] - element_, parse_xml_, __init_ = load_fixture[4:] - # exercise --------------------- - part = XmlPart.load(partname_, content_type_, blob_, package_) - # verify ----------------------- - parse_xml_.assert_called_once_with(blob_) - __init_.assert_called_once_with(partname_, content_type_, element_, package_) - assert isinstance(part, XmlPart) + def it_raises_TypeError_on_key_not_instance_of_PackURI(self, content_type_map): + with pytest.raises(TypeError) as e: + content_type_map["/part/name1.xml"] + assert str(e.value) == "_ContentTypeMap key must be , got str" - def it_can_serialize_to_xml(self, blob_fixture): - xml_part, element_, serialize_part_xml_ = blob_fixture - blob = xml_part.blob - serialize_part_xml_.assert_called_once_with(element_) - assert blob is serialize_part_xml_.return_value + # fixtures --------------------------------------------- - 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.fixture(scope="class") + def content_type_map(self): + return _ContentTypeMap.from_xml(testfile_bytes("expanded_pptx", "[Content_Types].xml")) - # fixtures ------------------------------------------------------- - @pytest.fixture - def blob_fixture(self, request, element_, serialize_part_xml_): - xml_part = XmlPart(None, None, element_, None) - return xml_part, element_, serialize_part_xml_ +class Describe_Relationships: + """Unit-test suite for `pptx.opc.package._Relationships` objects.""" - @pytest.fixture - def load_fixture( - self, - request, - partname_, - content_type_, - blob_, - package_, - element_, - parse_xml_, - __init_, + @pytest.mark.parametrize("rId, expected_value", (("rId1", True), ("rId2", False))) + def it_knows_whether_it_contains_a_relationship_with_rId( + self, _rels_prop_, rId, expected_value ): - return ( - partname_, - content_type_, - blob_, - package_, - element_, - parse_xml_, - __init_, + _rels_prop_.return_value = {"rId1": None} + assert (rId in _Relationships(None)) is expected_value + + def it_has_dict_style_lookup_of_rel_by_rId(self, _rels_prop_, relationship_): + _rels_prop_.return_value = {"rId17": relationship_} + assert _Relationships(None)["rId17"] is relationship_ + + def but_it_raises_KeyError_when_no_relationship_has_rId(self, _rels_prop_): + _rels_prop_.return_value = {} + with pytest.raises(KeyError) as e: + _Relationships(None)["rId6"] + assert str(e.value) == "\"no relationship with key 'rId6'\"" + + def it_can_iterate_the_rIds_of_the_relationships_it_contains(self, request, _rels_prop_): + rels_ = set(instance_mock(request, _Relationship) for n in range(5)) + _rels_prop_.return_value = {"rId%d" % (i + 1): r for i, r in enumerate(rels_)} + relationships = _Relationships(None) + + for rId in relationships: + rels_.remove(relationships[rId]) + + assert len(rels_) == 0 + + def it_has_a_len(self, _rels_prop_): + _rels_prop_.return_value = {"a": 0, "b": 1} + assert len(_Relationships(None)) == 2 + + def it_can_add_a_relationship_to_a_target_part(self, part_, _get_matching_, _add_relationship_): + _get_matching_.return_value = None + _add_relationship_.return_value = "rId7" + relationships = _Relationships(None) + + rId = relationships.get_or_add(RT.IMAGE, part_) + + _get_matching_.assert_called_once_with(relationships, RT.IMAGE, part_) + _add_relationship_.assert_called_once_with(relationships, RT.IMAGE, part_) + assert rId == "rId7" + + def but_it_returns_an_existing_relationship_if_it_matches(self, part_, _get_matching_): + _get_matching_.return_value = "rId3" + relationships = _Relationships(None) + + rId = relationships.get_or_add(RT.IMAGE, part_) + + _get_matching_.assert_called_once_with(relationships, RT.IMAGE, part_) + assert rId == "rId3" + + def it_can_add_an_external_relationship_to_a_URI(self, _get_matching_, _add_relationship_): + _get_matching_.return_value = None + _add_relationship_.return_value = "rId2" + relationships = _Relationships(None) + + rId = relationships.get_or_add_ext_rel(RT.HYPERLINK, "http://url") + + _get_matching_.assert_called_once_with( + relationships, RT.HYPERLINK, "http://url", is_external=True + ) + _add_relationship_.assert_called_once_with( + relationships, RT.HYPERLINK, "http://url", is_external=True ) + assert rId == "rId2" - @pytest.fixture - def part_fixture(self): - return XmlPart(None, None, None, None) + def but_it_returns_an_existing_external_relationship_if_it_matches(self, part_, _get_matching_): + _get_matching_.return_value = "rId10" + relationships = _Relationships(None) - # fixture components --------------------------------------------- + rId = relationships.get_or_add_ext_rel(RT.HYPERLINK, "http://url") - @pytest.fixture - def blob_(self, request): - return instance_mock(request, str) + _get_matching_.assert_called_once_with( + relationships, RT.HYPERLINK, "http://url", is_external=True + ) + assert rId == "rId10" - @pytest.fixture - def content_type_(self, request): - return instance_mock(request, str) + def it_can_load_from_the_xml_in_a_rels_part(self, request, _Relationship_, part_): + rels_ = tuple( + instance_mock(request, _Relationship, rId="rId%d" % (i + 1)) for i in range(5) + ) + _Relationship_.from_xml.side_effect = iter(rels_) + parts = {"/ppt/slideLayouts/slideLayout1.xml": part_} + xml_rels = parse_xml(snippet_bytes("rels-load-from-xml")) + relationships = _Relationships(None) - @pytest.fixture - def element_(self, request): - return instance_mock(request, BaseOxmlElement) + relationships.load_from_xml("/ppt/slides", xml_rels, parts) - @pytest.fixture - def __init_(self, request): - return initializer_mock(request, XmlPart) + assert _Relationship_.from_xml.call_args_list == [ + call("/ppt/slides", xml_rels[0], parts), + call("/ppt/slides", xml_rels[1], parts), + ] + assert relationships._rels == {"rId1": rels_[0], "rId2": rels_[1]} - @pytest.fixture - def package_(self, request): - return instance_mock(request, OpcPackage) + def it_can_find_a_part_with_reltype(self, _rels_by_reltype_prop_, relationship_, part_): + relationship_.target_part = part_ + _rels_by_reltype_prop_.return_value = collections.defaultdict( + list, ((RT.SLIDE_LAYOUT, [relationship_]),) + ) + relationships = _Relationships(None) - @pytest.fixture - def parse_xml_(self, request, element_): - return function_mock( - request, "pptx.opc.package.parse_xml", return_value=element_ + assert relationships.part_with_reltype(RT.SLIDE_LAYOUT) is part_ + + def but_it_raises_KeyError_when_there_is_no_such_part(self, _rels_by_reltype_prop_): + _rels_by_reltype_prop_.return_value = collections.defaultdict(list) + relationships = _Relationships(None) + + with pytest.raises(KeyError) as e: + relationships.part_with_reltype(RT.SLIDE_LAYOUT) + assert str(e.value) == ( + "\"no relationship of type 'http://schemas.openxmlformats.org/" + "officeDocument/2006/relationships/slideLayout' in collection\"" ) - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) + def and_it_raises_ValueError_when_there_is_more_than_one_part_with_reltype( + self, _rels_by_reltype_prop_, relationship_, part_ + ): + relationship_.target_part = part_ + _rels_by_reltype_prop_.return_value = collections.defaultdict( + list, ((RT.SLIDE_LAYOUT, [relationship_, relationship_]),) + ) + relationships = _Relationships(None) - @pytest.fixture - def serialize_part_xml_(self, request): - return function_mock(request, "pptx.opc.package.serialize_part_xml") + with pytest.raises(ValueError) as e: + relationships.part_with_reltype(RT.SLIDE_LAYOUT) + assert str(e.value) == ( + "multiple relationships of type 'http://schemas.openxmlformats.org/" + "officeDocument/2006/relationships/slideLayout' in collection" + ) + + def it_can_pop_a_relationship_to_remove_it_from_the_collection( + self, _rels_prop_, relationship_ + ): + _rels_prop_.return_value = {"rId22": relationship_} + relationships = _Relationships(None) + + relationships.pop("rId22") + + assert relationships._rels == {} + + def it_can_serialize_itself_to_XML(self, request, _rels_prop_): + _rels_prop_.return_value = { + "rId11": instance_mock( + request, + _Relationship, + rId="rId11", + reltype=RT.SLIDE, + target_ref="../slides/slide1.xml", + is_external=False, + ), + "rId2": instance_mock( + request, + _Relationship, + rId="rId2", + reltype=RT.HYPERLINK, + target_ref="http://url", + is_external=True, + ), + "foo7W": instance_mock( + request, + _Relationship, + rId="foo7W", + reltype=RT.IMAGE, + target_ref="../media/image1.png", + is_external=False, + ), + } + relationships = _Relationships(None) + assert relationships.xml == snippet_bytes("relationships") -class DescribePartFactory(object): - def it_constructs_custom_part_type_for_registered_content_types( - self, part_args_, CustomPartClass_, part_of_custom_type_ + def it_can_add_a_relationship_to_a_part_to_help( + self, + request, + _next_rId_prop_, + _Relationship_, + relationship_, + _rels_prop_, + part_, ): - # fixture ---------------------- - partname, content_type, pkg, blob = part_args_ - # exercise --------------------- - PartFactory.part_type_for[content_type] = CustomPartClass_ - part = PartFactory(partname, content_type, pkg, blob) - # verify ----------------------- - CustomPartClass_.load.assert_called_once_with(partname, content_type, pkg, blob) - assert part is part_of_custom_type_ + _next_rId_prop_.return_value = "rId8" + _Relationship_.return_value = relationship_ + _rels_prop_.return_value = {} + relationships = _Relationships("/ppt") - def it_constructs_part_using_default_class_when_no_custom_registered( - self, part_args_2_, DefaultPartClass_, part_of_default_type_ + rId = relationships._add_relationship(RT.SLIDE, part_) + + _Relationship_.assert_called_once_with( + "/ppt", "rId8", RT.SLIDE, target_mode=RTM.INTERNAL, target=part_ + ) + assert relationships._rels == {"rId8": relationship_} + assert rId == "rId8" + + def and_it_can_add_an_external_relationship_to_help( + self, request, _next_rId_prop_, _rels_prop_, _Relationship_, relationship_ ): - partname, content_type, pkg, blob = part_args_2_ - part = PartFactory(partname, content_type, pkg, blob) - DefaultPartClass_.load.assert_called_once_with( - partname, content_type, pkg, blob + _next_rId_prop_.return_value = "rId9" + _Relationship_.return_value = relationship_ + _rels_prop_.return_value = {} + relationships = _Relationships("/ppt") + + rId = relationships._add_relationship(RT.HYPERLINK, "http://url", is_external=True) + + _Relationship_.assert_called_once_with( + "/ppt", "rId9", RT.HYPERLINK, target_mode=RTM.EXTERNAL, target="http://url" ) - assert part is part_of_default_type_ + assert relationships._rels == {"rId9": relationship_} + assert rId == "rId9" + + @pytest.mark.parametrize( + "target_ref, is_external, expected_value", + ( + ("http://url", True, "rId1"), + ("part_1", False, "rId2"), + ("http://foo", True, "rId3"), + ("part_2", False, "rId4"), + ("http://bar", True, None), + ), + ) + def it_can_get_a_matching_relationship_to_help( + self, request, _rels_by_reltype_prop_, target_ref, is_external, expected_value + ): + part_1, part_2 = (instance_mock(request, Part) for _ in range(2)) + _rels_by_reltype_prop_.return_value = { + RT.SLIDE: [ + instance_mock( + request, + _Relationship, + rId=rId, + target_part=target_part, + target_ref=ref, + is_external=external, + ) + for rId, target_part, ref, external in ( + ("rId1", None, "http://url", True), + ("rId2", part_1, "/ppt/foo.bar", False), + ("rId3", None, "http://foo", True), + ("rId4", part_2, "/ppt/bar.foo", False), + ) + ] + } + target = target_ref if is_external else part_1 if target_ref == "part_1" else part_2 + relationships = _Relationships(None) + + matching = relationships._get_matching(RT.SLIDE, target, is_external) + + assert matching == expected_value + + def but_it_returns_None_when_there_is_no_matching_relationship(self, _rels_by_reltype_prop_): + _rels_by_reltype_prop_.return_value = collections.defaultdict(list) + relationships = _Relationships(None) + + assert relationships._get_matching(RT.HYPERLINK, "http://url", True) is None + + @pytest.mark.parametrize( + "rIds, expected_value", + ( + ((), "rId1"), + (("rId1",), "rId2"), + (("rId1", "rId2"), "rId3"), + (("rId1", "rId4"), "rId3"), + (("rId1", "rId4", "rId6"), "rId3"), + (("rId1", "rId2", "rId6"), "rId4"), + ), + ) + def it_finds_the_next_rId_to_help(self, _rels_prop_, rIds, expected_value): + _rels_prop_.return_value = {rId: None for rId in rIds} + relationships = _Relationships(None) - # fixtures --------------------------------------------- + assert relationships._next_rId == expected_value - @pytest.fixture - def part_of_custom_type_(self, request): - return instance_mock(request, Part) + def it_collects_relationships_by_reltype_to_help(self, request, _rels_prop_): + rels = { + "rId%d" % (i + 1): instance_mock(request, _Relationship, reltype=reltype) + for i, reltype in enumerate((RT.SLIDE, RT.IMAGE, RT.SLIDE, RT.HYPERLINK)) + } + _rels_prop_.return_value = rels + relationships = _Relationships(None) - @pytest.fixture - def CustomPartClass_(self, request, part_of_custom_type_): - CustomPartClass_ = Mock(name="CustomPartClass", spec=Part) - CustomPartClass_.load.return_value = part_of_custom_type_ - return CustomPartClass_ + rels_by_reltype = relationships._rels_by_reltype - @pytest.fixture - def part_of_default_type_(self, request): - return instance_mock(request, Part) + assert rels["rId1"] in rels_by_reltype[RT.SLIDE] + assert rels["rId2"] in rels_by_reltype[RT.IMAGE] + assert rels["rId3"] in rels_by_reltype[RT.SLIDE] + assert rels["rId4"] in rels_by_reltype[RT.HYPERLINK] + assert rels_by_reltype[RT.CHART] == [] - @pytest.fixture - def DefaultPartClass_(self, request, part_of_default_type_): - DefaultPartClass_ = cls_attr_mock(request, PartFactory, "default_part_type") - DefaultPartClass_.load.return_value = part_of_default_type_ - return DefaultPartClass_ + # fixture components ----------------------------------- @pytest.fixture - def part_args_(self, request): - partname_ = PackURI("/foo/bar.xml") - content_type_ = "content/type" - pkg_ = instance_mock(request, Package, name="pkg_") - blob_ = b"blob_" - return partname_, content_type_, pkg_, blob_ + def _add_relationship_(self, request): + return method_mock(request, _Relationships, "_add_relationship") @pytest.fixture - def part_args_2_(self, request): - partname_2_ = PackURI("/bar/foo.xml") - content_type_2_ = "foobar/type" - pkg_2_ = instance_mock(request, Package, name="pkg_2_") - blob_2_ = b"blob_2_" - return partname_2_, content_type_2_, pkg_2_, blob_2_ - - -class Describe_Relationship(object): - def it_remembers_construction_values(self): - # test data -------------------- - rId = "rId9" - reltype = "reltype" - target = Mock(name="target_part") - external = False - # exercise --------------------- - rel = _Relationship(rId, reltype, target, None, external) - # verify ----------------------- - assert rel.rId == rId - assert rel.reltype == reltype - assert rel.target_part == target - assert rel.is_external == external - - def it_should_raise_on_target_part_access_on_external_rel(self): - rel = _Relationship(None, None, None, None, external=True) - with pytest.raises(ValueError): - 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" - - def it_should_have_relative_ref_for_internal_rel(self): - """ - Internal relationships (TargetMode == 'Internal' in the XML) should - 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" - rel = _Relationship(None, None, part, baseURI) # external=False - assert rel.target_ref == "../media/image1.png" - - -class DescribeRelationshipCollection(object): - def it_has_a_len(self): - rels = RelationshipCollection(None) - assert len(rels) == 0 - - def it_has_dict_style_lookup_of_rel_by_rId(self): - rel = Mock(name="rel", rId="foobar") - rels = RelationshipCollection(None) - rels["foobar"] = rel - assert rels["foobar"] == rel - - def it_should_raise_on_failed_lookup_by_rId(self): - rels = RelationshipCollection(None) - with pytest.raises(KeyError): - rels["barfoo"] - - def it_can_add_a_relationship(self, _Relationship_): - baseURI, rId, reltype, target, external = ( - "baseURI", - "rId9", - "reltype", - "target", - False, - ) - rels = RelationshipCollection(baseURI) - rel = rels.add_relationship(reltype, target, rId, external) - _Relationship_.assert_called_once_with(rId, reltype, target, baseURI, external) - assert rels[rId] == rel - assert rel == _Relationship_.return_value - - def it_can_add_an_external_relationship(self, add_ext_rel_fixture_): - rels, reltype, url = add_ext_rel_fixture_ - rId = rels.get_or_add_ext_rel(reltype, url) - rel = rels[rId] - assert rel.is_external - assert rel.target_ref == url - assert rel.reltype == reltype - - def it_should_return_an_existing_one_if_it_matches( - 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 - assert len(rels) == 1 - - def it_can_compose_rels_xml(self, rels, rels_elm): - # exercise --------------------- - rels.xml - # 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(), - ], - any_order=True, - ) - - # fixtures --------------------------------------------- + def _get_matching_(self, request): + return method_mock(request, _Relationships, "_get_matching") @pytest.fixture - def add_ext_rel_fixture_(self, reltype, url): - rels = RelationshipCollection(None) - return rels, reltype, url + def _next_rId_prop_(self, request): + return property_mock(request, _Relationships, "_next_rId") @pytest.fixture - def add_matching_ext_rel_fixture_(self, request, reltype, url): - rId = "rId369" - rels = RelationshipCollection(None) - rels.add_relationship(reltype, url, rId, is_external=True) - return rels, reltype, url, rId + def part_(self, request): + return instance_mock(request, Part) @pytest.fixture def _Relationship_(self, request): return class_mock(request, "pptx.opc.package._Relationship") @pytest.fixture - def rels(self): - """ - Populated RelationshipCollection instance that will exercise the - rels.xml property. - """ - rels = RelationshipCollection("/baseURI") - rels.add_relationship( - 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") - return rels + def relationship_(self, request): + return instance_mock(request, _Relationship) @pytest.fixture - def rels_elm(self, request): - """ - Return a rels_elm mock that will be returned from - CT_Relationships.new() - """ - # create rels_elm mock with a .xml property - rels_elm = Mock(name="rels_elm") - xml = PropertyMock(name="xml") - type(rels_elm).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_.start() - request.addfinalizer(patch_.stop) - return rels_elm + def _rels_by_reltype_prop_(self, request): + return property_mock(request, _Relationships, "_rels_by_reltype") @pytest.fixture - def reltype(self): - return "http://rel/type" + def _rels_prop_(self, request): + return property_mock(request, _Relationships, "_rels") - @pytest.fixture - def url(self): - return "https://github.com/scanny/python-pptx" +class Describe_Relationship: + """Unit-test suite for `pptx.opc.package._Relationship` objects.""" -class DescribeUnmarshaller(object): - def it_can_unmarshal_from_a_pkg_reader( - self, - pkg_reader_, - pkg_, - part_factory_, - _unmarshal_parts, - _unmarshal_relationships, - parts_dict_, - ): - # exercise --------------------- - Unmarshaller.unmarshal(pkg_reader_, pkg_, part_factory_) - # verify ----------------------- - _unmarshal_parts.assert_called_once_with(pkg_reader_, pkg_, part_factory_) - _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() - - def it_can_unmarshal_parts( - self, - pkg_reader_, - pkg_, - part_factory_, - parts_dict_, - partnames_, - content_types_, - blobs_, - ): - # fixture ---------------------- - partname_, partname_2_ = partnames_ - content_type_, content_type_2_ = content_types_ - blob_, blob_2_ = blobs_ - # exercise --------------------- - parts = Unmarshaller._unmarshal_parts(pkg_reader_, pkg_, part_factory_) - # verify ----------------------- - assert part_factory_.call_args_list == [ - call(partname_, content_type_, blob_, pkg_), - call(partname_2_, content_type_2_, blob_2_, pkg_), - ] - assert parts == parts_dict_ - - def it_can_unmarshal_relationships(self): - # test data -------------------- - reltype = "http://reltype" - # mockery ---------------------- - 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, - ), - ), + def it_can_construct_from_xml(self, request, part_): + _init_ = initializer_mock(request, _Relationship) + rel_elm = instance_mock( + request, + CT_Relationship, + rId="rId42", + reltype=RT.SLIDE, + targetMode=RTM.INTERNAL, + target_ref="slides/slide7.xml", ) - pkg = Mock(name="pkg") - parts = {} - for num in range(1, 3): - name = "part%d" % num - part = Mock(name=name) - 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), - ] - assert pkg.mock_calls == expected_pkg_calls - - # fixtures --------------------------------------------- + parts = {"/ppt/slides/slide7.xml": part_} - @pytest.fixture - def blobs_(self, request): - blob_ = loose_mock(request, spec=str, name="blob_") - blob_2_ = loose_mock(request, spec=str, name="blob_2_") - return blob_, blob_2_ + relationship = _Relationship.from_xml("/ppt", rel_elm, parts) - @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_") - return content_type_, content_type_2_ + _init_.assert_called_once_with(relationship, "/ppt", "rId42", RT.SLIDE, RTM.INTERNAL, part_) + assert isinstance(relationship, _Relationship) - @pytest.fixture - def part_factory_(self, request, parts_): - part_factory_ = loose_mock(request, spec=Part) - part_factory_.side_effect = parts_ - return part_factory_ + @pytest.mark.parametrize( + "target_mode, expected_value", + ((RTM.INTERNAL, False), (RTM.EXTERNAL, True), (None, False)), + ) + def it_knows_whether_it_is_external(self, target_mode, expected_value): + relationship = _Relationship(None, None, None, target_mode, None) + assert relationship.is_external == expected_value + + def it_knows_its_relationship_type(self): + relationship = _Relationship(None, None, RT.SLIDE, None, None) + assert relationship.reltype == RT.SLIDE + + def it_knows_its_rId(self): + relationship = _Relationship(None, "rId42", None, None, None) + assert relationship.rId == "rId42" + + def it_provides_access_to_its_target_part(self, part_): + relationship = _Relationship(None, None, None, RTM.INTERNAL, part_) + assert relationship.target_part is part_ + + def but_it_raises_ValueError_on_target_part_for_external_rel(self): + relationship = _Relationship(None, None, None, RTM.EXTERNAL, None) + with pytest.raises(ValueError) as e: + relationship.target_part + assert str(e.value) == ( + "`.target_part` property on _Relationship is undefined when " "target-mode is external" + ) - @pytest.fixture - def partnames_(self, request): - partname_ = loose_mock(request, spec=str, name="partname_") - partname_2_ = loose_mock(request, spec=str, name="partname_2_") - return partname_, partname_2_ + def it_knows_its_target_partname(self, part_): + part_.partname = PackURI("/ppt/slideLayouts/slideLayout4.xml") + relationship = _Relationship(None, None, None, RTM.INTERNAL, part_) - @pytest.fixture - def parts_(self, request): - part_ = instance_mock(request, Part, name="part_") - part_2_ = instance_mock(request, Part, name="part_2") - return part_, part_2_ + assert relationship.target_partname == "/ppt/slideLayouts/slideLayout4.xml" - @pytest.fixture - def parts_dict_(self, request, partnames_, parts_): - partname_, partname_2_ = partnames_ - part_, part_2_ = parts_ - return {partname_: part_, partname_2_: part_2_} + def but_it_raises_ValueError_on_target_partname_for_external_rel(self): + relationship = _Relationship(None, None, None, RTM.EXTERNAL, None) - @pytest.fixture - def pkg_(self, request): - return instance_mock(request, Package) + with pytest.raises(ValueError) as e: + relationship.target_partname - @pytest.fixture - def pkg_reader_(self, request, partnames_, content_types_, blobs_): - partname_, partname_2_ = partnames_ - content_type_, content_type_2_ = content_types_ - blob_, blob_2_ = blobs_ - spart_return_values = ( - (partname_, content_type_, blob_), - (partname_2_, content_type_2_, blob_2_), + assert str(e.value) == ( + "`.target_partname` property on _Relationship is undefined when " + "target-mode is external" ) - pkg_reader_ = instance_mock(request, PackageReader) - pkg_reader_.iter_sparts.return_value = spart_return_values - return pkg_reader_ - @pytest.fixture - def _unmarshal_parts(self, request, parts_dict_): - return method_mock( - request, Unmarshaller, "_unmarshal_parts", return_value=parts_dict_ + def it_knows_the_target_uri_for_an_external_rel(self): + relationship = _Relationship(None, None, None, RTM.EXTERNAL, "http://url") + assert relationship.target_ref == "http://url" + + def and_it_knows_the_relative_partname_for_an_internal_rel(self, request): + """Internal relationships have a relative reference for `.target_ref`. + + A relative reference looks like "../slideLayouts/slideLayout1.xml". This form + is suitable for writing to a .rels file. + """ + property_mock( + request, + _Relationship, + "target_partname", + return_value=PackURI("/ppt/media/image1.png"), ) + relationship = _Relationship("/ppt/slides", None, None, None, None) + + assert relationship.target_ref == "../media/image1.png" + + # --- fixture components ------------------------------- @pytest.fixture - def _unmarshal_relationships(self, request): - return method_mock(request, Unmarshaller, "_unmarshal_relationships") + def part_(self, request): + return instance_mock(request, Part) diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index 5c80bafe7..5b7e64a2f 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.opc.packuri` module.""" -""" -Test suite for the pptx.opc.packuri module -""" +from __future__ import annotations import pytest @@ -10,76 +8,87 @@ 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"] - if len(expected_values) != len(uri_str_cases): - msg = "len(expected_values) differs from len(uri_str_cases)" - raise AssertionError(msg) - pack_uris = [PackURI(uri_str) for uri_str in uri_str_cases] - return zip(pack_uris, expected_values) + """Unit-test suite for the `pptx.opc.packuri.PackURI` objects.""" def it_can_construct_from_relative_ref(self): - baseURI = "/ppt/slides" - relative_ref = "../slideLayouts/slideLayout1.xml" - pack_uri = PackURI.from_rel_ref(baseURI, relative_ref) + pack_uri = PackURI.from_rel_ref("/ppt/slides", "../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") - def it_can_calculate_baseURI(self): - expected_values = ("/", "/ppt", "/ppt/slides") - for pack_uri, expected_baseURI in self.cases(expected_values): - assert pack_uri.baseURI == expected_baseURI + @pytest.mark.parametrize( + ("uri", "expected_value"), + [ + ("/", "/"), + ("/ppt/presentation.xml", "/ppt"), + ("/ppt/slides/slide1.xml", "/ppt/slides"), + ], + ) + def it_knows_its_base_URI(self, uri: str, expected_value: str): + assert PackURI(uri).baseURI == expected_value - def it_can_calculate_extension(self): - expected_values = ("", "xml", "xml") - for pack_uri, expected_ext in self.cases(expected_values): - assert pack_uri.ext == expected_ext + @pytest.mark.parametrize( + ("uri", "expected_value"), + [ + ("/", ""), + ("/ppt/presentation.xml", "xml"), + ("/ppt/media/image.PnG", "PnG"), + ], + ) + def it_knows_its_extension(self, uri: str, expected_value: str): + assert PackURI(uri).ext == expected_value - def it_can_calculate_filename(self): - expected_values = ("", "presentation.xml", "slide1.xml") - for pack_uri, expected_filename in self.cases(expected_values): - assert pack_uri.filename == expected_filename + @pytest.mark.parametrize( + ("uri", "expected_value"), + [ + ("/", ""), + ("/ppt/presentation.xml", "presentation.xml"), + ("/ppt/media/image.png", "image.png"), + ], + ) + def it_knows_its_filename(self, uri: str, expected_value: str): + assert PackURI(uri).filename == expected_value - def it_knows_the_filename_index(self): - expected_values = (None, None, 1) - for pack_uri, expected_idx in self.cases(expected_values): - assert pack_uri.idx == expected_idx + @pytest.mark.parametrize( + ("uri", "expected_value"), + [ + ("/", None), + ("/ppt/presentation.xml", None), + ("/ppt/,foo,grob!.xml", None), + ("/ppt/media/image42.png", 42), + ], + ) + def it_knows_the_filename_index(self, uri: str, expected_value: str): + assert PackURI(uri).idx == expected_value - def it_can_calculate_membername(self): - expected_values = ("", "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"), + @pytest.mark.parametrize( + ("uri", "base_uri", "expected_value"), + [ + ("/ppt/presentation.xml", "/", "ppt/presentation.xml"), ( - "/ppt", "/ppt/slideMasters/slideMaster1.xml", + "/ppt", "slideMasters/slideMaster1.xml", ), ( - "/ppt/slides", "/ppt/slideLayouts/slideLayout1.xml", + "/ppt/slides", "../slideLayouts/slideLayout1.xml", ), - ) - for baseURI, uri_str, expected_relative_ref in cases: - pack_uri = PackURI(uri_str) - assert pack_uri.relative_ref(baseURI) == expected_relative_ref + ], + ) + def it_can_compute_its_relative_reference(self, uri: str, base_uri: str, expected_value: str): + assert PackURI(uri).relative_ref(base_uri) == expected_value - def it_can_calculate_rels_uri(self): - expected_values = ( - "/_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 + @pytest.mark.parametrize( + ("uri", "expected_value"), + [ + ("/", "/_rels/.rels"), + ("/ppt/presentation.xml", "/ppt/_rels/presentation.xml.rels"), + ("/ppt/slides/slide42.xml", "/ppt/slides/_rels/slide42.xml.rels"), + ], + ) + def it_knows_the_uri_of_its_rels_part(self, uri: str, expected_value: str): + assert PackURI(uri).rels_uri == expected_value diff --git a/tests/opc/test_phys_pkg.py b/tests/opc/test_phys_pkg.py deleted file mode 100644 index aa6db8bae..000000000 --- a/tests/opc/test_phys_pkg.py +++ /dev/null @@ -1,193 +0,0 @@ -# encoding: utf-8 - -""" -Test suite for pptx.opc.packaging module -""" - -from __future__ import absolute_import - -try: - from io import BytesIO # Python 3 -except ImportError: - from StringIO import StringIO as BytesIO - -import hashlib -import pytest - -from zipfile import ZIP_DEFLATED, ZipFile - -from pptx.exceptions import PackageNotFoundError -from pptx.opc.packuri import PACKAGE_URI, PackURI -from pptx.opc.phys_pkg import ( - _DirPkgReader, - PhysPkgReader, - PhysPkgWriter, - _ZipPkgReader, - _ZipPkgWriter, -) - -from ..unitutil.file import absjoin, test_file_dir -from ..unitutil.mock import class_mock, loose_mock, Mock - - -test_pptx_path = absjoin(test_file_dir, "test.pptx") -dir_pkg_path = absjoin(test_file_dir, "expanded_pptx") -zip_pkg_path = test_pptx_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): - dir_reader.close() - - def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_reader): - pack_uri = PackURI("/ppt/presentation.xml") - blob = dir_reader.blob_for(pack_uri) - sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" - - def it_can_get_the_content_types_xml(self, dir_reader): - sha1 = hashlib.sha1(dir_reader.content_types_xml).hexdigest() - assert sha1 == "a68cf138be3c4eb81e47e2550166f9949423c7df" - - 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 == "64ffe86bb2bbaad53c3c1976042b907f8e10c5a3" - - def it_returns_none_when_part_has_no_rels_xml(self, dir_reader): - partname = PackURI("/ppt/viewProps.xml") - rels_xml = dir_reader.rels_xml_for(partname) - assert rels_xml is None - - # fixtures --------------------------------------------- - - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) - - @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") - - -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: - 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") - - def it_can_be_closed(self, ZipFile_): - # mockery ---------------------- - zipf = ZipFile_.return_value - zip_pkg_reader = _ZipPkgReader(None) - # exercise --------------------- - zip_pkg_reader.close() - # verify ----------------------- - zipf.close.assert_called_once_with() - - def it_can_retrieve_the_blob_for_a_pack_uri(self, phys_reader): - pack_uri = PackURI("/ppt/presentation.xml") - blob = phys_reader.blob_for(pack_uri) - sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "efa7bee0ac72464903a67a6744c1169035d52a54" - - def it_has_the_content_types_xml(self, phys_reader): - sha1 = hashlib.sha1(phys_reader.content_types_xml).hexdigest() - assert sha1 == "ab762ac84414fce18893e18c3f53700c01db56c3" - - 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 == "e31451d4bbe7d24adbe21454b8e9fdae92f50de5" - - def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): - partname = PackURI("/ppt/viewProps.xml") - rels_xml = phys_reader.rels_xml_for(partname) - assert rels_xml is None - - # fixtures --------------------------------------------- - - @pytest.fixture(scope="class") - def phys_reader(self, request): - phys_reader = _ZipPkgReader(zip_pkg_path) - request.addfinalizer(phys_reader.close) - return phys_reader - - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) - - -class DescribeZipPkgWriter(object): - def it_is_used_by_PhysPkgWriter_unconditionally(self, tmp_pptx_path): - phys_writer = PhysPkgWriter(tmp_pptx_path) - assert isinstance(phys_writer, _ZipPkgWriter) - - def it_opens_pkg_file_zip_on_construction(self, ZipFile_): - pkg_file = Mock(name="pkg_file") - _ZipPkgWriter(pkg_file) - ZipFile_.assert_called_once_with(pkg_file, "w", compression=ZIP_DEFLATED) - - def it_can_be_closed(self, ZipFile_): - # mockery ---------------------- - zipf = ZipFile_.return_value - zip_pkg_writer = _ZipPkgWriter(None) - # exercise --------------------- - zip_pkg_writer.close() - # verify ----------------------- - zipf.close.assert_called_once_with() - - def it_can_write_a_blob(self, pkg_file): - # setup ------------------------ - 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") - retrieved_blob = zipf.read(pack_uri.membername) - zipf.close() - retrieved_blob_sha1 = hashlib.sha1(retrieved_blob).hexdigest() - assert retrieved_blob_sha1 == written_blob_sha1 - - # fixtures --------------------------------------------- - - @pytest.fixture - def pkg_file(self, request): - pkg_file = BytesIO() - request.addfinalizer(pkg_file.close) - return pkg_file - - -# fixtures ------------------------------------------------- - - -@pytest.fixture -def tmp_pptx_path(tmpdir): - return str(tmpdir.join("test_python-pptx.pptx")) - - -@pytest.fixture -def ZipFile_(request): - return class_mock(request, "pptx.opc.phys_pkg.ZipFile") diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py deleted file mode 100644 index b561e6421..000000000 --- a/tests/opc/test_pkgreader.py +++ /dev/null @@ -1,432 +0,0 @@ -# encoding: utf-8 - -""" -Test suite for opc.pkgreader module -""" - -from __future__ import absolute_import, print_function, unicode_literals - -import pytest - -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM -from pptx.opc.oxml import CT_Relationship -from pptx.opc.packuri import PackURI -from pptx.opc.phys_pkg import _ZipPkgReader -from pptx.opc.pkgreader import ( - _ContentTypeMap, - PackageReader, - _SerializedPart, - _SerializedRelationship, - _SerializedRelationshipCollection, -) - -from .unitdata.types import a_Default, a_Types, an_Override -from ..unitutil.mock import ( - call, - class_mock, - function_mock, - initializer_mock, - method_mock, - Mock, - patch, -) - - -class DescribePackageReader(object): - @pytest.fixture - def from_xml(self, request): - return method_mock(request, _ContentTypeMap, "from_xml") - - @pytest.fixture - def init(self, request): - return initializer_mock(request, PackageReader) - - @pytest.fixture - def _load_serialized_parts(self, request): - return method_mock(request, PackageReader, "_load_serialized_parts") - - @pytest.fixture - def PhysPkgReader_(self, request): - _patch = patch("pptx.opc.pkgreader.PhysPkgReader", spec_set=_ZipPkgReader) - request.addfinalizer(_patch.stop) - return _patch.start() - - @pytest.fixture - def _SerializedPart_(self, request): - return class_mock(request, "pptx.opc.pkgreader._SerializedPart") - - @pytest.fixture - def _SerializedRelationshipCollection_(self, request): - return class_mock( - request, "pptx.opc.pkgreader._SerializedRelationshipCollection" - ) - - @pytest.fixture - def _srels_for(self, request): - return method_mock(request, PackageReader, "_srels_for") - - @pytest.fixture - def _walk_phys_parts(self, request): - return method_mock(request, PackageReader, "_walk_phys_parts") - - def it_can_construct_from_pkg_file( - self, init, PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts - ): - # mockery ---------------------- - phys_reader = PhysPkgReader_.return_value - content_types = from_xml.return_value - pkg_srels = _srels_for.return_value - sparts = _load_serialized_parts.return_value - pkg_file = Mock(name="pkg_file") - # exercise --------------------- - pkg_reader = PackageReader.from_file(pkg_file) - # verify ----------------------- - 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 - ) - phys_reader.close.assert_called_once_with() - init.assert_called_once_with(content_types, pkg_srels, sparts) - assert isinstance(pkg_reader, PackageReader) - - def it_can_iterate_over_the_serialized_parts(self): - # mockery ---------------------- - partname, content_type, blob = ("part/name.xml", "app/vnd.type", "") - spart = Mock( - name="spart", partname=partname, content_type=content_type, blob=blob - ) - pkg_reader = PackageReader(None, None, [spart]) - iter_count = 0 - # exercise --------------------- - for retval in pkg_reader.iter_sparts(): - iter_count += 1 - # verify ----------------------- - assert retval == (partname, content_type, blob) - assert iter_count == 1 - - def it_can_iterate_over_all_the_srels(self): - # mockery ---------------------- - pkg_srels = ["srel1", "srel2"] - sparts = [ - 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"), - ] - 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", "", "srels_1"), - ("/part/name2.xml", "app/vnd.type_2", "", "srels_2"), - ) - iter_vals = [(t[0], t[2], t[3]) 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") - _walk_phys_parts.return_value = iter_vals - _SerializedPart_.side_effect = expected_sparts = ( - Mock(name="spart_1"), - Mock(name="spart_2"), - ) - # exercise --------------------- - retval = PackageReader._load_serialized_parts( - phys_reader, pkg_srels, content_types - ) - # verify ----------------------- - expected_calls = [ - call("/part/name1.xml", "app/vnd.type_1", "", "srels_1"), - call("/part/name2.xml", "app/vnd.type_2", "", "srels_2"), - ] - assert _SerializedPart_.call_args_list == expected_calls - assert retval == expected_sparts - - def it_can_walk_phys_pkg_parts(self, _srels_for): - # test data -------------------- - # +----------+ +--------+ - # | pkg_rels |-----> | part_1 | - # +----------+ +--------+ - # | | ^ - # v v | - # external +--------+ +--------+ - # | 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 = ("", "", "") - srels = [ - Mock(name="rId1", is_external=True), - Mock(name="rId2", is_external=False, target_partname=partname_1), - Mock(name="rId3", is_external=False, target_partname=partname_2), - Mock(name="rId4", is_external=False, target_partname=partname_1), - Mock(name="rId5", is_external=False, 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") - _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] - # exercise --------------------- - generated_tuples = [ - t for t in PackageReader._walk_phys_parts(phys_reader, pkg_srels) - ] - # verify ----------------------- - expected_tuples = [ - (partname_1, part_1_blob, part_1_srels), - (partname_2, part_2_blob, part_2_srels), - (partname_3, part_3_blob, part_3_srels), - ] - assert generated_tuples == expected_tuples - - def it_can_retrieve_srels_for_a_source_uri( - self, _SerializedRelationshipCollection_ - ): - # mockery ---------------------- - phys_reader = Mock(name="phys_reader") - source_uri = Mock(name="source_uri") - rels_xml = phys_reader.rels_xml_for.return_value - load_from_xml = _SerializedRelationshipCollection_.load_from_xml - srels = load_from_xml.return_value - # exercise --------------------- - retval = PackageReader._srels_for(phys_reader, source_uri) - # verify ----------------------- - phys_reader.rels_xml_for.assert_called_once_with(source_uri) - load_from_xml.assert_called_once_with(source_uri.baseURI, rels_xml) - assert retval == srels - - -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 - 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 - ): - 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 - ): - 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&")] - - def it_should_raise_on_key_not_instance_of_PackURI(self): - ct_map = _ContentTypeMap() - ct_map._add_override(PackURI("/part/name1.xml"), "app/vnd.type1") - with pytest.raises(KeyError): - 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), - ) - content_types_xml = self._xml_from(entries) - expected_defaults = {} - expected_overrides = {} - for entry in entries: - if entry[0] == "Default": - ext = entry[1].lower() - content_type = entry[2] - expected_defaults[ext] = content_type - 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"), - ] - ) - 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_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"), - ] - ) - 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" - ct_map = _ContentTypeMap() - ct_map._add_override(partname, content_type) - return ct_map, should_match_partname, content_type - - def _xml_from(self, entries): - """ - Return XML for a [Content_Types].xml based on items in *entries*. - """ - types_bldr = a_Types().with_nsdecls() - for entry in entries: - 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": - partname, content_type = entry[1:] - override_bldr = an_Override() - override_bldr.with_PartName(partname) - override_bldr.with_ContentType(content_type) - types_bldr.with_child(override_bldr) - return types_bldr.xml() - - -class Describe_SerializedPart(object): - def it_remembers_construction_values(self): - # test data -------------------- - partname = "/part/name.xml" - content_type = "app/vnd.type" - blob = "" - srels = "srels proxy" - # exercise --------------------- - spart = _SerializedPart(partname, content_type, blob, srels) - # verify ----------------------- - assert spart.partname == partname - assert spart.content_type == content_type - assert spart.blob == blob - assert spart.srels == srels - - -class Describe_SerializedRelationship(object): - def it_remembers_construction_values(self): - # test data -------------------- - rel_elm = CT_Relationship.new( - "rId9", "ReLtYpE", "docProps/core.xml", RTM.INTERNAL - ) - # exercise --------------------- - srel = _SerializedRelationship("/", rel_elm) - # verify ----------------------- - 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) - expected_values = (False, True) - for target_mode, expected_value in zip(cases, expected_values): - rel_elm = CT_Relationship.new( - "rId9", "ReLtYpE", "docProps/core.xml", 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", - ), - ) - 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, - ) - # exercise ----------------- - srel = _SerializedRelationship(baseURI, rel_elm) - # verify ------------------- - assert srel.target_partname == expected_partname - - def it_raises_on_target_partname_when_external(self): - rel_elm = CT_Relationship.new( - "rId9", "ReLtYpE", "docProps/core.xml", RTM.EXTERNAL - ) - srel = _SerializedRelationship("/", rel_elm) - with pytest.raises(ValueError): - srel.target_partname - - -class Describe_SerializedRelationshipCollection(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]) - parse_xml.return_value = rels_elm - # exercise --------------------- - srels = _SerializedRelationshipCollection.load_from_xml(baseURI, rels_item_xml) - # verify ----------------------- - expected_calls = [call(baseURI, rel_elm_1), call(baseURI, rel_elm_2)] - parse_xml.assert_called_once_with(rels_item_xml) - assert _SerializedRelationship_.call_args_list == expected_calls - assert isinstance(srels, _SerializedRelationshipCollection) - - def it_should_be_iterable(self): - srels = _SerializedRelationshipCollection() - try: - for x in srels: - pass - except TypeError: - msg = "_SerializedRelationshipCollection object is not iterable" - pytest.fail(msg) - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def parse_xml(self, request): - return function_mock(request, "pptx.opc.pkgreader.parse_xml") - - @pytest.fixture - def _SerializedRelationship_(self, request): - return class_mock(request, "pptx.opc.pkgreader._SerializedRelationship") diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py deleted file mode 100644 index 72d2b09ac..000000000 --- a/tests/opc/test_pkgwriter.py +++ /dev/null @@ -1,171 +0,0 @@ -# encoding: utf-8 - -""" -Test suite for opc.pkgwriter module -""" - -import pytest - -from pptx.opc.constants import CONTENT_TYPE as CT -from pptx.opc.package import Part -from pptx.opc.packuri import PackURI -from pptx.opc.pkgwriter import _ContentTypesItem, PackageWriter - -from .unitdata.types import a_Default, a_Types, an_Override -from ..unitutil.mock import ( - call, - function_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") - phys_writer = PhysPkgWriter_.return_value - # exercise --------------------- - PackageWriter.write(pkg_file, pkg_rels, parts) - # verify ----------------------- - expected_calls = [ - call._write_content_types_stream(phys_writer, parts), - call._write_pkg_rels(phys_writer, pkg_rels), - call._write_parts(phys_writer, parts), - ] - PhysPkgWriter_.assert_called_once_with(pkg_file) - assert _write_methods.mock_calls == expected_calls - phys_writer.close.assert_called_once_with() - - def it_can_write_a_content_types_stream(self, xml_for, serialize_part_xml_): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - parts = Mock(name="parts") - # exercise --------------------- - PackageWriter._write_content_types_stream(phys_writer, parts) - # verify ----------------------- - xml_for.assert_called_once_with(parts) - serialize_part_xml_.assert_called_once_with(xml_for.return_value) - phys_writer.write.assert_called_once_with( - "/[Content_Types].xml", serialize_part_xml_.return_value - ) - - def it_can_write_a_pkg_rels_item(self): - # mockery ---------------------- - 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) - - 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 ----------------------- - expected_calls = [ - call(part1.partname, part1.blob), - call(part1.partname.rels_uri, part1._rels.xml), - call(part2.partname, part2.blob), - ] - assert phys_writer.write.mock_calls == expected_calls - - # fixtures --------------------------------------------- - - @pytest.fixture - def PhysPkgWriter_(self, request): - _patch = patch("pptx.opc.pkgwriter.PhysPkgWriter") - request.addfinalizer(_patch.stop) - return _patch.start() - - @pytest.fixture - def serialize_part_xml_(self, request): - return function_mock(request, "pptx.opc.pkgwriter.serialize_part_xml") - - @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") - - def fin(): - patch1.stop() - patch2.stop() - patch3.stop() - - request.addfinalizer(fin) - return root_mock - - @pytest.fixture - def xml_for(self, request): - return method_mock(request, _ContentTypesItem, "xml_for") - - -class Describe_ContentTypesItem(object): - def it_can_compose_content_types_xml(self, xml_for_fixture): - parts, expected_xml = xml_for_fixture - types_elm = _ContentTypesItem.xml_for(parts) - assert types_elm.xml == expected_xml - - # fixtures --------------------------------------------- - - 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 - ) - - @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) - # expected_xml ----------------- - types_bldr = a_Types().with_nsdecls() - 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) - ) - types_bldr.with_child( - a_Default().with_Extension("xml").with_ContentType(CT.XML) - ) - - if elm_type == "Override": - override_bldr = an_Override() - override_bldr.with_PartName(partname_str) - override_bldr.with_ContentType(content_type) - types_bldr.with_child(override_bldr) - - expected_xml = types_bldr.xml() - return [part_], expected_xml diff --git a/tests/opc/test_rels.py b/tests/opc/test_rels.py deleted file mode 100644 index 489cba27a..000000000 --- a/tests/opc/test_rels.py +++ /dev/null @@ -1,268 +0,0 @@ -# encoding: utf-8 - -"""Test suite for pptx.part module.""" - -from __future__ import absolute_import - -import pytest - -from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.oxml import CT_Relationships -from pptx.opc.package import Part, _Relationship, RelationshipCollection -from pptx.opc.packuri import PackURI - -from ..unitutil.mock import ( - call, - class_mock, - instance_mock, - loose_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") - external = False - # exercise --------------------- - rel = _Relationship(rId, reltype, target, None, external) - # verify ----------------------- - assert rel.rId == rId - assert rel.reltype == reltype - assert rel.target_part == target - assert rel.is_external == external - - def it_should_raise_on_target_part_access_on_external_rel(self): - rel = _Relationship(None, None, None, None, external=True) - with pytest.raises(ValueError): - 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" - - def it_should_have_relative_ref_for_internal_rel(self): - """ - Internal relationships (TargetMode == 'Internal' in the XML) should - 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" - rel = _Relationship(None, None, part, baseURI) # external=False - assert rel.target_ref == "../media/image1.png" - - -class DescribeRelationshipCollection(object): - def it_also_has_dict_style_get_rel_by_rId(self, rels_with_known_rel): - rels, rId, known_rel = rels_with_known_rel - assert rels[rId] == known_rel - - def it_should_raise_on_failed_lookup_by_rId(self, rels): - with pytest.raises(KeyError): - rels["rId666"] - - def it_has_a_len(self, rels): - assert len(rels) == 0 - - def it_can_add_a_relationship(self, _Relationship_): - baseURI, rId, reltype, target, is_external = ( - "baseURI", - "rId9", - "reltype", - "target", - False, - ) - rels = RelationshipCollection(baseURI) - rel = rels.add_relationship(reltype, target, rId, is_external) - _Relationship_.assert_called_once_with( - rId, reltype, target, baseURI, is_external - ) - assert rels[rId] == rel - assert rel == _Relationship_.return_value - - def it_can_add_a_relationship_if_not_found( - 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_knows_the_next_available_rId(self, rels_with_rId_gap): - rels, expected_next_rId = rels_with_rId_gap - next_rId = rels._next_rId - assert next_rId == expected_next_rId - - 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 - - def it_can_find_a_related_part_by_rId(self, rels_with_known_target_part): - rels, rId, known_target_part = rels_with_known_target_part - part = rels.related_parts[rId] - assert part is known_target_part - - def it_raises_KeyError_on_part_with_rId_not_found(self, rels): - with pytest.raises(KeyError): - rels.related_parts["rId666"] - - def it_can_compose_rels_xml(self, rels_with_known_rels, rels_elm): - rels_with_known_rels.xml - 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(), - ], - any_order=True, - ) - - # def it_raises_on_add_rel_with_duplicate_rId(self, rels, rel): - # with pytest.raises(ValueError): - # rels.add_rel(rel) - - # fixtures --------------------------------------------- - - @pytest.fixture - def _Relationship_(self, request): - return class_mock(request, "pptx.opc.package._Relationship") - - @pytest.fixture - def rel(self, _rId, _reltype, _target_part, _baseURI): - return _Relationship(_rId, _reltype, _target_part, _baseURI) - - @pytest.fixture - def rels(self, _baseURI): - return RelationshipCollection(_baseURI) - - @pytest.fixture - def rels_elm(self, request): - """ - Return a rels_elm mock that will be returned from - CT_Relationships.new() - """ - # create rels_elm mock with a .xml property - rels_elm = Mock(name="rels_elm") - xml = PropertyMock(name="xml") - type(rels_elm).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_.start() - request.addfinalizer(patch_.stop) - return rels_elm - - @pytest.fixture - def rels_with_known_rel(self, rels, _rId, rel): - rels[_rId] = rel - return rels, _rId, rel - - @pytest.fixture - def rels_with_known_rels(self): - """ - Populated RelationshipCollection instance that will exercise the - rels.xml property. - """ - rels = RelationshipCollection("/baseURI") - rels.add_relationship( - 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") - return rels - - @pytest.fixture - def rels_with_known_target_part(self, rels, _rel_with_known_target_part): - rel, rId, target_part = _rel_with_known_target_part - rels.add_relationship(None, target_part, rId) - return rels, rId, 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_rel_ = instance_mock( - 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_") - new_rel_ = instance_mock( - 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_ - - @pytest.fixture - def rels_with_rId_gap(self, request, rels): - 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 - ): - rel, reltype, target_part = _rel_with_target_known_by_reltype - rels[1] = rel - return rels, reltype, target_part - - @pytest.fixture - def _baseURI(self): - return "/baseURI" - - @pytest.fixture - def _rel_with_known_target_part(self, _rId, _reltype, _target_part, _baseURI): - rel = _Relationship(_rId, _reltype, _target_part, _baseURI) - return rel, _rId, _target_part - - @pytest.fixture - 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 - - @pytest.fixture - def _reltype(self): - return RT.SLIDE - - @pytest.fixture - def _rId(self): - return "rId6" - - @pytest.fixture - def _target_part(self, request): - return loose_mock(request) diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py new file mode 100644 index 000000000..8dcc8cf56 --- /dev/null +++ b/tests/opc/test_serialized.py @@ -0,0 +1,410 @@ +# pyright: reportPrivateUsage=false + +"""Unit-test suite for `pptx.opc.serialized` module.""" + +from __future__ import annotations + +import hashlib +import io +import zipfile + +import pytest + +from pptx.exc import PackageNotFoundError +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.package import Part, _Relationships +from pptx.opc.packuri import CONTENT_TYPES_URI, PackURI +from pptx.opc.serialized import ( + PackageReader, + PackageWriter, + _ContentTypesItem, + _DirPkgReader, + _PhysPkgReader, + _PhysPkgWriter, + _ZipPkgReader, + _ZipPkgWriter, +) + +from ..unitutil.file import absjoin, snippet_text, test_file_dir +from ..unitutil.mock import ( + ANY, + FixtureRequest, + Mock, + call, + class_mock, + function_mock, + initializer_mock, + instance_mock, + method_mock, + property_mock, +) + +test_pptx_path = absjoin(test_file_dir, "test.pptx") +dir_pkg_path = absjoin(test_file_dir, "expanded_pptx") +zip_pkg_path = test_pptx_path + + +class DescribePackageReader: + """Unit-test suite for `pptx.opc.serialized.PackageReader` objects.""" + + def it_knows_whether_it_contains_a_partname(self, _blob_reader_prop_: Mock): + _blob_reader_prop_.return_value = {"/ppt", "/docProps"} + package_reader = PackageReader("") + + assert "/ppt" in package_reader + assert "/xyz" not in package_reader + + def it_can_get_a_blob_by_partname(self, _blob_reader_prop_: Mock): + _blob_reader_prop_.return_value = {"/ppt/slides/slide1.xml": b"blob"} + package_reader = PackageReader("") + + assert package_reader[PackURI("/ppt/slides/slide1.xml")] == b"blob" + + def it_can_get_the_rels_xml_for_a_partname(self, _blob_reader_prop_: Mock): + _blob_reader_prop_.return_value = {"/ppt/_rels/presentation.xml.rels": b"blob"} + package_reader = PackageReader("") + + assert package_reader.rels_xml_for(PackURI("/ppt/presentation.xml")) == b"blob" + + def but_it_returns_None_when_the_part_has_no_rels(self, _blob_reader_prop_: Mock): + _blob_reader_prop_.return_value = {"/ppt/_rels/presentation.xml.rels": b"blob"} + package_reader = PackageReader("") + + assert package_reader.rels_xml_for(PackURI("/ppt/slides.slide1.xml")) is None + + def it_constructs_its_blob_reader_to_help(self, request: FixtureRequest): + phys_pkg_reader_ = instance_mock(request, _PhysPkgReader) + _PhysPkgReader_ = class_mock(request, "pptx.opc.serialized._PhysPkgReader") + _PhysPkgReader_.factory.return_value = phys_pkg_reader_ + package_reader = PackageReader("prs.pptx") + + blob_reader = package_reader._blob_reader + + _PhysPkgReader_.factory.assert_called_once_with("prs.pptx") + assert blob_reader is phys_pkg_reader_ + + # fixture components ----------------------------------- + + @pytest.fixture + def _blob_reader_prop_(self, request: FixtureRequest): + return property_mock(request, PackageReader, "_blob_reader") + + +class DescribePackageWriter: + """Unit-test suite for `pptx.opc.serialized.PackageWriter` objects.""" + + def it_provides_a_write_interface_classmethod( + self, request: FixtureRequest, relationships_: Mock, part_: Mock + ): + _init_ = initializer_mock(request, PackageWriter) + _write_ = method_mock(request, PackageWriter, "_write") + + PackageWriter.write("prs.pptx", relationships_, (part_, part_)) + + _init_.assert_called_once_with(ANY, "prs.pptx", relationships_, (part_, part_)) + _write_.assert_called_once_with(ANY) + + def it_can_write_a_package( + self, request: FixtureRequest, phys_writer_: Mock, relationships_: Mock + ): + _PhysPkgWriter_ = class_mock(request, "pptx.opc.serialized._PhysPkgWriter") + phys_writer_.__enter__.return_value = phys_writer_ + _PhysPkgWriter_.factory.return_value = phys_writer_ + _write_content_types_stream_ = method_mock( + request, PackageWriter, "_write_content_types_stream" + ) + _write_pkg_rels_ = method_mock(request, PackageWriter, "_write_pkg_rels") + _write_parts_ = method_mock(request, PackageWriter, "_write_parts") + package_writer = PackageWriter("prs.pptx", relationships_, []) + + package_writer._write() + + _PhysPkgWriter_.factory.assert_called_once_with("prs.pptx") + _write_content_types_stream_.assert_called_once_with(package_writer, phys_writer_) + _write_pkg_rels_.assert_called_once_with(package_writer, phys_writer_) + _write_parts_.assert_called_once_with(package_writer, phys_writer_) + + def it_can_write_a_content_types_stream( + self, request: FixtureRequest, phys_writer_: Mock, relationships_: Mock, part_: Mock + ): + _ContentTypesItem_ = class_mock(request, "pptx.opc.serialized._ContentTypesItem") + _ContentTypesItem_.xml_for.return_value = "part_xml" + serialize_part_xml_ = function_mock( + request, "pptx.opc.serialized.serialize_part_xml", return_value=b"xml" + ) + package_writer = PackageWriter("", relationships_, (part_, part_)) + + package_writer._write_content_types_stream(phys_writer_) + + _ContentTypesItem_.xml_for.assert_called_once_with((part_, part_)) + serialize_part_xml_.assert_called_once_with("part_xml") + phys_writer_.write.assert_called_once_with(CONTENT_TYPES_URI, b"xml") + + def it_can_write_a_sequence_of_parts( + self, request: FixtureRequest, relationships_: Mock, phys_writer_: Mock + ): + parts_ = [ + instance_mock( + request, + Part, + partname=PackURI("/ppt/%s.xml" % x), + blob="blob_%s" % x, + rels=instance_mock(request, _Relationships, xml="rels_xml_%s" % x), + ) + for x in ("a", "b", "c") + ] + package_writer = PackageWriter("", relationships_, parts_) + + package_writer._write_parts(phys_writer_) + + assert phys_writer_.write.call_args_list == [ + call("/ppt/a.xml", "blob_a"), + call("/ppt/_rels/a.xml.rels", "rels_xml_a"), + call("/ppt/b.xml", "blob_b"), + call("/ppt/_rels/b.xml.rels", "rels_xml_b"), + call("/ppt/c.xml", "blob_c"), + call("/ppt/_rels/c.xml.rels", "rels_xml_c"), + ] + + def it_can_write_a_pkg_rels_item(self, phys_writer_: Mock, relationships_: Mock): + relationships_.xml = b"pkg-rels-xml" + package_writer = PackageWriter("", relationships_, []) + + package_writer._write_pkg_rels(phys_writer_) + + phys_writer_.write.assert_called_once_with("/_rels/.rels", b"pkg-rels-xml") + + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) + + @pytest.fixture + def phys_writer_(self, request: FixtureRequest): + return instance_mock(request, _ZipPkgWriter) + + @pytest.fixture + def relationships_(self, request: FixtureRequest): + return instance_mock(request, _Relationships) + + +class Describe_PhysPkgReader: + """Unit-test suite for `pptx.opc.serialized._PhysPkgReader` objects.""" + + def it_constructs_ZipPkgReader_when_pkg_is_file_like( + self, _ZipPkgReader_: Mock, zip_pkg_reader_: Mock + ): + _ZipPkgReader_.return_value = zip_pkg_reader_ + file_like_pkg = io.BytesIO(b"pkg-bytes") + + phys_reader = _PhysPkgReader.factory(file_like_pkg) + + _ZipPkgReader_.assert_called_once_with(file_like_pkg) + assert phys_reader is zip_pkg_reader_ + + def and_it_constructs_DirPkgReader_when_pkg_is_a_dir(self, request: FixtureRequest): + dir_pkg_reader_ = instance_mock(request, _DirPkgReader) + _DirPkgReader_ = class_mock( + request, "pptx.opc.serialized._DirPkgReader", return_value=dir_pkg_reader_ + ) + + phys_reader = _PhysPkgReader.factory(dir_pkg_path) + + _DirPkgReader_.assert_called_once_with(dir_pkg_path) + assert phys_reader is dir_pkg_reader_ + + def and_it_constructs_ZipPkgReader_when_pkg_is_a_zip_file_path( + self, _ZipPkgReader_: Mock, zip_pkg_reader_: Mock + ): + _ZipPkgReader_.return_value = zip_pkg_reader_ + pkg_file_path = test_pptx_path + + phys_reader = _PhysPkgReader.factory(pkg_file_path) + + _ZipPkgReader_.assert_called_once_with(pkg_file_path) + assert phys_reader is zip_pkg_reader_ + + def but_it_raises_when_pkg_path_is_not_a_package(self): + with pytest.raises(PackageNotFoundError) as e: + _PhysPkgReader.factory("foobar") + assert str(e.value) == "Package not found at 'foobar'" + + # --- fixture components ------------------------------- + + @pytest.fixture + def zip_pkg_reader_(self, request: FixtureRequest): + return instance_mock(request, _ZipPkgReader) + + @pytest.fixture + def _ZipPkgReader_(self, request: FixtureRequest): + return class_mock(request, "pptx.opc.serialized._ZipPkgReader") + + +class Describe_DirPkgReader: + """Unit-test suite for `pptx.opc.serialized._DirPkgReader` objects.""" + + def it_knows_whether_it_contains_a_partname(self, dir_pkg_reader: _DirPkgReader): + assert PackURI("/ppt/presentation.xml") in dir_pkg_reader + assert PackURI("/ppt/foobar.xml") not in dir_pkg_reader + + def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_pkg_reader: _DirPkgReader): + blob = dir_pkg_reader[PackURI("/ppt/presentation.xml")] + assert hashlib.sha1(blob).hexdigest() == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" + + def but_it_raises_KeyError_when_requested_member_is_not_present( + self, dir_pkg_reader: _DirPkgReader + ): + with pytest.raises(KeyError) as e: + dir_pkg_reader[PackURI("/ppt/foobar.xml")] + assert str(e.value) == "\"no member '/ppt/foobar.xml' in package\"" + + # --- fixture components ------------------------------- + + @pytest.fixture(scope="class") + def dir_pkg_reader(self): + return _DirPkgReader(dir_pkg_path) + + +class Describe_ZipPkgReader: + """Unit-test suite for `pptx.opc.serialized._ZipPkgReader` objects.""" + + def it_knows_whether_it_contains_a_partname(self, zip_pkg_reader: _ZipPkgReader): + assert PackURI("/ppt/presentation.xml") in zip_pkg_reader + assert PackURI("/ppt/foobar.xml") not in zip_pkg_reader + + def it_can_get_a_blob_by_partname(self, zip_pkg_reader: _ZipPkgReader): + blob = zip_pkg_reader[PackURI("/ppt/presentation.xml")] + assert hashlib.sha1(blob).hexdigest() == ("efa7bee0ac72464903a67a6744c1169035d52a54") + + def but_it_raises_KeyError_when_requested_member_is_not_present( + self, zip_pkg_reader: _ZipPkgReader + ): + with pytest.raises(KeyError) as e: + zip_pkg_reader[PackURI("/ppt/foobar.xml")] + assert str(e.value) == "\"no member '/ppt/foobar.xml' in package\"" + + def it_loads_the_package_blobs_on_first_access_to_help(self, zip_pkg_reader: _ZipPkgReader): + blobs = zip_pkg_reader._blobs + assert len(blobs) == 38 + assert "/ppt/presentation.xml" in blobs + assert "/ppt/_rels/presentation.xml.rels" in blobs + + # --- fixture components ------------------------------- + + @pytest.fixture(scope="class") + def zip_pkg_reader(self): + return _ZipPkgReader(zip_pkg_path) + + +class Describe_PhysPkgWriter: + """Unit-test suite for `pptx.opc.serialized._PhysPkgWriter` objects.""" + + def it_constructs_ZipPkgWriter_unconditionally(self, request: FixtureRequest): + zip_pkg_writer_ = instance_mock(request, _ZipPkgWriter) + _ZipPkgWriter_ = class_mock( + request, "pptx.opc.serialized._ZipPkgWriter", return_value=zip_pkg_writer_ + ) + + phys_writer = _PhysPkgWriter.factory("prs.pptx") + + _ZipPkgWriter_.assert_called_once_with("prs.pptx") + assert phys_writer is zip_pkg_writer_ + + +class Describe_ZipPkgWriter: + """Unit-test suite for `pptx.opc.serialized._ZipPkgWriter` objects.""" + + def it_has_an__enter__method_for_context_management(self): + pkg_writer = _ZipPkgWriter("") + assert pkg_writer.__enter__() is pkg_writer + + def and_it_closes_the_zip_archive_on_context__exit__(self, _zipf_prop_: Mock): + _ZipPkgWriter("").__exit__() + _zipf_prop_.return_value.close.assert_called_once_with() + + def it_can_write_a_blob(self, _zipf_prop_: Mock): + """Integrates with zipfile.ZipFile.""" + pack_uri = PackURI("/part/name.xml") + _zipf_prop_.return_value = zipf = zipfile.ZipFile(io.BytesIO(), "w") + pkg_writer = _ZipPkgWriter("") + + pkg_writer.write(pack_uri, b"blob") + + members = {PackURI("/%s" % name): zipf.read(name) for name in zipf.namelist()} + assert len(members) == 1 + assert members[pack_uri] == b"blob" + + def it_provides_access_to_the_open_zip_file_to_help(self, request: FixtureRequest): + ZipFile_ = class_mock(request, "pptx.opc.serialized.zipfile.ZipFile") + pkg_writer = _ZipPkgWriter("prs.pptx") + + zipf = pkg_writer._zipf + + ZipFile_.assert_called_once_with( + "prs.pptx", "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) + assert zipf is ZipFile_.return_value + + # fixtures --------------------------------------------- + + @pytest.fixture + def _zipf_prop_(self, request: FixtureRequest): + return property_mock(request, _ZipPkgWriter, "_zipf") + + +class Describe_ContentTypesItem: + """Unit-test suite for `pptx.opc.serialized._ContentTypesItem` objects.""" + + def it_provides_an_interface_classmethod(self, request: FixtureRequest, part_: Mock): + _init_ = initializer_mock(request, _ContentTypesItem) + property_mock(request, _ContentTypesItem, "_xml", return_value=b"xml") + + xml = _ContentTypesItem.xml_for((part_, part_)) + + _init_.assert_called_once_with(ANY, (part_, part_)) + assert xml == b"xml" + + def it_can_compose_content_types_xml(self, request: FixtureRequest): + defaults = {"png": CT.PNG, "xml": CT.XML, "rels": CT.OPC_RELATIONSHIPS} + overrides = { + "/docProps/core.xml": "app/vnd.core", + "/ppt/slides/slide1.xml": "app/vnd.ct_sld", + "/zebra/foo.bar": "app/vnd.foobar", + } + property_mock( + request, + _ContentTypesItem, + "_defaults_and_overrides", + return_value=(defaults, overrides), + ) + + content_types = _ContentTypesItem([])._xml + + assert content_types.xml == snippet_text("content-types-xml").strip() + + def it_computes_defaults_and_overrides_to_help(self, request: FixtureRequest): + parts = [ + instance_mock(request, Part, partname=PackURI(partname), content_type=content_type) + for partname, content_type in ( + ("/media/image1.png", CT.PNG), + ("/ppt/slides/slide1.xml", CT.PML_SLIDE), + ("/foo/bar.xml", CT.XML), + ("/docProps/core.xml", CT.OPC_CORE_PROPERTIES), + ) + ] + content_types = _ContentTypesItem(parts) + + defaults, overrides = content_types._defaults_and_overrides + + assert defaults == {"png": CT.PNG, "rels": CT.OPC_RELATIONSHIPS, "xml": CT.XML} + assert overrides == { + "/ppt/slides/slide1.xml": CT.PML_SLIDE, + "/docProps/core.xml": CT.OPC_CORE_PROPERTIES, + } + + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py deleted file mode 100644 index a28788126..000000000 --- a/tests/opc/unitdata/rels.py +++ /dev/null @@ -1,310 +0,0 @@ -# encoding: utf-8 - -""" -Test data for relationship-related unit tests. -""" - -from __future__ import absolute_import - -from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.package import RelationshipCollection - -from pptx.opc.constants import NAMESPACE as NS -from pptx.oxml import parse_xml - - -class BaseBuilder(object): - """ - Provides common behavior for all data builders. - """ - - @property - def element(self): - """Return element based on XML generated by builder""" - return parse_xml(self.xml) - - def with_indent(self, indent): - """Add integer *indent* spaces at beginning of element XML""" - self._indent = indent - return self - - -class RelationshipCollectionBuilder(object): - """Builder class for test RelationshipCollections""" - - partname_tmpls = { - RT.SLIDE_MASTER: "/ppt/slideMasters/slideMaster%d.xml", - RT.SLIDE: "/ppt/slides/slide%d.xml", - } - - def __init__(self): - self.relationships = [] - self.next_rel_num = 1 - self.next_partnums = {} - - def _next_partnum(self, reltype): - if reltype not in self.next_partnums: - self.next_partnums[reltype] = 1 - partnum = self.next_partnums[reltype] - self.next_partnums[reltype] = partnum + 1 - return partnum - - @property - def next_rId(self): - rId = "rId%d" % self.next_rel_num - self.next_rel_num += 1 - return rId - - def _next_tuple_partname(self, reltype): - partname_tmpl = self.partname_tmpls[reltype] - partnum = self._next_partnum(reltype) - return partname_tmpl % partnum - - def build(self): - rels = RelationshipCollection() - for rel in self.relationships: - rels.add_rel(rel) - return rels - - -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._indent = 0 - self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES - - def with_content_type(self, 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*""" - self._extension = extension - return self - - def without_namespace(self): - """Don't include an 'xmlns=' attribute""" - 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) - - -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._indent = 0 - self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES - self._partname = "/part/name.xml" - - def with_content_type(self, 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*""" - self._partname = partname - return self - - def without_namespace(self): - """Don't include an 'xmlns=' attribute""" - 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) - - -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._target_mode = None - self._indent = 0 - self._namespace = ' xmlns="%s"' % NS.OPC_RELATIONSHIPS - - def with_rId(self, rId): - """Set Id attribute to *rId*""" - self._rId = rId - return self - - def with_reltype(self, reltype): - """Set Type attribute to *reltype*""" - self._reltype = reltype - return self - - def with_target(self, target): - """Set XXX attribute to *target*""" - self._target = target - return self - - def with_target_mode(self, target_mode): - """Set TargetMode attribute to *target_mode*""" - self._target_mode = None if target_mode == "Internal" else target_mode - return self - - def without_namespace(self): - """Don't include an 'xmlns=' attribute""" - self._namespace = "" - return self - - @property - def target_mode(self): - if self._target_mode is None: - 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, - ) - - -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"), - ) - - @property - def xml(self): - """ - Return XML string based on settings accumulated via method calls. - """ - 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" - return xml - - -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")) - self._empty = False - self._overrides = ( - ("/docProps/core.xml", "app/vnd.type1"), - ("/ppt/presentation.xml", "app/vnd.type2"), - ("/docProps/thumbnail.jpeg", "image/jpeg"), - ) - - def empty(self): - self._empty = True - return self - - @property - def xml(self): - """ - Return XML string based on settings accumulated via method calls - """ - if self._empty: - return '\n' % NS.OPC_CONTENT_TYPES - - 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 - ) - 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" - return xml - - -def a_Default(): - return CT_DefaultBuilder() - - -def a_Relationship(): - return CT_RelationshipBuilder() - - -def a_Relationships(): - return CT_RelationshipsBuilder() - - -def a_rels(): - return RelationshipCollectionBuilder() - - -def a_Types(): - return CT_TypesBuilder() - - -def an_Override(): - return CT_OverrideBuilder() diff --git a/tests/opc/unitdata/types.py b/tests/opc/unitdata/types.py deleted file mode 100644 index c78621f55..000000000 --- a/tests/opc/unitdata/types.py +++ /dev/null @@ -1,45 +0,0 @@ -# encoding: utf-8 - -""" -XML test data builders for [Content_Types].xml elements -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from pptx.opc.oxml import nsmap - -from ...unitdata import BaseBuilder - - -class CT_DefaultBuilder(BaseBuilder): - __tag__ = "Default" - __nspfxs__ = ("ct",) - __attrs__ = ("Extension", "ContentType") - - -class CT_OverrideBuilder(BaseBuilder): - __tag__ = "Override" - __nspfxs__ = ("ct",) - __attrs__ = ("PartName", "ContentType") - - -class CT_TypesBuilder(BaseBuilder): - __tag__ = "Types" - __nspfxs__ = ("ct",) - __attrs__ = () - - def with_nsdecls(self, *nspfxs): - self._nsdecls = ' xmlns="%s"' % nsmap["ct"] - return self - - -def a_Default(): - return CT_DefaultBuilder() - - -def a_Types(): - return CT_TypesBuilder() - - -def an_Override(): - return CT_OverrideBuilder() diff --git a/tests/oxml/shapes/test_autoshape.py b/tests/oxml/shapes/test_autoshape.py index 020246d58..a03bc7f22 100644 --- a/tests/oxml/shapes/test_autoshape.py +++ b/tests/oxml/shapes/test_autoshape.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.autoshape` module.""" -""" -Test suite for pptx.oxml.autoshape module. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -13,8 +9,8 @@ from pptx.oxml.shapes.autoshape import CT_Shape from pptx.oxml.shapes.shared import ST_Direction, ST_PlaceholderSize -from ..unitdata.shape import a_gd, a_prstGeom, an_avLst from ...unitutil.cxml import element +from ..unitdata.shape import a_gd, a_prstGeom, an_avLst class DescribeCT_PresetGeometry2D(object): @@ -77,9 +73,7 @@ def prstGeom_bldr(self, prst, gd_vals): for name, fmla in gd_vals: gd_bldr = a_gd().with_name(name).with_fmla(fmla) avLst_bldr.with_child(gd_bldr) - prstGeom_bldr = ( - a_prstGeom().with_nsdecls().with_prst(prst).with_child(avLst_bldr) - ) + prstGeom_bldr = a_prstGeom().with_nsdecls().with_prst(prst).with_child(avLst_bldr) return prstGeom_bldr @@ -103,8 +97,7 @@ def it_knows_how_to_create_a_new_autoshape_sp(self): 'schemeClr val="lt1"/>\n \n \n \n \n ' '\n \n \n \n \n\n" - % (nsdecls("a", "p"), id_, name, left, top, width, height, prst) + ">\n\n" % (nsdecls("a", "p"), id_, name, left, top, width, height, prst) ) # exercise --------------------- sp = CT_Shape.new_autoshape_sp(id_, name, prst, left, top, width, height) diff --git a/tests/oxml/shapes/test_graphfrm.py b/tests/oxml/shapes/test_graphfrm.py index 887d95290..1f1124ec5 100644 --- a/tests/oxml/shapes/test_graphfrm.py +++ b/tests/oxml/shapes/test_graphfrm.py @@ -1,14 +1,13 @@ -# encoding: utf-8 - """Unit-test suite for pptx.oxml.graphfrm module.""" +from __future__ import annotations + import pytest from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame from ...unitutil.cxml import xml - CHART_URI = "http://schemas.openxmlformats.org/drawingml/2006/chart" TABLE_URI = "http://schemas.openxmlformats.org/drawingml/2006/table" @@ -23,9 +22,7 @@ def it_can_construct_a_new_graphicFrame(self, new_graphicFrame_fixture): def it_can_construct_a_new_chart_graphicFrame(self, new_chart_graphicFrame_fixture): id_, name, rId, x, y, cx, cy, expected_xml = new_chart_graphicFrame_fixture - graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame( - id_, name, rId, x, y, cx, cy - ) + graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy) assert graphicFrame.xml == expected_xml def it_can_construct_a_new_table_graphicFrame(self, new_table_graphicFrame_fixture): diff --git a/tests/oxml/shapes/test_groupshape.py b/tests/oxml/shapes/test_groupshape.py index 318bc6e3d..6884b06cd 100644 --- a/tests/oxml/shapes/test_groupshape.py +++ b/tests/oxml/shapes/test_groupshape.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.shapes.groupshape` module.""" -"""Test suite for pptx.oxml.shapes.shapetree module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -16,6 +14,8 @@ class DescribeCT_GroupShape(object): + """Unit-test suite for `pptx.oxml.shapes.groupshape.CT_GroupShape` objects.""" + def it_can_add_a_graphicFrame_element_containing_a_table(self, add_table_fixt): spTree, id_, name, rows, cols, x, y, cx, cy = add_table_fixt[:9] new_table_graphicFrame_ = add_table_fixt[9] @@ -23,10 +23,8 @@ def it_can_add_a_graphicFrame_element_containing_a_table(self, add_table_fixt): graphicFrame = spTree.add_table(id_, name, rows, cols, x, y, cx, cy) - new_table_graphicFrame_.assert_called_once_with( - id_, name, rows, cols, x, y, cx, cy - ) - insert_element_before_.assert_called_once_with(graphicFrame_, "p:extLst") + new_table_graphicFrame_.assert_called_once_with(id_, name, rows, cols, x, y, cx, cy) + insert_element_before_.assert_called_once_with(spTree, graphicFrame_, "p:extLst") assert graphicFrame is graphicFrame_ def it_can_add_a_grpSp_element(self, add_grpSp_fixture): @@ -40,37 +38,41 @@ def it_can_add_a_grpSp_element(self, add_grpSp_fixture): def it_can_add_a_pic_element_representing_a_picture(self, add_pic_fixt): spTree, id_, name, desc, rId, x, y, cx, cy = add_pic_fixt[:9] CT_Picture_, insert_element_before_, pic_ = add_pic_fixt[9:] + pic = spTree.add_pic(id_, name, desc, rId, x, y, cx, cy) + CT_Picture_.new_pic.assert_called_once_with(id_, name, desc, rId, x, y, cx, cy) - insert_element_before_.assert_called_once_with(pic_, "p:extLst") + insert_element_before_.assert_called_once_with(spTree, pic_, "p:extLst") assert pic is pic_ def it_can_add_an_sp_element_for_a_placeholder(self, add_placeholder_fixt): spTree, id_, name, ph_type, orient, sz, idx = add_placeholder_fixt[:7] CT_Shape_, insert_element_before_, sp_ = add_placeholder_fixt[7:] + sp = spTree.add_placeholder(id_, name, ph_type, orient, sz, idx) - CT_Shape_.new_placeholder_sp.assert_called_once_with( - id_, name, ph_type, orient, sz, idx - ) - insert_element_before_.assert_called_once_with(sp_, "p:extLst") + + CT_Shape_.new_placeholder_sp.assert_called_once_with(id_, name, ph_type, orient, sz, idx) + insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ def it_can_add_an_sp_element_for_an_autoshape(self, add_autoshape_fixt): spTree, id_, name, prst, x, y, cx, cy = add_autoshape_fixt[:8] CT_Shape_, insert_element_before_, sp_ = add_autoshape_fixt[8:] + sp = spTree.add_autoshape(id_, name, prst, x, y, cx, cy) - CT_Shape_.new_autoshape_sp.assert_called_once_with( - id_, name, prst, x, y, cx, cy - ) - insert_element_before_.assert_called_once_with(sp_, "p:extLst") + + CT_Shape_.new_autoshape_sp.assert_called_once_with(id_, name, prst, x, y, cx, cy) + insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ def it_can_add_a_textbox_sp_element(self, add_textbox_fixt): spTree, id_, name, x, y, cx, cy, CT_Shape_ = add_textbox_fixt[:8] insert_element_before_, sp_ = add_textbox_fixt[8:] + sp = spTree.add_textbox(id_, name, x, y, cx, cy) + CT_Shape_.new_textbox_sp.assert_called_once_with(id_, name, x, y, cx, cy) - insert_element_before_.assert_called_once_with(sp_, "p:extLst") + insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ def it_can_recalculate_its_pos_and_size(self, recalc_fixture): diff --git a/tests/oxml/shapes/test_picture.py b/tests/oxml/shapes/test_picture.py index cddd0d9d5..546d6b0fd 100644 --- a/tests/oxml/shapes/test_picture.py +++ b/tests/oxml/shapes/test_picture.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.shapes.picture` module.""" -""" -Test suite for pptx.oxml.shapes.picture module. -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import pytest @@ -13,82 +9,99 @@ class DescribeCT_Picture(object): - def it_can_create_a_new_pic_element(self, pic_fixture): - shape_id, name, desc, rId, x, y, cx, cy, expected_xml = pic_fixture - pic = CT_Picture.new_pic(shape_id, name, desc, rId, x, y, cx, cy) - assert pic.xml == expected_xml - - def it_can_create_a_new_video_pic_element(self, video_pic_fixture): - shape_id, shape_name, video_rId, media_rId = video_pic_fixture[:4] - poster_frame_rId, x, y, cx, cy, expected_xml = video_pic_fixture[4:] - pic = CT_Picture.new_video_pic( - shape_id, shape_name, video_rId, media_rId, poster_frame_rId, x, y, cx, cy - ) - print(pic.xml) - assert pic.xml == expected_xml + """Unit-test suite for `pptx.oxml.shapes.picture.CT_Picture` objects.""" - # fixtures ------------------------------------------------------- + @pytest.mark.parametrize( + "desc, xml_desc", + ( + ("kittens.jpg", "kittens.jpg"), + ("bits&bobs.png", "bits&bobs.png"), + ("img&.png", "img&.png"), + ("ime.png", "im<ag>e.png"), + ), + ) + def it_can_create_a_new_pic_element(self, desc, xml_desc): + """`desc` attr (often filename) is XML-escaped to handle special characters. - @pytest.fixture - def pic_fixture(self): - shape_id, name, desc, rId = 9, "Diam. > 1 mm", "desc", "rId1" - x, y, cx, cy = 1, 2, 3, 4 - expected_xml = ( - '\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("a", "p", "r"), - shape_id, - "Diam. > 1 mm", - desc, - rId, - x, - y, - cx, - cy, - ) + In particular, ampersand ('&'), less/greater-than ('') etc. + """ + pic = CT_Picture.new_pic( + shape_id=9, name="Picture 8", desc=desc, rId="rId42", x=1, y=2, cx=3, cy=4 ) - return shape_id, name, desc, rId, x, y, cx, cy, expected_xml - @pytest.fixture - def video_pic_fixture(self): - shape_id, shape_name = 42, "media.mp4" - video_rId, media_rId, poster_frame_rId = "rId1", "rId2", "rId3" - x, y, cx, cy = 1, 2, 3, 4 - expected_xml = ( - '\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 \n\n" + assert pic.xml == ( + "\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("a", "p", "r"), xml_desc) ) - return ( - shape_id, - shape_name, - video_rId, - media_rId, - poster_frame_rId, - x, - y, - cx, - cy, - expected_xml, + + def it_can_create_a_new_video_pic_element(self): + pic = CT_Picture.new_video_pic( + shape_id=42, + shape_name="Media 41", + video_rId="rId1", + media_rId="rId2", + poster_frame_rId="rId3", + x=1, + y=2, + cx=3, + cy=4, ) + + assert pic.xml == ( + "\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" + " \n" + "\n" + ) % nsdecls("a", "p", "r") diff --git a/tests/oxml/test___init__.py b/tests/oxml/test___init__.py index 176d8ace4..d4d163d09 100644 --- a/tests/oxml/test___init__.py +++ b/tests/oxml/test___init__.py @@ -1,13 +1,8 @@ -# encoding: utf-8 +"""Test suite for pptx.oxml.__init__.py module, primarily XML parser-related.""" -""" -Test suite for pptx.oxml.__init__.py module, primarily XML parser-related. -""" - -from __future__ import print_function, unicode_literals +from __future__ import annotations import pytest - from lxml import etree from pptx.oxml import oxml_parser, parse_xml, register_element_cls diff --git a/tests/oxml/test_dml.py b/tests/oxml/test_dml.py index 8befa16c2..cc205b701 100644 --- a/tests/oxml/test_dml.py +++ b/tests/oxml/test_dml.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.dml` module.""" -""" -Test suite for pptx.oxml.dml module. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest diff --git a/tests/oxml/test_ns.py b/tests/oxml/test_ns.py index d4c4cc65d..0c4896f76 100644 --- a/tests/oxml/test_ns.py +++ b/tests/oxml/test_ns.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Test suite for pptx.oxml.ns.py module.""" -""" -Test suite for pptx.oxml.ns.py module. -""" - -from __future__ import print_function, unicode_literals +from __future__ import annotations import pytest @@ -44,16 +40,12 @@ def it_formats_namespace_declarations_from_a_list_of_prefixes(self, nsdecls_str) class DescribeNsuri(object): - def it_finds_the_namespace_uri_corresponding_to_a_namespace_prefix( - self, namespace_uri_a - ): + def it_finds_the_namespace_uri_corresponding_to_a_namespace_prefix(self, namespace_uri_a): assert nsuri("a") == namespace_uri_a class DescribeQn(object): - def it_calculates_the_clark_name_for_an_ns_prefixed_tag_string( - self, nsptag_str, clark_name - ): + def it_calculates_the_clark_name_for_an_ns_prefixed_tag_string(self, nsptag_str, clark_name): assert qn(nsptag_str) == clark_name diff --git a/tests/oxml/test_presentation.py b/tests/oxml/test_presentation.py index fc09cb444..d2a47b27d 100644 --- a/tests/oxml/test_presentation.py +++ b/tests/oxml/test_presentation.py @@ -1,46 +1,63 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.oxml.presentation module -""" +"""Unit-test suite for `pptx.oxml.presentation` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations + +from typing import cast import pytest +from pptx.oxml.presentation import CT_SlideIdList + from ..unitutil.cxml import element, xml class DescribeCT_SlideIdList(object): - def it_can_add_a_sldId_element_as_a_child(self, add_fixture): - sldIdLst, expected_xml = add_fixture - sldIdLst.add_sldId("rId1") - assert sldIdLst.xml == expected_xml + """Unit-test suite for `pptx.oxml.presentation.CT_SlideIdLst` objects.""" - def it_knows_the_next_available_slide_id(self, next_id_fixture): - sldIdLst, expected_id = next_id_fixture - assert sldIdLst._next_id == expected_id + def it_can_add_a_sldId_element_as_a_child(self): + sldIdLst = cast(CT_SlideIdList, element("p:sldIdLst/p:sldId{r:id=rId4,id=256}")) - # fixtures ------------------------------------------------------- + sldIdLst.add_sldId("rId1") - @pytest.fixture - def add_fixture(self): - sldIdLst = element("p:sldIdLst/p:sldId{r:id=rId4,id=256}") - expected_xml = xml( + assert sldIdLst.xml == xml( "p:sldIdLst/(p:sldId{r:id=rId4,id=256},p:sldId{r:id=rId1,id=257})" ) - return sldIdLst, expected_xml - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("sldIdLst_cxml", "expected_value"), + [ ("p:sldIdLst", 256), ("p:sldIdLst/p:sldId{id=42}", 256), ("p:sldIdLst/p:sldId{id=256}", 257), ("p:sldIdLst/(p:sldId{id=256},p:sldId{id=712})", 713), ("p:sldIdLst/(p:sldId{id=280},p:sldId{id=257})", 281), - ] + ], ) - def next_id_fixture(self, request): - sldIdLst_cxml, expected_value = request.param - sldIdLst = element(sldIdLst_cxml) - return sldIdLst, expected_value + def it_knows_the_next_available_slide_id(self, sldIdLst_cxml: str, expected_value: int): + sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml)) + assert sldIdLst._next_id == expected_value + + @pytest.mark.parametrize( + ("sldIdLst_cxml", "expected_value"), + [ + ("p:sldIdLst/p:sldId{id=2147483646}", 2147483647), + ("p:sldIdLst/p:sldId{id=2147483647}", 256), + # -- 2147483648 is not a valid id but shouldn't stop us from finding a one that is -- + ("p:sldIdLst/p:sldId{id=2147483648}", 256), + ("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483647})", 257), + ("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483647},p:sldId{id=257})", 258), + # -- 245 is also not a valid id but that shouldn't change the result either -- + ("p:sldIdLst/(p:sldId{id=245},p:sldId{id=2147483647},p:sldId{id=256})", 257), + ], + ) + def and_it_chooses_a_valid_slide_id_when_max_slide_id_is_used_for_a_slide( + self, sldIdLst_cxml: str, expected_value: int + ): + sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml)) + + slide_id = sldIdLst._next_id + + assert 256 <= slide_id <= 2147483647 + assert slide_id == expected_value diff --git a/tests/oxml/test_simpletypes.py b/tests/oxml/test_simpletypes.py index 3ef34a936..261edf550 100644 --- a/tests/oxml/test_simpletypes.py +++ b/tests/oxml/test_simpletypes.py @@ -1,13 +1,19 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.simpletypes` module. -""" -Test suite for pptx.oxml.simpletypes module, which contains simple type class -definitions. A simple type in this context corresponds to an -```` definition in the XML schema and provides data -validation and type conversion services for use by xmlchemy. +The `simpletypes` module contains classes that each define a scalar-type that appears as an XML +attribute. + +The term "simple-type", as distinct from "complex-type", is an XML Schema distinction. An XML +attribute value must be a single string, and corresponds to a scalar value, like `bool`, `int`, or +`str`. Complex-types describe _elements_, which can have multiple attributes as well as child +elements. + +A simple type corresponds to an `` definition in the XML schema e.g. `ST_Foobar`. +The `BaseSimpleType` subclass provides data validation and type conversion services for use by +`xmlchemy`. """ -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -19,14 +25,18 @@ ST_Percentage, ) -from ..unitutil.mock import method_mock, instance_mock +from ..unitutil.mock import instance_mock, method_mock class DescribeBaseSimpleType(object): - def it_can_convert_attr_value_to_python_type(self, from_xml_fixture): - SimpleType, str_value_, py_value_ = from_xml_fixture - py_value = SimpleType.from_xml(str_value_) - SimpleType.convert_from_xml.assert_called_once_with(str_value_) + """Unit-test suite for `pptx.oxml.simpletypes.BaseSimpleType` objects.""" + + def it_can_convert_an_XML_attribute_value_to_a_python_type( + self, str_value_, py_value_, convert_from_xml_ + ): + py_value = ST_SimpleType.from_xml(str_value_) + + ST_SimpleType.convert_from_xml.assert_called_once_with(str_value_) assert py_value is py_value_ def it_can_convert_python_value_to_string(self, to_xml_fixture): @@ -54,10 +64,6 @@ def it_can_validate_a_value_as_a_python_string(self, valid_str_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def from_xml_fixture(self, request, str_value_, py_value_, convert_from_xml_): - return ST_SimpleType, str_value_, py_value_ - @pytest.fixture def to_xml_fixture( self, request, py_value_, str_value_, convert_to_xml_, validate_ @@ -98,13 +104,21 @@ def valid_str_fixture(self, request): @pytest.fixture def convert_from_xml_(self, request, py_value_): return method_mock( - request, ST_SimpleType, "convert_from_xml", return_value=py_value_ + request, + ST_SimpleType, + "convert_from_xml", + autospec=False, + return_value=py_value_, ) @pytest.fixture def convert_to_xml_(self, request, str_value_): return method_mock( - request, ST_SimpleType, "convert_to_xml", return_value=str_value_ + request, + ST_SimpleType, + "convert_to_xml", + autospec=False, + return_value=str_value_, ) @pytest.fixture @@ -117,7 +131,7 @@ def str_value_(self, request): @pytest.fixture def validate_(self, request): - return method_mock(request, ST_SimpleType, "validate") + return method_mock(request, ST_SimpleType, "validate", autospec=False) class DescribeBaseIntType(object): @@ -190,7 +204,7 @@ def it_can_validate_a_hex_RGB_string(self, valid_fixture): if exception is None: try: ST_HexColorRGB.validate(str_value) - except ValueError: + except ValueError: # pragma: no cover raise AssertionError("string '%s' did not validate" % str_value) else: with pytest.raises(exception): @@ -258,12 +272,12 @@ def percent_fixture(self, request): class ST_SimpleType(BaseSimpleType): @classmethod def convert_from_xml(cls, str_value): - return 666 + return 666 # pragma: no cover @classmethod def convert_to_xml(cls, value): - return "666" + return "666" # pragma: no cover @classmethod def validate(cls, value): - pass + pass # pragma: no cover diff --git a/tests/oxml/test_slide.py b/tests/oxml/test_slide.py index c8c336a5b..63b321da7 100644 --- a/tests/oxml/test_slide.py +++ b/tests/oxml/test_slide.py @@ -1,12 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.slide` module.""" -""" -Test suite for pptx.oxml.slide module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import pytest +from __future__ import annotations from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide @@ -14,28 +8,16 @@ class DescribeCT_NotesMaster(object): - def it_can_create_a_default_notesMaster_element(self, new_fixture): - expected_xml = new_fixture - notesMaster = CT_NotesMaster.new_default() - assert notesMaster.xml == expected_xml - - # fixtures ------------------------------------------------------- + """Unit-test suite for `pptx.oxml.slide.CT_NotesMaster` objects.""" - @pytest.fixture - def new_fixture(self): - expected_xml = snippet_text("default-notesMaster") - return expected_xml + def it_can_create_a_default_notesMaster_element(self): + notesMaster = CT_NotesMaster.new_default() + assert notesMaster.xml == snippet_text("default-notesMaster") class DescribeCT_NotesSlide(object): - def it_can_create_a_new_notes_element(self, new_fixture): - expected_xml = new_fixture - notes = CT_NotesSlide.new() - assert notes.xml == expected_xml + """Unit-test suite for `pptx.oxml.slide.CT_NotesSlide` objects.""" - # fixtures ------------------------------------------------------- - - @pytest.fixture - def new_fixture(self): - expected_xml = snippet_text("default-notes") - return expected_xml + def it_can_create_a_new_notes_element(self): + notes = CT_NotesSlide.new() + assert notes.xml == snippet_text("default-notes") diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 02ce4b302..c64196f9b 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Unit-test suite for pptx.oxml.table module""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -36,8 +34,7 @@ def it_can_create_a_new_tbl_element_tree(self): "dyPr/>\n \n \n " "\n \n \n \n \n " " \n \n \n " - "\n \n \n \n\n" - % nsdecls("a") + "\n \n \n \n\n" % nsdecls("a") ) tbl = CT_Table.new_tbl(2, 3, 334, 445) assert tbl.xml == expected_xml @@ -154,8 +151,7 @@ def dimensions_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", [0, 1], []), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", [2, 1], [1, 3]), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", [0, 8], [1, 2, 4, 5, 7, 8], ), @@ -174,8 +170,7 @@ def except_left_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", [0, 1], [1]), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", [2, 1], [2, 3]), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", [0, 8], [3, 4, 5, 6, 7, 8], ), @@ -194,9 +189,7 @@ def in_same_table_fixture(self, request): tbl = element("a:tbl/a:tr/(a:tc,a:tc)") other_tbl = element("a:tbl/a:tr/(a:tc,a:tc)") tc = tbl.xpath("//a:tc")[0] - other_tc = ( - tbl.xpath("//a:tc")[1] if expected_value else other_tbl.xpath("//a:tc")[1] - ) + other_tc = tbl.xpath("//a:tc")[1] if expected_value else other_tbl.xpath("//a:tc")[1] return tc, other_tc, expected_value @pytest.fixture( @@ -205,8 +198,7 @@ def in_same_table_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", (0, 1), (0, 1)), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", (2, 1), (0, 2)), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", (4, 8), (4, 7), ), @@ -225,8 +217,7 @@ def left_col_fixture(self, request): ('a:tbl/a:tr/(a:tc/a:txBody/a:p,a:tc/a:txBody/a:p/a:r/a:t"b")', "b"), ('a:tbl/a:tr/(a:tc/a:txBody/a:p/a:r/a:t"a",a:tc/a:txBody/a:p)', "a"), ( - 'a:tbl/a:tr/(a:tc/a:txBody/a:p/a:r/a:t"a",a:tc/a:txBody/a:p/a:r/a:t' - '"b")', + 'a:tbl/a:tr/(a:tc/a:txBody/a:p/a:r/a:t"a",a:tc/a:txBody/a:p/a:r/a:t' '"b")', "a\nb", ), ( @@ -250,8 +241,7 @@ def move_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", (0, 1), (0,)), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", (2, 1), (0, 1)), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", (4, 8), (4, 5), ), diff --git a/tests/oxml/test_theme.py b/tests/oxml/test_theme.py index 9bff00568..87d051726 100644 --- a/tests/oxml/test_theme.py +++ b/tests/oxml/test_theme.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.theme` module.""" -""" -Test suite for pptx.oxml.theme module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index 6fd88f831..abb38b7f8 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -1,12 +1,10 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.oxml.xmlchemy` module. -""" -Test suite for the pptx.oxml.xmlchemy module, focused on the metaclass and -element and attribute definition classes. A major part of the fixture is -provided by the metaclass-built test classes at the end of the file. +Focused on the metaclass and element and attribute definition classes. A major part of the fixture +is provided by the metaclass-built test classes at the end of the file. """ -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -48,22 +46,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) @@ -77,9 +69,7 @@ def add_fixture(self): 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 @@ -96,10 +86,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 = ( @@ -156,27 +143,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 ------------------------------------------------------- @@ -238,9 +219,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 ------------------------------------------------------- @@ -271,9 +250,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 @@ -320,18 +297,14 @@ 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_removes_the_property_root_name_used_for_declaration(self): assert not hasattr(CT_Parent, "zomChild") @@ -393,17 +366,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 @@ -522,9 +491,7 @@ class CT_Parent(BaseOxmlElement): (Choice("p:choice"), Choice("p:choice2")), successors=("p:oomChild", "p:oooChild"), ) - oomChild = OneOrMore( - "p:oomChild", successors=("p:oooChild", "p:zomChild", "p:zooChild") - ) + oomChild = OneOrMore("p:oomChild", successors=("p:oooChild", "p:zomChild", "p:zooChild")) oooChild = OneAndOnlyOne("p:oooChild") zomChild = ZeroOrMore("p:zomChild", successors=("p:zooChild",)) zooChild = ZeroOrOne("p:zooChild", successors=()) diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index 5573116f2..8c716ab81 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""XML test data builders for pptx.oxml.dml unit tests.""" -""" -XML test data builders for pptx.oxml.dml unit tests -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/shape.py b/tests/oxml/unitdata/shape.py index 23310b4dd..a5a39360a 100644 --- a/tests/oxml/unitdata/shape.py +++ b/tests/oxml/unitdata/shape.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Test data for autoshape-related unit tests.""" -""" -Test data for autoshape-related unit tests. -""" - -from __future__ import absolute_import +from __future__ import annotations from ...unitdata import BaseBuilder @@ -33,18 +29,6 @@ class CT_GeomGuideListBuilder(BaseBuilder): __attrs__ = () -class CT_GraphicalObjectBuilder(BaseBuilder): - __tag__ = "a:graphic" - __nspfxs__ = ("a",) - __attrs__ = () - - -class CT_GraphicalObjectDataBuilder(BaseBuilder): - __tag__ = "a:graphicData" - __nspfxs__ = ("a",) - __attrs__ = ("uri",) - - class CT_GraphicalObjectFrameBuilder(BaseBuilder): __tag__ = "p:graphicFrame" __nspfxs__ = ("p", "a") @@ -163,14 +147,6 @@ def a_gd(): return CT_GeomGuideBuilder() -def a_graphic(): - return CT_GraphicalObjectBuilder() - - -def a_graphicData(): - return CT_GraphicalObjectDataBuilder() - - def a_graphicFrame(): return CT_GraphicalObjectFrameBuilder() diff --git a/tests/oxml/unitdata/text.py b/tests/oxml/unitdata/text.py index 5236d05d8..b86ff45d7 100644 --- a/tests/oxml/unitdata/text.py +++ b/tests/oxml/unitdata/text.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""XML test data builders for `pptx.oxml.text` unit tests.""" -""" -XML test data builders for pptx.oxml.text unit tests -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations from ...unitdata import BaseBuilder @@ -28,50 +24,12 @@ def with_rId(self, rId): return self -class CT_OfficeArtExtensionList(BaseBuilder): - __tag__ = "a:extLst" - __nspfxs__ = ("a",) - __attrs__ = () - - class CT_RegularTextRunBuilder(BaseBuilder): __tag__ = "a:r" __nspfxs__ = ("a",) __attrs__ = () -class CT_TextBodyBuilder(BaseBuilder): - __tag__ = "p:txBody" - __nspfxs__ = ("p", "a") - __attrs__ = () - - -class CT_TextBodyPropertiesBuilder(BaseBuilder): - __tag__ = "a:bodyPr" - __nspfxs__ = ("a",) - __attrs__ = ( - "rot", - "spcFirstLastPara", - "vertOverflow", - "horzOverflow", - "vert", - "wrap", - "lIns", - "tIns", - "rIns", - "bIns", - "numCol", - "spcCol", - "rtlCol", - "fromWordArt", - "anchor", - "anchorCtr", - "forceAA", - "upright", - "compatLnSpc", - ) - - class CT_TextCharacterPropertiesBuilder(BaseBuilder): """ Test data builder for CT_TextCharacterProperties XML element that appears @@ -86,24 +44,6 @@ def __init__(self, tag): super(CT_TextCharacterPropertiesBuilder, self).__init__() -class CT_TextFontBuilder(BaseBuilder): - __tag__ = "a:latin" - __nspfxs__ = ("a",) - __attrs__ = ("typeface", "panose", "pitchFamily", "charset") - - -class CT_TextNoAutofitBuilder(BaseBuilder): - __tag__ = "a:noAutofit" - __nspfxs__ = ("a",) - __attrs__ = () - - -class CT_TextNormalAutofitBuilder(BaseBuilder): - __tag__ = "a:normAutofit" - __nspfxs__ = ("a",) - __attrs__ = ("fontScale", "lnSpcReduction") - - class CT_TextParagraphBuilder(BaseBuilder): """ Test data builder for CT_TextParagraph () XML element that appears @@ -115,35 +55,6 @@ class CT_TextParagraphBuilder(BaseBuilder): __attrs__ = () -class CT_TextParagraphPropertiesBuilder(BaseBuilder): - """ - Test data builder for CT_TextParagraphProperties () XML element - that appears as a child of . - """ - - __tag__ = "a:pPr" - __nspfxs__ = ("a",) - __attrs__ = ( - "marL", - "marR", - "lvl", - "indent", - "algn", - "defTabSz", - "rtl", - "eaLnBrk", - "fontAlgn", - "latinLnBrk", - "hangingPunct", - ) - - -class CT_TextShapeAutofitBuilder(BaseBuilder): - __tag__ = "a:spAutoFit" - __nspfxs__ = ("a",) - __attrs__ = () - - class XsdString(BaseBuilder): __attrs__ = () @@ -153,52 +64,15 @@ def __init__(self, tag, nspfxs): super(XsdString, self).__init__() -def a_bodyPr(): - return CT_TextBodyPropertiesBuilder() - - -def a_defRPr(): - return CT_TextCharacterPropertiesBuilder("a:defRPr") - - -def a_latin(): - return CT_TextFontBuilder() - - -def a_noAutofit(): - return CT_TextNoAutofitBuilder() - - -def a_normAutofit(): - return CT_TextNormalAutofitBuilder() - - def a_p(): """Return a CT_TextParagraphBuilder instance""" return CT_TextParagraphBuilder() -def a_pPr(): - """Return a CT_TextParagraphPropertiesBuilder instance""" - return CT_TextParagraphPropertiesBuilder() - - def a_t(): return XsdString("a:t", ("a",)) -def a_txBody(): - return CT_TextBodyBuilder() - - -def an_endParaRPr(): - return CT_TextCharacterPropertiesBuilder("a:endParaRPr") - - -def an_extLst(): - return CT_OfficeArtExtensionList() - - def an_hlinkClick(): return CT_Hyperlink() @@ -209,7 +83,3 @@ def an_r(): def an_rPr(): return CT_TextCharacterPropertiesBuilder("a:rPr") - - -def an_spAutoFit(): - return CT_TextShapeAutofitBuilder() diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index 8281c1685..b0a41f581 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -1,17 +1,14 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.parts.chart` module.""" -""" -Test suite for pptx.parts.chart module. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest from pptx.chart.chart import Chart from pptx.chart.data import ChartData -from pptx.enum.base import EnumValue -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.enum.chart import XL_CHART_TYPE as XCT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import OpcPackage from pptx.opc.packuri import PackURI from pptx.oxml.chart.chart import CT_ChartSpace @@ -23,204 +20,77 @@ class DescribeChartPart(object): - def it_can_construct_from_chart_type_and_data(self, new_fixture): - chart_type_, chart_data_, package_ = new_fixture[:3] - partname_template, load_, partname_ = new_fixture[3:6] - content_type, chart_blob_, chart_part_, xlsx_blob_ = new_fixture[6:] - - chart_part = ChartPart.new(chart_type_, chart_data_, package_) - - chart_data_.xml_bytes.assert_called_once_with(chart_type_) - package_.next_partname.assert_called_once_with(partname_template) - load_.assert_called_once_with(partname_, content_type, chart_blob_, package_) - chart_workbook_ = chart_part_.chart_workbook - chart_workbook_.update_from_xlsx_blob.assert_called_once_with(xlsx_blob_) + """Unit-test suite for `pptx.parts.chart.ChartPart` objects.""" + + def it_can_construct_from_chart_type_and_data(self, request): + chart_data_ = instance_mock(request, ChartData, xlsx_blob=b"xlsx-blob") + chart_data_.xml_bytes.return_value = b"chart-blob" + package_ = instance_mock(request, OpcPackage) + package_.next_partname.return_value = PackURI("/ppt/charts/chart42.xml") + chart_part_ = instance_mock(request, ChartPart) + # --- load() must have autospec turned off to work in Python 2.7 mock --- + load_ = method_mock(request, ChartPart, "load", autospec=False, return_value=chart_part_) + + chart_part = ChartPart.new(XCT.RADAR, chart_data_, package_) + + package_.next_partname.assert_called_once_with("/ppt/charts/chart%d.xml") + chart_data_.xml_bytes.assert_called_once_with(XCT.RADAR) + load_.assert_called_once_with( + "/ppt/charts/chart42.xml", CT.DML_CHART, package_, b"chart-blob" + ) + chart_part_.chart_workbook.update_from_xlsx_blob.assert_called_once_with(b"xlsx-blob") assert chart_part is chart_part_ - def it_provides_access_to_the_chart_object(self, chart_fixture): - chart_part, chart_, Chart_ = chart_fixture + def it_provides_access_to_the_chart_object(self, request, chartSpace_): + chart_ = instance_mock(request, Chart) + Chart_ = class_mock(request, "pptx.parts.chart.Chart", return_value=chart_) + chart_part = ChartPart(None, None, None, chartSpace_) + chart = chart_part.chart + Chart_.assert_called_once_with(chart_part._element, chart_part) assert chart is chart_ - def it_provides_access_to_the_chart_workbook(self, workbook_fixture): - chart_part, ChartWorkbook_, chartSpace_, chart_workbook_ = workbook_fixture + def it_provides_access_to_the_chart_workbook(self, request, chartSpace_): + chart_workbook_ = instance_mock(request, ChartWorkbook) + ChartWorkbook_ = class_mock( + request, "pptx.parts.chart.ChartWorkbook", return_value=chart_workbook_ + ) + chart_part = ChartPart(None, None, None, chartSpace_) + chart_workbook = chart_part.chart_workbook + ChartWorkbook_.assert_called_once_with(chartSpace_, chart_part) assert chart_workbook is chart_workbook_ - # fixtures ------------------------------------------------------- - - @pytest.fixture - def chart_fixture(self, chartSpace_, Chart_, chart_): - chart_part = ChartPart(None, None, chartSpace_) - return chart_part, chart_, Chart_ - - @pytest.fixture - def new_fixture( - self, - chart_type_, - chart_data_, - package_, - load_, - partname_, - chart_blob_, - chart_part_, - xlsx_blob_, - ): - partname_template = "/ppt/charts/chart%d.xml" - content_type = CT.DML_CHART - return ( - chart_type_, - chart_data_, - package_, - partname_template, - load_, - partname_, - content_type, - chart_blob_, - chart_part_, - xlsx_blob_, - ) - - @pytest.fixture - def workbook_fixture(self, chartSpace_, ChartWorkbook_, chart_workbook_): - chart_part = ChartPart(None, None, chartSpace_) - return chart_part, ChartWorkbook_, chartSpace_, chart_workbook_ - # fixture components --------------------------------------------- - @pytest.fixture - def ChartWorkbook_(self, request, chart_workbook_): - return class_mock( - request, "pptx.parts.chart.ChartWorkbook", return_value=chart_workbook_ - ) - - @pytest.fixture - def Chart_(self, request, chart_): - return class_mock(request, "pptx.parts.chart.Chart", return_value=chart_) - @pytest.fixture def chartSpace_(self, request): return instance_mock(request, CT_ChartSpace) - @pytest.fixture - def chart_(self, request): - return instance_mock(request, Chart) - - @pytest.fixture - def chart_blob_(self, request): - return instance_mock(request, bytes) - - @pytest.fixture - def chart_data_(self, request, chart_blob_, xlsx_blob_): - chart_data_ = instance_mock(request, ChartData) - chart_data_.xml_bytes.return_value = chart_blob_ - chart_data_.xlsx_blob = xlsx_blob_ - return chart_data_ - - @pytest.fixture - def chart_part_(self, request, chart_workbook_): - chart_part_ = instance_mock(request, ChartPart) - chart_part_.chart_workbook = chart_workbook_ - return chart_part_ - - @pytest.fixture - def chart_type_(self, request): - return instance_mock(request, EnumValue) - - @pytest.fixture - def chart_workbook_(self, request): - return instance_mock(request, ChartWorkbook) - - @pytest.fixture - def load_(self, request, chart_part_): - return method_mock(request, ChartPart, "load", return_value=chart_part_) - - @pytest.fixture - def package_(self, request, partname_): - package_ = instance_mock(request, OpcPackage) - package_.next_partname.return_value = partname_ - return package_ - - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - - @pytest.fixture - def xlsx_blob_(self, request): - return instance_mock(request, bytes) - class DescribeChartWorkbook(object): - def it_can_get_the_chart_xlsx_part(self, xlsx_part_get_fixture): - chart_data, expected_object = xlsx_part_get_fixture - assert chart_data.xlsx_part is expected_object - - def it_can_change_the_chart_xlsx_part(self, xlsx_part_set_fixture): - chart_data, xlsx_part_, expected_xml = xlsx_part_set_fixture - chart_data.xlsx_part = xlsx_part_ - chart_data._chart_part.relate_to.assert_called_once_with(xlsx_part_, RT.PACKAGE) - assert chart_data._chartSpace.xml == expected_xml - - def it_adds_an_xlsx_part_on_update_if_needed(self, add_part_fixture): - chart_data, xlsx_blob_, EmbeddedXlsxPart_ = add_part_fixture[:3] - package_, xlsx_part_prop_, xlsx_part_ = add_part_fixture[3:] + """Unit-test suite for `pptx.parts.chart.ChartWorkbook` objects.""" - chart_data.update_from_xlsx_blob(xlsx_blob_) - - EmbeddedXlsxPart_.new.assert_called_once_with(xlsx_blob_, package_) - xlsx_part_prop_.assert_called_with(xlsx_part_) - - def but_replaces_xlsx_blob_when_part_exists(self, update_blob_fixture): - chart_data, xlsx_blob_ = update_blob_fixture - chart_data.update_from_xlsx_blob(xlsx_blob_) - assert chart_data.xlsx_part.blob is xlsx_blob_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def add_part_fixture( - self, - request, - chart_part_, - xlsx_blob_, - EmbeddedXlsxPart_, - package_, - xlsx_part_, - xlsx_part_prop_, - ): - chartSpace_cxml = "c:chartSpace" - chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - xlsx_part_prop_.return_value = None - return ( - chart_data, - xlsx_blob_, - EmbeddedXlsxPart_, - package_, - xlsx_part_prop_, - xlsx_part_, + def it_can_get_the_chart_xlsx_part(self, chart_part_, xlsx_part_): + chart_part_.related_part.return_value = xlsx_part_ + chart_workbook = ChartWorkbook( + element("c:chartSpace/c:externalData{r:id=rId42}"), chart_part_ ) - @pytest.fixture - def update_blob_fixture(self, request, xlsx_blob_, xlsx_part_prop_): - chart_data = ChartWorkbook(None, None) - return chart_data, xlsx_blob_ + xlsx_part = chart_workbook.xlsx_part - @pytest.fixture( - params=[ - ("c:chartSpace", None), - ("c:chartSpace/c:externalData{r:id=rId42}", "rId42"), - ] - ) - def xlsx_part_get_fixture(self, request, chart_part_, xlsx_part_): - chartSpace_cxml, xlsx_part_rId = request.param - chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - expected_object = xlsx_part_ if xlsx_part_rId else None - return chart_data, expected_object + chart_part_.related_part.assert_called_once_with("rId42") + assert xlsx_part is xlsx_part_ + + def but_it_returns_None_when_the_chart_has_no_xlsx_part(self): + chart_workbook = ChartWorkbook(element("c:chartSpace"), None) + assert chart_workbook.xlsx_part is None - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + "chartSpace_cxml, expected_cxml", + ( ( "c:chartSpace{r:a=b}", "c:chartSpace{r:a=b}/c:externalData{r:id=rId" "42}/c:autoUpdate{val=0}", @@ -229,42 +99,54 @@ def xlsx_part_get_fixture(self, request, chart_part_, xlsx_part_): "c:chartSpace/c:externalData{r:id=rId66}", "c:chartSpace/c:externalData{r:id=rId42}", ), - ] + ), ) - def xlsx_part_set_fixture(self, request, chart_part_, xlsx_part_): - chartSpace_cxml, expected_cxml = request.param + def it_can_change_the_chart_xlsx_part( + self, chart_part_, xlsx_part_, chartSpace_cxml, expected_cxml + ): + chart_part_.relate_to.return_value = "rId42" chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - expected_xml = xml(expected_cxml) - return chart_data, xlsx_part_, expected_xml - # fixture components --------------------------------------------- + chart_data.xlsx_part = xlsx_part_ - @pytest.fixture - def chart_part_(self, request, package_, xlsx_part_): - chart_part_ = instance_mock(request, ChartPart) - chart_part_.package = package_ - chart_part_.related_parts = {"rId42": xlsx_part_} - chart_part_.relate_to.return_value = "rId42" - return chart_part_ + chart_part_.relate_to.assert_called_once_with(xlsx_part_, RT.PACKAGE) + assert chart_data._chartSpace.xml == xml(expected_cxml) - @pytest.fixture - def EmbeddedXlsxPart_(self, request, xlsx_part_): + def it_adds_an_xlsx_part_on_update_if_needed( + self, request, chart_part_, package_, xlsx_part_, xlsx_part_prop_ + ): EmbeddedXlsxPart_ = class_mock(request, "pptx.parts.chart.EmbeddedXlsxPart") EmbeddedXlsxPart_.new.return_value = xlsx_part_ - return EmbeddedXlsxPart_ + chart_part_.package = package_ + xlsx_part_prop_.return_value = None + chart_data = ChartWorkbook(element("c:chartSpace"), chart_part_) + + chart_data.update_from_xlsx_blob(b"xlsx-blob") + + EmbeddedXlsxPart_.new.assert_called_once_with(b"xlsx-blob", package_) + xlsx_part_prop_.assert_called_with(xlsx_part_) + + def but_it_replaces_the_xlsx_blob_when_the_part_exists(self, xlsx_part_prop_, xlsx_part_): + xlsx_part_prop_.return_value = xlsx_part_ + chart_data = ChartWorkbook(None, None) + chart_data.update_from_xlsx_blob(b"xlsx-blob") + + assert chart_data.xlsx_part.blob == b"xlsx-blob" + + # fixture components --------------------------------------------- @pytest.fixture - def package_(self, request): - return instance_mock(request, OpcPackage) + def chart_part_(self, request, package_, xlsx_part_): + return instance_mock(request, ChartPart) @pytest.fixture - def xlsx_blob_(self, request): - return instance_mock(request, bytes) + def package_(self, request): + return instance_mock(request, OpcPackage) @pytest.fixture def xlsx_part_(self, request): return instance_mock(request, EmbeddedXlsxPart) @pytest.fixture - def xlsx_part_prop_(self, request, xlsx_part_): + def xlsx_part_prop_(self, request): return property_mock(request, ChartWorkbook, "xlsx_part") diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 6026ac854..0983218e4 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -1,173 +1,160 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.coreprops module -""" +"""Unit-test suite for `pptx.parts.coreprops` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -import pytest +import datetime as dt -from datetime import datetime, timedelta +import pytest from pptx.opc.constants import CONTENT_TYPE as CT from pptx.oxml.coreprops import CT_CoreProperties from pptx.parts.coreprops import CorePropertiesPart -class DescribeCoreProperties(object): - def it_knows_the_string_property_values(self, str_prop_get_fixture): - core_properties, prop_name, expected_value = str_prop_get_fixture - actual_value = getattr(core_properties, prop_name) - assert actual_value == expected_value - - def it_can_change_the_string_property_values(self, str_prop_set_fixture): - core_properties, prop_name, value, expected_xml = str_prop_set_fixture - setattr(core_properties, prop_name, value) - assert core_properties._element.xml == expected_xml +class DescribeCorePropertiesPart(object): + """Unit-test suite for `pptx.parts.coreprops.CorePropertiesPart` objects.""" - 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", "expected_value"), + [ + ("author", "python-pptx"), + ("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", "Presentation"), + ("version", "1.2.88"), + ], + ) + def it_knows_the_string_property_values( + self, core_properties: CorePropertiesPart, prop_name: str, expected_value: str + ): + assert getattr(core_properties, prop_name) == expected_value + + @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_xml(None, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore - 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 - - def it_can_construct_a_default_core_props(self): - core_props = CorePropertiesPart.default() - # verify ----------------------- - assert isinstance(core_props, CorePropertiesPart) - assert core_props.content_type is CT.OPC_CORE_PROPERTIES - assert core_props.partname == "/docProps/core.xml" - assert isinstance(core_props._element, CT_CoreProperties) - assert core_props.title == "PowerPoint Presentation" - assert core_props.last_modified_by == "python-pptx" - assert core_props.revision == 1 - # core_props.modified only stores time with seconds resolution, so - # comparison needs to be a little loose (within two seconds) - modified_timedelta = datetime.utcnow() - core_props.modified - max_expected_timedelta = timedelta(seconds=2) - assert modified_timedelta < max_expected_timedelta - # fixtures ------------------------------------------------------- + assert core_properties._element.xml == self.coreProperties_xml(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_value"), + [ + ("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, + core_properties: CorePropertiesPart, + prop_name: str, + expected_value: dt.datetime | None, + ): + actual_datetime = getattr(core_properties, prop_name) + assert actual_datetime == expected_value - @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 = CorePropertiesPart.load(None, None, coreProperties, None) - expected_xml = self.coreProperties(tagname, str_val, attrs) - return core_properties, prop_name, value, expected_xml - - @pytest.fixture( - params=[ - ("author", "python-pptx"), - ("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", "Presentation"), - ("version", "1.2.88"), - ] - ) - def str_prop_get_fixture(self, request, core_properties): - prop_name, expected_value = request.param - return core_properties, prop_name, expected_value + 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_xml(None, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore - @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 str_prop_set_fixture(self, request): - prop_name, tagname, value = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) - expected_xml = self.coreProperties(tagname, value) - return core_properties, prop_name, value, expected_xml - - @pytest.fixture( - params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)] + setattr(core_properties, prop_name, value) + + assert core_properties._element.xml == self.coreProperties_xml(tagname, str_val, attrs) + + @pytest.mark.parametrize( + ("str_val", "expected_value"), + [("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)], ) - def revision_get_fixture(self, request): - str_val, expected_revision = request.param + def it_knows_the_revision_number(self, str_val: str | None, expected_value: int): tagname = "" if str_val is None else "cp:revision" - coreProperties = self.coreProperties(tagname, str_val) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) - return core_properties, expected_revision + coreProperties = self.coreProperties_xml(tagname, str_val) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore + + assert core_properties.revision == expected_value - @pytest.fixture(params=[(42, "42")]) - def revision_set_fixture(self, request): - value, str_val = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) - expected_xml = self.coreProperties("cp:revision", str_val) - return core_properties, value, expected_xml + def it_can_change_the_revision_number(self): + coreProperties = self.coreProperties_xml(None, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore + + core_properties.revision = 42 + + assert core_properties._element.xml == self.coreProperties_xml("cp:revision", "42") + + def it_can_construct_a_default_core_props(self): + core_props = CorePropertiesPart.default(None) # type: ignore + # verify ----------------------- + assert isinstance(core_props, CorePropertiesPart) + assert core_props.content_type is CT.OPC_CORE_PROPERTIES + assert core_props.partname == "/docProps/core.xml" + assert isinstance(core_props._element, CT_CoreProperties) + assert core_props.title == "PowerPoint Presentation" + assert core_props.last_modified_by == "python-pptx" + assert core_props.revision == 1 + assert core_props.modified is not None + # core_props.modified only stores time with seconds resolution, so + # comparison needs to be a little loose (within two seconds) + modified_timedelta = ( + dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) - core_props.modified + ) + max_expected_timedelta = dt.timedelta(seconds=2) + assert modified_timedelta < max_expected_timedelta - # fixture components --------------------------------------------- + # -- fixtures ---------------------------------------------------- - def coreProperties(self, tagname, str_val, attrs=""): + def coreProperties_xml(self, tagname: str | None, str_val: str | None, attrs: str = "") -> str: tmpl = ( '1.2.88\n" b"\n" ) - return CorePropertiesPart.load(None, None, xml, None) + return CorePropertiesPart.load(None, None, None, xml) # type: ignore diff --git a/tests/parts/test_embeddedpackage.py b/tests/parts/test_embeddedpackage.py index 3b4584175..ae2aca82f 100644 --- a/tests/parts/test_embeddedpackage.py +++ b/tests/parts/test_embeddedpackage.py @@ -1,6 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.parts.embeddedpackage` module.""" -"""Test suite for `pptx.parts.embeddedpackage` module.""" +from __future__ import annotations import pytest @@ -8,28 +8,28 @@ from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import OpcPackage, PackURI from pptx.parts.embeddedpackage import ( - EmbeddedPackagePart, EmbeddedDocxPart, + EmbeddedPackagePart, EmbeddedPptxPart, EmbeddedXlsxPart, ) -from ..unitutil.mock import ANY, class_mock, initializer_mock, instance_mock +from ..unitutil.mock import ANY, FixtureRequest, class_mock, initializer_mock, instance_mock class DescribeEmbeddedPackagePart(object): """Unit-test suite for `pptx.parts.embeddedpackage.EmbeddedPackagePart` objects.""" @pytest.mark.parametrize( - "prog_id, EmbeddedPartCls", - ( + ("prog_id", "EmbeddedPartCls"), + [ (PROG_ID.DOCX, EmbeddedDocxPart), (PROG_ID.PPTX, EmbeddedPptxPart), (PROG_ID.XLSX, EmbeddedXlsxPart), - ), + ], ) def it_provides_a_factory_that_creates_a_package_part_for_MS_Office_files( - self, request, prog_id, EmbeddedPartCls + self, request: FixtureRequest, prog_id: PROG_ID, EmbeddedPartCls: type ): object_blob_ = b"0123456789" package_ = instance_mock(request, OpcPackage) @@ -44,7 +44,7 @@ def it_provides_a_factory_that_creates_a_package_part_for_MS_Office_files( EmbeddedPartCls_.new.assert_called_once_with(object_blob_, package_) assert ole_object_part is embedded_object_part_ - def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request): + def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request: FixtureRequest): progId = "Foo.Bar.42" object_blob_ = b"0123456789" package_ = instance_mock(request, OpcPackage) @@ -54,15 +54,11 @@ def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request): ole_object_part = EmbeddedPackagePart.factory(progId, object_blob_, package_) - package_.next_partname.assert_called_once_with( - "/ppt/embeddings/oleObject%d.bin" - ) - _init_.assert_called_once_with( - ANY, partname_, CT.OFC_OLE_OBJECT, object_blob_, package_ - ) + package_.next_partname.assert_called_once_with("/ppt/embeddings/oleObject%d.bin") + _init_.assert_called_once_with(ANY, partname_, CT.OFC_OLE_OBJECT, package_, object_blob_) assert isinstance(ole_object_part, EmbeddedPackagePart) - def it_provides_a_contructor_classmethod_for_subclasses(self, request): + def it_provides_a_contructor_classmethod_for_subclasses(self, request: FixtureRequest): blob_ = b"0123456789" package_ = instance_mock(request, OpcPackage) _init_ = initializer_mock(request, EmbeddedXlsxPart, autospec=True) @@ -71,10 +67,8 @@ def it_provides_a_contructor_classmethod_for_subclasses(self, request): xlsx_part = EmbeddedXlsxPart.new(blob_, package_) - package_.next_partname.assert_called_once_with( - EmbeddedXlsxPart.partname_template - ) + package_.next_partname.assert_called_once_with(EmbeddedXlsxPart.partname_template) _init_.assert_called_once_with( - ANY, partname_, EmbeddedXlsxPart.content_type, blob_, package_ + xlsx_part, partname_, EmbeddedXlsxPart.content_type, package_, blob_ ) assert isinstance(xlsx_part, EmbeddedXlsxPart) diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 47ea1ae80..386e3fce9 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -1,12 +1,11 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.parts.image` module.""" -"""Unit test suite for pptx.parts.image module.""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +import io import pytest -from pptx.compat import BytesIO from pptx.package import Package from pptx.parts.image import Image, ImagePart from pptx.util import Emu @@ -20,7 +19,6 @@ property_mock, ) - images_pptx_path = absjoin(test_file_dir, "with_images.pptx") test_image_path = absjoin(test_file_dir, "python-icon.jpeg") @@ -29,90 +27,78 @@ class DescribeImagePart(object): - def it_can_construct_from_an_image_object(self, new_fixture): - package_, image_, _init_, partname_ = new_fixture + """Unit-test suite for `pptx.parts.image.ImagePart` objects.""" + + def it_can_construct_from_an_image_object(self, request, image_): + package_ = instance_mock(request, Package) + _init_ = initializer_mock(request, ImagePart) + partname_ = package_.next_image_partname.return_value image_part = ImagePart.new(package_, image_) package_.next_image_partname.assert_called_once_with(image_.ext) _init_.assert_called_once_with( - partname_, image_.content_type, image_.blob, package_, image_.filename + image_part, + partname_, + image_.content_type, + package_, + image_.blob, + image_.filename, ) assert isinstance(image_part, ImagePart) - def it_provides_access_to_its_image(self, image_fixture): - image_part, Image_, blob, desc, image_ = image_fixture - image = image_part.image - Image_.assert_called_once_with(blob, desc) - assert image is image_ - - def it_can_scale_its_dimensions(self, scale_fixture): - image_part, width, height, expected_values = scale_fixture - assert image_part.scale(width, height) == expected_values - - def it_knows_its_pixel_dimensions(self, size_fixture): - image, expected_size = size_fixture - assert image._px_size == expected_size + def it_provides_access_to_its_image(self, request, image_): + Image_ = class_mock(request, "pptx.parts.image.Image") + Image_.return_value = image_ + property_mock(request, ImagePart, "desc", return_value="foobar.png") + image_part = ImagePart(None, None, None, b"blob", None) - # fixtures ------------------------------------------------------- - - @pytest.fixture - def image_fixture(self, Image_, image_): - blob, filename = "blob", "foobar.png" - image_part = ImagePart(None, None, blob, None, filename) - return image_part, Image_, blob, filename, image_ + image = image_part.image - @pytest.fixture - def new_fixture(self, request, package_, image_, _init_): - partname_ = package_.next_image_partname.return_value - return package_, image_, _init_, partname_ + Image_.assert_called_once_with(b"blob", "foobar.png") + assert image is image_ - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + "width, height, expected_width, expected_height", + ( (None, None, Emu(2590800), Emu(2590800)), (1000, None, 1000, 1000), (None, 3000, 3000, 3000), (3337, 9999, 3337, 9999), - ] + ), ) - def scale_fixture(self, request): - width, height, expected_width, expected_height = request.param + def it_can_scale_its_dimensions(self, width, height, expected_width, expected_height): with open(test_image_path, "rb") as f: blob = f.read() - image = ImagePart(None, None, blob, None) - return image, width, height, (expected_width, expected_height) + image_part = ImagePart(None, None, None, blob) - @pytest.fixture - def size_fixture(self): + assert image_part.scale(width, height) == (expected_width, expected_height) + + def it_knows_its_pixel_dimensions_to_help(self): with open(test_image_path, "rb") as f: blob = f.read() - image = ImagePart(None, None, blob, None) - return image, (204, 204) + image_part = ImagePart(None, None, None, blob) - # fixture components --------------------------------------------- + assert image_part._px_size == (204, 204) - @pytest.fixture - def Image_(self, request, image_): - return class_mock(request, "pptx.parts.image.Image", return_value=image_) + # fixture components --------------------------------------------- @pytest.fixture def image_(self, request): return instance_mock(request, Image) - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, ImagePart) - @pytest.fixture - def package_(self, request): - return instance_mock(request, Package) +class DescribeImage(object): + """Unit-test suite for `pptx.parts.image.Image` objects.""" + def it_can_construct_from_a_path(self, from_blob_, image_): + with open(test_image_path, "rb") as f: + blob = f.read() + from_blob_.return_value = image_ -class DescribeImage(object): - def it_can_construct_from_a_path(self, from_path_fixture): - image_file, blob, filename, image_ = from_path_fixture - image = Image.from_file(image_file) - Image.from_blob.assert_called_once_with(blob, filename) + image = Image.from_file(test_image_path) + + Image.from_blob.assert_called_once_with(blob, "python-icon.jpeg") assert image is image_ def it_can_construct_from_a_stream(self, from_stream_fixture): @@ -121,10 +107,10 @@ def it_can_construct_from_a_stream(self, from_stream_fixture): Image.from_blob.assert_called_once_with(blob, None) assert image is image_ - def it_can_construct_from_a_blob(self, from_blob_fixture): - blob, filename = from_blob_fixture - image = Image.from_blob(blob, filename) - Image.__init__.assert_called_once_with(blob, filename) + def it_can_construct_from_a_blob(self, _init_): + image = Image.from_blob(b"blob", "foo.png") + + _init_.assert_called_once_with(image, b"blob", "foo.png") assert isinstance(image, Image) def it_knows_its_blob(self, blob_fixture): @@ -219,25 +205,11 @@ def filename_fixture(self, request): image = Image(None, filename) return image, filename - @pytest.fixture - def from_blob_fixture(self, _init_): - blob, filename = b"foobar", "foo.png" - return blob, filename - - @pytest.fixture - def from_path_fixture(self, from_blob_, image_): - image_file = test_image_path - with open(test_image_path, "rb") as f: - blob = f.read() - filename = "python-icon.jpeg" - from_blob_.return_value = image_ - return image_file, blob, filename, image_ - @pytest.fixture def from_stream_fixture(self, from_blob_, image_): with open(test_image_path, "rb") as f: blob = f.read() - image_file = BytesIO(blob) + image_file = io.BytesIO(blob) from_blob_.return_value = image_ return image_file, blob, image_ @@ -259,7 +231,7 @@ def _format_(self, request): @pytest.fixture def from_blob_(self, request): - return method_mock(request, Image, "from_blob") + return method_mock(request, Image, "from_blob", autospec=False) @pytest.fixture def image_(self, request): diff --git a/tests/parts/test_media.py b/tests/parts/test_media.py index 636e50ecb..f7095f35d 100644 --- a/tests/parts/test_media.py +++ b/tests/parts/test_media.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit test suite for `pptx.parts.media` module.""" -"""Unit test suite for pptx.parts.image module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import pytest +from __future__ import annotations from pptx.media import Video from pptx.package import Package @@ -14,47 +10,24 @@ class DescribeMediaPart(object): - def it_can_construct_from_a_media_object(self, new_fixture): - package_, media_, _init_, partname_ = new_fixture + """Unit-test suite for `pptx.parts.media.MediaPart` objects.""" + + def it_can_construct_from_a_media_object(self, request): + media_ = instance_mock(request, Video) + _init_ = initializer_mock(request, MediaPart) + package_ = instance_mock(request, Package) + package_.next_media_partname.return_value = "media42.mp4" + media_.blob, media_.content_type = b"blob-bytes", "video/mp4" media_part = MediaPart.new(package_, media_) package_.next_media_partname.assert_called_once_with(media_.ext) _init_.assert_called_once_with( - media_part, partname_, media_.content_type, media_.blob, package_ + media_part, "media42.mp4", media_.content_type, package_, media_.blob ) assert isinstance(media_part, MediaPart) - def it_knows_the_sha1_hash_of_the_media(self, sha1_fixture): - media_part, expected_value = sha1_fixture - sha1 = media_part.sha1 - assert sha1 == expected_value - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def new_fixture(self, request, package_, media_, _init_): - partname_ = package_.next_media_partname.return_value = "media42.mp4" - media_.blob, media_.content_type = b"blob-bytes", "video/mp4" - return package_, media_, _init_, partname_ - - @pytest.fixture - def sha1_fixture(self): - blob = b"blobish-bytes" - media_part = MediaPart(None, None, blob, None) - expected_value = "61efc464c21e54cfc1382fb5b6ef7512e141ceae" - return media_part, expected_value - - # fixture components --------------------------------------------- - - @pytest.fixture - def media_(self, request): - return instance_mock(request, Video) - - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, MediaPart, autospec=True) - - @pytest.fixture - def package_(self, request): - return instance_mock(request, Package) + def it_knows_the_sha1_hash_of_the_media(self): + assert MediaPart(None, None, None, b"blobish-bytes").sha1 == ( + "61efc464c21e54cfc1382fb5b6ef7512e141ceae" + ) diff --git a/tests/parts/test_presentation.py b/tests/parts/test_presentation.py index 8d6d9fda3..edde4c44c 100644 --- a/tests/parts/test_presentation.py +++ b/tests/parts/test_presentation.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.parts.presentation` module.""" -""" -Test suite for pptx.parts.presentation module -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -13,7 +9,7 @@ from pptx.package import Package from pptx.parts.coreprops import CorePropertiesPart from pptx.parts.presentation import PresentationPart -from pptx.parts.slide import NotesMasterPart, SlidePart +from pptx.parts.slide import NotesMasterPart, SlideMasterPart, SlidePart from pptx.presentation import Presentation from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster @@ -22,315 +18,204 @@ class DescribePresentationPart(object): - def it_provides_access_to_its_presentation(self, prs_fixture): - prs_part, Presentation_, prs_elm, prs_ = prs_fixture + """Unit-test suite for `pptx.parts.presentation.PresentationPart` objects.""" + + def it_provides_access_to_its_presentation(self, request): + prs_ = instance_mock(request, Presentation) + Presentation_ = class_mock( + request, "pptx.parts.presentation.Presentation", return_value=prs_ + ) + prs_elm = element("p:presentation") + prs_part = PresentationPart(None, None, None, prs_elm) + prs = prs_part.presentation + Presentation_.assert_called_once_with(prs_elm, prs_part) assert prs is prs_ - def it_provides_access_to_its_core_properties(self, core_props_fixture): - prs_part, core_properties_ = core_props_fixture - core_properties = prs_part.core_properties - assert core_properties is core_properties_ + def it_provides_access_to_its_core_properties(self, request, package_): + core_properties_ = instance_mock(request, CorePropertiesPart) + package_.core_properties = core_properties_ + prs_part = PresentationPart(None, None, package_, None) - def it_provides_access_to_the_notes_master_part(self, nmp_get_fixture): - """ - This is the first of a two-part test to cover the existing notes - master case. The notes master not-present case follows. + assert prs_part.core_properties is core_properties_ + + def it_provides_access_to_an_existing_notes_master_part( + self, notes_master_part_, part_related_by_ + ): + """This is the first of a two-part test to cover the existing notes master case. + + The notes master not-present case follows. """ - prs_part, notes_master_part_ = nmp_get_fixture + prs_part = PresentationPart(None, None, None, None) + part_related_by_.return_value = notes_master_part_ + notes_master_part = prs_part.notes_master_part + prs_part.part_related_by.assert_called_once_with(prs_part, RT.NOTES_MASTER) assert notes_master_part is notes_master_part_ - def it_adds_a_notes_master_part_when_needed(self, nmp_add_fixture): - """ - This is the second of a two-part test to cover the - notes-master-not-present case. The notes master present case is just - above. + def but_it_adds_a_notes_master_part_when_needed( + self, request, package_, notes_master_part_, part_related_by_, relate_to_ + ): + """This is the second of a two-part test to cover notes-master-not-present case. + + The notes master present case is just above. """ - prs_part, NotesMasterPart_ = nmp_add_fixture[:2] - package_, notes_master_part_ = nmp_add_fixture[2:] + NotesMasterPart_ = class_mock(request, "pptx.parts.presentation.NotesMasterPart") + NotesMasterPart_.create_default.return_value = notes_master_part_ + part_related_by_.side_effect = KeyError + prs_part = PresentationPart(None, None, package_, None) notes_master_part = prs_part.notes_master_part NotesMasterPart_.create_default.assert_called_once_with(package_) - prs_part.relate_to.assert_called_once_with( - prs_part, notes_master_part_, RT.NOTES_MASTER - ) + relate_to_.assert_called_once_with(prs_part, notes_master_part_, RT.NOTES_MASTER) assert notes_master_part is notes_master_part_ - def it_provides_access_to_its_notes_master(self, notes_master_fixture): - prs_part, notes_master_ = notes_master_fixture - notes_master = prs_part.notes_master - assert notes_master is notes_master_ - - def it_provides_access_to_a_related_slide(self, slide_fixture): - prs_part, rId, slide_ = slide_fixture - slide = prs_part.related_slide(rId) - prs_part.related_parts.__getitem__.assert_called_once_with(rId) - assert slide is slide_ + def it_provides_access_to_its_notes_master(self, request, notes_master_part_): + notes_master_ = instance_mock(request, NotesMaster) + property_mock( + request, + PresentationPart, + "notes_master_part", + return_value=notes_master_part_, + ) + notes_master_part_.notes_master = notes_master_ + prs_part = PresentationPart(None, None, None, None) - def it_provides_access_to_a_related_master(self, master_fixture): - prs_part, rId, slide_master_ = master_fixture - slide_master = prs_part.related_slide_master(rId) - prs_part.related_parts.__getitem__.assert_called_once_with(rId) - assert slide_master is slide_master_ + assert prs_part.notes_master is notes_master_ - def it_can_rename_related_slide_parts(self, rename_fixture): - prs_part, rIds, getitem_ = rename_fixture[:3] - calls, slide_parts, expected_names = rename_fixture[3:] - prs_part.rename_slide_parts(rIds) - assert getitem_.call_args_list == calls - assert [sp.partname for sp in slide_parts] == expected_names + def it_provides_access_to_a_related_slide(self, request, slide_, related_part_): + slide_part_ = instance_mock(request, SlidePart, slide=slide_) + related_part_.return_value = slide_part_ + prs_part = PresentationPart(None, None, None, None) - def it_can_save_the_package_to_a_file(self, save_fixture): - prs_part, file_, package_ = save_fixture - prs_part.save(file_) - package_.save.assert_called_once_with(file_) + slide = prs_part.related_slide("rId42") - def it_can_add_a_new_slide(self, add_slide_fixture): - prs_part, slide_layout_, SlidePart_, partname = add_slide_fixture[:4] - package_, slide_layout_part_, slide_part_ = add_slide_fixture[4:7] - rId_, slide_ = add_slide_fixture[7:] + related_part_.assert_called_once_with(prs_part, "rId42") + assert slide is slide_ - rId, slide = prs_part.add_slide(slide_layout_) + def it_provides_access_to_a_related_master(self, request, slide_master_, related_part_): + slide_master_part_ = instance_mock(request, SlideMasterPart, slide_master=slide_master_) + related_part_.return_value = slide_master_part_ + prs_part = PresentationPart(None, None, None, None) - SlidePart_.new.assert_called_once_with(partname, package_, slide_layout_part_) - prs_part.relate_to.assert_called_once_with(prs_part, slide_part_, RT.SLIDE) - assert rId is rId_ - assert slide is slide_ + slide_master = prs_part.related_slide_master("rId42") - def it_finds_the_slide_id_of_a_slide_part(self, slide_id_fixture): - prs_part, slide_part_, expected_value = slide_id_fixture - _slide_id = prs_part.slide_id(slide_part_) - assert _slide_id == expected_value + related_part_.assert_called_once_with(prs_part, "rId42") + assert slide_master is slide_master_ - def it_raises_on_slide_id_not_found(self, slide_id_raises_fixture): - prs_part, slide_part_ = slide_id_raises_fixture - with pytest.raises(ValueError): - prs_part.slide_id(slide_part_) + def it_can_rename_related_slide_parts(self, request, related_part_): + rIds = tuple("rId%d" % n for n in range(5, 0, -1)) + slide_parts = tuple(instance_mock(request, SlidePart) for _ in range(5)) + related_part_.side_effect = iter(slide_parts) + prs_part = PresentationPart(None, None, None, None) - def it_finds_a_slide_by_slide_id(self, get_slide_fixture): - prs_part, slide_id, expected_value = get_slide_fixture - slide = prs_part.get_slide(slide_id) - assert slide == expected_value + prs_part.rename_slide_parts(rIds) - def it_knows_the_next_slide_partname_to_help(self, next_fixture): - prs_part, partname = next_fixture - assert prs_part._next_slide_partname == partname + assert related_part_.call_args_list == [call(prs_part, rId) for rId in rIds] + assert [s.partname for s in slide_parts] == [ + PackURI("/ppt/slides/slide%d.xml" % (i + 1)) for i in range(len(rIds)) + ] - # fixtures ------------------------------------------------------- + def it_can_save_the_package_to_a_file(self, package_): + PresentationPart(None, None, package_, None).save("prs.pptx") + package_.save.assert_called_once_with("prs.pptx") - @pytest.fixture - def add_slide_fixture( - self, - package_, - slide_layout_, - SlidePart_, - slide_part_, - slide_, - _next_slide_partname_prop_, - relate_to_, - ): - prs_part = PresentationPart(None, None, None, package_) - partname = _next_slide_partname_prop_.return_value - rId_ = "rId42" + def it_can_add_a_new_slide(self, request, package_, slide_part_, slide_, relate_to_): + slide_layout_ = instance_mock(request, SlideLayout) + partname = PackURI("/ppt/slides/slide9.xml") + property_mock(request, PresentationPart, "_next_slide_partname", return_value=partname) + SlidePart_ = class_mock(request, "pptx.parts.presentation.SlidePart") SlidePart_.new.return_value = slide_part_ - relate_to_.return_value = rId_ + relate_to_.return_value = "rId42" slide_layout_part_ = slide_layout_.part slide_part_.slide = slide_ - return ( - prs_part, - slide_layout_, - SlidePart_, - partname, - package_, - slide_layout_part_, - slide_part_, - rId_, - slide_, - ) + prs_part = PresentationPart(None, None, package_, None) - @pytest.fixture - def core_props_fixture(self, package_, core_properties_): - prs_part = PresentationPart(None, None, None, package_) - package_.core_properties = core_properties_ - return prs_part, core_properties_ + rId, slide = prs_part.add_slide(slide_layout_) - @pytest.fixture(params=[True, False]) - def get_slide_fixture(self, request, slide_, slide_part_, related_parts_prop_): - is_present = request.param + SlidePart_.new.assert_called_once_with(partname, package_, slide_layout_part_) + prs_part.relate_to.assert_called_once_with(prs_part, slide_part_, RT.SLIDE) + assert rId == "rId42" + assert slide is slide_ + + def it_finds_the_slide_id_of_a_slide_part(self, slide_part_, related_part_): prs_elm = element( "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" "b,id=257},p:sldId{r:id=c,id=258})" ) - prs_part = PresentationPart(None, None, prs_elm) - slide_id = 257 if is_present else 666 - expected_value = slide_ if is_present else None - related_parts_prop_.return_value = {"a": None, "b": slide_part_, "c": None} - slide_part_.slide = slide_ - return prs_part, slide_id, expected_value + related_part_.side_effect = iter((None, slide_part_, None)) + prs_part = PresentationPart(None, None, None, prs_elm) - @pytest.fixture - def master_fixture(self, slide_master_, related_parts_prop_): - prs_part = PresentationPart(None, None, None, None) - rId = "rId42" - related_parts_ = related_parts_prop_.return_value - related_parts_.__getitem__.return_value.slide_master = slide_master_ - return prs_part, rId, slide_master_ - - @pytest.fixture - def next_fixture(self): - prs_elm = element("p:presentation/p:sldIdLst/(p:sldId,p:sldId)") - prs_part = PresentationPart(None, None, prs_elm) - partname = PackURI("/ppt/slides/slide3.xml") - return prs_part, partname - - @pytest.fixture - def notes_master_fixture( - self, notes_master_part_prop_, notes_master_part_, notes_master_ - ): - prs_part = PresentationPart(None, None, None, None) - notes_master_part_prop_.return_value = notes_master_part_ - notes_master_part_.notes_master = notes_master_ - return prs_part, notes_master_ - - @pytest.fixture - def nmp_add_fixture( - self, - package_, - NotesMasterPart_, - notes_master_part_, - part_related_by_, - relate_to_, - ): - prs_part = PresentationPart(None, None, None, package_) - part_related_by_.side_effect = KeyError - NotesMasterPart_.create_default.return_value = notes_master_part_ - return prs_part, NotesMasterPart_, package_, notes_master_part_ - - @pytest.fixture - def nmp_get_fixture(self, notes_master_part_, part_related_by_): - prs_part = PresentationPart(None, None, None, None) - part_related_by_.return_value = notes_master_part_ - return prs_part, notes_master_part_ - - @pytest.fixture - def prs_fixture(self, Presentation_, prs_): - prs_elm = element("p:presentation") - prs_part = PresentationPart(None, None, prs_elm) - return prs_part, Presentation_, prs_elm, prs_ + _slide_id = prs_part.slide_id(slide_part_) - @pytest.fixture - def rename_fixture(self, related_parts_prop_): - prs_part = PresentationPart(None, None, None) - rIds = ("rId1", "rId2") - getitem_ = related_parts_prop_.return_value.__getitem__ - calls = [call("rId1"), call("rId2")] - slide_parts = [SlidePart(None, None, None), SlidePart(None, None, None)] - expected_names = [ - PackURI("/ppt/slides/slide1.xml"), - PackURI("/ppt/slides/slide2.xml"), + assert related_part_.call_args_list == [ + call(prs_part, "a"), + call(prs_part, "b"), ] - getitem_.side_effect = slide_parts - return (prs_part, rIds, getitem_, calls, slide_parts, expected_names) - - @pytest.fixture - def save_fixture(self, package_): - prs_part = PresentationPart(None, None, None, package_) - file_ = "foobar.docx" - return prs_part, file_, package_ - - @pytest.fixture - def slide_fixture(self, slide_, related_parts_prop_): - prs_part = PresentationPart(None, None, None, None) - rId = "rId42" - related_parts_ = related_parts_prop_.return_value - related_parts_.__getitem__.return_value.slide = slide_ - return prs_part, rId, slide_ + assert _slide_id == 257 - @pytest.fixture - def slide_id_fixture(self, slide_part_, related_parts_prop_): + def it_raises_on_slide_id_not_found(self, slide_part_, related_part_): prs_elm = element( "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" "b,id=257},p:sldId{r:id=c,id=258})" ) - prs_part = PresentationPart(None, None, prs_elm) - expected_value = 257 - related_parts_prop_.return_value = {"a": None, "b": slide_part_, "c": None} - return prs_part, slide_part_, expected_value + related_part_.return_value = "not the slide you're looking for" + prs_part = PresentationPart(None, None, None, prs_elm) - @pytest.fixture - def slide_id_raises_fixture(self, slide_part_, related_parts_prop_): + with pytest.raises(ValueError): + prs_part.slide_id(slide_part_) + + @pytest.mark.parametrize("is_present", (True, False)) + def it_finds_a_slide_by_slide_id(self, is_present, slide_, slide_part_, related_part_): prs_elm = element( "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" "b,id=257},p:sldId{r:id=c,id=258})" ) - prs_part = PresentationPart(None, None, prs_elm) - related_parts_prop_.return_value = {"a": None, "b": None, "c": None} - return prs_part, slide_part_ + slide_id = 257 if is_present else 666 + expected_value = slide_ if is_present else None + related_part_.return_value = slide_part_ + slide_part_.slide = slide_ + prs_part = PresentationPart(None, None, None, prs_elm) - # fixture components --------------------------------------------- + slide = prs_part.get_slide(slide_id) - @pytest.fixture - def core_properties_(self, request): - return instance_mock(request, CorePropertiesPart) + assert slide == expected_value - @pytest.fixture - def _next_slide_partname_prop_(self, request): - return property_mock(request, PresentationPart, "_next_slide_partname") + def it_knows_the_next_slide_partname_to_help(self): + prs_elm = element("p:presentation/p:sldIdLst/(p:sldId,p:sldId)") + prs_part = PresentationPart(None, None, None, prs_elm) - @pytest.fixture - def NotesMasterPart_(self, request, prs_): - return class_mock(request, "pptx.parts.presentation.NotesMasterPart") + assert prs_part._next_slide_partname == PackURI("/ppt/slides/slide3.xml") - @pytest.fixture - def notes_master_(self, request): - return instance_mock(request, NotesMaster) + # fixture components --------------------------------------------- @pytest.fixture def notes_master_part_(self, request): return instance_mock(request, NotesMasterPart) - @pytest.fixture - def notes_master_part_prop_(self, request, notes_master_part_): - return property_mock(request, PresentationPart, "notes_master_part") - @pytest.fixture def package_(self, request): return instance_mock(request, Package) @pytest.fixture def part_related_by_(self, request): - return method_mock(request, PresentationPart, "part_related_by", autospec=True) - - @pytest.fixture - def Presentation_(self, request, prs_): - return class_mock( - request, "pptx.parts.presentation.Presentation", return_value=prs_ - ) - - @pytest.fixture - def prs_(self, request): - return instance_mock(request, Presentation) + return method_mock(request, PresentationPart, "part_related_by") @pytest.fixture def relate_to_(self, request): - return method_mock(request, PresentationPart, "relate_to", autospec=True) + return method_mock(request, PresentationPart, "relate_to") @pytest.fixture - def related_parts_prop_(self, request): - return property_mock(request, PresentationPart, "related_parts") + def related_part_(self, request): + return method_mock(request, PresentationPart, "related_part") @pytest.fixture def slide_(self, request): return instance_mock(request, Slide) - @pytest.fixture - def slide_layout_(self, request): - return instance_mock(request, SlideLayout) - @pytest.fixture def slide_master_(self, request): return instance_mock(request, SlideMaster) @@ -338,7 +223,3 @@ def slide_master_(self, request): @pytest.fixture def slide_part_(self, request): return instance_mock(request, SlidePart) - - @pytest.fixture - def SlidePart_(self, request): - return class_mock(request, "pptx.parts.presentation.SlidePart") diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index aaf11e5a7..9eb2f11b0 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -1,14 +1,15 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.slide` module.""" +from __future__ import annotations + import pytest from pptx.chart.data import ChartData -from pptx.enum.base import EnumValue +from pptx.enum.chart import XL_CHART_TYPE as XCT from pptx.enum.shapes import PROG_ID from pptx.media import Video -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import Part from pptx.opc.packuri import PackURI from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide @@ -37,85 +38,67 @@ initializer_mock, instance_mock, method_mock, - property_mock, ) class DescribeBaseSlidePart(object): - def it_knows_its_name(self, name_fixture): - base_slide, expected_value = name_fixture - assert base_slide.name == expected_value + """Unit-test suite for `pptx.parts.slide.BaseSlidePart` objects.""" - def it_can_get_a_related_image_by_rId(self, get_image_fixture): - slide, rId, image_ = get_image_fixture - assert slide.get_image(rId) is image_ + def it_knows_its_name(self): + slide_part = BaseSlidePart(None, None, None, element("p:sld/p:cSld{name=Foobar}")) + assert slide_part.name == "Foobar" - def it_can_add_an_image_part(self, image_part_fixture): - slide, image_file, image_part_, rId_ = image_part_fixture + def it_can_get_a_related_image_by_rId(self, request, image_part_): + image_ = instance_mock(request, Image) + image_part_.image = image_ + related_part_ = method_mock( + request, BaseSlidePart, "related_part", return_value=image_part_ + ) + slide_part = BaseSlidePart(None, None, None, None) - image_part, rId = slide.get_or_add_image_part(image_file) + image = slide_part.get_image("rId42") - slide._package.get_or_add_image_part.assert_called_once_with(image_file) - slide.relate_to.assert_called_once_with(image_part_, RT.IMAGE) - assert image_part is image_part_ - assert rId is rId_ + related_part_.assert_called_once_with(slide_part, "rId42") + assert image is image_ - # fixtures ------------------------------------------------------- - - @pytest.fixture - def get_image_fixture(self, related_parts_prop_, image_part_, image_): - slide = BaseSlidePart(None, None, None, None) - rId = "rId42" - related_parts_prop_.return_value = {rId: image_part_} - image_part_.image = image_ - return slide, rId, image_ - - @pytest.fixture - def image_part_fixture(self, partname_, package_, image_part_, relate_to_): - slide = BaseSlidePart(partname_, None, None, package_) - image_file, rId = "foobar.png", "rId6" + def it_can_add_an_image_part(self, request, image_part_): + package_ = instance_mock(request, Package) package_.get_or_add_image_part.return_value = image_part_ - relate_to_.return_value = rId - return slide, image_file, image_part_, rId + relate_to_ = method_mock(request, BaseSlidePart, "relate_to", return_value="rId6") + slide_part = BaseSlidePart(None, None, package_, None) - @pytest.fixture - def name_fixture(self): - sld_cxml, expected_value = "p:sld/p:cSld{name=Foobar}", "Foobar" - sld = element(sld_cxml) - base_slide = BaseSlidePart(None, None, sld, None) - return base_slide, expected_value + image_part, rId = slide_part.get_or_add_image_part("foobar.png") - # fixture components --------------------------------------------- + package_.get_or_add_image_part.assert_called_once_with("foobar.png") + relate_to_.assert_called_once_with(slide_part, image_part_, RT.IMAGE) + assert image_part is image_part_ + assert rId == "rId6" - @pytest.fixture - def image_(self, request): - return instance_mock(request, Image) + # fixture components --------------------------------------------- @pytest.fixture def image_part_(self, request): return instance_mock(request, ImagePart) - @pytest.fixture - def package_(self, request): - return instance_mock(request, Package) - - @pytest.fixture - def partname_(self): - return PackURI("/foo/bar.xml") - - @pytest.fixture - def relate_to_(self, request): - return method_mock(request, BaseSlidePart, "relate_to") - - @pytest.fixture - def related_parts_prop_(self, request): - return property_mock(request, BaseSlidePart, "related_parts") - class DescribeNotesMasterPart(object): - def it_can_create_a_notes_master_part(self, create_fixture): - package_, theme_part_, notes_master_part_ = create_fixture + """Unit-test suite for `pptx.parts.slide.NotesMasterPart` objects.""" + def it_can_create_a_notes_master_part(self, request, package_, notes_master_part_, theme_part_): + method_mock( + request, + NotesMasterPart, + "_new", + autospec=False, + return_value=notes_master_part_, + ) + method_mock( + request, + NotesMasterPart, + "_new_theme_part", + autospec=False, + return_value=theme_part_, + ) notes_master_part = NotesMasterPart.create_default(package_) NotesMasterPart._new.assert_called_once_with(package_) @@ -123,108 +106,66 @@ def it_can_create_a_notes_master_part(self, create_fixture): notes_master_part.relate_to.assert_called_once_with(theme_part_, RT.THEME) assert notes_master_part is notes_master_part_ - def it_provides_access_to_its_notes_master(self, notes_master_fixture): - notes_master_part, NotesMaster_ = notes_master_fixture[:2] - notesMaster, notes_master_ = notes_master_fixture[2:] + def it_provides_access_to_its_notes_master(self, request): + notes_master_ = instance_mock(request, NotesMaster) + NotesMaster_ = class_mock( + request, "pptx.parts.slide.NotesMaster", return_value=notes_master_ + ) + notesMaster = element("p:notesMaster") + notes_master_part = NotesMasterPart(None, None, None, notesMaster) notes_master = notes_master_part.notes_master NotesMaster_.assert_called_once_with(notesMaster, notes_master_part) assert notes_master is notes_master_ - def it_creates_a_new_notes_master_part_to_help(self, new_fixture): - package_, NotesMasterPart_, partname = new_fixture[:3] - notesMaster_, notes_master_part_ = new_fixture[3:] + def it_creates_a_new_notes_master_part_to_help(self, request, package_, notes_master_part_): + NotesMasterPart_ = class_mock( + request, "pptx.parts.slide.NotesMasterPart", return_value=notes_master_part_ + ) + notesMaster = element("p:notesMaster") + method_mock( + request, + CT_NotesMaster, + "new_default", + autospec=False, + return_value=notesMaster, + ) notes_master_part = NotesMasterPart._new(package_) CT_NotesMaster.new_default.assert_called_once_with() NotesMasterPart_.assert_called_once_with( - partname, CT.PML_NOTES_MASTER, notesMaster_, package_ + PackURI("/ppt/notesMasters/notesMaster1.xml"), + CT.PML_NOTES_MASTER, + package_, + notesMaster, ) assert notes_master_part is notes_master_part_ - def it_creates_a_new_theme_part_to_help(self, theme_fixture): - package_, pn_tmpl, XmlPart_, partname = theme_fixture[:4] - theme_elm_, theme_part_ = theme_fixture[4:] + def it_creates_a_new_theme_part_to_help(self, request, package_, theme_part_): + XmlPart_ = class_mock(request, "pptx.parts.slide.XmlPart", return_value=theme_part_) + theme_elm = element("p:theme") + method_mock( + request, + CT_OfficeStyleSheet, + "new_default", + autospec=False, + return_value=theme_elm, + ) + pn_tmpl = "/ppt/theme/theme%d.xml" + partname = PackURI("/ppt/theme/theme2.xml") + package_.next_partname.return_value = partname theme_part = NotesMasterPart._new_theme_part(package_) package_.next_partname.assert_called_once_with(pn_tmpl) CT_OfficeStyleSheet.new_default.assert_called_once_with() - XmlPart_.assert_called_once_with(partname, CT.OFC_THEME, theme_elm_, package_) + XmlPart_.assert_called_once_with(partname, CT.OFC_THEME, package_, theme_elm) assert theme_part is theme_part_ - # fixtures ------------------------------------------------------- - - @pytest.fixture - def create_fixture( - self, package_, theme_part_, notes_master_part_, _new_, _new_theme_part_ - ): - return package_, theme_part_, notes_master_part_ - - @pytest.fixture - def new_fixture( - self, package_, NotesMasterPart_, notesMaster_, notes_master_part_, new_default_ - ): - partname = PackURI("/ppt/notesMasters/notesMaster1.xml") - return (package_, NotesMasterPart_, partname, notesMaster_, notes_master_part_) - - @pytest.fixture - def notes_master_fixture(self, NotesMaster_, notes_master_): - notesMaster = element("p:notesMaster") - notes_master_part = NotesMasterPart(None, None, notesMaster, None) - return notes_master_part, NotesMaster_, notesMaster, notes_master_ - - @pytest.fixture - def theme_fixture( - self, package_, XmlPart_, theme_elm_, theme_part_, theme_new_default_ - ): - pn_tmpl = "/ppt/theme/theme%d.xml" - partname = PackURI("/ppt/theme/theme2.xml") - package_.next_partname.return_value = partname - return (package_, pn_tmpl, XmlPart_, partname, theme_elm_, theme_part_) - # fixture components --------------------------------------------- - @pytest.fixture - def _new_(self, request, notes_master_part_): - return method_mock( - request, NotesMasterPart, "_new", return_value=notes_master_part_ - ) - - @pytest.fixture - def new_default_(self, request, notesMaster_): - return method_mock( - request, CT_NotesMaster, "new_default", return_value=notesMaster_ - ) - - @pytest.fixture - def _new_theme_part_(self, request, theme_part_): - return method_mock( - request, NotesMasterPart, "_new_theme_part", return_value=theme_part_ - ) - - @pytest.fixture - def NotesMaster_(self, request, notes_master_): - return class_mock( - request, "pptx.parts.slide.NotesMaster", return_value=notes_master_ - ) - - @pytest.fixture - def NotesMasterPart_(self, request, notes_master_part_): - return class_mock( - request, "pptx.parts.slide.NotesMasterPart", return_value=notes_master_part_ - ) - - @pytest.fixture - def notesMaster_(self, request): - return instance_mock(request, CT_NotesMaster) - - @pytest.fixture - def notes_master_(self, request): - return instance_mock(request, NotesMaster) - @pytest.fixture def notes_master_part_(self, request): return instance_mock(request, NotesMasterPart) @@ -233,152 +174,95 @@ def notes_master_part_(self, request): def package_(self, request): return instance_mock(request, Package) - @pytest.fixture - def theme_elm_(self, request): - return instance_mock(request, CT_OfficeStyleSheet) - - @pytest.fixture - def theme_new_default_(self, request, theme_elm_): - return method_mock( - request, CT_OfficeStyleSheet, "new_default", return_value=theme_elm_ - ) - @pytest.fixture def theme_part_(self, request): return instance_mock(request, Part) - @pytest.fixture - def XmlPart_(self, request, theme_part_): - return class_mock(request, "pptx.parts.slide.XmlPart", return_value=theme_part_) - class DescribeNotesSlidePart(object): - def it_can_create_a_notes_slide_part(self, new_fixture): - package_, slide_part_, notes_master_part_ = new_fixture[:3] - notes_slide_, notes_master_, notes_slide_part_ = new_fixture[3:] + """Unit-test suite for `pptx.parts.slide.NotesSlidePart` objects.""" + + def it_can_create_a_notes_slide_part( + self, + request, + package_, + slide_part_, + notes_master_part_, + notes_slide_, + notes_master_, + notes_slide_part_, + ): + presentation_part_ = instance_mock(request, PresentationPart) + package_.presentation_part = presentation_part_ + presentation_part_.notes_master_part = notes_master_part_ + _add_notes_slide_part_ = method_mock( + request, + NotesSlidePart, + "_add_notes_slide_part", + autospec=False, + return_value=notes_slide_part_, + ) + notes_slide_part_.notes_slide = notes_slide_ + notes_master_part_.notes_master = notes_master_ notes_slide_part = NotesSlidePart.new(package_, slide_part_) - NotesSlidePart._add_notes_slide_part.assert_called_once_with( - package_, slide_part_, notes_master_part_ - ) + _add_notes_slide_part_.assert_called_once_with(package_, slide_part_, notes_master_part_) notes_slide_.clone_master_placeholders.assert_called_once_with(notes_master_) assert notes_slide_part is notes_slide_part_ - def it_provides_access_to_the_notes_master(self, notes_master_fixture): - notes_slide_part, notes_master_ = notes_master_fixture - notes_master = notes_slide_part.notes_master - notes_slide_part.part_related_by.assert_called_once_with( - notes_slide_part, RT.NOTES_MASTER + def it_provides_access_to_the_notes_master(self, request, notes_master_, notes_master_part_): + part_related_by_ = method_mock( + request, NotesSlidePart, "part_related_by", return_value=notes_master_part_ ) + notes_slide_part = NotesSlidePart(None, None, None, None) + notes_master_part_.notes_master = notes_master_ + + notes_master = notes_slide_part.notes_master + + part_related_by_.assert_called_once_with(notes_slide_part, RT.NOTES_MASTER) assert notes_master is notes_master_ - def it_provides_access_to_its_notes_slide(self, notes_slide_fixture): - notes_slide_part, NotesSlide_, notes, notes_slide_ = notes_slide_fixture + def it_provides_access_to_its_notes_slide(self, request, notes_slide_): + NotesSlide_ = class_mock(request, "pptx.parts.slide.NotesSlide", return_value=notes_slide_) + notes = element("p:notes") + notes_slide_part = NotesSlidePart(None, None, None, notes) + notes_slide = notes_slide_part.notes_slide + NotesSlide_.assert_called_once_with(notes, notes_slide_part) assert notes_slide is notes_slide_ - def it_adds_a_notes_slide_part_to_help(self, add_fixture): - package_, slide_part_, notes_master_part_ = add_fixture[:3] - notes_slide_part_, NotesSlidePart_, partname = add_fixture[3:6] - content_type, notes, calls = add_fixture[6:] + def it_adds_a_notes_slide_part_to_help( + self, request, package_, slide_part_, notes_master_part_, notes_slide_part_ + ): + NotesSlidePart_ = class_mock( + request, "pptx.parts.slide.NotesSlidePart", return_value=notes_slide_part_ + ) + notes = element("p:notes") + new_ = method_mock(request, CT_NotesSlide, "new", autospec=False, return_value=notes) + package_.next_partname.return_value = PackURI("/ppt/notesSlides/notesSlide42.xml") notes_slide_part = NotesSlidePart._add_notes_slide_part( package_, slide_part_, notes_master_part_ ) - package_.next_partname.assert_called_once_with( - "/ppt/notesSlides/notesSlide%d.xml" - ) - CT_NotesSlide.new.assert_called_once_with() - NotesSlidePart_.assert_called_once_with(partname, content_type, notes, package_) - assert notes_slide_part_.relate_to.call_args_list == calls - assert notes_slide_part is notes_slide_part_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def add_fixture( - self, - package_, - slide_part_, - notes_master_part_, - notes_slide_part_, - NotesSlidePart_, - new_, - ): - partname = PackURI("/ppt/notesSlides/notesSlide42.xml") - content_type = CT.PML_NOTES_SLIDE - notes = element("p:notes") - calls = [call(notes_master_part_, RT.NOTES_MASTER), call(slide_part_, RT.SLIDE)] - package_.next_partname.return_value = partname - new_.return_value = notes - return ( + package_.next_partname.assert_called_once_with("/ppt/notesSlides/notesSlide%d.xml") + new_.assert_called_once_with() + NotesSlidePart_.assert_called_once_with( + PackURI("/ppt/notesSlides/notesSlide42.xml"), + CT.PML_NOTES_SLIDE, package_, - slide_part_, - notes_master_part_, - notes_slide_part_, - NotesSlidePart_, - partname, - content_type, notes, - calls, ) - - @pytest.fixture - def new_fixture( - self, - package_, - slide_part_, - notes_master_part_, - notes_slide_, - notes_master_, - notes_slide_part_, - _add_notes_slide_part_, - presentation_part_, - ): - package_.presentation_part = presentation_part_ - presentation_part_.notes_master_part = notes_master_part_ - notes_slide_part_.notes_slide = notes_slide_ - notes_master_part_.notes_master = notes_master_ - return ( - package_, - slide_part_, - notes_master_part_, - notes_slide_, - notes_master_, - notes_slide_part_, - ) - - @pytest.fixture - def notes_master_fixture(self, notes_master_, part_related_by_, notes_master_part_): - notes_slide_part = NotesSlidePart(None, None, None, None) - part_related_by_.return_value = notes_master_part_ - notes_master_part_.notes_master = notes_master_ - return notes_slide_part, notes_master_ - - @pytest.fixture - def notes_slide_fixture(self, NotesSlide_, notes_slide_): - notes = element("p:notes") - notes_slide_part = NotesSlidePart(None, None, notes, None) - return notes_slide_part, NotesSlide_, notes, notes_slide_ + assert notes_slide_part_.relate_to.call_args_list == [ + call(notes_master_part_, RT.NOTES_MASTER), + call(slide_part_, RT.SLIDE), + ] + assert notes_slide_part is notes_slide_part_ # fixture components --------------------------------------------- - @pytest.fixture - def _add_notes_slide_part_(self, request, notes_slide_part_): - return method_mock( - request, - NotesSlidePart, - "_add_notes_slide_part", - return_value=notes_slide_part_, - ) - - @pytest.fixture - def new_(self, request): - return method_mock(request, CT_NotesSlide, "new") - @pytest.fixture def notes_master_(self, request): return instance_mock(request, NotesMaster) @@ -387,34 +271,14 @@ def notes_master_(self, request): def notes_master_part_(self, request): return instance_mock(request, NotesMasterPart) - @pytest.fixture - def NotesSlide_(self, request, notes_slide_): - return class_mock( - request, "pptx.parts.slide.NotesSlide", return_value=notes_slide_ - ) - @pytest.fixture def notes_slide_(self, request): return instance_mock(request, NotesSlide) - @pytest.fixture - def NotesSlidePart_(self, request, notes_slide_part_): - return class_mock( - request, "pptx.parts.slide.NotesSlidePart", return_value=notes_slide_part_ - ) - @pytest.fixture def notes_slide_part_(self, request): return instance_mock(request, NotesSlidePart) - @pytest.fixture - def part_related_by_(self, request): - return method_mock(request, NotesSlidePart, "part_related_by", autospec=True) - - @pytest.fixture - def presentation_part_(self, request): - return instance_mock(request, PresentationPart) - @pytest.fixture def package_(self, request): return instance_mock(request, Package) @@ -440,19 +304,18 @@ def it_knows_whether_it_has_a_notes_slide(self, has_notes_slide_fixture): assert value is expected_value def it_can_add_a_chart_part(self, request, package_, relate_to_): + chart_data_ = instance_mock(request, ChartData) chart_part_ = instance_mock(request, ChartPart) ChartPart_ = class_mock(request, "pptx.parts.slide.ChartPart") ChartPart_.new.return_value = chart_part_ - chart_type_ = instance_mock(request, EnumValue) - chart_data_ = instance_mock(request, ChartData) relate_to_.return_value = "rId42" - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) - _rId = slide_part.add_chart_part(chart_type_, chart_data_) + rId = slide_part.add_chart_part(XCT.RADAR, chart_data_) - ChartPart_.new.assert_called_once_with(chart_type_, chart_data_, package_) + ChartPart_.new.assert_called_once_with(XCT.RADAR, chart_data_, package_) relate_to_.assert_called_once_with(slide_part, chart_part_, RT.CHART) - assert _rId == "rId42" + assert rId == "rId42" @pytest.mark.parametrize( "prog_id, rel_type", @@ -467,22 +330,18 @@ def it_can_add_an_embedded_ole_object_part( self, request, package_, relate_to_, prog_id, rel_type ): _blob_from_file_ = method_mock( - request, SlidePart, "_blob_from_file", autospec=True, return_value=b"012345" + request, SlidePart, "_blob_from_file", return_value=b"012345" ) embedded_package_part_ = instance_mock(request, EmbeddedPackagePart) - EmbeddedPackagePart_ = class_mock( - request, "pptx.parts.slide.EmbeddedPackagePart" - ) + EmbeddedPackagePart_ = class_mock(request, "pptx.parts.slide.EmbeddedPackagePart") EmbeddedPackagePart_.factory.return_value = embedded_package_part_ relate_to_.return_value = "rId9" - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) _rId = slide_part.add_embedded_ole_object_part(prog_id, "workbook.xlsx") _blob_from_file_.assert_called_once_with(slide_part, "workbook.xlsx") - EmbeddedPackagePart_.factory.assert_called_once_with( - prog_id, b"012345", package_ - ) + EmbeddedPackagePart_.factory.assert_called_once_with(prog_id, b"012345", package_) relate_to_.assert_called_once_with(slide_part, embedded_package_part_, rel_type) assert _rId == "rId9" @@ -490,7 +349,7 @@ def it_can_get_or_add_a_video_part(self, package_, video_, relate_to_, media_par media_rId, video_rId = "rId1", "rId2" package_.get_or_add_media_part.return_value = media_part_ relate_to_.side_effect = [media_rId, video_rId] - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) result = slide_part.get_or_add_video_media_part(video_) @@ -503,14 +362,14 @@ def it_can_get_or_add_a_video_part(self, package_, video_, relate_to_, media_par def it_can_create_a_new_slide_part(self, request, package_, relate_to_): partname = PackURI("/foobar.xml") - SlidePart_init_ = initializer_mock(request, SlidePart) + _init_ = initializer_mock(request, SlidePart) slide_layout_part_ = instance_mock(request, SlideLayoutPart) CT_Slide_ = class_mock(request, "pptx.parts.slide.CT_Slide") CT_Slide_.new.return_value = sld = element("c:sld") slide_part = SlidePart.new(partname, package_, slide_layout_part_) - SlidePart_init_.assert_called_once_with(partname, CT.PML_SLIDE, sld, package_) + _init_.assert_called_once_with(slide_part, partname, CT.PML_SLIDE, package_, sld) slide_part.relate_to.assert_called_once_with( slide_part, slide_layout_part_, RT.SLIDE_LAYOUT ) @@ -548,7 +407,7 @@ def it_adds_a_notes_slide_part_to_help( self, package_, NotesSlidePart_, notes_slide_part_, relate_to_ ): NotesSlidePart_.new.return_value = notes_slide_part_ - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) notes_slide_part = slide_part._add_notes_slide_part() @@ -594,12 +453,12 @@ def notes_slide_fixture( @pytest.fixture def slide_fixture(self, Slide_, slide_): sld = element("p:sld") - slide_part = SlidePart(None, None, sld, None) + slide_part = SlidePart(None, None, None, sld) return slide_part, Slide_, sld, slide_ @pytest.fixture def slide_id_fixture(self, package_, presentation_part_): - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) slide_id = 256 package_.presentation_part = presentation_part_ presentation_part_.slide_id.return_value = slide_id @@ -669,104 +528,60 @@ def video_(self, request): class DescribeSlideLayoutPart(object): - def it_provides_access_to_its_slide_master(self, master_fixture): - slide_layout_part, part_related_by_, slide_master_ = master_fixture - slide_master = slide_layout_part.slide_master - part_related_by_.assert_called_once_with(slide_layout_part, RT.SLIDE_MASTER) - assert slide_master is slide_master_ + """Unit-test suite for `pptx.parts.slide.SlideLayoutPart` objects.""" - def it_provides_access_to_its_slide_layout(self, layout_fixture): - slide_layout_part, SlideLayout_ = layout_fixture[:2] - sldLayout, slide_layout_ = layout_fixture[2:] - slide_layout = slide_layout_part.slide_layout - SlideLayout_.assert_called_once_with(sldLayout, slide_layout_part) - assert slide_layout is slide_layout_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def layout_fixture(self, SlideLayout_, slide_layout_): - sldLayout = element("p:sldLayout") - slide_layout_part = SlideLayoutPart(None, None, sldLayout) - return slide_layout_part, SlideLayout_, sldLayout, slide_layout_ - - @pytest.fixture - def master_fixture(self, part_related_by_, slide_master_, slide_master_part_): + def it_provides_access_to_its_slide_master(self, request): + slide_master_ = instance_mock(request, SlideMaster) + slide_master_part_ = instance_mock(request, SlideMasterPart, slide_master=slide_master_) + part_related_by_ = method_mock( + request, SlideLayoutPart, "part_related_by", return_value=slide_master_part_ + ) slide_layout_part = SlideLayoutPart(None, None, None, None) - part_related_by_.return_value = slide_master_part_ - slide_master_part_.slide_master = slide_master_ - return slide_layout_part, part_related_by_, slide_master_ - # fixture components ----------------------------------- + slide_master = slide_layout_part.slide_master - @pytest.fixture - def part_related_by_(self, request): - return method_mock(request, SlideLayoutPart, "part_related_by", autospec=True) + part_related_by_.assert_called_once_with(slide_layout_part, RT.SLIDE_MASTER) + assert slide_master is slide_master_ - @pytest.fixture - def SlideLayout_(self, request, slide_layout_): - return class_mock( + def it_provides_access_to_its_slide_layout(self, request): + slide_layout_ = instance_mock(request, SlideLayout) + SlideLayout_ = class_mock( request, "pptx.parts.slide.SlideLayout", return_value=slide_layout_ ) + sldLayout = element("p:sldLayout") + slide_layout_part = SlideLayoutPart(None, None, None, sldLayout) - @pytest.fixture - def slide_layout_(self, request): - return instance_mock(request, SlideLayout) - - @pytest.fixture - def slide_master_(self, request): - return instance_mock(request, SlideMaster) + slide_layout = slide_layout_part.slide_layout - @pytest.fixture - def slide_master_part_(self, request): - return instance_mock(request, SlideMasterPart) + SlideLayout_.assert_called_once_with(sldLayout, slide_layout_part) + assert slide_layout is slide_layout_ class DescribeSlideMasterPart(object): - def it_provides_access_to_its_slide_master(self, master_fixture): - slide_master_part, SlideMaster_, sldMaster, slide_master_ = master_fixture - slide_master = slide_master_part.slide_master - SlideMaster_.assert_called_once_with(sldMaster, slide_master_part) - assert slide_master is slide_master_ - - def it_provides_access_to_a_related_slide_layout(self, related_fixture): - slide_master_part, rId, getitem_, slide_layout_ = related_fixture - slide_layout = slide_master_part.related_slide_layout(rId) - getitem_.assert_called_once_with(rId) - assert slide_layout is slide_layout_ + """Unit-test suite for `pptx.parts.slide.SlideMasterPart` objects.""" - # fixtures ------------------------------------------------------- - - @pytest.fixture - def master_fixture(self, SlideMaster_, slide_master_): + def it_provides_access_to_its_slide_master(self, request): + slide_master_ = instance_mock(request, SlideMaster) + SlideMaster_ = class_mock( + request, "pptx.parts.slide.SlideMaster", return_value=slide_master_ + ) sldMaster = element("p:sldMaster") - slide_master_part = SlideMasterPart(None, None, sldMaster) - return slide_master_part, SlideMaster_, sldMaster, slide_master_ - - @pytest.fixture - def related_fixture(self, slide_layout_, related_parts_prop_): - slide_master_part = SlideMasterPart(None, None, None, None) - rId = "rId42" - getitem_ = related_parts_prop_.return_value.__getitem__ - getitem_.return_value.slide_layout = slide_layout_ - return slide_master_part, rId, getitem_, slide_layout_ + slide_master_part = SlideMasterPart(None, None, None, sldMaster) - # fixture components --------------------------------------------- - - @pytest.fixture - def related_parts_prop_(self, request): - return property_mock(request, SlideMasterPart, "related_parts") + slide_master = slide_master_part.slide_master - @pytest.fixture - def slide_layout_(self, request): - return instance_mock(request, SlideLayout) + SlideMaster_.assert_called_once_with(sldMaster, slide_master_part) + assert slide_master is slide_master_ - @pytest.fixture - def SlideMaster_(self, request, slide_master_): - return class_mock( - request, "pptx.parts.slide.SlideMaster", return_value=slide_master_ + def it_provides_access_to_a_related_slide_layout(self, request): + slide_layout_ = instance_mock(request, SlideLayout) + slide_layout_part_ = instance_mock(request, SlideLayoutPart, slide_layout=slide_layout_) + related_part_ = method_mock( + request, SlideMasterPart, "related_part", return_value=slide_layout_part_ ) + slide_master_part = SlideMasterPart(None, None, None, None) - @pytest.fixture - def slide_master_(self, request): - return instance_mock(request, SlideMaster) + slide_layout = slide_master_part.related_slide_layout("rId42") + + related_part_.assert_called_once_with(slide_master_part, "rId42") + assert slide_layout is slide_layout_ diff --git a/tests/shapes/test_autoshape.py b/tests/shapes/test_autoshape.py index ce901d11c..efb38e6b9 100644 --- a/tests/shapes/test_autoshape.py +++ b/tests/shapes/test_autoshape.py @@ -1,8 +1,10 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.shapes.autoshape module.""" +"""Unit-test suite for `pptx.shapes.autoshape` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, cast import pytest @@ -26,45 +28,76 @@ from ..unitutil.cxml import element, xml from ..unitutil.mock import class_mock, instance_mock, property_mock +if TYPE_CHECKING: + from pptx.spec import AdjustmentValue -class DescribeAdjustment(object): - def it_knows_its_effective_value(self, effective_val_fixture_): - adjustment, expected_effective_value = effective_val_fixture_ - assert adjustment.effective_value == expected_effective_value - # fixture -------------------------------------------------------- +class DescribeAdjustment(object): + """Unit-test suite for `pptx.shapes.autoshape.Adjustment`.""" - def _effective_adj_val_cases(): - return [ - # no actual, effective should be determined by default value + @pytest.mark.parametrize( + ("def_val", "actual", "expected_value"), + [ + # -- no actual, effective should be determined by default value -- (50000, None, 0.5), - # actual matches default + # -- actual matches default -- (50000, 50000, 0.5), - # actual is different than default + # -- actual is different than default -- (50000, 12500, 0.125), - # actual is zero + # -- actual is zero -- (50000, 0, 0.0), - # negative default + # -- negative default -- (-20833, None, -0.20833), - # negative actual + # -- negative actual -- (-20833, -5678901, -56.78901), - ] - - @pytest.fixture(params=_effective_adj_val_cases()) - def effective_val_fixture_(self, request): - name = None - def_val, actual, expected_effective_value = request.param - adjustment = Adjustment(name, def_val, actual) - return adjustment, expected_effective_value + ], + ) + def it_knows_its_effective_value(self, def_val: int, actual: int | None, expected_value: float): + assert Adjustment("foobar", def_val, actual).effective_value == expected_value class DescribeAdjustmentCollection(object): - def it_should_load_default_adjustment_values(self, prstGeom_cases_): - prstGeom, prst, expected = prstGeom_cases_ + """Unit-test suite for `pptx.shapes.autoshape.AdjustmentCollection`.""" + + @pytest.mark.parametrize( + ("prst", "expected_values"), + [ + # -- rect has no adjustments -- + ("rect", ()), + # -- chevron has one simple one + ("chevron", (("adj", 50000),)), + # -- one with several and some negative -- + ( + "accentBorderCallout1", + (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)), + ), + # -- another one with some negative -- + ( + "wedgeRoundRectCallout", + (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)), + ), + # -- one with values outside normal range -- + ( + "circularArrow", + ( + ("adj1", 12500), + ("adj2", 1142319), + ("adj3", 20457681), + ("adj4", 10800000), + ("adj5", 12500), + ), + ), + ], + ) + def it_should_load_default_adjustment_values( + self, prst: str, expected_values: tuple[str, tuple[tuple[str, int], ...]] + ): + prstGeom = cast(CT_PresetGeometry2D, element(f"a:prstGeom{{prst={prst}}}/a:avLst")) + adjustments = AdjustmentCollection(prstGeom)._adjustments + actuals = tuple([(adj.name, adj.def_val) for adj in adjustments]) - assert len(adjustments) == len(expected) - assert actuals == expected + assert actuals == expected_values def it_should_load_adj_val_actuals_from_xml(self, load_adj_actuals_fixture_): prstGeom, expected_actuals, prstGeom_xml = load_adj_actuals_fixture_ @@ -72,17 +105,13 @@ def it_should_load_adj_val_actuals_from_xml(self, load_adj_actuals_fixture_): actual_actuals = dict([(a.name, a.actual) for a in adjustments]) assert actual_actuals == expected_actuals - def it_provides_normalized_effective_value_on_indexed_access( - self, indexed_access_fixture_ - ): + def it_provides_normalized_effective_value_on_indexed_access(self, indexed_access_fixture_): prstGeom, prst, expected_values = indexed_access_fixture_ adjustments = AdjustmentCollection(prstGeom) actual_values = [adjustments[idx] for idx in range(len(adjustments))] assert actual_values == expected_values - def it_should_update_actual_value_on_indexed_assignment( - self, indexed_assignment_fixture_ - ): + def it_should_update_actual_value_on_indexed_assignment(self, indexed_assignment_fixture_): """ Assignment to AdjustmentCollection[n] updates nth actual """ @@ -147,9 +176,7 @@ def adjustments_with_prstGeom_(self, request): def _adj_actuals_cases(): gd_bldr = a_gd().with_name("adj2").with_fmla("val 25000") avLst_bldr = an_avLst().with_child(gd_bldr) - mathDivide_bldr = ( - a_prstGeom().with_nsdecls().with_prst("mathDivide").with_child(avLst_bldr) - ) + mathDivide_bldr = a_prstGeom().with_nsdecls().with_prst("mathDivide").with_child(avLst_bldr) gd_bldr = a_gd().with_name("adj").with_fmla("val 25000") avLst_bldr = an_avLst().with_child(gd_bldr) @@ -158,14 +185,9 @@ def _adj_actuals_cases(): gd_bldr_1 = a_gd().with_name("adj1").with_fmla("val 111") gd_bldr_2 = a_gd().with_name("adj2").with_fmla("val 222") gd_bldr_3 = a_gd().with_name("adj3").with_fmla("val 333") - avLst_bldr = ( - an_avLst().with_child(gd_bldr_1).with_child(gd_bldr_2).with_child(gd_bldr_3) - ) + avLst_bldr = an_avLst().with_child(gd_bldr_1).with_child(gd_bldr_2).with_child(gd_bldr_3) wedgeRoundRectCallout_bldr = ( - a_prstGeom() - .with_nsdecls() - .with_prst("wedgeRoundRectCallout") - .with_child(avLst_bldr) + a_prstGeom().with_nsdecls().with_prst("wedgeRoundRectCallout").with_child(avLst_bldr) ) return [ @@ -198,43 +220,6 @@ def load_adj_actuals_fixture_(self, request): prstGeom_xml = prstGeom_bldr.xml return prstGeom, expected, prstGeom_xml - def _prstGeom_cases(): - return [ - # rect has no adjustments - ("rect", ()), - # chevron has one simple one - ("chevron", (("adj", 50000),)), - # one with several and some negative - ( - "accentBorderCallout1", - (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)), - ), - # another one with some negative - ( - "wedgeRoundRectCallout", - (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)), - ), - # one with values outside normal range - ( - "circularArrow", - ( - ("adj1", 12500), - ("adj2", 1142319), - ("adj3", 20457681), - ("adj4", 10800000), - ("adj5", 12500), - ), - ), - ] - - @pytest.fixture(params=_prstGeom_cases()) - def prstGeom_cases_(self, request): - prst, expected_values = request.param - prstGeom = ( - a_prstGeom().with_nsdecls().with_prst(prst).with_child(an_avLst()).element - ) - return prstGeom, prst, expected_values - def _effective_val_cases(): return [ ("rect", ()), @@ -260,16 +245,40 @@ def indexed_assignment_fixture_(self, request): class DescribeAutoShapeType(object): + """Unit-test suite for `pptx.shapes.autoshape.AutoShapeType`""" + def it_knows_the_details_of_the_auto_shape_type_it_represents(self): autoshape_type = AutoShapeType(MSO_SHAPE.ROUNDED_RECTANGLE) assert autoshape_type.autoshape_type_id == MSO_SHAPE.ROUNDED_RECTANGLE assert autoshape_type.prst == "roundRect" assert autoshape_type.basename == "Rounded Rectangle" + def it_xml_escapes_the_basename_when_the_name_contains_special_characters(self): + autoshape_type = AutoShapeType(MSO_SHAPE.NO_SYMBOL) + assert autoshape_type.autoshape_type_id == MSO_SHAPE.NO_SYMBOL + assert autoshape_type.prst == "noSmoking" + assert autoshape_type.basename == ""No" Symbol" + + @pytest.mark.parametrize( + ("prst", "default_adj_vals"), + [ + (MSO_SHAPE.RECTANGLE, ()), + (MSO_SHAPE.CHEVRON, (("adj", 50000),)), + ( + MSO_SHAPE.LEFT_CIRCULAR_ARROW, + ( + ("adj1", 12500), + ("adj2", -1142319), + ("adj3", 1142319), + ("adj4", 10800000), + ("adj5", 12500), + ), + ), + ], + ) def it_knows_the_default_adj_vals_for_its_autoshape_type( - self, default_adj_vals_fixture_ + self, prst: MSO_SHAPE, default_adj_vals: tuple[AdjustmentValue, ...] ): - prst, default_adj_vals = default_adj_vals_fixture_ _default_adj_vals = AutoShapeType.default_adjustment_values(prst) assert _default_adj_vals == default_adj_vals @@ -277,7 +286,7 @@ def it_knows_the_autoshape_type_id_for_each_prst_key(self): assert AutoShapeType.id_from_prst("rect") == MSO_SHAPE.RECTANGLE def it_raises_when_asked_for_autoshape_type_id_with_a_bad_prst(self): - with pytest.raises(KeyError): + with pytest.raises(ValueError, match="MSO_AUTO_SHAPE_TYPE has no XML mapping for 'badPr"): AutoShapeType.id_from_prst("badPrst") def it_caches_autoshape_type_lookups(self): @@ -290,29 +299,6 @@ def it_raises_on_construction_with_bad_autoshape_type_id(self): with pytest.raises(KeyError): AutoShapeType(9999) - # fixtures ------------------------------------------------------- - - def _default_adj_vals_cases(): - return [ - (MSO_SHAPE.RECTANGLE, ()), - (MSO_SHAPE.CHEVRON, (("adj", 50000),)), - ( - MSO_SHAPE.LEFT_CIRCULAR_ARROW, - ( - ("adj1", 12500), - ("adj2", -1142319), - ("adj3", 1142319), - ("adj4", 10800000), - ("adj5", 12500), - ), - ), - ] - - @pytest.fixture(params=_default_adj_vals_cases()) - def default_adj_vals_fixture_(self, request): - prst, default_adj_vals = request.param - return prst, default_adj_vals - class DescribeShape(object): """Unit-test suite for `pptx.shapes.autoshape.Shape` object.""" @@ -327,9 +313,7 @@ def it_knows_its_autoshape_type(self, autoshape_type_fixture_): auto_shape_type = shape.auto_shape_type assert auto_shape_type == expected_value - def but_it_raises_when_auto_shape_type_called_on_non_autoshape( - self, non_autoshape_shape_ - ): + def but_it_raises_when_auto_shape_type_called_on_non_autoshape(self, non_autoshape_shape_): with pytest.raises(ValueError): non_autoshape_shape_.auto_shape_type @@ -346,9 +330,7 @@ def it_has_a_line(self, shape): def it_knows_its_shape_type_when_its_a_placeholder(self, placeholder_shape_): assert placeholder_shape_.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER - def and_it_knows_its_shape_type_when_its_not_a_placeholder( - self, non_placeholder_shapes_ - ): + def and_it_knows_its_shape_type_when_its_not_a_placeholder(self, non_placeholder_shapes_): autoshape_shape_, textbox_shape_, freeform_ = non_placeholder_shapes_ assert autoshape_shape_.shape_type == MSO_SHAPE_TYPE.AUTO_SHAPE assert textbox_shape_.shape_type == MSO_SHAPE_TYPE.TEXT_BOX @@ -409,9 +391,7 @@ def autoshape_type_fixture_(self, shape, prst): return shape, MSO_SHAPE.CHEVRON @pytest.fixture - def init_adjs_fixture_( - self, request, shape, sp_, adjustments_, AdjustmentCollection_ - ): + def init_adjs_fixture_(self, request, shape, sp_, adjustments_, AdjustmentCollection_): return shape, adjustments_, AdjustmentCollection_, sp_ @pytest.fixture @@ -500,9 +480,7 @@ def sp_(self, request, prst): @pytest.fixture def TextFrame_(self, request, text_frame_): - return class_mock( - request, "pptx.shapes.autoshape.TextFrame", return_value=text_frame_ - ) + return class_mock(request, "pptx.shapes.autoshape.TextFrame", return_value=text_frame_) @pytest.fixture def text_frame_(self, request): diff --git a/tests/shapes/test_base.py b/tests/shapes/test_base.py index 26b441d0c..89632ca80 100644 --- a/tests/shapes/test_base.py +++ b/tests/shapes/test_base.py @@ -1,10 +1,10 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.shapes.shape module -""" +"""Unit-test suite for `pptx.shapes.base` module.""" -from __future__ import absolute_import +from __future__ import annotations + +from typing import TYPE_CHECKING, cast import pytest @@ -36,10 +36,17 @@ an_xfrm, ) from ..unitutil.cxml import element, xml -from ..unitutil.mock import class_mock, instance_mock, loose_mock, property_mock +from ..unitutil.mock import class_mock, instance_mock, loose_mock + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.oxml.shapes import ShapeElement + from pptx.types import ProvidesPart class DescribeBaseShape(object): + """Unit-test suite for `pptx.shapes.base.BaseShape` objects.""" + def it_provides_access_to_its_click_action(self, click_action_fixture): shape, ActionSetting_, cNvPr, click_action_ = click_action_fixture click_action = shape.click_action @@ -59,10 +66,47 @@ def it_can_change_its_name(self, name_set_fixture): shape.name = new_value assert shape._element.xml == expected_xml - def it_has_a_position(self, position_get_fixture): - shape, expected_left, expected_top = position_get_fixture - assert shape.left == expected_left - assert shape.top == expected_top + @pytest.mark.parametrize( + ("shape_cxml", "expected_x", "expected_y"), + [ + ("p:cxnSp/p:spPr", None, None), + ("p:cxnSp/p:spPr/a:xfrm", None, None), + ("p:cxnSp/p:spPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:graphicFrame/p:xfrm", None, None), + ("p:graphicFrame/p:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:grpSp/p:grpSpPr", None, None), + ("p:grpSp/p:grpSpPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:pic/p:spPr", None, None), + ("p:pic/p:spPr/a:xfrm", None, None), + ("p:pic/p:spPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:sp/p:spPr", None, None), + ("p:sp/p:spPr/a:xfrm", None, None), + ("p:sp/p:spPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ], + ) + def it_has_a_position( + self, + shape_cxml: str, + expected_x: int | None, + expected_y: int | None, + provides_part: ProvidesPart, + ): + shape_elm = cast("ShapeElement", element(shape_cxml)) + + shape = BaseShape(shape_elm, provides_part) + + assert shape.left == expected_x + assert shape.top == expected_y + + @pytest.fixture + def provides_part(self) -> ProvidesPart: + + class FakeProvidesPart: + @property + def part(self) -> XmlPart: + raise NotImplementedError + + return FakeProvidesPart() def it_can_change_its_position(self, position_set_fixture): shape, left, top, expected_xml = position_set_fixture @@ -274,28 +318,6 @@ def phfmt_fixture(self, _PlaceholderFormat_, placeholder_format_): def phfmt_raise_fixture(self): return BaseShape(element("p:sp/p:nvSpPr/p:nvPr"), None) - @pytest.fixture( - params=[ - ("sp", False), - ("sp_with_off", True), - ("pic", False), - ("pic_with_off", True), - ("graphicFrame", False), - ("graphicFrame_with_off", True), - ("grpSp", False), - ("grpSp_with_off", True), - ("cxnSp", False), - ("cxnSp_with_off", True), - ] - ) - def position_get_fixture(self, request, left, top): - shape_elm_fixt_name, expect_values = request.param - shape_elm = request.getfixturevalue(shape_elm_fixt_name) - shape = BaseShape(shape_elm, None) - if not expect_values: - left = top = None - return shape, left, top - @pytest.fixture( params=[ ("sp", "sp_with_off"), @@ -365,9 +387,7 @@ def shadow_fixture(self, request, ShadowFormat_, shadow_): @pytest.fixture def ActionSetting_(self, request, action_setting_): - return class_mock( - request, "pptx.shapes.base.ActionSetting", return_value=action_setting_ - ) + return class_mock(request, "pptx.shapes.base.ActionSetting", return_value=action_setting_) @pytest.fixture def action_setting_(self, request): @@ -383,9 +403,7 @@ def cxnSp_with_ext(self, width, height): a_cxnSp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_ext().with_cx(width).with_cy(height)) - ) + an_spPr().with_child(an_xfrm().with_child(an_ext().with_cx(width).with_cy(height))) ) ).element @@ -395,9 +413,7 @@ def cxnSp_with_off(self, left, top): a_cxnSp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + an_spPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element @@ -444,9 +460,7 @@ def grpSp_with_off(self, left, top): a_grpSp() .with_nsdecls("p", "a") .with_child( - a_grpSpPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + a_grpSpPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element @@ -468,9 +482,7 @@ def pic_with_off(self, left, top): a_pic() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + an_spPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element @@ -480,9 +492,7 @@ def pic_with_ext(self, width, height): a_pic() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_ext().with_cx(width).with_cy(height)) - ) + an_spPr().with_child(an_xfrm().with_child(an_ext().with_cx(width).with_cy(height))) ) ).element @@ -524,10 +534,6 @@ def shape_id(self): def shape_name(self): return "Foobar 41" - @pytest.fixture - def shape_text_frame_(self, request): - return property_mock(request, BaseShape, "text_frame") - @pytest.fixture def shapes_(self, request): return instance_mock(request, SlideShapes) @@ -542,9 +548,7 @@ def sp_with_ext(self, width, height): an_sp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_ext().with_cx(width).with_cy(height)) - ) + an_spPr().with_child(an_xfrm().with_child(an_ext().with_cx(width).with_cy(height))) ) ).element @@ -554,9 +558,7 @@ def sp_with_off(self, left, top): an_sp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + an_spPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element diff --git a/tests/shapes/test_connector.py b/tests/shapes/test_connector.py index 3bafa9f96..f61f0a029 100644 --- a/tests/shapes/test_connector.py +++ b/tests/shapes/test_connector.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Unit test suite for pptx.shapes.connector module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/shapes/test_freeform.py b/tests/shapes/test_freeform.py index 55a0e4d24..dd5f53f0d 100644 --- a/tests/shapes/test_freeform.py +++ b/tests/shapes/test_freeform.py @@ -1,24 +1,27 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Unit-test suite for pptx.shapes.freeform module""" +"""Unit-test suite for `pptx.shapes.freeform` module""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest from pptx.shapes.autoshape import Shape from pptx.shapes.freeform import ( + FreeformBuilder, _BaseDrawingOperation, _Close, - FreeformBuilder, _LineSegment, _MoveTo, ) from pptx.shapes.shapetree import SlideShapes +from pptx.util import Emu, Mm from ..unitutil.cxml import element, xml from ..unitutil.file import snippet_seq from ..unitutil.mock import ( + FixtureRequest, + Mock, call, initializer_mock, instance_mock, @@ -28,28 +31,37 @@ class DescribeFreeformBuilder(object): - def it_provides_a_constructor(self, new_fixture): - shapes_, start_x, start_y, x_scale, y_scale = new_fixture[:5] - _init_, start_x_int, start_y_int = new_fixture[5:] + """Unit-test suite for `pptx.shapes.freeform.FreeformBuilder` objects.""" + + def it_provides_a_constructor(self, shapes_: Mock, _init_: Mock): + start_x, start_y, x_scale, y_scale = 99.56, 200.49, 4.2, 2.4 + start_x_int, start_y_int = 100, 200 builder = FreeformBuilder.new(shapes_, start_x, start_y, x_scale, y_scale) - _init_.assert_called_once_with( - builder, shapes_, start_x_int, start_y_int, x_scale, y_scale - ) + _init_.assert_called_once_with(builder, shapes_, start_x_int, start_y_int, x_scale, y_scale) assert isinstance(builder, FreeformBuilder) - def it_can_add_straight_line_segments(self, add_segs_fixture): - builder, vertices, close, add_calls, close_calls = add_segs_fixture + @pytest.mark.parametrize("close", [True, False]) + def it_can_add_straight_line_segments(self, request: FixtureRequest, close: bool): + _add_line_segment_ = method_mock(request, FreeformBuilder, "_add_line_segment") + _add_close_ = method_mock(request, FreeformBuilder, "_add_close") + builder = FreeformBuilder(None, None, None, None, None) # type: ignore - return_value = builder.add_line_segments(vertices, close) + return_value = builder.add_line_segments(((1, 2), (3, 4), (5, 6)), close) - assert builder._add_line_segment.call_args_list == add_calls - assert builder._add_close.call_args_list == close_calls + assert _add_line_segment_.call_args_list == [ + call(builder, 1, 2), + call(builder, 3, 4), + call(builder, 5, 6), + ] + assert _add_close_.call_args_list == ([call(builder)] if close else []) assert return_value is builder - def it_can_move_the_pen_location(self, move_to_fixture): - builder, x, y, _MoveTo_new_, move_to_ = move_to_fixture + def it_can_move_the_pen_location(self, _MoveTo_new_: Mock, move_to_: Mock): + x, y = 42, 24 + _MoveTo_new_.return_value = move_to_ + builder = FreeformBuilder(None, None, None, None, None) # type: ignore return_value = builder.move_to(x, y) @@ -57,46 +69,99 @@ def it_can_move_the_pen_location(self, move_to_fixture): assert builder._drawing_operations[-1] is move_to_ assert return_value is builder - def it_can_build_the_specified_freeform_shape(self, convert_fixture): - builder, origin_x, origin_y, sp = convert_fixture[:4] - apply_operation_to_, calls, shape_ = convert_fixture[4:] + def it_can_build_the_specified_freeform_shape( + self, + shapes_: Mock, + apply_operation_to_: Mock, + _add_freeform_sp_: Mock, + _start_path_: Mock, + shape_: Mock, + ): + origin_x, origin_y = Mm(42), Mm(24) + sp, path = element("p:sp"), element("a:path") + drawing_ops = ( + _LineSegment(None, None, None), # type: ignore + _LineSegment(None, None, None), # type: ignore + ) + shapes_._shape_factory.return_value = shape_ + _add_freeform_sp_.return_value = sp + _start_path_.return_value = path + builder = FreeformBuilder(shapes_, None, None, None, None) # type: ignore + builder._drawing_operations.extend(drawing_ops) + calls = [call(drawing_ops[0], path), call(drawing_ops[1], path)] shape = builder.convert_to_shape(origin_x, origin_y) - builder._add_freeform_sp.assert_called_once_with(builder, origin_x, origin_y) - builder._start_path.assert_called_once_with(builder, sp) + _add_freeform_sp_.assert_called_once_with(builder, origin_x, origin_y) + _start_path_.assert_called_once_with(builder, sp) assert apply_operation_to_.call_args_list == calls - builder._shapes._shape_factory.assert_called_once_with(sp) + shapes_._shape_factory.assert_called_once_with(sp) assert shape is shape_ - def it_knows_the_shape_x_offset(self, shape_offset_x_fixture): - builder, expected_value = shape_offset_x_fixture - x_offset = builder.shape_offset_x - assert x_offset == expected_value + @pytest.mark.parametrize( + ("start_x", "xs", "expected_value"), + [ + (Mm(0), (1, None, 2, 3), Mm(0)), + (Mm(6), (1, None, 2, 3), Mm(1)), + (Mm(50), (150, -5, None, 100), Mm(-5)), + ], + ) + def it_knows_the_shape_x_offset( + self, start_x: int, xs: tuple[int | None, ...], expected_value: int + ): + builder = FreeformBuilder(None, start_x, None, None, None) # type: ignore + drawing_ops = [_Close() if x is None else _LineSegment(builder, Mm(x), Mm(0)) for x in xs] + builder._drawing_operations.extend(drawing_ops) - def it_knows_the_shape_y_offset(self, shape_offset_y_fixture): - builder, expected_value = shape_offset_y_fixture - y_offset = builder.shape_offset_y - assert y_offset == expected_value + assert builder.shape_offset_x == expected_value - def it_adds_a_freeform_sp_to_help(self, sp_fixture): - builder, origin_x, origin_y, spTree, expected_xml = sp_fixture + @pytest.mark.parametrize( + ("start_y", "ys", "expected_value"), + [ + (Mm(0), (2, None, 6, 8), Mm(0)), + (Mm(4), (2, None, 6, 8), Mm(2)), + (Mm(19), (213, -22, None, 100), Mm(-22)), + ], + ) + def it_knows_the_shape_y_offset( + self, start_y: int, ys: tuple[int | None, ...], expected_value: int + ): + builder = FreeformBuilder(None, None, start_y, None, None) # type: ignore + drawing_ops = [_Close() if y is None else _LineSegment(builder, Mm(0), Mm(y)) for y in ys] + builder._drawing_operations.extend(drawing_ops) + + assert builder.shape_offset_y == expected_value + + def it_adds_a_freeform_sp_to_help( + self, _left_prop_: Mock, _top_prop_: Mock, _width_prop_: Mock, _height_prop_: Mock + ): + origin_x, origin_y = Emu(42), Emu(24) + spTree = element("p:spTree") + shapes = SlideShapes(spTree, None) # type: ignore + _left_prop_.return_value, _top_prop_.return_value = Emu(12), Emu(34) + _width_prop_.return_value, _height_prop_.return_value = 56, 78 + builder = FreeformBuilder(shapes, None, None, None, None) # type: ignore + expected_xml = snippet_seq("freeform")[0] sp = builder._add_freeform_sp(origin_x, origin_y) assert spTree.xml == expected_xml assert sp is spTree.xpath("p:sp")[0] - def it_adds_a_line_segment_to_help(self, add_seg_fixture): - builder, x, y, _LineSegment_new_, line_segment_ = add_seg_fixture + def it_adds_a_line_segment_to_help(self, _LineSegment_new_: Mock, line_segment_: Mock): + x, y = 4, 2 + _LineSegment_new_.return_value = line_segment_ + + builder = FreeformBuilder(None, None, None, None, None) # type: ignore builder._add_line_segment(x, y) _LineSegment_new_.assert_called_once_with(builder, x, y) assert builder._drawing_operations == [line_segment_] - def it_closes_a_contour_to_help(self, add_close_fixture): - builder, _Close_new_, close_ = add_close_fixture + def it_closes_a_contour_to_help(self, _Close_new_: Mock, close_: Mock): + _Close_new_.return_value = close_ + builder = FreeformBuilder(None, None, None, None, None) # type: ignore builder._add_close() @@ -118,8 +183,15 @@ def it_knows_the_freeform_width_to_help(self, width_fixture): width = builder._width assert width == expected_value - def it_knows_the_freeform_height_to_help(self, height_fixture): - builder, expected_value = height_fixture + @pytest.mark.parametrize( + ("dy", "y_scale", "expected_value"), + [(0, 2.0, 0), (24, 10.0, 240), (914400, 314.1, 287213040)], + ) + def it_knows_the_freeform_height_to_help( + self, dy: int, y_scale: float, expected_value: int, _dy_prop_: Mock + ): + _dy_prop_.return_value = dy + builder = FreeformBuilder(None, None, None, None, y_scale) # type: ignore height = builder._height assert height == expected_value @@ -133,14 +205,23 @@ def it_knows_the_local_coordinate_height_to_help(self, dy_fixture): dy = builder._dy assert dy == expected_value - def it_can_start_a_new_path_to_help(self, start_path_fixture): - builder, sp, _local_to_shape_ = start_path_fixture[:3] - start_x, start_y, expected_xml = start_path_fixture[3:] + def it_can_start_a_new_path_to_help( + self, request: FixtureRequest, _dx_prop_: Mock, _dy_prop_: Mock + ): + _local_to_shape_ = method_mock( + request, FreeformBuilder, "_local_to_shape", return_value=(101, 202) + ) + sp = element("p:sp/p:spPr/a:custGeom") + start_x, start_y = 42, 24 + _dx_prop_.return_value, _dy_prop_.return_value = 1001, 2002 + builder = FreeformBuilder(None, start_x, start_y, None, None) path = builder._start_path(sp) - _local_to_shape_.assert_called_once_with(start_x, start_y) - assert sp.xml == expected_xml + _local_to_shape_.assert_called_once_with(builder, start_x, start_y) + assert sp.xml == xml( + "p:sp/p:spPr/a:custGeom/a:pathLst/a:path{w=1001,h=2002}/a:moveTo" "/a:pt{x=101,y=202}" + ) assert path is sp.xpath(".//a:path")[-1] def it_translates_local_to_shape_coordinates_to_help(self, local_fixture): @@ -150,47 +231,6 @@ def it_translates_local_to_shape_coordinates_to_help(self, local_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def add_close_fixture(self, _Close_new_, close_): - _Close_new_.return_value = close_ - builder = FreeformBuilder(None, None, None, None, None) - return builder, _Close_new_, close_ - - @pytest.fixture - def add_seg_fixture(self, _LineSegment_new_, line_segment_): - x, y = 4, 2 - _LineSegment_new_.return_value = line_segment_ - - builder = FreeformBuilder(None, None, None, None, None) - return builder, x, y, _LineSegment_new_, line_segment_ - - @pytest.fixture(params=[(True, [call()]), (False, [])]) - def add_segs_fixture(self, request, _add_line_segment_, _add_close_): - close, close_calls = request.param - vertices = ((1, 2), (3, 4), (5, 6)) - builder = FreeformBuilder(None, None, None, None, None) - add_calls = [call(1, 2), call(3, 4), call(5, 6)] - return builder, vertices, close, add_calls, close_calls - - @pytest.fixture - def convert_fixture( - self, shapes_, apply_operation_to_, _add_freeform_sp_, _start_path_, shape_ - ): - origin_x, origin_y = 42, 24 - sp, path = element("p:sp"), element("a:path") - drawing_ops = ( - _BaseDrawingOperation(None, None, None), - _BaseDrawingOperation(None, None, None), - ) - shapes_._shape_factory.return_value = shape_ - _add_freeform_sp_.return_value = sp - _start_path_.return_value = path - - builder = FreeformBuilder(shapes_, None, None, None, None) - builder._drawing_operations.extend(drawing_ops) - calls = [call(drawing_ops[0], path), call(drawing_ops[1], path)] - return (builder, origin_x, origin_y, sp, apply_operation_to_, calls, shape_) - @pytest.fixture( params=[ (0, (1, None, 2, 3), 3), @@ -198,7 +238,7 @@ def convert_fixture( (50, (150, -5, None, 100), 155), ] ) - def dx_fixture(self, request): + def dx_fixture(self, request: FixtureRequest): start_x, xs, expected_value = request.param drawing_ops = [] for x in xs: @@ -218,7 +258,7 @@ def dx_fixture(self, request): (32, (160, -8, None, 101), 168), ] ) - def dy_fixture(self, request): + def dy_fixture(self, request: FixtureRequest): start_y, ys, expected_value = request.param drawing_ops = [] for y in ys: @@ -231,16 +271,8 @@ def dy_fixture(self, request): builder._drawing_operations.extend(drawing_ops) return builder, expected_value - @pytest.fixture(params=[(0, 2.0, 0), (24, 10.0, 240), (914400, 314.1, 287213040)]) - def height_fixture(self, request, _dy_prop_): - dy, y_scale, expected_value = request.param - _dy_prop_.return_value = dy - - builder = FreeformBuilder(None, None, None, None, y_scale) - return builder, expected_value - @pytest.fixture(params=[(0, 1.0, 0), (4, 10.0, 40), (914400, 914.3, 836035920)]) - def left_fixture(self, request, shape_offset_x_prop_): + def left_fixture(self, request: FixtureRequest, shape_offset_x_prop_: Mock): offset_x, x_scale, expected_value = request.param shape_offset_x_prop_.return_value = offset_x @@ -248,7 +280,7 @@ def left_fixture(self, request, shape_offset_x_prop_): return builder, expected_value @pytest.fixture - def local_fixture(self, shape_offset_x_prop_, shape_offset_y_prop_): + def local_fixture(self, shape_offset_x_prop_: Mock, shape_offset_y_prop_: Mock): local_x, local_y = 123, 456 shape_offset_x_prop_.return_value = 23 shape_offset_y_prop_.return_value = 156 @@ -258,70 +290,9 @@ def local_fixture(self, shape_offset_x_prop_, shape_offset_y_prop_): return builder, local_x, local_y, expected_value @pytest.fixture - def move_to_fixture(self, _MoveTo_new_, move_to_): - x, y = 42, 24 - _MoveTo_new_.return_value = move_to_ - - builder = FreeformBuilder(None, None, None, None, None) - return builder, x, y, _MoveTo_new_, move_to_ - - @pytest.fixture - def new_fixture(self, shapes_, _init_): - start_x, start_y, x_scale, y_scale = 99.56, 200.49, 4.2, 2.4 - start_x_int, start_y_int = 100, 200 - return ( - shapes_, - start_x, - start_y, - x_scale, - y_scale, - _init_, - start_x_int, - start_y_int, - ) - - @pytest.fixture( - params=[ - (0, (1, None, 2, 3), 0), - (6, (1, None, 2, 3), 1), - (50, (150, -5, None, 100), -5), - ] - ) - def shape_offset_x_fixture(self, request): - start_x, xs, expected_value = request.param - drawing_ops = [] - for x in xs: - if x is None: - drawing_ops.append(_Close()) - else: - drawing_ops.append(_BaseDrawingOperation(None, x, None)) - - builder = FreeformBuilder(None, start_x, None, None, None) - builder._drawing_operations.extend(drawing_ops) - return builder, expected_value - - @pytest.fixture( - params=[ - (0, (2, None, 6, 8), 0), - (4, (2, None, 6, 8), 2), - (19, (213, -22, None, 100), -22), - ] - ) - def shape_offset_y_fixture(self, request): - start_y, ys, expected_value = request.param - drawing_ops = [] - for y in ys: - if y is None: - drawing_ops.append(_Close()) - else: - drawing_ops.append(_BaseDrawingOperation(None, None, y)) - - builder = FreeformBuilder(None, None, start_y, None, None) - builder._drawing_operations.extend(drawing_ops) - return builder, expected_value - - @pytest.fixture - def sp_fixture(self, _left_prop_, _top_prop_, _width_prop_, _height_prop_): + def sp_fixture( + self, _left_prop_: Mock, _top_prop_: Mock, _width_prop_: Mock, _height_prop_: Mock + ): origin_x, origin_y = 42, 24 spTree = element("p:spTree") shapes = SlideShapes(spTree, None) @@ -332,24 +303,8 @@ def sp_fixture(self, _left_prop_, _top_prop_, _width_prop_, _height_prop_): expected_xml = snippet_seq("freeform")[0] return builder, origin_x, origin_y, spTree, expected_xml - @pytest.fixture - def start_path_fixture(self, _dx_prop_, _dy_prop_, _local_to_shape_): - sp = element("p:sp/p:spPr/a:custGeom") - start_x, start_y = 42, 24 - _dx_prop_.return_value, _dy_prop_.return_value = 1001, 2002 - _local_to_shape_.return_value = 101, 202 - - builder = FreeformBuilder(None, start_x, start_y, None, None) - expected_xml = xml( - "p:sp/p:spPr/a:custGeom/a:pathLst/a:path{w=1001,h=2002}/a:moveTo" - "/a:pt{x=101,y=202}" - ) - return builder, sp, _local_to_shape_, start_x, start_y, expected_xml - - @pytest.fixture( - params=[(0, 11.0, 0), (100, 10.36, 1036), (914242, 943.1, 862221630)] - ) - def top_fixture(self, request, shape_offset_y_prop_): + @pytest.fixture(params=[(0, 11.0, 0), (100, 10.36, 1036), (914242, 943.1, 862221630)]) + def top_fixture(self, request: FixtureRequest, shape_offset_y_prop_: Mock): offset_y, y_scale, expected_value = request.param shape_offset_y_prop_.return_value = offset_y @@ -357,7 +312,7 @@ def top_fixture(self, request, shape_offset_y_prop_): return builder, expected_value @pytest.fixture(params=[(0, 1.0, 0), (42, 10.0, 420), (914400, 914.4, 836127360)]) - def width_fixture(self, request, _dx_prop_): + def width_fixture(self, request: FixtureRequest, _dx_prop_: Mock): dx, x_scale, expected_value = request.param _dx_prop_.return_value = dx @@ -367,101 +322,89 @@ def width_fixture(self, request, _dx_prop_): # fixture components ----------------------------------- @pytest.fixture - def _add_close_(self, request): - return method_mock(request, FreeformBuilder, "_add_close") - - @pytest.fixture - def _add_freeform_sp_(self, request): + def _add_freeform_sp_(self, request: FixtureRequest): return method_mock(request, FreeformBuilder, "_add_freeform_sp", autospec=True) @pytest.fixture - def _add_line_segment_(self, request): - return method_mock(request, FreeformBuilder, "_add_line_segment") + def apply_operation_to_(self, request: FixtureRequest): + return method_mock(request, _LineSegment, "apply_operation_to", autospec=True) @pytest.fixture - def apply_operation_to_(self, request): - return method_mock( - request, _BaseDrawingOperation, "apply_operation_to", autospec=True - ) - - @pytest.fixture - def close_(self, request): + def close_(self, request: FixtureRequest): return instance_mock(request, _Close) @pytest.fixture - def _Close_new_(self, request): - return method_mock(request, _Close, "new") + def _Close_new_(self, request: FixtureRequest): + return method_mock(request, _Close, "new", autospec=False) @pytest.fixture - def _dx_prop_(self, request): + def _dx_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_dx") @pytest.fixture - def _dy_prop_(self, request): + def _dy_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_dy") @pytest.fixture - def _height_prop_(self, request): + def _height_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_height") @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, FreeformBuilder, autospec=True) @pytest.fixture - def _left_prop_(self, request): + def _left_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_left") @pytest.fixture - def line_segment_(self, request): + def line_segment_(self, request: FixtureRequest): return instance_mock(request, _LineSegment) @pytest.fixture - def _LineSegment_new_(self, request): - return method_mock(request, _LineSegment, "new") - - @pytest.fixture - def _local_to_shape_(self, request): - return method_mock(request, FreeformBuilder, "_local_to_shape") + def _LineSegment_new_(self, request: FixtureRequest): + return method_mock(request, _LineSegment, "new", autospec=False) @pytest.fixture - def move_to_(self, request): + def move_to_(self, request: FixtureRequest): return instance_mock(request, _MoveTo) @pytest.fixture - def _MoveTo_new_(self, request): - return method_mock(request, _MoveTo, "new") + def _MoveTo_new_(self, request: FixtureRequest): + return method_mock(request, _MoveTo, "new", autospec=False) @pytest.fixture - def shape_(self, request): + def shape_(self, request: FixtureRequest): return instance_mock(request, Shape) @pytest.fixture - def shape_offset_x_prop_(self, request): + def shape_offset_x_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "shape_offset_x") @pytest.fixture - def shape_offset_y_prop_(self, request): + def shape_offset_y_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "shape_offset_y") @pytest.fixture - def shapes_(self, request): + def shapes_(self, request: FixtureRequest): return instance_mock(request, SlideShapes) @pytest.fixture - def _start_path_(self, request): + def _start_path_(self, request: FixtureRequest): return method_mock(request, FreeformBuilder, "_start_path", autospec=True) @pytest.fixture - def _top_prop_(self, request): + def _top_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_top") @pytest.fixture - def _width_prop_(self, request): + def _width_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_width") class Describe_BaseDrawingOperation(object): + """Unit-test suite for `pptx.shapes.freeform.BaseDrawingOperation` objects.""" + def it_knows_its_x_coordinate(self, x_fixture): drawing_operation, expected_value = x_fixture x = drawing_operation.x @@ -490,6 +433,8 @@ def y_fixture(self): class Describe_Close(object): + """Unit-test suite for `pptx.shapes.freeform._Close` objects.""" + def it_provides_a_constructor(self, new_fixture): _init_ = new_fixture @@ -522,11 +467,13 @@ def apply_fixture(self): return close, path, expected_xml @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, _Close, autospec=True) class Describe_LineSegment(object): + """Unit-test suite for `pptx.shapes.freeform._LineSegment` objects.""" + def it_provides_a_constructor(self, new_fixture): builder_, x, y, _init_, x_int, y_int = new_fixture @@ -563,15 +510,17 @@ def new_fixture(self, builder_, _init_): # fixture components ----------------------------------- @pytest.fixture - def builder_(self, request): + def builder_(self, request: FixtureRequest): return instance_mock(request, FreeformBuilder) @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, _LineSegment, autospec=True) class Describe_MoveTo(object): + """Unit-test suite for `pptx.shapes.freeform._MoveTo` objects.""" + def it_provides_a_constructor(self, new_fixture): builder_, x, y, _init_, x_int, y_int = new_fixture @@ -608,9 +557,9 @@ def new_fixture(self, builder_, _init_): # fixture components ----------------------------------- @pytest.fixture - def builder_(self, request): + def builder_(self, request: FixtureRequest): return instance_mock(request, FreeformBuilder) @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, _MoveTo, autospec=True) diff --git a/tests/shapes/test_graphfrm.py b/tests/shapes/test_graphfrm.py index 0b19e3bad..3324fcfe0 100644 --- a/tests/shapes/test_graphfrm.py +++ b/tests/shapes/test_graphfrm.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for pptx.shapes.graphfrm module.""" +from __future__ import annotations + import pytest from pptx.chart.chart import Chart @@ -41,20 +41,17 @@ def but_it_raises_on_chart_if_there_isnt_one(self, has_chart_prop_): assert str(e.value) == "shape does not contain a chart" def it_provides_access_to_its_chart_part(self, request, chart_part_): - graphicFrame = element( - "p:graphicFrame/a:graphic/a:graphicData/c:chart{r:id=rId42}" - ) - property_mock( - request, - GraphicFrame, - "part", - return_value=instance_mock( - request, SlidePart, related_parts={"rId42": chart_part_} - ), + slide_part_ = instance_mock(request, SlidePart) + slide_part_.related_part.return_value = chart_part_ + property_mock(request, GraphicFrame, "part", return_value=slide_part_) + graphic_frame = GraphicFrame( + element("p:graphicFrame/a:graphic/a:graphicData/c:chart{r:id=rId42}"), None ) - graphic_frame = GraphicFrame(graphicFrame, None) - assert graphic_frame.chart_part is chart_part_ + chart_part = graphic_frame.chart_part + + slide_part_.related_part.assert_called_once_with("rId42") + assert chart_part is chart_part_ @pytest.mark.parametrize( "graphicData_uri, expected_value", @@ -65,9 +62,7 @@ def it_provides_access_to_its_chart_part(self, request, chart_part_): ), ) def it_knows_whether_it_contains_a_chart(self, graphicData_uri, expected_value): - graphicFrame = element( - "p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri - ) + graphicFrame = element("p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri) assert GraphicFrame(graphicFrame, None).has_chart is expected_value @pytest.mark.parametrize( @@ -79,9 +74,7 @@ def it_knows_whether_it_contains_a_chart(self, graphicData_uri, expected_value): ), ) def it_knows_whether_it_contains_a_table(self, graphicData_uri, expected_value): - graphicFrame = element( - "p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri - ) + graphicFrame = element("p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri) assert GraphicFrame(graphicFrame, None).has_table is expected_value def it_provides_access_to_the_OleFormat_object(self, request): @@ -130,10 +123,7 @@ def it_raises_on_shadow(self): ) def it_knows_its_shape_type(self, uri, oleObj_child, expected_value): graphicFrame = element( - ( - "p:graphicFrame/a:graphic/a:graphicData{uri=%s}/p:oleObj/p:%s" - % (uri, oleObj_child) - ) + ("p:graphicFrame/a:graphic/a:graphicData{uri=%s}/p:oleObj/p:%s" % (uri, oleObj_child)) if oleObj_child else "p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % uri ) @@ -159,17 +149,15 @@ class Describe_OleFormat(object): def it_provides_access_to_the_OLE_object_blob(self, request): ole_obj_part_ = instance_mock(request, EmbeddedPackagePart, blob=b"0123456789") - property_mock( - request, - _OleFormat, - "part", - return_value=instance_mock( - request, SlidePart, related_parts={"rId7": ole_obj_part_} - ), - ) - graphicData = element("a:graphicData/p:oleObj{r:id=rId7}") + slide_part_ = instance_mock(request, SlidePart) + slide_part_.related_part.return_value = ole_obj_part_ + property_mock(request, _OleFormat, "part", return_value=slide_part_) + ole_format = _OleFormat(element("a:graphicData/p:oleObj{r:id=rId7}"), None) + + blob = ole_format.blob - assert _OleFormat(graphicData, None).blob == b"0123456789" + slide_part_.related_part.assert_called_once_with("rId7") + assert blob == b"0123456789" def it_knows_the_OLE_object_prog_id(self): graphicData = element("a:graphicData/p:oleObj{progId=Excel.Sheet.12}") diff --git a/tests/shapes/test_group.py b/tests/shapes/test_group.py index f9e1248d4..93c06d029 100644 --- a/tests/shapes/test_group.py +++ b/tests/shapes/test_group.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Test suite for pptx.shapes.group module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/shapes/test_picture.py b/tests/shapes/test_picture.py index 3be7c6b89..75728da21 100644 --- a/tests/shapes/test_picture.py +++ b/tests/shapes/test_picture.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Test suite for pptx.shapes.picture module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -10,7 +8,7 @@ from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE from pptx.parts.image import Image from pptx.parts.slide import SlidePart -from pptx.shapes.picture import _BasePicture, _MediaFormat, Movie, Picture +from pptx.shapes.picture import Movie, Picture, _BasePicture, _MediaFormat from pptx.util import Pt from ..unitutil.cxml import element, xml @@ -206,9 +204,7 @@ def image_(self, request): @pytest.fixture def _MediaFormat_(self, request, media_format_): - return class_mock( - request, "pptx.shapes.picture._MediaFormat", return_value=media_format_ - ) + return class_mock(request, "pptx.shapes.picture._MediaFormat", return_value=media_format_) @pytest.fixture def media_format_(self, request): diff --git a/tests/shapes/test_placeholder.py b/tests/shapes/test_placeholder.py index 0d02f21f4..4d9b26ea0 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -1,23 +1,18 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.shapes.placeholder` module.""" -""" -Test suite for pptx.shapes.placeholder module -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import pytest from pptx.chart.data import ChartData +from pptx.enum.chart import XL_CHART_TYPE as XCT from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.oxml.shapes.shared import ST_Direction, ST_PlaceholderSize from pptx.parts.image import ImagePart -from pptx.parts.slide import NotesSlidePart, SlideLayoutPart, SlidePart +from pptx.parts.slide import NotesSlidePart, SlidePart from pptx.shapes.placeholder import ( BasePlaceholder, - _BaseSlidePlaceholder, ChartPlaceholder, - _InheritsDimensions, LayoutPlaceholder, MasterPlaceholder, NotesSlidePlaceholder, @@ -25,6 +20,8 @@ PlaceholderGraphicFrame, PlaceholderPicture, TablePlaceholder, + _BaseSlidePlaceholder, + _InheritsDimensions, ) from pptx.shapes.shapetree import NotesSlidePlaceholders from pptx.slide import NotesMaster, SlideLayout, SlideMaster @@ -44,6 +41,8 @@ class Describe_BaseSlidePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder._BaseSlidePlaceholder` object.""" + def it_knows_its_shape_type(self): placeholder = _BaseSlidePlaceholder(None, None) assert placeholder.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER @@ -52,11 +51,15 @@ def it_provides_override_dimensions_when_present(self, override_fixture): placeholder, prop_name, expected_value = override_fixture assert getattr(placeholder, prop_name) == expected_value - def it_provides_inherited_dims_when_no_override(self, inherited_fixture): - placeholder, prop_name, expected_value = inherited_fixture + @pytest.mark.parametrize("prop_name", ("left", "top", "width", "height")) + def it_provides_inherited_dims_when_no_override(self, request, prop_name): + method_mock(request, _BaseSlidePlaceholder, "_inherited_value", return_value=42) + placeholder = _BaseSlidePlaceholder(element("p:sp/p:spPr"), None) + value = getattr(placeholder, prop_name) - placeholder._inherited_value.assert_called_once_with(prop_name) - assert value == expected_value + + placeholder._inherited_value.assert_called_once_with(placeholder, prop_name) + assert value == 42 def it_gets_an_inherited_dim_value_to_help(self, base_val_fixture): placeholder, attr_name, expected_value = base_val_fixture @@ -115,13 +118,6 @@ def dim_set_fixture(self, request): expected_xml = xml(expected_cxml) return placeholder, prop_name, value, expected_xml - @pytest.fixture(params=["left", "top", "width", "height"]) - def inherited_fixture(self, request, _inherited_value_): - prop_name = request.param - placeholder = _BaseSlidePlaceholder(element("p:sp/p:spPr"), None) - _inherited_value_.return_value = expected_value = 42 - return placeholder, prop_name, expected_value - @pytest.fixture( params=[ ("left", "p:sp/p:spPr/a:xfrm/a:off{x=12}", 12), @@ -149,19 +145,13 @@ def replace_fixture(self): def _base_placeholder_prop_(self, request): return property_mock(request, _BaseSlidePlaceholder, "_base_placeholder") - @pytest.fixture - def _inherited_value_(self, request): - return method_mock(request, _BaseSlidePlaceholder, "_inherited_value") - @pytest.fixture def layout_placeholder_(self, request): return instance_mock(request, LayoutPlaceholder) @pytest.fixture def part_prop_(self, request, slide_part_): - return property_mock( - request, _BaseSlidePlaceholder, "part", return_value=slide_part_ - ) + return property_mock(request, _BaseSlidePlaceholder, "part", return_value=slide_part_) @pytest.fixture def slide_layout_(self, request): @@ -173,6 +163,8 @@ def slide_part_(self, request): class DescribeBasePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.BasePlaceholder` object.""" + def it_knows_its_idx_value(self, idx_fixture): placeholder, idx = idx_fixture assert placeholder.idx == idx @@ -211,9 +203,7 @@ def idx_fixture(self, request): placeholder = BasePlaceholder(shape_elm, None) return placeholder, expected_idx - @pytest.fixture( - params=[(None, ST_Direction.HORZ), (ST_Direction.VERT, ST_Direction.VERT)] - ) + @pytest.fixture(params=[(None, ST_Direction.HORZ), (ST_Direction.VERT, ST_Direction.VERT)]) def orient_fixture(self, request): orient, expected_orient = request.param ph_bldr = a_ph() @@ -285,25 +275,44 @@ def shape_elm_factory(tagname, ph_type, idx): "pic": a_ph().with_type("pic").with_idx(idx), "tbl": a_ph().with_type("tbl").with_idx(idx), }[ph_type] - return ( - root_bldr.with_child(nvXxPr_bldr.with_child(an_nvPr().with_child(ph_bldr))) - ).element + return (root_bldr.with_child(nvXxPr_bldr.with_child(an_nvPr().with_child(ph_bldr)))).element class DescribeChartPlaceholder(object): - def it_can_insert_a_chart_into_itself(self, insert_fixture): - chart_ph, chart_type, chart_data_, graphicFrame = insert_fixture[:4] - rId, PlaceholderGraphicFrame_, ph_graphic_frame_ = insert_fixture[4:] + """Unit-test suite for `pptx.shapes.placeholder.ChartPlaceholder` object.""" + + def it_can_insert_a_chart_into_itself(self, request, part_prop_): + slide_part_ = instance_mock(request, SlidePart) + slide_part_.add_chart_part.return_value = "rId6" + part_prop_.return_value = slide_part_ + graphicFrame = element("p:graphicFrame") + _new_chart_graphicFrame_ = method_mock( + request, + ChartPlaceholder, + "_new_chart_graphicFrame", + return_value=graphicFrame, + ) + _replace_placeholder_with_ = method_mock( + request, ChartPlaceholder, "_replace_placeholder_with" + ) + placeholder_graphic_frame_ = instance_mock(request, PlaceholderGraphicFrame) + PlaceholderGraphicFrame_ = class_mock( + request, + "pptx.shapes.placeholder.PlaceholderGraphicFrame", + return_value=placeholder_graphic_frame_, + ) + chart_data_ = instance_mock(request, ChartData) + chart_ph = ChartPlaceholder( + element("p:sp/p:spPr/a:xfrm/(a:off{x=1,y=2},a:ext{cx=3,cy=4})"), "parent" + ) - ph_graphic_frame = chart_ph.insert_chart(chart_type, chart_data_) + ph_graphic_frame = chart_ph.insert_chart(XCT.PIE, chart_data_) - chart_ph.part.add_chart_part.assert_called_once_with(chart_type, chart_data_) - chart_ph._new_chart_graphicFrame.assert_called_once_with( - rId, chart_ph.left, chart_ph.top, chart_ph.width, chart_ph.height - ) - chart_ph._replace_placeholder_with.assert_called_once_with(graphicFrame) + slide_part_.add_chart_part.assert_called_once_with(XCT.PIE, chart_data_) + _new_chart_graphicFrame_.assert_called_once_with(chart_ph, "rId6", 1, 2, 3, 4) + _replace_placeholder_with_.assert_called_once_with(chart_ph, graphicFrame) PlaceholderGraphicFrame_.assert_called_once_with(graphicFrame, chart_ph._parent) - assert ph_graphic_frame is ph_graphic_frame_ + assert ph_graphic_frame is placeholder_graphic_frame_ def it_creates_a_graphicFrame_element_to_help(self, new_fixture): chart_ph, rId, x, y, cx, cy, expected_xml = new_fixture @@ -312,31 +321,6 @@ def it_creates_a_graphicFrame_element_to_help(self, new_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def insert_fixture( - self, - part_prop_, - chart_data_, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - _new_chart_graphicFrame_, - _replace_placeholder_with_, - ): - sp_cxml = "p:sp/p:spPr/a:xfrm/(a:off{x=1,y=2},a:ext{cx=3,cy=4})" - chart_ph = ChartPlaceholder(element(sp_cxml), "parent") - chart_type, rId, graphicFrame = 42, "rId6", element("p:graphicFrame") - part_prop_.return_value.add_chart_part.return_value = rId - _new_chart_graphicFrame_.return_value = graphicFrame - return ( - chart_ph, - chart_type, - chart_data_, - graphicFrame, - rId, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - ) - @pytest.fixture def new_fixture(self): sp_cxml = "p:sp/p:nvSpPr/p:cNvPr{id=4,name=bar}" @@ -347,40 +331,18 @@ def new_fixture(self): # fixture components --------------------------------------------- - @pytest.fixture - def chart_data_(self, request): - return instance_mock(request, ChartData) - - @pytest.fixture - def _new_chart_graphicFrame_(self, request): - return method_mock(request, ChartPlaceholder, "_new_chart_graphicFrame") - @pytest.fixture def part_prop_(self, request, slide_): return property_mock(request, ChartPlaceholder, "part", return_value=slide_) - @pytest.fixture - def PlaceholderGraphicFrame_(self, request, placeholder_graphic_frame_): - return class_mock( - request, - "pptx.shapes.placeholder.PlaceholderGraphicFrame", - return_value=placeholder_graphic_frame_, - ) - - @pytest.fixture - def placeholder_graphic_frame_(self, request): - return instance_mock(request, PlaceholderGraphicFrame) - - @pytest.fixture - def _replace_placeholder_with_(self, request): - return method_mock(request, ChartPlaceholder, "_replace_placeholder_with") - @pytest.fixture def slide_(self, request): return instance_mock(request, SlidePart) class DescribeLayoutPlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.LayoutPlaceholder` object.""" + def it_uses_InheritsDimensions_mixin(self): layout_placeholder = LayoutPlaceholder(None, None) assert isinstance(layout_placeholder, _InheritsDimensions) @@ -415,14 +377,8 @@ def master_placeholder_(self, request): return instance_mock(request, MasterPlaceholder) @pytest.fixture - def part_prop_(self, request, slide_layout_part_): - return property_mock( - request, LayoutPlaceholder, "part", return_value=slide_layout_part_ - ) - - @pytest.fixture - def slide_layout_part_(self, request): - return instance_mock(request, SlideLayoutPart) + def part_prop_(self, request): + return property_mock(request, LayoutPlaceholder, "part") @pytest.fixture def slide_master_(self, request): @@ -430,6 +386,8 @@ def slide_master_(self, request): class DescribeNotesSlidePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.NotesSlidePlaceholder` object.""" + def it_finds_its_base_placeholder_to_help(self, base_ph_fixture): placeholder, notes_master_, ph_type, master_placeholder_ = base_ph_fixture base_placeholder = placeholder._base_placeholder @@ -475,28 +433,60 @@ def notes_slide_part_(self, request): @pytest.fixture def part_prop_(self, request, notes_slide_part_): - return property_mock( - request, NotesSlidePlaceholder, "part", return_value=notes_slide_part_ - ) + return property_mock(request, NotesSlidePlaceholder, "part", return_value=notes_slide_part_) class DescribePicturePlaceholder(object): - def it_can_insert_a_picture_into_itself(self, insert_fixture): - picture_ph, image_file, pic = insert_fixture[:3] - PlaceholderPicture_, placeholder_picture_ = insert_fixture[3:] + """Unit-test suite for `pptx.shapes.placeholder.PicturePlaceholder` object.""" - placeholder_picture = picture_ph.insert_picture(image_file) + def it_can_insert_a_picture_into_itself(self, request): + pic = element("p:pic") + _new_placeholder_pic_ = method_mock( + request, PicturePlaceholder, "_new_placeholder_pic", return_value=pic + ) + _replace_placeholder_with_ = method_mock( + request, PicturePlaceholder, "_replace_placeholder_with" + ) + placeholder_picture_ = instance_mock(request, PlaceholderPicture) + PlaceholderPicture_ = class_mock( + request, + "pptx.shapes.placeholder.PlaceholderPicture", + return_value=placeholder_picture_, + ) + picture_ph = PicturePlaceholder(None, "parent") - picture_ph._new_placeholder_pic.assert_called_once_with(image_file) - picture_ph._replace_placeholder_with.assert_called_once_with(pic) + placeholder_picture = picture_ph.insert_picture("foobar.png") + + _new_placeholder_pic_.assert_called_once_with(picture_ph, "foobar.png") + _replace_placeholder_with_.assert_called_once_with(picture_ph, pic) PlaceholderPicture_.assert_called_once_with(pic, picture_ph._parent) assert placeholder_picture is placeholder_picture_ - def it_creates_a_pic_element_to_help(self, pic_fixture): - picture_ph, image_file, expected_xml = pic_fixture - pic = picture_ph._new_placeholder_pic(image_file) - picture_ph._get_or_add_image.assert_called_once_with(image_file) - assert pic.xml == expected_xml + @pytest.mark.parametrize( + "image_size, crop_attr_names", + (((444, 333), ("l", "r")), ((333, 444), ("t", "b"))), + ) + def it_creates_a_pic_element_to_help(self, request, image_size, crop_attr_names): + _get_or_add_image_ = method_mock( + request, + PicturePlaceholder, + "_get_or_add_image", + return_value=(42, "bar", image_size), + ) + picture_ph = PicturePlaceholder( + element("p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" ",cy=99})"), + None, + ) + + pic = picture_ph._new_placeholder_pic("foobar.png") + + _get_or_add_image_.assert_called_once_with(picture_ph, "foobar.png") + assert pic.xml == xml( + "p:pic/(p:nvPicPr/(p:cNvPr{id=2,name=foo,descr=bar},p:cNvPicPr/a" + ":picLocks{noGrp=1,noChangeAspect=1},p:nvPr),p:blipFill/(a:blip{" + "r:embed=42},a:srcRect{%s=12500,%s=12500},a:stretch/a:fillRect)," + "p:spPr)" % crop_attr_names + ) def it_adds_an_image_to_help(self, get_or_add_fixture): placeholder, image_file, expected_value = get_or_add_fixture @@ -517,138 +507,58 @@ def get_or_add_fixture(self, part_prop_, image_part_): expected_value = rId, desc, image_size return placeholder, image_file, expected_value - @pytest.fixture - def insert_fixture( - self, - PlaceholderPicture_, - placeholder_picture_, - _new_placeholder_pic_, - _replace_placeholder_with_, - ): - picture_ph = PicturePlaceholder(None, "parent") - image_file, pic = "foobar.png", element("p:pic") - _new_placeholder_pic_.return_value = pic - PlaceholderPicture_.return_value = placeholder_picture_ - return (picture_ph, image_file, pic, PlaceholderPicture_, placeholder_picture_) - - @pytest.fixture(params=[((444, 333), ("l", "r")), ((333, 444), ("t", "b"))]) - def pic_fixture(self, request, _get_or_add_image_): - image_size, crop_attr_names = request.param - sp_cxml = ( - "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" ",cy=99})" - ) - sp = element(sp_cxml) - picture_ph = PicturePlaceholder(sp, None) - image_file = "foobar.png" - _get_or_add_image_.return_value = 42, "bar", image_size - expected_xml = xml( - "p:pic/(p:nvPicPr/(p:cNvPr{id=2,name=foo,descr=bar},p:cNvPicPr/a" - ":picLocks{noGrp=1,noChangeAspect=1},p:nvPr),p:blipFill/(a:blip{" - "r:embed=42},a:srcRect{%s=12500,%s=12500},a:stretch/a:fillRect)," - "p:spPr)" % crop_attr_names - ) - return picture_ph, image_file, expected_xml - # fixture components --------------------------------------------- - @pytest.fixture - def _get_or_add_image_(self, request): - return method_mock(request, PicturePlaceholder, "_get_or_add_image") - @pytest.fixture def image_part_(self, request): return instance_mock(request, ImagePart) - @pytest.fixture - def _new_placeholder_pic_(self, request): - return method_mock(request, PicturePlaceholder, "_new_placeholder_pic") - @pytest.fixture def part_prop_(self, request, slide_): return property_mock(request, PicturePlaceholder, "part", return_value=slide_) - @pytest.fixture - def PlaceholderPicture_(self, request): - return class_mock(request, "pptx.shapes.placeholder.PlaceholderPicture") - - @pytest.fixture - def placeholder_picture_(self, request): - return instance_mock(request, PlaceholderPicture) - - @pytest.fixture - def _replace_placeholder_with_(self, request): - return method_mock(request, PicturePlaceholder, "_replace_placeholder_with") - @pytest.fixture def slide_(self, request): return instance_mock(request, SlidePart) class DescribeTablePlaceholder(object): - def it_can_insert_a_table_into_itself(self, insert_fixture): - table_ph, rows, cols, graphicFrame = insert_fixture[:4] - PlaceholderGraphicFrame_, ph_graphic_frame_ = insert_fixture[4:] + """Unit-test suite for `pptx.shapes.placeholder.TablePlaceholder` object.""" - ph_graphic_frame = table_ph.insert_table(rows, cols) - - table_ph._new_placeholder_table.assert_called_once_with(rows, cols) - table_ph._replace_placeholder_with.assert_called_once_with(graphicFrame) - PlaceholderGraphicFrame_.assert_called_once_with(graphicFrame, table_ph._parent) - assert ph_graphic_frame is ph_graphic_frame_ - - def it_creates_a_graphicFrame_element_to_help(self, new_fixture): - table_ph, rows, cols, expected_xml = new_fixture - graphicFrame = table_ph._new_placeholder_table(rows, cols) - assert graphicFrame.xml == expected_xml - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def insert_fixture( - self, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - _new_placeholder_table_, - _replace_placeholder_with_, - ): - table_ph = TablePlaceholder(None, "parent") - rows, cols, graphicFrame = 4, 2, element("p:graphicFrame") - _new_placeholder_table_.return_value = graphicFrame - PlaceholderGraphicFrame_.return_value = placeholder_graphic_frame_ - return ( - table_ph, - rows, - cols, - graphicFrame, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, + def it_can_insert_a_table_into_itself(self, request): + graphicFrame = element("p:graphicFrame") + _new_placeholder_table_ = method_mock( + request, + TablePlaceholder, + "_new_placeholder_table", + return_value=graphicFrame, ) - - @pytest.fixture - def new_fixture(self): - sp_cxml = ( - "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/(a:off{x=1," - "y=2},a:ext{cx=3,cy=4}))" + _replace_placeholder_with_ = method_mock( + request, TablePlaceholder, "_replace_placeholder_with" ) - table_ph = TablePlaceholder(element(sp_cxml), None) - rows, cols = 1, 1 - expected_xml = snippet_seq("placeholders")[0] - return table_ph, rows, cols, expected_xml - - # fixture components --------------------------------------------- - - @pytest.fixture - def _new_placeholder_table_(self, request): - return method_mock(request, TablePlaceholder, "_new_placeholder_table") + placeholder_graphic_frame_ = instance_mock(request, PlaceholderGraphicFrame) + PlaceholderGraphicFrame_ = class_mock( + request, + "pptx.shapes.placeholder.PlaceholderGraphicFrame", + return_value=placeholder_graphic_frame_, + ) + table_ph = TablePlaceholder(None, "parent") - @pytest.fixture - def PlaceholderGraphicFrame_(self, request): - return class_mock(request, "pptx.shapes.placeholder.PlaceholderGraphicFrame") + ph_graphic_frame = table_ph.insert_table(4, 2) - @pytest.fixture - def placeholder_graphic_frame_(self, request): - return instance_mock(request, PlaceholderGraphicFrame) + _new_placeholder_table_.assert_called_once_with(table_ph, 4, 2) + _replace_placeholder_with_.assert_called_once_with(table_ph, graphicFrame) + PlaceholderGraphicFrame_.assert_called_once_with(graphicFrame, table_ph._parent) + assert ph_graphic_frame is placeholder_graphic_frame_ + + def it_creates_a_graphicFrame_element_to_help(self): + table_ph = TablePlaceholder( + element( + "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/(a:off{x=1,y=2}," + "a:ext{cx=3,cy=4}))" + ), + None, + ) + graphicFrame = table_ph._new_placeholder_table(1, 1) - @pytest.fixture - def _replace_placeholder_with_(self, request): - return method_mock(request, TablePlaceholder, "_replace_placeholder_with") + assert graphicFrame.xml == snippet_seq("placeholders")[0] diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 34f6e41ba..3cf1ab225 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -1,19 +1,21 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit test suite for pptx.shapes.shapetree module""" +from __future__ import annotations + +import io + import pytest -from pptx.compat import BytesIO from pptx.chart.data import ChartData from pptx.enum.chart import XL_CHART_TYPE from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_CONNECTOR, PP_PLACEHOLDER, PROG_ID +from pptx.media import SPEAKER_IMAGE_BYTES, Video from pptx.oxml import parse_xml -from pptx.oxml.shapes.autoshape import CT_Shape from pptx.oxml.shapes.groupshape import CT_GroupShape from pptx.oxml.shapes.picture import CT_Picture from pptx.oxml.shapes.shared import BaseShapeElement, ST_Direction -from pptx.media import SPEAKER_IMAGE_BYTES, Video from pptx.parts.image import ImagePart from pptx.parts.slide import SlidePart from pptx.shapes.autoshape import AutoShapeType, Shape @@ -24,36 +26,36 @@ from pptx.shapes.group import GroupShape from pptx.shapes.picture import Movie, Picture from pptx.shapes.placeholder import ( - _BaseSlidePlaceholder, LayoutPlaceholder, MasterPlaceholder, NotesSlidePlaceholder, + _BaseSlidePlaceholder, ) from pptx.shapes.shapetree import ( - _BaseGroupShapes, BasePlaceholders, BaseShapeFactory, - _BaseShapes, GroupShapes, LayoutPlaceholders, - _LayoutShapeFactory, LayoutShapes, MasterPlaceholders, - _MasterShapeFactory, MasterShapes, - _MoviePicElementCreator, NotesSlidePlaceholders, - _NotesSlideShapeFactory, NotesSlideShapes, - _OleObjectElementCreator, - _SlidePlaceholderFactory, SlidePlaceholders, SlideShapeFactory, SlideShapes, + _BaseGroupShapes, + _BaseShapes, + _LayoutShapeFactory, + _MasterShapeFactory, + _MoviePicElementCreator, + _NotesSlideShapeFactory, + _OleObjectElementCreator, + _SlidePlaceholderFactory, ) from pptx.slide import SlideLayout, SlideMaster from pptx.table import Table -from pptx.util import Emu +from pptx.util import Emu, Inches from ..oxml.unitdata.shape import a_ph, a_pic, an_nvPr, an_nvSpPr, an_sp from ..unitutil.cxml import element, xml @@ -219,8 +221,7 @@ def len_fixture(self): ("p:spTree/p:nvSpPr/(p:cNvPr{id=foo},p:cNvPr{id=2})", 3), ("p:spTree/p:nvSpPr/(p:cNvPr{id=1fo},p:cNvPr{id=2})", 3), ( - "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" - "cNvPr{id=1},p:cNvPr{id=4})", + "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" "cNvPr{id=1},p:cNvPr{id=4})", 5, ), ] @@ -245,9 +246,7 @@ def next_id_fixture(self, request): ) def ph_name_fixture(self, request): ph_type, sp_id, orient, expected_name = request.param - spTree = element( - "p:spTree/(p:cNvPr{name=Title 1},p:cNvPr{name=Table Placeholder " "3})" - ) + spTree = element("p:spTree/(p:cNvPr{name=Title 1},p:cNvPr{name=Table Placeholder " "3})") shapes = SlideShapes(spTree, None) return shapes, ph_type, sp_id, orient, expected_name @@ -265,8 +264,7 @@ def turbo_fixture(self, request): ("p:spTree/p:nvSpPr/p:cNvPr{id=2}", True), ("p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=3})", False), ( - "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" - "cNvPr{id=1},p:cNvPr{id=4})", + "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" "cNvPr{id=1},p:cNvPr{id=4})", True, ), ] @@ -320,9 +318,7 @@ def it_can_add_a_chart( graphic_frame = shapes.add_chart(XL_CHART_TYPE.PIE, x, y, cx, cy, chart_data_) - shapes.part.add_chart_part.assert_called_once_with( - XL_CHART_TYPE.PIE, chart_data_ - ) + shapes.part.add_chart_part.assert_called_once_with(XL_CHART_TYPE.PIE, chart_data_) _add_chart_graphicFrame_.assert_called_once_with(shapes, "rId42", x, y, cx, cy) _recalculate_extents_.assert_called_once_with(shapes) _shape_factory_.assert_called_once_with(shapes, graphicFrame) @@ -348,9 +344,7 @@ def it_can_provide_a_freeform_builder(self, freeform_fixture): builder = shapes.build_freeform(start_x, start_y, scale) - FreeformBuilder_new_.assert_called_once_with( - shapes, start_x, start_y, x_scale, y_scale - ) + FreeformBuilder_new_.assert_called_once_with(shapes, start_x, start_y, x_scale, y_scale) assert builder is builder_ def it_can_add_a_group_shape(self, group_fixture): @@ -376,10 +370,30 @@ def it_can_add_an_ole_object( x, y, cx, cy = 1, 2, 3, 4 shapes = _BaseGroupShapes(element("p:spTree"), None) - shape = shapes.add_ole_object("worksheet.xlsx", PROG_ID.XLSX, x, y, cx, cy) + shape = shapes.add_ole_object( + "worksheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "test.xlsx", + Inches(0.5), + Inches(0.75), + ) _OleObjectElementCreator_.graphicFrame.assert_called_once_with( - shapes, 42, "worksheet.xlsx", PROG_ID.XLSX, x, y, cx, cy, None + shapes, + 42, + "worksheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "test.xlsx", + Inches(0.5), + Inches(0.75), ) assert shapes._spTree[-1] is graphicFrame _recalculate_extents_.assert_called_once_with(shapes) @@ -603,9 +617,7 @@ def add_textbox_sp_fixture(self, _next_shape_id_prop_): return shapes, x, y, cx, cy, expected_xml @pytest.fixture - def connector_fixture( - self, _add_cxnSp_, _shape_factory_, _recalculate_extents_, connector_ - ): + def connector_fixture(self, _add_cxnSp_, _shape_factory_, _recalculate_extents_, connector_): shapes = _BaseGroupShapes(element("p:spTree"), None) connector_type = MSO_CONNECTOR.STRAIGHT begin_x, begin_y, end_x, end_y = 1, 2, 3, 4 @@ -747,9 +759,7 @@ def shape_fixture( ) @pytest.fixture - def textbox_fixture( - self, _add_textbox_sp_, _recalculate_extents_, _shape_factory_, shape_ - ): + def textbox_fixture(self, _add_textbox_sp_, _recalculate_extents_, _shape_factory_, shape_): shapes = _BaseGroupShapes(None, None) x, y, cx, cy = 31, 32, 33, 34 sp = element("p:sp") @@ -763,9 +773,7 @@ def textbox_fixture( @pytest.fixture def _add_chart_graphicFrame_(self, request): - return method_mock( - request, _BaseGroupShapes, "_add_chart_graphicFrame", autospec=True - ) + return method_mock(request, _BaseGroupShapes, "_add_chart_graphicFrame", autospec=True) @pytest.fixture def _add_cxnSp_(self, request): @@ -773,9 +781,7 @@ def _add_cxnSp_(self, request): @pytest.fixture def _add_pic_from_image_part_(self, request): - return method_mock( - request, _BaseGroupShapes, "_add_pic_from_image_part", autospec=True - ) + return method_mock(request, _BaseGroupShapes, "_add_pic_from_image_part", autospec=True) @pytest.fixture def _add_sp_(self, request): @@ -811,7 +817,7 @@ def CT_GroupShape_add_grpSp_(self, request): @pytest.fixture def FreeformBuilder_new_(self, request): - return method_mock(request, FreeformBuilder, "new") + return method_mock(request, FreeformBuilder, "new", autospec=False) @pytest.fixture def graphic_frame_(self, request): @@ -835,9 +841,7 @@ def picture_(self, request): @pytest.fixture def _recalculate_extents_(self, request): - return method_mock( - request, _BaseGroupShapes, "_recalculate_extents", autospec=True - ) + return method_mock(request, _BaseGroupShapes, "_recalculate_extents", autospec=True) @pytest.fixture def shape_(self, request): @@ -1241,9 +1245,7 @@ def it_can_add_a_movie(self, movie_fixture): _MoviePicElementCreator_, movie_pic = movie_fixture[9:11] _add_video_timing_, _shape_factory_, movie_ = movie_fixture[11:] - movie = shapes.add_movie( - movie_file, x, y, cx, cy, poster_frame_image, mime_type - ) + movie = shapes.add_movie(movie_file, x, y, cx, cy, poster_frame_image, mime_type) _MoviePicElementCreator_.new_movie_pic.assert_called_once_with( shapes, shape_id_, movie_file, x, y, cx, cy, poster_frame_image, mime_type @@ -1400,15 +1402,11 @@ def movie_(self, request): @pytest.fixture def _MoviePicElementCreator_(self, request): - return class_mock( - request, "pptx.shapes.shapetree._MoviePicElementCreator", autospec=True - ) + return class_mock(request, "pptx.shapes.shapetree._MoviePicElementCreator", autospec=True) @pytest.fixture def _next_shape_id_prop_(self, request, shape_id_): - return property_mock( - request, SlideShapes, "_next_shape_id", return_value=shape_id_ - ) + return property_mock(request, SlideShapes, "_next_shape_id", return_value=shape_id_) @pytest.fixture def placeholder_(self, request): @@ -1535,9 +1533,7 @@ def parent_(self, request): @pytest.fixture def ph_bldr(self): - return an_sp().with_child( - an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1))) - ) + return an_sp().with_child(an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1)))) class DescribeLayoutPlaceholders(object): @@ -1597,10 +1593,6 @@ def _LayoutShapeFactory_(self, request, placeholder_): autospec=True, ) - @pytest.fixture - def ph_elm_(self, request): - return instance_mock(request, CT_Shape) - @pytest.fixture def placeholder_(self, request): return instance_mock(request, LayoutPlaceholder) @@ -1664,9 +1656,7 @@ def master_placeholder_(self, request): @pytest.fixture def ph_bldr(self): - return an_sp().with_child( - an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1))) - ) + return an_sp().with_child(an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1)))) @pytest.fixture def slide_master_(self, request): @@ -1833,17 +1823,35 @@ def it_adds_the_poster_frame_image_to_help(self, pfrm_rId_fixture): poster_frame_rId = movie_pic_element_creator._poster_frame_rId - slide_part_.get_or_add_image_part.assert_called_once_with( - poster_frame_image_file - ) + slide_part_.get_or_add_image_part.assert_called_once_with(poster_frame_image_file) assert poster_frame_rId == expected_value - def it_gets_the_poster_frame_image_file_to_help(self, pfrm_img_fixture): - movie_pic_element_creator, BytesIO_ = pfrm_img_fixture[:2] - calls, expected_value = pfrm_img_fixture[2:] + def it_gets_the_poster_frame_image_from_the_specified_path_to_help( + self, request: pytest.FixtureRequest + ): + BytesIO_ = class_mock(request, "pptx.shapes.shapetree.io.BytesIO") + movie_pic_element_creator = _MoviePicElementCreator( + None, None, None, None, None, None, None, "image.png", None # type: ignore + ) + image_file = movie_pic_element_creator._poster_frame_image_file - assert BytesIO_.call_args_list == calls - assert image_file == expected_value + + BytesIO_.assert_not_called() + assert image_file == "image.png" + + def but_it_gets_the_poster_frame_image_from_the_default_bytes_when_None_specified( + self, request: pytest.FixtureRequest + ): + stream_ = instance_mock(request, io.BytesIO) + BytesIO_ = class_mock(request, "pptx.shapes.shapetree.io.BytesIO", return_value=stream_) + movie_pic_element_creator = _MoviePicElementCreator( + None, None, None, None, None, None, None, None, None # type: ignore + ) + + image_file = movie_pic_element_creator._poster_frame_image_file + + BytesIO_.assert_called_once_with(SPEAKER_IMAGE_BYTES) + assert image_file == stream_ def it_gets_the_video_part_rIds_to_help(self, part_rIds_fixture): movie_pic_element_creator, slide_part_ = part_rIds_fixture[:2] @@ -1871,9 +1879,7 @@ def media_rId_fixture(self, _video_part_rIds_prop_): return movie_pic_element_creator, expected_value @pytest.fixture - def movie_pic_fixture( - self, shapes_, _MoviePicElementCreator_init_, _pic_prop_, pic_ - ): + def movie_pic_fixture(self, shapes_, _MoviePicElementCreator_init_, _pic_prop_, pic_): shape_id, movie_file, x, y, cx, cy = 42, "movie.mp4", 1, 2, 3, 4 poster_frame_image, mime_type = "image.png", "video/mp4" return ( @@ -1902,25 +1908,8 @@ def part_rIds_fixture(self, slide_part_, video_, _slide_part_prop_, _video_prop_ _video_prop_.return_value = video_ return (movie_pic_element_creator, slide_part_, video_, media_rId, video_rId) - @pytest.fixture(params=["image.png", None]) - def pfrm_img_fixture(self, request, BytesIO_, stream_): - poster_frame_file = request.param - movie_pic_element_creator = _MoviePicElementCreator( - None, None, None, None, None, None, None, poster_frame_file, None - ) - if poster_frame_file is None: - calls = [call(SPEAKER_IMAGE_BYTES)] - BytesIO_.return_value = stream_ - expected_value = stream_ - else: - calls = [] - expected_value = poster_frame_file - return movie_pic_element_creator, BytesIO_, calls, expected_value - @pytest.fixture - def pfrm_rId_fixture( - self, _slide_part_prop_, slide_part_, _poster_frame_image_file_prop_ - ): + def pfrm_rId_fixture(self, _slide_part_prop_, slide_part_, _poster_frame_image_file_prop_): movie_pic_element_creator = _MoviePicElementCreator( None, None, None, None, None, None, None, None, None ) @@ -2006,13 +1995,9 @@ def video_fixture(self, video_, from_path_or_file_like_): # fixture components --------------------------------------------- - @pytest.fixture - def BytesIO_(self, request): - return class_mock(request, "pptx.shapes.shapetree.BytesIO") - @pytest.fixture def from_path_or_file_like_(self, request): - return method_mock(request, Video, "from_path_or_file_like") + return method_mock(request, Video, "from_path_or_file_like", autospec=False) @pytest.fixture def _media_rId_prop_(self, request): @@ -2024,7 +2009,7 @@ def _MoviePicElementCreator_init_(self, request): @pytest.fixture def new_video_pic_(self, request): - return method_mock(request, CT_Picture, "new_video_pic") + return method_mock(request, CT_Picture, "new_video_pic", autospec=False) @pytest.fixture def pic_(self): @@ -2032,15 +2017,11 @@ def pic_(self): @pytest.fixture def _pic_prop_(self, request, pic_): - return property_mock( - request, _MoviePicElementCreator, "_pic", return_value=pic_ - ) + return property_mock(request, _MoviePicElementCreator, "_pic", return_value=pic_) @pytest.fixture def _poster_frame_image_file_prop_(self, request): - return property_mock( - request, _MoviePicElementCreator, "_poster_frame_image_file" - ) + return property_mock(request, _MoviePicElementCreator, "_poster_frame_image_file") @pytest.fixture def _poster_frame_rId_prop_(self, request): @@ -2062,10 +2043,6 @@ def slide_part_(self, request): def _slide_part_prop_(self, request): return property_mock(request, _MoviePicElementCreator, "_slide_part") - @pytest.fixture - def stream_(self, request): - return instance_mock(request, BytesIO) - @pytest.fixture def video_(self, request): return instance_mock(request, Video) @@ -2098,33 +2075,46 @@ def it_provides_a_graphicFrame_interface_method(self, request, shapes_): ) graphicFrame = _OleObjectElementCreator.graphicFrame( - shapes_, shape_id, "sheet.xlsx", PROG_ID.XLSX, x, y, cx, cy, "icon.png" + shapes_, + shape_id, + "sheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "icon.png", + Inches(0.5), + Inches(0.75), ) _init_.assert_called_once_with( - ANY, shapes_, shape_id, "sheet.xlsx", PROG_ID.XLSX, x, y, cx, cy, "icon.png" + ANY, + shapes_, + shape_id, + "sheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "icon.png", + Inches(0.5), + Inches(0.75), ) _graphicFrame_prop_.assert_called_once_with() assert graphicFrame is graphicFrame_ def it_creates_the_graphicFrame_element(self, request): shape_id, x, y, cx, cy = 7, 1, 2, 3, 4 - property_mock( - request, _OleObjectElementCreator, "_shape_name", return_value="Object 42" - ) - property_mock( - request, _OleObjectElementCreator, "_ole_object_rId", return_value="rId42" - ) - property_mock( - request, _OleObjectElementCreator, "_progId", return_value="Excel.Sheet.42" - ) - property_mock( - request, _OleObjectElementCreator, "_icon_rId", return_value="rId24" - ) + property_mock(request, _OleObjectElementCreator, "_shape_name", return_value="Object 42") + property_mock(request, _OleObjectElementCreator, "_ole_object_rId", return_value="rId42") + property_mock(request, _OleObjectElementCreator, "_progId", return_value="Excel.Sheet.42") + property_mock(request, _OleObjectElementCreator, "_icon_rId", return_value="rId24") property_mock(request, _OleObjectElementCreator, "_cx", return_value=cx) property_mock(request, _OleObjectElementCreator, "_cy", return_value=cy) element_creator = _OleObjectElementCreator( - None, shape_id, None, None, x, y, cx, cy, None + None, shape_id, None, None, x, y, cx, cy, None, Inches(0.5), Inches(0.75) ) assert element_creator._graphicFrame.xml == ( @@ -2147,7 +2137,7 @@ def it_creates_the_graphicFrame_element(self, request): " \n" " \n' - ' \n' " \n" " \n" @@ -2188,9 +2178,9 @@ def it_creates_the_graphicFrame_element(self, request): (None, "Foo.Bar.6", Emu(965200)), ), ) - def it_determines_the_icon_width_to_help(self, cx_arg, prog_id, expected_value): + def it_determines_the_shape_width_to_help(self, cx_arg, prog_id, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id, None, None, cx_arg, None, None + None, None, None, prog_id, None, None, cx_arg, None, None, None, None ) assert element_creator._cx == expected_value @@ -2204,12 +2194,25 @@ def it_determines_the_icon_width_to_help(self, cx_arg, prog_id, expected_value): (None, "Foo.Bar.6", Emu(609600)), ), ) - def it_determines_the_icon_height_to_help(self, cy_arg, prog_id, expected_value): + def it_determines_the_shape_height_to_help(self, cy_arg, prog_id, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id, None, None, None, cy_arg, None + None, None, None, prog_id, None, None, None, cy_arg, None, None, None ) assert element_creator._cy == expected_value + @pytest.mark.parametrize( + "icon_height_arg, expected_value", + ( + (Emu(666666), Emu(666666)), + (None, Emu(609600)), + ), + ) + def it_determines_the_icon_height_to_help(self, icon_height_arg, expected_value): + element_creator = _OleObjectElementCreator( + None, None, None, None, None, None, None, None, None, None, icon_height_arg + ) + assert element_creator._icon_height == expected_value + @pytest.mark.parametrize( "icon_file_arg, prog_id, expected_value", ( @@ -2220,11 +2223,9 @@ def it_determines_the_icon_height_to_help(self, cy_arg, prog_id, expected_value) (None, PROG_ID.XLSX, "xlsx-icon.emf"), ), ) - def it_resolves_the_icon_image_file_to_help( - self, icon_file_arg, prog_id, expected_value - ): + def it_resolves_the_icon_image_file_to_help(self, icon_file_arg, prog_id, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id, None, None, None, None, icon_file_arg + None, None, None, prog_id, None, None, None, None, icon_file_arg, None, None ) assert element_creator._icon_image_file.endswith(expected_value) @@ -2240,7 +2241,7 @@ def it_adds_and_relates_the_icon_image_part_to_help( slide_part_.get_or_add_image_part.return_value = None, "rId16" _slide_part_prop_.return_value = slide_part_ element_creator = _OleObjectElementCreator( - None, None, None, None, None, None, None, None, None + None, None, None, None, None, None, None, None, None, None, None ) rId = element_creator._icon_rId @@ -2248,6 +2249,16 @@ def it_adds_and_relates_the_icon_image_part_to_help( slide_part_.get_or_add_image_part.assert_called_once_with("obj-icon.emf") assert rId == "rId16" + @pytest.mark.parametrize( + "icon_width_arg, expected_value", + ((Emu(666666), Emu(666666)), (None, Emu(965200))), + ) + def it_determines_the_icon_width_to_help(self, icon_width_arg, expected_value): + element_creator = _OleObjectElementCreator( + None, None, None, None, None, None, None, None, None, icon_width_arg, None + ) + assert element_creator._icon_width == expected_value + def it_adds_and_relates_the_ole_object_part_to_help( self, request, _slide_part_prop_, slide_part_ ): @@ -2255,7 +2266,17 @@ def it_adds_and_relates_the_ole_object_part_to_help( slide_part_.add_embedded_ole_object_part.return_value = "rId14" _slide_part_prop_.return_value = slide_part_ element_creator = _OleObjectElementCreator( - None, None, ole_object_file, PROG_ID.DOCX, None, None, None, None, None + None, + None, + ole_object_file, + PROG_ID.DOCX, + None, + None, + None, + None, + None, + None, + None, ) rId = element_creator._ole_object_rId @@ -2276,21 +2297,21 @@ def it_adds_and_relates_the_ole_object_part_to_help( ) def it_resolves_the_progId_str_to_help(self, prog_id_arg, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id_arg, None, None, None, None, None + None, None, None, prog_id_arg, None, None, None, None, None, None, None ) assert element_creator._progId == expected_value def it_computes_the_shape_name_to_help(self): shape_id = 42 element_creator = _OleObjectElementCreator( - None, shape_id, None, None, None, None, None, None, None + None, shape_id, None, None, None, None, None, None, None, None, None ) assert element_creator._shape_name == "Object 41" def it_provides_access_to_the_slide_part_to_help(self, shapes_, slide_part_): shapes_.part = slide_part_ element_creator = _OleObjectElementCreator( - shapes_, None, None, None, None, None, None, None, None + shapes_, None, None, None, None, None, None, None, None, None, None ) assert element_creator._slide_part is slide_part_ diff --git a/tests/test_action.py b/tests/test_action.py index 3d9ef020c..dd0193ca6 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -1,16 +1,13 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.action` module.""" -""" -Test suite for pptx.action module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest from pptx.action import ActionSetting, Hyperlink from pptx.enum.action import PP_ACTION from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.package import XmlPart from pptx.parts.slide import SlidePart from pptx.slide import Slide @@ -19,6 +16,8 @@ class DescribeActionSetting(object): + """Unit-test suite for `pptx.action.ActionSetting` objects.""" + def it_knows_its_action_type(self, action_fixture): action_setting, expected_action = action_fixture action = action_setting.action @@ -36,15 +35,30 @@ def it_can_find_its_slide_jump_target(self, target_get_fixture): target_slide = action_setting.target_slide assert target_slide == expected_value - def it_can_change_its_slide_jump_target(self, target_set_fixture): - action_setting, value, expected_xml = target_set_fixture[:3] - slide_part_, calls = target_set_fixture[3:] + def it_can_change_its_slide_jump_target( + self, request, _clear_click_action_, slide_, part_prop_, part_ + ): + part_prop_.return_value = part_ + part_.relate_to.return_value = "rId42" + slide_part_ = instance_mock(request, SlidePart) + slide_.part = slide_part_ + action_setting = ActionSetting(element("p:cNvPr{a:b=c,r:s=t}"), None) + + action_setting.target_slide = slide_ - action_setting.target_slide = value + _clear_click_action_.assert_called_once_with(action_setting) + part_.relate_to.assert_called_once_with(slide_part_, RT.SLIDE) + assert action_setting._element.xml == xml( + "p:cNvPr{a:b=c,r:s=t}/a:hlinkClick{action=ppaction://hlinksldjump,r:id=rI" "d42}", + ) - action_setting._clear_click_action.assert_called_once_with() - assert action_setting._element.xml == expected_xml - assert slide_part_.relate_to.call_args_list == calls + def but_it_clears_the_target_slide_if_None_is_assigned(self, _clear_click_action_): + action_setting = ActionSetting(element("p:cNvPr{a:b=c,r:s=t}"), None) + + action_setting.target_slide = None + + _clear_click_action_.assert_called_once_with(action_setting) + assert action_setting._element.xml == xml("p:cNvPr{a:b=c,r:s=t}") def it_raises_on_no_next_prev_slide(self, target_raise_fixture): action_setting = target_raise_fixture @@ -112,6 +126,7 @@ def it_clears_the_click_action_to_help(self, clear_fixture): "ppaction://hlinkshowjump?jump=lastslideviewed", PP_ACTION.LAST_SLIDE_VIEWED, ), + ("p:cNvPr/a:hlinkClick", "ppaction://media", PP_ACTION.NONE), ] ) def action_fixture(self, request): @@ -132,11 +147,11 @@ def action_fixture(self, request): ), ] ) - def clear_fixture(self, request, part_prop_, slide_part_): + def clear_fixture(self, request, part_prop_, part_): xPr_cxml, rId, expected_cxml = request.param action_setting = ActionSetting(element(xPr_cxml), None) - part_prop_.return_value = slide_part_ + part_prop_.return_value = part_ calls = [call(rId)] if rId else [] expected_xml = xml(expected_cxml) @@ -186,39 +201,12 @@ def target_get_fixture(self, request, action_prop_, _slide_index_prop_, part_pro # this becomes the return value of ActionSetting._slides prs_part_ = part_prop_.return_value.package.presentation_part prs_part_.presentation.slides = [0, 1, 2, 3, 4, 5] - related_parts_ = part_prop_.return_value.related_parts - related_parts_.__getitem__.return_value.slide = 4 + related_part_ = part_prop_.return_value.related_part + related_part_.return_value.slide = 4 return action_setting, expected_value - @pytest.fixture( - params=[ - (None, "p:cNvPr{a:b=c,r:s=t}"), - ( - "slide_", - "p:cNvPr{a:b=c,r:s=t}/a:hlinkClick{action=ppaction://hlinksldjump,r" - ":id=rId42}", - ), - ] - ) - def target_set_fixture( - self, request, slide_, _clear_click_action_, part_prop_, slide_part_ - ): - value_key, expected_cxml = request.param - action_setting = ActionSetting(element("p:cNvPr{a:b=c,r:s=t}"), None) - value = None if value_key is None else slide_ - - part_prop_.return_value = slide_part_ - slide_part_.relate_to.return_value = "rId42" - slide_.part = slide_part_ - - expected_xml = xml(expected_cxml) - calls = [] if value is None else [call(slide_part_, RT.SLIDE)] - return action_setting, value, expected_xml, slide_part_, calls - @pytest.fixture(params=[(PP_ACTION.NEXT_SLIDE, 2), (PP_ACTION.PREVIOUS_SLIDE, 0)]) - def target_raise_fixture( - self, request, action_prop_, part_prop_, _slide_index_prop_ - ): + def target_raise_fixture(self, request, action_prop_, part_prop_, _slide_index_prop_): action_type, slide_idx = request.param action_setting = ActionSetting(None, None) action_prop_.return_value = action_type @@ -245,6 +233,10 @@ def Hyperlink_(self, request, hyperlink_): def hyperlink_(self, request): return instance_mock(request, Hyperlink) + @pytest.fixture + def part_(self, request): + return instance_mock(request, XmlPart) + @pytest.fixture def part_prop_(self, request): return property_mock(request, ActionSetting, "part") @@ -257,12 +249,10 @@ def slide_(self, request): def _slide_index_prop_(self, request): return property_mock(request, ActionSetting, "_slide_index") - @pytest.fixture - def slide_part_(self, request): - return instance_mock(request, SlidePart) - class DescribeHyperlink(object): + """Unit-test suite for `pptx.action.Hyperlink` objects.""" + def it_knows_the_target_url_of_the_hyperlink(self, address_fixture): hyperlink, rId, expected_address = address_fixture address = hyperlink.address diff --git a/tests/test_api.py b/tests/test_api.py index d86d728e6..a48f48912 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,15 +1,12 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.api` module.""" -""" -Test suite for pptx.api module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import os import pytest +import pptx from pptx.api import Presentation from pptx.opc.constants import CONTENT_TYPE as CT from pptx.parts.presentation import PresentationPart @@ -29,9 +26,7 @@ def it_opens_default_template_on_no_path_provided(self, call_fixture): @pytest.fixture def call_fixture(self, Package_, prs_, prs_part_): path = os.path.abspath( - os.path.join( - os.path.split(__file__)[0], "../pptx/templates", "default.pptx" - ) + os.path.join(os.path.split(pptx.__file__)[0], "templates", "default.pptx") ) Package_.open.return_value.main_document_part = prs_part_ prs_part_.content_type = CT.PML_PRESENTATION_MAIN diff --git a/tests/test_enum.py b/tests/test_enum.py deleted file mode 100644 index db1ff58d0..000000000 --- a/tests/test_enum.py +++ /dev/null @@ -1,120 +0,0 @@ -# encoding: utf-8 - -"""Unit-test suite for `pptx.enum` subpackage, 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 most of the tests themselves just -make assertions on those. -""" - -import pytest - -from pptx.enum.base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) -from pptx.enum.shapes import PROG_ID, _ProgIdEnum -from pptx.util import Emu - - -@alias("BARFOO") -class FOOBAR(Enumeration): - """ - Enumeration docstring - """ - - __ms_name__ = "MsoFoobar" - - __url__ = "http://msdn.microsoft.com/foobar.aspx" - - __members__ = ( - EnumMember("READ_WRITE", 1, "Readable and settable"), - ReturnValueOnlyEnumMember("READ_ONLY", -2, "Return value only"), - ) - - -@alias("XML-FU") -class XMLFOO(XmlEnumeration): - """ - XmlEnumeration docstring - """ - - __ms_name__ = "MsoXmlFoobar" - - __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;"), - ) - - -class DescribeEnumeration(object): - def it_has_the_right_metaclass(self): - assert type(FOOBAR).__name__ == "MetaEnumeration" - - def it_provides_an_EnumValue_instance_for_each_named_member(self): - for obj in (FOOBAR.READ_WRITE, FOOBAR.READ_ONLY): - assert type(obj).__name__ == "EnumValue" - - 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(FOOBAR.READ_WRITE) - with pytest.raises(ValueError): - FOOBAR.validate("foobar") - with pytest.raises(ValueError): - FOOBAR.validate(FOOBAR.READ_ONLY) - - def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): - assert BARFOO is FOOBAR # noqa - - -class DescribeEnumValue(object): - def it_provides_its_symbolic_name_as_its_string_value(self): - 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" - - -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): - 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)" - - -class Describe_ProgIdEnum(object): - """Unit-test suite for `pptx.enum.shapes._ProgIdEnum.""" - - def it_provides_access_to_its_members(self): - assert type(PROG_ID.XLSX) == _ProgIdEnum.Member - - def it_can_test_an_item_for_membership(self): - assert PROG_ID.XLSX in PROG_ID - - def it_has_a_readable_representation_for_itself(self): - assert repr(PROG_ID) == "pptx.enum.shapes.PROG_ID" - - def it_has_a_readable_representation_for_each_of_its_members(self): - assert repr(PROG_ID.XLSX) == "PROG_ID.XLSX" - - def it_has_attributes_on_each_member(self): - assert PROG_ID.XLSX.height == Emu(609600) - assert PROG_ID.XLSX.icon_filename == "xlsx-icon.emf" - assert PROG_ID.XLSX.progId == "Excel.Sheet.12" - assert PROG_ID.XLSX.width == Emu(965200) diff --git a/tests/test_files/snippets/content-types-xml.txt b/tests/test_files/snippets/content-types-xml.txt new file mode 100644 index 000000000..9e8ee1feb --- /dev/null +++ b/tests/test_files/snippets/content-types-xml.txt @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/test_files/snippets/package-rels-xml.txt b/tests/test_files/snippets/package-rels-xml.txt new file mode 100644 index 000000000..fc6731bc5 --- /dev/null +++ b/tests/test_files/snippets/package-rels-xml.txt @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/test_files/snippets/presentation-rels-xml.txt b/tests/test_files/snippets/presentation-rels-xml.txt new file mode 100644 index 000000000..bb27c2bfd --- /dev/null +++ b/tests/test_files/snippets/presentation-rels-xml.txt @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/test_files/snippets/relationships.txt b/tests/test_files/snippets/relationships.txt new file mode 100644 index 000000000..db1df33f8 --- /dev/null +++ b/tests/test_files/snippets/relationships.txt @@ -0,0 +1,2 @@ + + diff --git a/tests/test_files/snippets/rels-load-from-xml.txt b/tests/test_files/snippets/rels-load-from-xml.txt new file mode 100644 index 000000000..acbdfcc96 --- /dev/null +++ b/tests/test_files/snippets/rels-load-from-xml.txt @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/test_media.py b/tests/test_media.py index 8b9601f41..be72f6e0e 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -1,26 +1,30 @@ -# encoding: utf-8 +"""Unit test suite for `pptx.media` module.""" -"""Unit test suite for pptx.media module.""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +import io import pytest -from pptx.compat import BytesIO from pptx.media import Video from .unitutil.file import absjoin, test_file_dir from .unitutil.mock import initializer_mock, instance_mock, method_mock, property_mock - TEST_VIDEO_PATH = absjoin(test_file_dir, "dummy.mp4") class DescribeVideo(object): - def it_can_construct_from_a_path(self, from_path_fixture): - movie_path, mime_type, blob, filename, video_ = from_path_fixture - video = Video.from_path_or_file_like(movie_path, mime_type) - Video.from_blob.assert_called_once_with(blob, mime_type, filename) + """Unit-test suite for `pptx.media.Video` objects.""" + + def it_can_construct_from_a_path(self, video_, from_blob_): + with open(TEST_VIDEO_PATH, "rb") as f: + blob = f.read() + from_blob_.return_value = video_ + + video = Video.from_path_or_file_like(TEST_VIDEO_PATH, "video/mp4") + + Video.from_blob.assert_called_once_with(blob, "video/mp4", "dummy.mp4") assert video is video_ def it_can_construct_from_a_stream(self, from_stream_fixture): @@ -83,9 +87,7 @@ def ext_fixture(self, request): video = Video(None, mime_type, filename) return video, expected_value - @pytest.fixture( - params=[("foobar.mp4", None, "foobar.mp4"), (None, "vid", "movie.vid")] - ) + @pytest.fixture(params=[("foobar.mp4", None, "foobar.mp4"), (None, "vid", "movie.vid")]) def filename_fixture(self, request, ext_prop_): filename, ext, expected_value = request.param video = Video(None, None, filename) @@ -97,20 +99,11 @@ def from_blob_fixture(self, Video_init_): blob, mime_type, filename = "01234", "video/mp4", "movie.mp4" return blob, mime_type, filename, Video_init_ - @pytest.fixture - def from_path_fixture(self, video_, from_blob_): - movie_path, mime_type = TEST_VIDEO_PATH, "video/mp4" - with open(movie_path, "rb") as f: - blob = f.read() - filename = "dummy.mp4" - from_blob_.return_value = video_ - return movie_path, mime_type, blob, filename, video_ - @pytest.fixture def from_stream_fixture(self, video_, from_blob_): with open(TEST_VIDEO_PATH, "rb") as f: blob = f.read() - movie_stream = BytesIO(blob) + movie_stream = io.BytesIO(blob) mime_type = "video/mp4" from_blob_.return_value = video_ return movie_stream, mime_type, blob, video_ @@ -130,7 +123,7 @@ def ext_prop_(self, request): @pytest.fixture def from_blob_(self, request): - return method_mock(request, Video, "from_blob") + return method_mock(request, Video, "from_blob", autospec=False) @pytest.fixture def video_(self, request): diff --git a/tests/test_package.py b/tests/test_package.py index a4895a990..ee02af2d6 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,29 +1,34 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.package module -""" +"""Unit-test suite for `pptx.package` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +import os import pytest +import pptx from pptx.media import Video from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import Part, _Relationship from pptx.opc.packuri import PackURI -from pptx.package import _ImageParts, _MediaParts, Package +from pptx.package import Package, _ImageParts, _MediaParts from pptx.parts.coreprops import CorePropertiesPart from pptx.parts.image import Image, ImagePart from pptx.parts.media import MediaPart - from .unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock class DescribePackage(object): + """Unit-test suite for `pptx.package.Package` objects.""" + def it_provides_access_to_its_core_properties_part(self): - pkg = Package.open("pptx/templates/default.pptx") + default_pptx = os.path.abspath( + os.path.join(os.path.split(pptx.__file__)[0], "templates", "default.pptx") + ) + pkg = Package.open(default_pptx) assert isinstance(pkg.core_properties, CorePropertiesPart) def it_can_get_or_add_an_image_part(self, image_part_fixture): @@ -58,7 +63,7 @@ def it_provides_access_to_its_MediaParts_object(self, m_parts_fixture): @pytest.fixture def image_part_fixture(self, image_parts_, image_part_, _image_parts_prop_): - package = Package() + package = Package(None) image_file = "foobar.png" _image_parts_prop_.return_value = image_parts_ image_parts_.get_or_add_image_part.return_value = image_part_ @@ -66,21 +71,21 @@ def image_part_fixture(self, image_parts_, image_part_, _image_parts_prop_): @pytest.fixture def media_part_fixture(self, media_, media_part_, _media_parts_prop_, media_parts_): - package = Package() + package = Package(None) _media_parts_prop_.return_value = media_parts_ media_parts_.get_or_add_media_part.return_value = media_part_ return package, media_, media_part_ @pytest.fixture def m_parts_fixture(self, _MediaParts_, media_parts_): - package = Package() + package = Package(None) _MediaParts_.return_value = media_parts_ return package, _MediaParts_, media_parts_ @pytest.fixture(params=[((3, 4, 2), 1), ((4, 2, 1), 3), ((2, 3, 1), 4)]) def next_fixture(self, request, iter_parts_): idxs, idx = request.param - package = Package() + package = Package(None) package.iter_parts.return_value = self.i_image_parts(request, idxs) ext = "foo" expected_value = "/ppt/media/image%d.%s" % (idx, ext) @@ -89,7 +94,7 @@ def next_fixture(self, request, iter_parts_): @pytest.fixture(params=[((3, 4, 2), 1), ((4, 2, 1), 3), ((2, 3, 1), 4)]) def nmp_fixture(self, request, iter_parts_): idxs, idx = request.param - package = Package() + package = Package(None) package.iter_parts.return_value = self.i_media_parts(request, idxs) ext = "foo" expected_value = "/ppt/media/media%d.%s" % (idx, ext) @@ -149,27 +154,35 @@ def _media_parts_prop_(self, request): class Describe_ImageParts(object): + """Unit-test suite for `pptx.package._ImageParts` objects.""" + def it_can_iterate_over_the_package_image_parts(self, iter_fixture): image_parts, expected_parts = iter_fixture assert list(image_parts) == expected_parts - def it_can_get_a_matching_image_part(self, get_fixture): - image_parts, image_file, Image_, image_, image_part_ = get_fixture + def it_can_get_a_matching_image_part(self, Image_, image_, image_part_, _find_by_sha1_): + Image_.from_file.return_value = image_ + _find_by_sha1_.return_value = image_part_ + image_parts = _ImageParts(None) - image_part = image_parts.get_or_add_image_part(image_file) + image_part = image_parts.get_or_add_image_part("image.png") - Image_.from_file.assert_called_once_with(image_file) - image_parts._find_by_sha1.assert_called_once_with(image_.sha1) + Image_.from_file.assert_called_once_with("image.png") + _find_by_sha1_.assert_called_once_with(image_parts, image_.sha1) assert image_part is image_part_ - def it_can_add_an_image_part(self, add_fixture): - image_parts, image_file, Image_, image_ = add_fixture[:4] - ImagePart_, package_, image_part_ = add_fixture[4:] + def it_can_add_an_image_part( + self, package_, Image_, image_, _find_by_sha1_, ImagePart_, image_part_ + ): + Image_.from_file.return_value = image_ + _find_by_sha1_.return_value = None + ImagePart_.new.return_value = image_part_ + image_parts = _ImageParts(package_) - image_part = image_parts.get_or_add_image_part(image_file) + image_part = image_parts.get_or_add_image_part("image.png") - Image_.from_file.assert_called_once_with(image_file) - image_parts._find_by_sha1.assert_called_once_with(image_.sha1) + Image_.from_file.assert_called_once_with("image.png") + _find_by_sha1_.assert_called_once_with(image_parts, image_.sha1) ImagePart_.new.assert_called_once_with(package_, image_) assert image_part is image_part_ @@ -192,25 +205,6 @@ def but_it_skips_unsupported_image_types(self, request, _iter_): # fixtures --------------------------------------------- - @pytest.fixture - def add_fixture( - self, package_, Image_, image_, _find_by_sha1_, ImagePart_, image_part_ - ): - image_parts = _ImageParts(package_) - image_file = "foobar.png" - Image_.from_file.return_value = image_ - _find_by_sha1_.return_value = None - ImagePart_.new.return_value = image_part_ - return ( - image_parts, - image_file, - Image_, - image_, - ImagePart_, - package_, - image_part_, - ) - @pytest.fixture(params=[True, False]) def find_fixture(self, request, _iter_, image_part_): image_part_is_present = request.param @@ -225,14 +219,6 @@ def find_fixture(self, request, _iter_, image_part_): expected_value = None return image_parts, sha1, expected_value - @pytest.fixture - def get_fixture(self, Image_, image_, image_part_, _find_by_sha1_): - image_parts = _ImageParts(None) - image_file = "foobar.png" - Image_.from_file.return_value = image_ - _find_by_sha1_.return_value = image_part_ - return image_parts, image_file, Image_, image_, image_part_ - @pytest.fixture def iter_fixture(self, request, package_): def rel(is_external, reltype): @@ -284,6 +270,8 @@ def package_(self, request): class Describe_MediaParts(object): + """Unit-test suite for `pptx.package._MediaParts` objects.""" + def it_can_iterate_the_media_parts_in_the_package(self, iter_fixture): media_parts, expected_parts = iter_fixture assert list(media_parts) == expected_parts diff --git a/tests/test_presentation.py b/tests/test_presentation.py index 03d2b027a..7c5315143 100644 --- a/tests/test_presentation.py +++ b/tests/test_presentation.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.presentation` module.""" -""" -Test suite for pptx.presentation module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -71,9 +67,7 @@ def it_provides_access_to_its_slide_master(self, master_fixture): def it_provides_access_to_its_slide_masters(self, masters_fixture): prs, SlideMasters_, slide_masters_, expected_xml = masters_fixture slide_masters = prs.slide_masters - SlideMasters_.assert_called_once_with( - prs._element.xpath("p:sldMasterIdLst")[0], prs - ) + SlideMasters_.assert_called_once_with(prs._element.xpath("p:sldMasterIdLst")[0], prs) assert slide_masters is slide_masters_ assert prs._element.xml == expected_xml @@ -93,9 +87,7 @@ def core_props_fixture(self, prs_part_, core_properties_): @pytest.fixture def layouts_fixture(self, masters_prop_, slide_layouts_): prs = Presentation(None, None) - masters_prop_.return_value.__getitem__.return_value.slide_layouts = ( - slide_layouts_ - ) + masters_prop_.return_value.__getitem__.return_value.slide_layouts = slide_layouts_ return prs, slide_layouts_ @pytest.fixture @@ -134,9 +126,7 @@ def save_fixture(self, prs_part_): file_ = "foobar.docx" return prs, file_, prs_part_ - @pytest.fixture( - params=[("p:presentation", None), ("p:presentation/p:sldSz{cy=42}", 42)] - ) + @pytest.fixture(params=[("p:presentation", None), ("p:presentation/p:sldSz{cy=42}", 42)]) def sld_height_get_fixture(self, request): prs_cxml, expected_value = request.param prs = Presentation(element(prs_cxml), None) @@ -154,9 +144,7 @@ def sld_height_set_fixture(self, request): expected_xml = xml(expected_cxml) return prs, 914400, expected_xml - @pytest.fixture( - params=[("p:presentation", None), ("p:presentation/p:sldSz{cx=42}", 42)] - ) + @pytest.fixture(params=[("p:presentation", None), ("p:presentation/p:sldSz{cx=42}", 42)]) def sld_width_get_fixture(self, request): prs_cxml, expected_value = request.param prs = Presentation(element(prs_cxml), None) @@ -224,9 +212,7 @@ def slide_layouts_(self, request): @pytest.fixture def SlideMasters_(self, request, slide_masters_): - return class_mock( - request, "pptx.presentation.SlideMasters", return_value=slide_masters_ - ) + return class_mock(request, "pptx.presentation.SlideMasters", return_value=slide_masters_) @pytest.fixture def slide_master_(self, request): diff --git a/tests/test_shared.py b/tests/test_shared.py index 4b57d47c6..72a0ebc0e 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.shared` module.""" -""" -Test suite for the docx.shared module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -16,10 +12,7 @@ class DescribeElementProxy(object): - def it_raises_on_assign_to_undefined_attr(self): - element_proxy = ElementProxy(None) - with pytest.raises(AttributeError): - element_proxy.foobar = 42 + """Unit-test suite for `pptx.shared.ElementProxy` objects.""" def it_knows_when_its_equal_to_another_proxy_object(self, eq_fixture): proxy, proxy_2, proxy_3, not_a_proxy = eq_fixture @@ -55,6 +48,8 @@ def eq_fixture(self): class DescribeParentedElementProxy(object): + """Unit-test suite for `pptx.shared.ParentedElementProxy` objects.""" + def it_knows_its_parent(self, parent_fixture): proxy, parent = parent_fixture assert proxy.parent is parent diff --git a/tests/test_slide.py b/tests/test_slide.py index 520acba7c..74b528c3b 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -1,8 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.slide module""" +"""Unit-test suite for `pptx.slide` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -25,9 +25,6 @@ SlideShapes, ) from pptx.slide import ( - _Background, - _BaseMaster, - _BaseSlide, NotesMaster, NotesSlide, Slide, @@ -36,6 +33,9 @@ SlideMaster, SlideMasters, Slides, + _Background, + _BaseMaster, + _BaseSlide, ) from pptx.text.text import TextFrame @@ -44,6 +44,8 @@ class Describe_BaseSlide(object): + """Unit-test suite for `pptx.slide._BaseSlide` objects.""" + def it_knows_its_name(self, name_get_fixture): base_slide, expected_value = name_get_fixture assert base_slide.name == expected_value @@ -71,9 +73,7 @@ def background_fixture(self, _Background_, background_): _Background_.return_value = background_ return slide, _Background_, cSld, background_ - @pytest.fixture( - params=[("p:sld/p:cSld", ""), ("p:sld/p:cSld{name=Foobar}", "Foobar")] - ) + @pytest.fixture(params=[("p:sld/p:cSld", ""), ("p:sld/p:cSld{name=Foobar}", "Foobar")]) def name_get_fixture(self, request): sld_cxml, expected_name = request.param base_slide = _BaseSlide(element(sld_cxml), None) @@ -107,6 +107,8 @@ def background_(self, request): class Describe_BaseMaster(object): + """Unit-test suite for `pptx.slide._BaseMaster` objects.""" + def it_is_a_BaseSlide_subclass(self, subclass_fixture): base_master = subclass_fixture assert isinstance(base_master, _BaseSlide) @@ -147,9 +149,7 @@ def subclass_fixture(self): @pytest.fixture def MasterPlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.MasterPlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.MasterPlaceholders", return_value=placeholders_) @pytest.fixture def MasterShapes_(self, request, shapes_): @@ -165,10 +165,19 @@ def shapes_(self, request): class DescribeNotesSlide(object): - def it_can_clone_the_notes_master_placeholders(self, clone_fixture): - notes_slide, notes_master_, clone_placeholder_, calls = clone_fixture + """Unit-test suite for `pptx.slide.NotesSlide` objects.""" + + def it_can_clone_the_notes_master_placeholders(self, request, notes_master_, shapes_): + placeholders = notes_master_.placeholders = ( + BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=body}"), None), + BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=dt}"), None), + ) + property_mock(request, NotesSlide, "shapes", return_value=shapes_) + notes_slide = NotesSlide(None, None) + notes_slide.clone_master_placeholders(notes_master_) - assert clone_placeholder_.call_args_list == calls + + assert shapes_.clone_placeholder.call_args_list == [call(placeholders[0])] def it_provides_access_to_its_shapes(self, shapes_fixture): notes_slide, NotesSlideShapes_, spTree, shapes_ = shapes_fixture @@ -177,9 +186,12 @@ def it_provides_access_to_its_shapes(self, shapes_fixture): assert shapes is shapes_ def it_provides_access_to_its_placeholders(self, placeholders_fixture): - notes_slide, NotesSlidePlaceholders_, spTree, placeholders_ = ( - placeholders_fixture - ) + ( + notes_slide, + NotesSlidePlaceholders_, + spTree, + placeholders_, + ) = placeholders_fixture placeholders = notes_slide.placeholders NotesSlidePlaceholders_.assert_called_once_with(spTree, notes_slide) assert placeholders is placeholders_ @@ -196,17 +208,6 @@ def it_provides_access_to_its_notes_text_frame(self, notes_tf_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def clone_fixture(self, notes_master_, clone_placeholder_, shapes_prop_, shapes_): - notes_slide = NotesSlide(None, None) - placeholders = notes_master_.placeholders = ( - BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=body}"), None), - BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=dt}"), None), - ) - calls = [call(placeholders[0])] - shapes_.clone_placeholder = clone_placeholder_ - return notes_slide, notes_master_, clone_placeholder_, calls - @pytest.fixture( params=[ (("SLIDE_IMAGE", "BODY", "SLIDE_NUMBER"), 1), @@ -228,9 +229,7 @@ def notes_ph_fixture(self, request, placeholders_prop_): return notes_slide, expected_value @pytest.fixture(params=[True, False]) - def notes_tf_fixture( - self, request, notes_placeholder_prop_, placeholder_, text_frame_ - ): + def notes_tf_fixture(self, request, notes_placeholder_prop_, placeholder_, text_frame_): has_text_frame = request.param notes_slide = NotesSlide(None, None) if has_text_frame: @@ -258,25 +257,17 @@ def shapes_fixture(self, NotesSlideShapes_, shapes_): # fixture components --------------------------------------------- - @pytest.fixture - def clone_placeholder_(self, request): - return method_mock(request, NotesSlideShapes, "clone_placeholder") - @pytest.fixture def notes_master_(self, request): return instance_mock(request, NotesMaster) @pytest.fixture def notes_placeholder_prop_(self, request, placeholder_): - return property_mock( - request, NotesSlide, "notes_placeholder", return_value=placeholder_ - ) + return property_mock(request, NotesSlide, "notes_placeholder", return_value=placeholder_) @pytest.fixture def NotesSlidePlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.NotesSlidePlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.NotesSlidePlaceholders", return_value=placeholders_) @pytest.fixture def NotesSlideShapes_(self, request, shapes_): @@ -292,24 +283,20 @@ def placeholders_(self, request): @pytest.fixture def placeholders_prop_(self, request, placeholders_): - return property_mock( - request, NotesSlide, "placeholders", return_value=placeholders_ - ) + return property_mock(request, NotesSlide, "placeholders", return_value=placeholders_) @pytest.fixture def shapes_(self, request): return instance_mock(request, NotesSlideShapes) - @pytest.fixture - def shapes_prop_(self, request, shapes_): - return property_mock(request, NotesSlide, "shapes", return_value=shapes_) - @pytest.fixture def text_frame_(self, request): return instance_mock(request, TextFrame) class DescribeSlide(object): + """Unit-test suite for `pptx.slide.Slide` objects.""" + def it_is_a_BaseSlide_subclass(self, subclass_fixture): slide = subclass_fixture assert isinstance(slide, _BaseSlide) @@ -437,9 +424,7 @@ def placeholders_(self, request): @pytest.fixture def SlidePlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.SlidePlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.SlidePlaceholders", return_value=placeholders_) @pytest.fixture def SlideShapes_(self, request, shapes_): @@ -459,6 +444,8 @@ def slide_part_(self, request): class DescribeSlides(object): + """Unit-test suite for `pptx.slide.Slides` objects.""" + def it_supports_indexed_access(self, getitem_fixture): slides, prs_part_, rId, slide_ = getitem_fixture slide = slides[0] @@ -604,6 +591,8 @@ def slide_layout_(self, request): class DescribeSlideLayout(object): + """Unit-test suite for `pptx.slide.SlideLayout` objects.""" + def it_is_a_BaseSlide_subclass(self): slide_layout = SlideLayout(None, None) assert isinstance(slide_layout, _BaseSlide) @@ -613,9 +602,7 @@ def it_can_iterate_its_clonable_placeholders(self, cloneable_fixture): cloneable = list(slide_layout.iter_cloneable_placeholders()) assert cloneable == expected_placeholders - def it_provides_access_to_its_placeholders( - self, LayoutPlaceholders_, placeholders_ - ): + def it_provides_access_to_its_placeholders(self, LayoutPlaceholders_, placeholders_): sldLayout = element("p:sldLayout/p:cSld/p:spTree") spTree = sldLayout.xpath("//p:spTree")[0] slide_layout = SlideLayout(sldLayout, None) @@ -672,9 +659,7 @@ def it_knows_which_slides_are_based_on_it( ((PP_PLACEHOLDER.SLIDE_NUMBER, PP_PLACEHOLDER.FOOTER), ()), ] ) - def cloneable_fixture( - self, request, placeholders_prop_, placeholder_, placeholder_2_ - ): + def cloneable_fixture(self, request, placeholders_prop_, placeholder_, placeholder_2_): ph_types, expected_indices = request.param slide_layout = SlideLayout(None, None) placeholder_.element.ph_type = ph_types[0] @@ -699,9 +684,7 @@ def used_by_fixture(self, request, presentation_, slide_, slide_2_): @pytest.fixture def LayoutPlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.LayoutPlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.LayoutPlaceholders", return_value=placeholders_) @pytest.fixture def LayoutShapes_(self, request, shapes_): @@ -713,9 +696,7 @@ def package_(self, request): @pytest.fixture def part_prop_(self, request, slide_layout_part_): - return property_mock( - request, SlideLayout, "part", return_value=slide_layout_part_ - ) + return property_mock(request, SlideLayout, "part", return_value=slide_layout_part_) @pytest.fixture def placeholder_(self, request): @@ -731,9 +712,7 @@ def placeholders_(self, request): @pytest.fixture def placeholders_prop_(self, request, placeholders_): - return property_mock( - request, SlideLayout, "placeholders", return_value=placeholders_ - ) + return property_mock(request, SlideLayout, "placeholders", return_value=placeholders_) @pytest.fixture def presentation_(self, request): @@ -765,14 +744,14 @@ def slide_master_(self, request): class DescribeSlideLayouts(object): + """Unit-test suite for `pptx.slide.SlideLayouts` objects.""" + def it_supports_len(self, len_fixture): slide_layouts, expected_value = len_fixture assert len(slide_layouts) == expected_value def it_can_iterate_its_slide_layouts(self, part_prop_, slide_master_part_): - sldLayoutIdLst = element( - "p:sldLayoutIdLst/(p:sldLayoutId{r:id=a},p:sldLayoutId{r:id=b})" - ) + sldLayoutIdLst = element("p:sldLayoutIdLst/(p:sldLayoutId{r:id=a},p:sldLayoutId{r:id=b})") _slide_layouts = [ SlideLayout(element("p:sldLayout"), None), SlideLayout(element("p:sldLayout"), None), @@ -790,9 +769,7 @@ def it_can_iterate_its_slide_layouts(self, part_prop_, slide_master_part_): def it_supports_indexed_access(self, slide_layout_, part_prop_, slide_master_part_): part_prop_.return_value = slide_master_part_ slide_master_part_.related_slide_layout.return_value = slide_layout_ - slide_layouts = SlideLayouts( - element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None - ) + slide_layouts = SlideLayouts(element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None) slide_layout = slide_layouts[0] @@ -800,15 +777,11 @@ def it_supports_indexed_access(self, slide_layout_, part_prop_, slide_master_par assert slide_layout is slide_layout_ def but_it_raises_on_index_out_of_range(self, part_prop_): - slide_layouts = SlideLayouts( - element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None - ) + slide_layouts = SlideLayouts(element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None) with pytest.raises(IndexError): slide_layouts[1] - def it_can_find_a_slide_layout_by_name( - self, _iter_, slide_layout_, slide_layout_2_ - ): + def it_can_find_a_slide_layout_by_name(self, _iter_, slide_layout_, slide_layout_2_): _iter_.return_value = iter((slide_layout_, slide_layout_2_)) slide_layout_2_.name = "pick me!" slide_layouts = SlideLayouts(None, None) @@ -866,14 +839,10 @@ def it_can_remove_an_unused_slide_layout( slide_layouts.remove(slide_layout_) - assert slide_layouts._sldLayoutIdLst.xml == xml( - "p:sldLayoutIdLst/p:sldLayoutId{r:id=rId2}" - ) + assert slide_layouts._sldLayoutIdLst.xml == xml("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId2}") slide_master_part_.drop_rel.assert_called_once_with("rId1") - def but_it_raises_on_attempt_to_remove_slide_layout_in_use( - self, slide_layout_, slide_ - ): + def but_it_raises_on_attempt_to_remove_slide_layout_in_use(self, slide_layout_, slide_): slide_layout_.used_by_slides = (slide_,) slide_layouts = SlideLayouts(None, None) @@ -930,6 +899,8 @@ def slide_master_part_(self, request): class DescribeSlideMaster(object): + """Unit-test suite for `pptx.slide.SlideMaster` objects.""" + def it_is_a_BaseMaster_subclass(self, subclass_fixture): slide_master = subclass_fixture assert isinstance(slide_master, _BaseMaster) @@ -957,9 +928,7 @@ def subclass_fixture(self): @pytest.fixture def SlideLayouts_(self, request, slide_layouts_): - return class_mock( - request, "pptx.slide.SlideLayouts", return_value=slide_layouts_ - ) + return class_mock(request, "pptx.slide.SlideLayouts", return_value=slide_layouts_) @pytest.fixture def slide_layouts_(self, request): @@ -967,6 +936,8 @@ def slide_layouts_(self, request): class DescribeSlideMasters(object): + """Unit-test suite for `pptx.slide.SlideMasters` objects.""" + def it_knows_how_many_masters_it_contains(self, len_fixture): slide_masters, expected_value = len_fixture assert len(slide_masters) == expected_value @@ -992,9 +963,7 @@ def it_raises_on_index_out_of_range(self, getitem_raises_fixture): @pytest.fixture def getitem_fixture(self, part_, slide_master_, part_prop_): - slide_masters = SlideMasters( - element("p:sldMasterIdLst/p:sldMasterId{r:id=rId1}"), None - ) + slide_masters = SlideMasters(element("p:sldMasterIdLst/p:sldMasterId{r:id=rId1}"), None) part_.related_slide_master.return_value = slide_master_ return slide_masters, part_, slide_master_, "rId1" @@ -1004,9 +973,7 @@ def getitem_raises_fixture(self, part_prop_): @pytest.fixture def iter_fixture(self, part_prop_): - sldMasterIdLst = element( - "p:sldMasterIdLst/(p:sldMasterId{r:id=a},p:sldMasterId{r:id=b})" - ) + sldMasterIdLst = element("p:sldMasterIdLst/(p:sldMasterId{r:id=a},p:sldMasterId{r:id=b})") slide_masters = SlideMasters(sldMasterIdLst, None) related_slide_master_ = part_prop_.return_value.related_slide_master calls = [call("a"), call("b")] @@ -1045,44 +1012,29 @@ def slide_master_(self, request): class Describe_Background(object): - def it_provides_access_to_its_fill(self, fill_fixture): - background, cSld, expected_xml = fill_fixture[:3] - from_fill_parent_, fill_ = fill_fixture[3:] - - fill = background.fill + """Unit-test suite for `pptx.slide._Background` objects.""" - assert cSld.xml == expected_xml - from_fill_parent_.assert_called_once_with(cSld.xpath("p:bg/p:bgPr")[0]) - assert fill is fill_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + "cSld_xml, expected_cxml", + ( ("p:cSld{a:b=c}", "p:cSld{a:b=c}/p:bg/p:bgPr/(a:noFill,a:effectLst)"), ( "p:cSld{a:b=c}/p:bg/p:bgRef", "p:cSld{a:b=c}/p:bg/p:bgPr/(a:noFill,a:effectLst)", ), ("p:cSld/p:bg/p:bgPr/a:solidFill", "p:cSld/p:bg/p:bgPr/a:solidFill"), - ] + ), ) - def fill_fixture(self, request, from_fill_parent_, fill_): - cSld_xml, expected_cxml = request.param + def it_provides_access_to_its_fill(self, request, cSld_xml, expected_cxml): + fill_ = instance_mock(request, FillFormat) + from_fill_parent_ = method_mock( + request, FillFormat, "from_fill_parent", autospec=False, return_value=fill_ + ) cSld = element(cSld_xml) background = _Background(cSld) - from_fill_parent_.return_value = fill_ - - expected_xml = xml(expected_cxml) - return background, cSld, expected_xml, from_fill_parent_, fill_ - - # fixture components --------------------------------------------- - - @pytest.fixture - def fill_(self, request): - return instance_mock(request, FillFormat) + fill = background.fill - @pytest.fixture - def from_fill_parent_(self, request): - return method_mock(request, FillFormat, "from_fill_parent") + assert cSld.xml == xml(expected_cxml) + from_fill_parent_.assert_called_once_with(cSld.xpath("p:bg/p:bgPr")[0]) + assert fill is fill_ diff --git a/tests/test_table.py b/tests/test_table.py index b0505a539..c53f1261f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,8 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Unit-test suite for pptx.table module""" +"""Unit-test suite for `pptx.table` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -12,13 +12,13 @@ from pptx.oxml.table import CT_Table, CT_TableCell, TcRange from pptx.shapes.graphfrm import GraphicFrame from pptx.table import ( + Table, _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, - Table, ) from pptx.text.text import TextFrame from pptx.util import Inches, Length, Pt @@ -28,6 +28,8 @@ class DescribeTable(object): + """Unit-test suite for `pptx.table.Table` objects.""" + def it_provides_access_to_its_cells(self, tbl_, tc_, _Cell_, cell_): row_idx, col_idx = 4, 2 tbl_.tc.return_value = tc_ @@ -40,9 +42,18 @@ def it_provides_access_to_its_cells(self, tbl_, tc_, _Cell_, cell_): _Cell_.assert_called_once_with(tc_, table) assert cell is cell_ - def it_provides_access_to_its_columns(self, columns_fixture): - table, expected_columns_ = columns_fixture - assert table.columns is expected_columns_ + def it_provides_access_to_its_columns(self, request): + columns_ = instance_mock(request, _ColumnCollection) + _ColumnCollection_ = class_mock( + request, "pptx.table._ColumnCollection", return_value=columns_ + ) + tbl = element("a:tbl") + table = Table(tbl, None) + + columns = table.columns + + _ColumnCollection_.assert_called_once_with(tbl, table) + assert columns is columns_ def it_can_iterate_its_grid_cells(self, request, _Cell_): tbl = element("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))") @@ -57,9 +68,16 @@ def it_can_iterate_its_grid_cells(self, request, _Cell_): assert cells == expected_cells assert _Cell_.call_args_list == [call(tc, table) for tc in expected_tcs] - def it_provides_access_to_its_rows(self, rows_fixture): - table, expected_rows_ = rows_fixture - assert table.rows is expected_rows_ + def it_provides_access_to_its_rows(self, request): + rows_ = instance_mock(request, _RowCollection) + _RowCollection_ = class_mock(request, "pptx.table._RowCollection", return_value=rows_) + tbl = element("a:tbl") + table = Table(tbl, None) + + rows = table.rows + + _RowCollection_.assert_called_once_with(tbl, table) + assert rows is rows_ def it_updates_graphic_frame_width_on_width_change(self, dx_fixture): table, expected_width = dx_fixture @@ -73,11 +91,6 @@ def it_updates_graphic_frame_height_on_height_change(self, dy_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def columns_fixture(self, table, columns_): - table._columns = columns_ - return table, columns_ - @pytest.fixture def dx_fixture(self, graphic_frame_): tbl_cxml = "a:tbl/a:tblGrid/(a:gridCol{w=111},a:gridCol{w=222})" @@ -92,11 +105,6 @@ def dy_fixture(self, graphic_frame_): expected_height = 300 return table, expected_height - @pytest.fixture - def rows_fixture(self, table, rows_): - table._rows = rows_ - return table, rows_ - # fixture components --------------------------------------------- @pytest.fixture @@ -107,22 +115,10 @@ def _Cell_(self, request): def cell_(self, request): return instance_mock(request, _Cell) - @pytest.fixture - def columns_(self, request): - return instance_mock(request, _ColumnCollection) - @pytest.fixture def graphic_frame_(self, request): return instance_mock(request, GraphicFrame) - @pytest.fixture - def rows_(self, request): - return instance_mock(request, _RowCollection) - - @pytest.fixture - def table(self): - return Table(element("a:tbl"), None) - @pytest.fixture def tbl_(self, request): return instance_mock(request, CT_Table) @@ -241,9 +237,7 @@ def it_can_change_its_margin_settings(self, margin_set_fixture): setattr(cell, margin_prop_name, new_value) assert cell._tc.xml == expected_xml - def it_raises_on_margin_assigned_other_than_int_or_None( - self, margin_raises_fixture - ): + def it_raises_on_margin_assigned_other_than_int_or_None(self, margin_raises_fixture): cell, margin_attr_name, val_of_invalid_type = margin_raises_fixture with pytest.raises(TypeError): setattr(cell, margin_attr_name, val_of_invalid_type) @@ -385,9 +379,7 @@ def anchor_set_fixture(self, request): def fill_fixture(self, cell): return cell - @pytest.fixture( - params=[("a:tc", 1), ("a:tc{gridSpan=2}", 1), ("a:tc{rowSpan=42}", 42)] - ) + @pytest.fixture(params=[("a:tc", 1), ("a:tc{gridSpan=2}", 1), ("a:tc{rowSpan=42}", 42)]) def height_fixture(self, request): tc_cxml, expected_value = request.param tc = element(tc_cxml) @@ -426,9 +418,7 @@ def margin_set_fixture(self, request): expected_xml = xml(expected_tc_cxml) return cell, margin_prop_name, new_value, expected_xml - @pytest.fixture( - params=["margin_left", "margin_right", "margin_top", "margin_bottom"] - ) + @pytest.fixture(params=["margin_left", "margin_right", "margin_top", "margin_bottom"]) def margin_raises_fixture(self, request): margin_prop_name = request.param cell = _Cell(element("a:tc"), None) @@ -493,9 +483,7 @@ def split_fixture(self, request): range_tcs = tuple(tcs[idx] for idx in range_tc_idxs) return origin_tc, range_tcs - @pytest.fixture( - params=[("a:tc", 1), ("a:tc{rowSpan=2}", 1), ("a:tc{gridSpan=24}", 24)] - ) + @pytest.fixture(params=[("a:tc", 1), ("a:tc{rowSpan=2}", 1), ("a:tc{gridSpan=24}", 24)]) def width_fixture(self, request): tc_cxml, expected_value = request.param tc = element(tc_cxml) @@ -565,8 +553,7 @@ def iter_fixture(self, request, _Cell_): cell_collection = _CellCollection(tr, None) expected_cells = [ - instance_mock(request, _Cell, name="cell%d" % idx) - for idx in range(len(tcs)) + instance_mock(request, _Cell, name="cell%d" % idx) for idx in range(len(tcs)) ] _Cell_.side_effect = expected_cells calls = [call(tc, cell_collection) for tc in tcs] @@ -605,9 +592,7 @@ def it_can_change_its_width(self, width_set_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture( - params=[("a:gridCol{w=914400}", Inches(1)), ("a:gridCol{w=10pt}", Pt(10))] - ) + @pytest.fixture(params=[("a:gridCol{w=914400}", Inches(1)), ("a:gridCol{w=10pt}", Pt(10))]) def width_get_fixture(self, request): gridCol_cxml, expected_value = request.param column = _Column(element(gridCol_cxml), None) @@ -688,7 +673,6 @@ def iter_fixture(self, request): tbl = element(tbl_cxml) columns = _ColumnCollection(tbl, None) expected_column_lst = tbl.xpath("//a:gridCol") - print(expected_column_lst) return columns, expected_column_lst @pytest.fixture( diff --git a/tests/test_util.py b/tests/test_util.py index 4944d33f4..97e46fa4c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,21 +1,10 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.util` module.""" -""" -Test suite for pptx.util module. -""" - -from __future__ import absolute_import +from __future__ import annotations import pytest -from pptx.compat import to_unicode -from pptx.util import Length, Centipoints, Cm, Emu, Inches, Mm, Pt - - -def test_to_unicode_raises_on_non_string(): - """to_unicode(text) raises on *text* not a string""" - with pytest.raises(TypeError): - to_unicode(999) +from pptx.util import Centipoints, Cm, Emu, Inches, Length, Mm, Pt class DescribeLength(object): diff --git a/tests/text/test_fonts.py b/tests/text/test_fonts.py index e3fc2eeb0..995c78dd2 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -1,21 +1,18 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.text.fonts module -""" +"""Unit-test suite for `pptx.text.fonts` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import io -import pytest - from struct import calcsize -from pptx.compat import BytesIO +import pytest + from pptx.text.fonts import ( + FontFiles, _BaseTable, _Font, - FontFiles, _HeadTable, _NameTable, _Stream, @@ -37,6 +34,8 @@ class DescribeFontFiles(object): + """Unit-test suite for `pptx.text.fonts.FontFiles` object.""" + def it_can_find_a_system_font_file(self, find_fixture): family_name, is_bold, is_italic, expected_path = find_fixture path = FontFiles.find(family_name, is_bold, is_italic) @@ -65,9 +64,8 @@ def it_knows_windows_font_dirs_to_help_find(self, win_dirs_fixture): def it_iterates_over_fonts_in_dir_to_help_find(self, iter_fixture): directory, _Font_, expected_calls, expected_paths = iter_fixture - paths = list(FontFiles._iter_font_files_in(directory)) - print(directory) + paths = list(FontFiles._iter_font_files_in(directory)) assert _Font_.open.call_args_list == expected_calls assert paths == expected_paths @@ -86,9 +84,7 @@ def find_fixture(self, request, _installed_fonts_): return family_name, is_bold, is_italic, expected_path @pytest.fixture(params=[("darwin", ["a", "b"]), ("win32", ["c", "d"])]) - def font_dirs_fixture( - self, request, _os_x_font_directories_, _windows_font_directories_ - ): + def font_dirs_fixture(self, request, _os_x_font_directories_, _windows_font_directories_): platform, expected_dirs = request.param dirs_meth_mock = { "darwin": _os_x_font_directories_, @@ -147,32 +143,38 @@ def _Font_(self, request): @pytest.fixture def _font_directories_(self, request): - return method_mock(request, FontFiles, "_font_directories") + return method_mock(request, FontFiles, "_font_directories", autospec=False) @pytest.fixture def _installed_fonts_(self, request): - _installed_fonts_ = method_mock(request, FontFiles, "_installed_fonts") - _installed_fonts_.return_value = { - ("Foobar", False, False): "foobar.ttf", - ("Foobar", True, False): "foobarb.ttf", - ("Barfoo", False, True): "barfooi.ttf", - } - return _installed_fonts_ + return method_mock( + request, + FontFiles, + "_installed_fonts", + autospec=False, + return_value={ + ("Foobar", False, False): "foobar.ttf", + ("Foobar", True, False): "foobarb.ttf", + ("Barfoo", False, True): "barfooi.ttf", + }, + ) @pytest.fixture def _iter_font_files_in_(self, request): - return method_mock(request, FontFiles, "_iter_font_files_in") + return method_mock(request, FontFiles, "_iter_font_files_in", autospec=False) @pytest.fixture def _os_x_font_directories_(self, request): - return method_mock(request, FontFiles, "_os_x_font_directories") + return method_mock(request, FontFiles, "_os_x_font_directories", autospec=False) @pytest.fixture def _windows_font_directories_(self, request): - return method_mock(request, FontFiles, "_windows_font_directories") + return method_mock(request, FontFiles, "_windows_font_directories", autospec=False) class Describe_Font(object): + """Unit-test suite for `pptx.text.fonts._Font` object.""" + def it_can_construct_from_a_font_file_path(self, open_fixture): path, _Stream_, stream_ = open_fixture with _Font.open(path) as f: @@ -208,17 +210,19 @@ def it_knows_the_table_count_to_help_read(self, table_count_fixture): font, expected_value = table_count_fixture assert font._table_count == expected_value - def it_reads_the_header_to_help_read_font(self, fields_fixture): - font, expected_values = fields_fixture + def it_reads_the_header_to_help_read_font(self, request): + stream_ = instance_mock(request, _Stream) + stream_.read_fields.return_value = ("foob", 42, 64, 7, 16) + font = _Font(stream_) + fields = font._fields - font._stream.read_fields.assert_called_once_with(">4sHHHH", 0) - assert fields == expected_values + + stream_.read_fields.assert_called_once_with(">4sHHHH", 0) + assert fields == ("foob", 42, 64, 7, 16) # fixtures --------------------------------------------- - @pytest.fixture( - params=[("head", True, True), ("head", False, False), ("foob", True, False)] - ) + @pytest.fixture(params=[("head", True, True), ("head", False, False), ("foob", True, False)]) def bold_fixture(self, request, _tables_, head_table_): key, is_bold, expected_value = request.param head_table_.is_bold = is_bold @@ -234,16 +238,7 @@ def family_fixture(self, _tables_, name_table_): name_table_.family_name = expected_name return font, expected_name - @pytest.fixture - def fields_fixture(self, read_fields_): - stream = _Stream(None) - font = _Font(stream) - read_fields_.return_value = expected_values = ("foob", 42, 64, 7, 16) - return font, expected_values - - @pytest.fixture( - params=[("head", True, True), ("head", False, False), ("foob", True, False)] - ) + @pytest.fixture(params=[("head", True, True), ("head", False, False), ("foob", True, False)]) def italic_fixture(self, request, _tables_, head_table_): key, is_italic, expected_value = request.param head_table_.is_italic = is_italic @@ -312,10 +307,6 @@ def _iter_table_records_(self, request): def name_table_(self, request): return instance_mock(request, _NameTable) - @pytest.fixture - def read_fields_(self, request): - return method_mock(request, _Stream, "read_fields") - @pytest.fixture def _Stream_(self, request): return class_mock(request, "pptx.text.fonts._Stream") @@ -342,11 +333,17 @@ def _tables_(self, request): class Describe_Stream(object): - def it_can_construct_from_a_path(self, open_fixture): - path, open_, _init_, file_ = open_fixture - stream = _Stream.open(path) - open_.assert_called_once_with(path, "rb") - _init_.assert_called_once_with(file_) + """Unit-test suite for `pptx.text.fonts._Stream` object.""" + + def it_can_construct_from_a_path(self, request): + open_ = open_mock(request, "pptx.text.fonts") + _init_ = initializer_mock(request, _Stream) + file_ = open_.return_value + + stream = _Stream.open("foobar.ttf") + + open_.assert_called_once_with("foobar.ttf", "rb") + _init_.assert_called_once_with(stream, file_) assert isinstance(stream, _Stream) def it_can_be_closed(self, close_fixture): @@ -375,12 +372,6 @@ def close_fixture(self, file_): stream = _Stream(file_) return stream, file_ - @pytest.fixture - def open_fixture(self, open_, _init_): - path = "foobar.ttf" - file_ = open_.return_value - return path, open_, _init_, file_ - @pytest.fixture def read_fixture(self, file_): stream = _Stream(file_) @@ -403,16 +394,10 @@ def read_flds_fixture(self, file_): def file_(self, request): return instance_mock(request, io.RawIOBase) - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, _Stream) - - @pytest.fixture - def open_(self, request): - return open_mock(request, "pptx.text.fonts") - class Describe_TableFactory(object): + """Unit-test suite for `pptx.text.fonts._TableFactory` object.""" + def it_constructs_the_appropriate_table_object(self, fixture): tag, stream_, offset, length, TableClass_, TableClass = fixture table = _TableFactory(tag, stream_, offset, length) @@ -441,6 +426,8 @@ def stream_(self, request): class Describe_HeadTable(object): + """Unit-test suite for `pptx.text.fonts._HeadTable` object.""" + def it_knows_whether_the_font_is_bold(self, bold_fixture): head_table, expected_value = bold_fixture assert head_table.is_bold is expected_value @@ -472,7 +459,7 @@ def italic_fixture(self, request, _macStyle_): @pytest.fixture def macStyle_fixture(self): bytes_ = b"xxxxyyyy....................................\xF0\xBA........" - stream = _Stream(BytesIO(bytes_)) + stream = _Stream(io.BytesIO(bytes_)) offset, length = 0, len(bytes_) head_table = _HeadTable(None, stream, offset, length) expected_value = 61626 @@ -486,22 +473,46 @@ def _macStyle_(self, request): class Describe_NameTable(object): + """Unit-test suite for `pptx.text.fonts._NameTable` object.""" + def it_knows_the_font_family_name(self, family_fixture): name_table, expected_value = family_fixture family_name = name_table.family_name assert family_name == expected_value - def it_provides_access_to_its_names_to_help_props(self, names_fixture): - name_table, names_dict = names_fixture + def it_provides_access_to_its_names_to_help_props(self, request): + _iter_names_ = method_mock( + request, + _NameTable, + "_iter_names", + return_value=iter([((0, 1), "Foobar"), ((3, 1), "Barfoo")]), + ) + name_table = _NameTable(None, None, None, None) + names = name_table._names - name_table._iter_names.assert_called_once_with() - assert names == names_dict - def it_iterates_over_its_names_to_help_read_names(self, iter_fixture): - name_table, expected_calls, expected_names = iter_fixture + _iter_names_.assert_called_once_with(name_table) + assert names == {(0, 1): "Foobar", (3, 1): "Barfoo"} + + def it_iterates_over_its_names_to_help_read_names(self, request, _table_bytes_prop_): + property_mock(request, _NameTable, "_table_header", return_value=(0, 3, 42)) + _table_bytes_prop_.return_value = "xXx" + _read_name_ = method_mock( + request, + _NameTable, + "_read_name", + side_effect=iter([(0, 1, "Foobar"), (3, 1, "Barfoo"), (9, 9, None)]), + ) + name_table = _NameTable(None, None, None, None) + names = list(name_table._iter_names()) - assert name_table._read_name.call_args_list == expected_calls - assert names == expected_names + + assert _read_name_.call_args_list == [ + call(name_table, "xXx", 0, 42), + call(name_table, "xXx", 1, 42), + call(name_table, "xXx", 2, 42), + ] + assert names == [((0, 1), "Foobar"), ((3, 1), "Barfoo")] def it_reads_the_table_header_to_help_read_names(self, header_fixture): names_table, expected_value = header_fixture @@ -511,23 +522,42 @@ def it_reads_the_table_header_to_help_read_names(self, header_fixture): def it_buffers_the_table_bytes_to_help_read_names(self, bytes_fixture): name_table, expected_value = bytes_fixture table_bytes = name_table._table_bytes - name_table._stream.read.assert_called_once_with( - name_table._offset, name_table._length - ) + name_table._stream.read.assert_called_once_with(name_table._offset, name_table._length) assert table_bytes == expected_value - def it_reads_a_name_to_help_read_names(self, read_fixture): - name_table, bufr, idx, strs_offset, platform_id = read_fixture[:5] - encoding_id, name_str_offset, length = read_fixture[5:8] - expected_value = read_fixture[8] + def it_reads_a_name_to_help_read_names(self, request): + bufr, idx, strs_offset, platform_id, name_id = "buffer", 3, 47, 0, 1 + encoding_id, name_str_offset, length, name = 7, 36, 12, "Arial" + _name_header_ = method_mock( + request, + _NameTable, + "_name_header", + autospec=False, + return_value=( + platform_id, + encoding_id, + 666, + name_id, + length, + name_str_offset, + ), + ) + _read_name_text_ = method_mock(request, _NameTable, "_read_name_text", return_value=name) + name_table = _NameTable(None, None, None, None) - name = name_table._read_name(bufr, idx, strs_offset) + actual = name_table._read_name(bufr, idx, strs_offset) - name_table._name_header.assert_called_once_with(bufr, idx) - name_table._read_name_text.assert_called_once_with( - bufr, platform_id, encoding_id, strs_offset, name_str_offset, length + _name_header_.assert_called_once_with(bufr, idx) + _read_name_text_.assert_called_once_with( + name_table, + bufr, + platform_id, + encoding_id, + strs_offset, + name_str_offset, + length, ) - assert name == expected_value + assert actual == (platform_id, name_id, name) def it_reads_a_name_header_to_help_read_names(self, name_hdr_fixture): name_table, bufr, idx, expected_value = name_hdr_fixture @@ -546,9 +576,7 @@ def it_reads_name_text_to_help_read_names(self, name_text_fixture): name_table._raw_name_string.assert_called_once_with( bufr, strings_offset, name_str_offset, length ) - name_table._decode_name.assert_called_once_with( - raw_name, platform_id, encoding_id - ) + name_table._decode_name.assert_called_once_with(raw_name, platform_id, encoding_id) assert name is name_ def it_reads_name_bytes_to_help_read_names(self, raw_fixture): @@ -594,29 +622,19 @@ def decode_fixture(self, request): ({(9, 1): "Foobar", (6, 1): "Barfoo"}, None), ] ) - def family_fixture(self, request, _names_): + def family_fixture(self, request, _names_prop_): names, expected_value = request.param name_table = _NameTable(None, None, None, None) - _names_.return_value = names + _names_prop_.return_value = names return name_table, expected_value @pytest.fixture - def header_fixture(self, _table_bytes_): + def header_fixture(self, _table_bytes_prop_): name_table = _NameTable(None, None, None, None) - _table_bytes_.return_value = b"\x00\x00\x00\x02\x00\x2A" + _table_bytes_prop_.return_value = b"\x00\x00\x00\x02\x00\x2A" expected_value = (0, 2, 42) return name_table, expected_value - @pytest.fixture - def iter_fixture(self, _table_header_, _table_bytes_, _read_name): - name_table = _NameTable(None, None, None, None) - _table_header_.return_value = (0, 3, 42) - _table_bytes_.return_value = "xXx" - _read_name.side_effect = [(0, 1, "Foobar"), (3, 1, "Barfoo"), (9, 9, None)] - expected_calls = [call("xXx", 0, 42), call("xXx", 1, 42), call("xXx", 2, 42)] - expected_names = [((0, 1), "Foobar"), ((3, 1), "Barfoo")] - return name_table, expected_calls, expected_names - @pytest.fixture def name_hdr_fixture(self): name_table = _NameTable(None, None, None, None) @@ -634,13 +652,6 @@ def name_hdr_fixture(self): expected_value = (0, 1, 2, 3, 4, 5) return name_table, bufr, idx, expected_value - @pytest.fixture - def names_fixture(self, _iter_names_): - name_table = _NameTable(None, None, None, None) - _iter_names_.return_value = iter([((0, 1), "Foobar"), ((3, 1), "Barfoo")]) - names_dict = {(0, 1): "Foobar", (3, 1): "Barfoo"} - return name_table, names_dict - @pytest.fixture def name_text_fixture(self, _raw_name_string_, _decode_name_): name_table = _NameTable(None, None, None, None) @@ -668,71 +679,24 @@ def raw_fixture(self): expected_bytes = b"Foobar" return (name_table, bufr, strings_offset, str_offset, length, expected_bytes) - @pytest.fixture - def read_fixture(self, _name_header, _read_name_text): - name_table = _NameTable(None, None, None, None) - bufr, idx, strs_offset, platform_id, name_id = "buffer", 3, 47, 0, 1 - encoding_id, name_str_offset, length, name = 7, 36, 12, "Arial" - _name_header.return_value = ( - platform_id, - encoding_id, - 666, - name_id, - length, - name_str_offset, - ) - _read_name_text.return_value = name - expected_value = (platform_id, name_id, name) - return ( - name_table, - bufr, - idx, - strs_offset, - platform_id, - encoding_id, - name_str_offset, - length, - expected_value, - ) - # fixture components ----------------------------------- @pytest.fixture def _decode_name_(self, request): - return method_mock(request, _NameTable, "_decode_name") + return method_mock(request, _NameTable, "_decode_name", autospec=False) @pytest.fixture - def _iter_names_(self, request): - return method_mock(request, _NameTable, "_iter_names") - - @pytest.fixture - def _name_header(self, request): - return method_mock(request, _NameTable, "_name_header") + def _names_prop_(self, request): + return property_mock(request, _NameTable, "_names") @pytest.fixture def _raw_name_string_(self, request): - return method_mock(request, _NameTable, "_raw_name_string") - - @pytest.fixture - def _read_name(self, request): - return method_mock(request, _NameTable, "_read_name") - - @pytest.fixture - def _read_name_text(self, request): - return method_mock(request, _NameTable, "_read_name_text") + return method_mock(request, _NameTable, "_raw_name_string", autospec=False) @pytest.fixture def stream_(self, request): return instance_mock(request, _Stream) @pytest.fixture - def _table_bytes_(self, request): + def _table_bytes_prop_(self, request): return property_mock(request, _NameTable, "_table_bytes") - - @pytest.fixture - def _table_header_(self, request): - return property_mock(request, _NameTable, "_table_header") - - @pytest.fixture - def _names_(self, request): - return property_mock(request, _NameTable, "_names") diff --git a/tests/text/test_layout.py b/tests/text/test_layout.py index fbbb274cf..6e2c83d6a 100644 --- a/tests/text/test_layout.py +++ b/tests/text/test_layout.py @@ -1,16 +1,15 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.text.layout module -""" +"""Unit-test suite for `pptx.text.layout` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import pytest -from pptx.text.layout import _BinarySearchTree, _Line, _LineSource, TextFitter +from pptx.text.layout import TextFitter, _BinarySearchTree, _Line, _LineSource from ..unitutil.mock import ( + ANY, call, class_mock, function_mock, @@ -22,17 +21,24 @@ class DescribeTextFitter(object): - def it_can_determine_the_best_fit_font_size(self, best_fit_fixture): - text, extents, max_size, font_file = best_fit_fixture[:4] - _LineSource_, _init_, line_source_ = best_fit_fixture[4:7] - _best_fit_font_size_, font_size_ = best_fit_fixture[7:] + """Unit-test suite for `pptx.text.layout.TextFitter` object.""" - font_size = TextFitter.best_fit_font_size(text, extents, max_size, font_file) + def it_can_determine_the_best_fit_font_size(self, request, line_source_): + _LineSource_ = class_mock( + request, "pptx.text.layout._LineSource", return_value=line_source_ + ) + _init_ = initializer_mock(request, TextFitter) + _best_fit_font_size_ = method_mock( + request, TextFitter, "_best_fit_font_size", return_value=36 + ) + extents, max_size = (19, 20), 42 - _LineSource_.assert_called_once_with(text) - _init_.assert_called_once_with(line_source_, extents, font_file) - _best_fit_font_size_.assert_called_once_with(max_size) - assert font_size is font_size_ + font_size = TextFitter.best_fit_font_size("Foobar", extents, max_size, "foobar.ttf") + + _LineSource_.assert_called_once_with("Foobar") + _init_.assert_called_once_with(line_source_, extents, "foobar.ttf") + _best_fit_font_size_.assert_called_once_with(ANY, max_size) + assert font_size == 36 def it_finds_best_fit_font_size_to_help_best_fit(self, _best_fit_fixture): text_fitter, max_size, _BinarySearchTree_ = _best_fit_fixture[:3] @@ -40,26 +46,38 @@ def it_finds_best_fit_font_size_to_help_best_fit(self, _best_fit_fixture): font_size = text_fitter._best_fit_font_size(max_size) - _BinarySearchTree_.from_ordered_sequence.assert_called_once_with( - range(1, max_size + 1) - ) + _BinarySearchTree_.from_ordered_sequence.assert_called_once_with(range(1, max_size + 1)) sizes_.find_max.assert_called_once_with(predicate_) assert font_size is font_size_ - def it_provides_a_fits_inside_predicate_fn(self, fits_pred_fixture): - text_fitter, point_size = fits_pred_fixture[:2] - _rendered_size_, expected_bool_value = fits_pred_fixture[2:] + @pytest.mark.parametrize( + "extents, point_size, text_lines, expected_value", + ( + ((66, 99), 6, ("foo", "bar"), False), + ((66, 100), 6, ("foo", "bar"), True), + ((66, 101), 6, ("foo", "bar"), True), + ), + ) + def it_provides_a_fits_inside_predicate_fn( + self, + request, + line_source_, + _rendered_size_, + extents, + point_size, + text_lines, + expected_value, + ): + _wrap_lines_ = method_mock(request, TextFitter, "_wrap_lines", return_value=text_lines) + _rendered_size_.return_value = (None, 50) + text_fitter = TextFitter(line_source_, extents, "foobar.ttf") predicate = text_fitter._fits_inside_predicate result = predicate(point_size) - text_fitter._wrap_lines.assert_called_once_with( - text_fitter._line_source, point_size - ) - _rendered_size_.assert_called_once_with( - "Ty", point_size, text_fitter._font_file - ) - assert result is expected_bool_value + _wrap_lines_.assert_called_once_with(text_fitter, line_source_, point_size) + _rendered_size_.assert_called_once_with("Ty", point_size, text_fitter._font_file) + assert result is expected_value def it_provides_a_fits_in_width_predicate_fn(self, fits_cx_pred_fixture): text_fitter, point_size, line = fits_cx_pred_fixture[:3] @@ -68,53 +86,43 @@ def it_provides_a_fits_in_width_predicate_fn(self, fits_cx_pred_fixture): predicate = text_fitter._fits_in_width_predicate(point_size) result = predicate(line) - _rendered_size_.assert_called_once_with( - line.text, point_size, text_fitter._font_file - ) + _rendered_size_.assert_called_once_with(line.text, point_size, text_fitter._font_file) assert result is expected_value - def it_wraps_lines_to_help_best_fit(self, wrap_fixture): - text_fitter, line_source, point_size, remainder = wrap_fixture + def it_wraps_lines_to_help_best_fit(self, request): + line_source, remainder = _LineSource("foo bar"), _LineSource("bar") + _break_line_ = method_mock( + request, + TextFitter, + "_break_line", + side_effect=[("foo", remainder), ("bar", _LineSource(""))], + ) + text_fitter = TextFitter(None, (None, None), None) - text_fitter._wrap_lines(line_source, point_size) + text_fitter._wrap_lines(line_source, 21) - assert text_fitter._break_line.call_args_list == [ - call(line_source, point_size), - call(remainder, point_size), + assert _break_line_.call_args_list == [ + call(text_fitter, line_source, 21), + call(text_fitter, remainder, 21), ] - def it_breaks_off_a_line_to_help_wrap(self, break_fixture): - text_fitter, line_source_, point_size = break_fixture[:3] - _BinarySearchTree_, bst_, predicate_ = break_fixture[3:6] - max_value_ = break_fixture[6] + def it_breaks_off_a_line_to_help_wrap(self, request, line_source_, _BinarySearchTree_): + bst_ = instance_mock(request, _BinarySearchTree) + _fits_in_width_predicate_ = method_mock(request, TextFitter, "_fits_in_width_predicate") + _BinarySearchTree_.from_ordered_sequence.return_value = bst_ + predicate_ = _fits_in_width_predicate_.return_value + max_value_ = bst_.find_max.return_value + text_fitter = TextFitter(None, (None, None), None) - value = text_fitter._break_line(line_source_, point_size) + value = text_fitter._break_line(line_source_, 21) _BinarySearchTree_.from_ordered_sequence.assert_called_once_with(line_source_) - text_fitter._fits_in_width_predicate.assert_called_once_with(point_size) + text_fitter._fits_in_width_predicate.assert_called_once_with(text_fitter, 21) bst_.find_max.assert_called_once_with(predicate_) assert value is max_value_ # fixtures --------------------------------------------- - @pytest.fixture - def best_fit_fixture(self, _LineSource_, _init_, _best_fit_font_size_): - text, extents, max_size = "Foobar", (19, 20), 42 - font_file = "foobar.ttf" - line_source_ = _LineSource_.return_value - font_size_ = _best_fit_font_size_.return_value - return ( - text, - extents, - max_size, - font_file, - _LineSource_, - _init_, - line_source_, - _best_fit_font_size_, - font_size_, - ) - @pytest.fixture def _best_fit_fixture(self, _BinarySearchTree_, _fits_inside_predicate_): text_fitter = TextFitter(None, (None, None), None) @@ -131,25 +139,6 @@ def _best_fit_fixture(self, _BinarySearchTree_, _fits_inside_predicate_): font_size_, ) - @pytest.fixture - def break_fixture( - self, line_source_, _BinarySearchTree_, bst_, _fits_in_width_predicate_ - ): - text_fitter = TextFitter(None, (None, None), None) - point_size = 21 - _BinarySearchTree_.from_ordered_sequence.return_value = bst_ - predicate_ = _fits_in_width_predicate_.return_value - max_value_ = bst_.find_max.return_value - return ( - text_fitter, - line_source_, - point_size, - _BinarySearchTree_, - bst_, - predicate_, - max_value_, - ) - @pytest.fixture(params=[(49, True), (50, True), (51, False)]) def fits_cx_pred_fixture(self, request, _rendered_size_): rendered_width, expected_value = request.param @@ -158,64 +147,16 @@ def fits_cx_pred_fixture(self, request, _rendered_size_): _rendered_size_.return_value = (rendered_width, None) return (text_fitter, point_size, line, _rendered_size_, expected_value) - @pytest.fixture( - params=[ - ((66, 99), 6, ("foo", "bar"), False), - ((66, 100), 6, ("foo", "bar"), True), - ((66, 101), 6, ("foo", "bar"), True), - ] - ) - def fits_pred_fixture(self, request, line_source_, _wrap_lines_, _rendered_size_): - extents, point_size, text_lines, expected_value = request.param - text_fitter = TextFitter(line_source_, extents, "foobar.ttf") - _wrap_lines_.return_value = text_lines - _rendered_size_.return_value = (None, 50) - return text_fitter, point_size, _rendered_size_, expected_value - - @pytest.fixture - def wrap_fixture(self, _break_line_): - text_fitter = TextFitter(None, (None, None), None) - point_size = 21 - line_source, remainder = _LineSource("foo bar"), _LineSource("bar") - _break_line_.side_effect = [("foo", remainder), ("bar", _LineSource(""))] - return text_fitter, line_source, point_size, remainder - # fixture components ----------------------------------- - @pytest.fixture - def _best_fit_font_size_(self, request): - return method_mock(request, TextFitter, "_best_fit_font_size") - @pytest.fixture def _BinarySearchTree_(self, request): return class_mock(request, "pptx.text.layout._BinarySearchTree") - @pytest.fixture - def _break_line_(self, request): - return method_mock(request, TextFitter, "_break_line") - - @pytest.fixture - def bst_(self, request): - return instance_mock(request, _BinarySearchTree) - - @pytest.fixture - def _fits_in_width_predicate_(self, request): - return method_mock(request, TextFitter, "_fits_in_width_predicate") - @pytest.fixture def _fits_inside_predicate_(self, request): return property_mock(request, TextFitter, "_fits_inside_predicate") - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, TextFitter) - - @pytest.fixture - def _LineSource_(self, request, line_source_): - return class_mock( - request, "pptx.text.layout._LineSource", return_value=line_source_ - ) - @pytest.fixture def line_source_(self, request): return instance_mock(request, _LineSource) @@ -224,12 +165,10 @@ def line_source_(self, request): def _rendered_size_(self, request): return function_mock(request, "pptx.text.layout._rendered_size") - @pytest.fixture - def _wrap_lines_(self, request): - return method_mock(request, TextFitter, "_wrap_lines") - class Describe_BinarySearchTree(object): + """Unit-test suite for `pptx.text.layout._BinarySearchTree` object.""" + def it_can_construct_from_an_ordered_sequence(self): bst = _BinarySearchTree.from_ordered_sequence(range(10)) @@ -271,6 +210,8 @@ def max_fixture(self, request): class Describe_LineSource(object): + """Unit-test suite for `pptx.text.layout._LineSource` object.""" + def it_generates_text_remainder_pairs(self): line_source = _LineSource("foo bar baz") expected = ( diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 8e0f0f5b3..3a1a7a0bb 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -1,20 +1,21 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.text.text module.""" +"""Unit-test suite for `pptx.text.text` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, cast import pytest -from pptx.compat import is_unicode from pptx.dml.color import ColorFormat from pptx.dml.fill import FillFormat from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, MSO_UNDERLINE, PP_ALIGN from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.package import Part +from pptx.opc.package import XmlPart from pptx.shapes.autoshape import Shape -from pptx.text.text import Font, _Hyperlink, _Paragraph, _Run, TextFrame +from pptx.text.text import Font, TextFrame, _Hyperlink, _Paragraph, _Run from pptx.util import Inches, Pt from ..oxml.unitdata.text import a_p, a_t, an_hlinkClick, an_r, an_rPr @@ -27,6 +28,9 @@ property_mock, ) +if TYPE_CHECKING: + from pptx.oxml.text import CT_TextBody, CT_TextParagraph + class DescribeTextFrame(object): """Unit-test suite for `pptx.text.text.TextFrame` object.""" @@ -40,10 +44,43 @@ def it_knows_its_autosize_setting(self, autosize_get_fixture): text_frame, expected_value = autosize_get_fixture assert text_frame.auto_size == expected_value - def it_can_change_its_autosize_setting(self, autosize_set_fixture): - text_frame, value, expected_xml = autosize_set_fixture + @pytest.mark.parametrize( + ("txBody_cxml", "value", "expected_cxml"), + [ + ("p:txBody/a:bodyPr", MSO_AUTO_SIZE.NONE, "p:txBody/a:bodyPr/a:noAutofit"), + ( + "p:txBody/a:bodyPr/a:noAutofit", + MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT, + "p:txBody/a:bodyPr/a:spAutoFit", + ), + ( + "p:txBody/a:bodyPr/a:spAutoFit", + MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE, + "p:txBody/a:bodyPr/a:normAutofit", + ), + ("p:txBody/a:bodyPr/a:normAutofit", None, "p:txBody/a:bodyPr"), + ], + ) + def it_can_change_its_autosize_setting( + self, txBody_cxml: str, value: MSO_AUTO_SIZE | None, expected_cxml: str + ): + text_frame = TextFrame(element(txBody_cxml), None) text_frame.auto_size = value - assert text_frame._txBody.xml == expected_xml + assert text_frame._txBody.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + "txBody_cxml", + ( + "p:txBody/(a:p,a:p,a:p)", + 'p:txBody/a:p/a:r/a:t"foo"', + 'p:txBody/a:p/(a:br,a:r/a:t"foo")', + 'p:txBody/a:p/(a:fld,a:br,a:r/a:t"foo")', + ), + ) + def it_can_clear_itself_of_content(self, txBody_cxml): + text_frame = TextFrame(element(txBody_cxml), None) + text_frame.clear() + assert text_frame._element.xml == xml("p:txBody/a:p") def it_knows_its_margin_settings(self, margin_get_fixture): text_frame, prop_name, unit, expected_value = margin_get_fixture @@ -55,14 +92,41 @@ def it_can_change_its_margin_settings(self, margin_set_fixture): setattr(text_frame, prop_name, new_value) assert text_frame._txBody.xml == expected_xml - def it_knows_its_vertical_alignment(self, anchor_get_fixture): - text_frame, expected_value = anchor_get_fixture + @pytest.mark.parametrize( + ("txBody_cxml", "expected_value"), + [ + ("p:txBody/a:bodyPr", None), + ("p:txBody/a:bodyPr{anchor=t}", MSO_ANCHOR.TOP), + ("p:txBody/a:bodyPr{anchor=b}", MSO_ANCHOR.BOTTOM), + ], + ) + def it_knows_its_vertical_alignment(self, txBody_cxml: str, expected_value: MSO_ANCHOR | None): + text_frame = TextFrame(cast("CT_TextBody", element(txBody_cxml)), None) assert text_frame.vertical_anchor == expected_value - def it_can_change_its_vertical_alignment(self, anchor_set_fixture): - text_frame, new_value, expected_xml = anchor_set_fixture + @pytest.mark.parametrize( + ("txBody_cxml", "new_value", "expected_cxml"), + [ + ("p:txBody/a:bodyPr", MSO_ANCHOR.TOP, "p:txBody/a:bodyPr{anchor=t}"), + ( + "p:txBody/a:bodyPr{anchor=t}", + MSO_ANCHOR.MIDDLE, + "p:txBody/a:bodyPr{anchor=ctr}", + ), + ( + "p:txBody/a:bodyPr{anchor=ctr}", + MSO_ANCHOR.BOTTOM, + "p:txBody/a:bodyPr{anchor=b}", + ), + ("p:txBody/a:bodyPr{anchor=b}", None, "p:txBody/a:bodyPr"), + ], + ) + def it_can_change_its_vertical_alignment( + self, txBody_cxml: str, new_value: MSO_ANCHOR | None, expected_cxml: str + ): + text_frame = TextFrame(cast("CT_TextBody", element(txBody_cxml)), None) text_frame.vertical_anchor = new_value - assert text_frame._element.xml == expected_xml + assert text_frame._element.xml == xml(expected_cxml) def it_knows_its_word_wrap_setting(self, wrap_get_fixture): text_frame, expected_value = wrap_get_fixture @@ -91,9 +155,7 @@ def it_knows_the_part_it_belongs_to(self, text_frame_with_parent_): part = text_frame.part assert part is parent_.part - def it_knows_what_text_it_contains( - self, request, text_get_fixture, paragraphs_prop_ - ): + def it_knows_what_text_it_contains(self, request, text_get_fixture, paragraphs_prop_): paragraph_texts, expected_value = text_get_fixture paragraphs_prop_.return_value = tuple( instance_mock(request, _Paragraph, text=text) for text in paragraph_texts @@ -112,9 +174,7 @@ def it_can_replace_the_text_it_contains(self, text_set_fixture): assert text_frame._element.xml == expected_xml - def it_can_resize_its_text_to_best_fit( - self, text_prop_, _best_fit_font_size_, _apply_fit_ - ): + def it_can_resize_its_text_to_best_fit(self, request, text_prop_): family, max_size, bold, italic, font_file, font_size = ( "Family", 42, @@ -124,15 +184,18 @@ def it_can_resize_its_text_to_best_fit( 21, ) text_prop_.return_value = "some text" - _best_fit_font_size_.return_value = font_size + _best_fit_font_size_ = method_mock( + request, TextFrame, "_best_fit_font_size", return_value=font_size + ) + _apply_fit_ = method_mock(request, TextFrame, "_apply_fit") text_frame = TextFrame(None, None) text_frame.fit_text(family, max_size, bold, italic, font_file) - text_frame._best_fit_font_size.assert_called_once_with( - family, max_size, bold, italic, font_file + _best_fit_font_size_.assert_called_once_with( + text_frame, family, max_size, bold, italic, font_file ) - text_frame._apply_fit.assert_called_once_with(family, font_size, bold, italic) + _apply_fit_.assert_called_once_with(text_frame, family, font_size, bold, italic) def it_calculates_its_best_fit_font_size_to_help_fit_text(self, size_font_fixture): text_frame, family, max_size, bold, italic = size_font_fixture[:5] @@ -142,9 +205,7 @@ def it_calculates_its_best_fit_font_size_to_help_fit_text(self, size_font_fixtur font_size = text_frame._best_fit_font_size(family, max_size, bold, italic, None) FontFiles_.find.assert_called_once_with(family, bold, italic) - TextFitter_.best_fit_font_size.assert_called_once_with( - text, extents, max_size, font_file_ - ) + TextFitter_.best_fit_font_size.assert_called_once_with(text, extents, max_size, font_file_) assert font_size is font_size_ def it_calculates_its_effective_size_to_help_fit_text(self): @@ -155,12 +216,16 @@ def it_calculates_its_effective_size_to_help_fit_text(self): text_frame = Shape(element(sp_cxml), None).text_frame assert text_frame._extents == (731520, 822960) - def it_applies_fit_to_help_fit_text(self, apply_fit_fixture): - text_frame, family, font_size, bold, italic = apply_fit_fixture + def it_applies_fit_to_help_fit_text(self, request): + family, font_size, bold, italic = "Family", 42, True, False + _set_font_ = method_mock(request, TextFrame, "_set_font") + text_frame = TextFrame(element("p:txBody/a:bodyPr"), None) + text_frame._apply_fit(family, font_size, bold, italic) + assert text_frame.auto_size is MSO_AUTO_SIZE.NONE assert text_frame.word_wrap is True - text_frame._set_font.assert_called_once_with(family, font_size, bold, italic) + _set_font_.assert_called_once_with(text_frame, family, font_size, bold, italic) def it_sets_its_font_to_help_fit_text(self, set_font_fixture): text_frame, family, size, bold, italic, expected_xml = set_font_fixture @@ -181,47 +246,6 @@ def add_paragraph_fixture(self, request): expected_xml = xml(expected_cxml) return text_frame, expected_xml - @pytest.fixture( - params=[ - ("p:txBody/a:bodyPr", None), - ("p:txBody/a:bodyPr{anchor=t}", MSO_ANCHOR.TOP), - ("p:txBody/a:bodyPr{anchor=b}", MSO_ANCHOR.BOTTOM), - ] - ) - def anchor_get_fixture(self, request): - txBody_cxml, expected_value = request.param - text_frame = TextFrame(element(txBody_cxml), None) - return text_frame, expected_value - - @pytest.fixture( - params=[ - ("p:txBody/a:bodyPr", MSO_ANCHOR.TOP, "p:txBody/a:bodyPr{anchor=t}"), - ( - "p:txBody/a:bodyPr{anchor=t}", - MSO_ANCHOR.MIDDLE, - "p:txBody/a:bodyPr{anchor=ctr}", - ), - ( - "p:txBody/a:bodyPr{anchor=ctr}", - MSO_ANCHOR.BOTTOM, - "p:txBody/a:bodyPr{anchor=b}", - ), - ("p:txBody/a:bodyPr{anchor=b}", None, "p:txBody/a:bodyPr"), - ] - ) - def anchor_set_fixture(self, request): - txBody_cxml, new_value, expected_cxml = request.param - text_frame = TextFrame(element(txBody_cxml), None) - expected_xml = xml(expected_cxml) - return text_frame, new_value, expected_xml - - @pytest.fixture - def apply_fit_fixture(self, _set_font_): - txBody = element("p:txBody/a:bodyPr") - text_frame = TextFrame(txBody, None) - family, font_size, bold, italic = "Family", 42, True, False - return text_frame, family, font_size, bold, italic - @pytest.fixture( params=[ ("p:txBody/a:bodyPr", None), @@ -235,28 +259,6 @@ def autosize_get_fixture(self, request): text_frame = TextFrame(element(txBody_cxml), None) return text_frame, expected_value - @pytest.fixture( - params=[ - ("p:txBody/a:bodyPr", MSO_AUTO_SIZE.NONE, "p:txBody/a:bodyPr/a:noAutofit"), - ( - "p:txBody/a:bodyPr/a:noAutofit", - MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT, - "p:txBody/a:bodyPr/a:spAutoFit", - ), - ( - "p:txBody/a:bodyPr/a:spAutoFit", - MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE, - "p:txBody/a:bodyPr/a:normAutofit", - ), - ("p:txBody/a:bodyPr/a:normAutofit", None, "p:txBody/a:bodyPr"), - ] - ) - def autosize_set_fixture(self, request): - txBody_cxml, value, expected_cxml = request.param - text_frame = TextFrame(element(txBody_cxml), None) - expected_xml = xml(expected_cxml) - return text_frame, value, expected_xml - @pytest.fixture( params=[ ("p:txBody/a:bodyPr", "left", "emu", Inches(0.1)), @@ -377,9 +379,7 @@ def size_font_fixture(self, FontFiles_, TextFitter_, text_prop_, _extents_prop_) font_size, ) - @pytest.fixture( - params=[(["foobar"], "foobar"), (["foo", "bar", "baz"], "foo\nbar\nbaz")] - ) + @pytest.fixture(params=[(["foobar"], "foobar"), (["foo", "bar", "baz"], "foo\nbar\nbaz")]) def text_get_fixture(self, request): paragraph_texts, expected_value = request.param return paragraph_texts, expected_value @@ -441,14 +441,6 @@ def wrap_set_fixture(self, request): # fixture components ----------------------------------- - @pytest.fixture - def _apply_fit_(self, request): - return method_mock(request, TextFrame, "_apply_fit") - - @pytest.fixture - def _best_fit_font_size_(self, request): - return method_mock(request, TextFrame, "_best_fit_font_size") - @pytest.fixture def _extents_prop_(self, request): return property_mock(request, TextFrame, "_extents") @@ -461,10 +453,6 @@ def FontFiles_(self, request): def paragraphs_prop_(self, request): return property_mock(request, TextFrame, "paragraphs") - @pytest.fixture - def _set_font_(self, request): - return method_mock(request, TextFrame, "_set_font") - @pytest.fixture def TextFitter_(self, request): return class_mock(request, "pptx.text.text.TextFitter") @@ -481,6 +469,8 @@ def text_prop_(self, request): class DescribeFont(object): + """Unit-test suite for `pptx.text.text.Font` object.""" + def it_knows_its_bold_setting(self, bold_get_fixture): font, expected_value = bold_get_fixture assert font.bold == expected_value @@ -543,9 +533,7 @@ def it_provides_access_to_its_fill(self, font): # fixtures --------------------------------------------- - @pytest.fixture( - params=[("a:rPr", None), ("a:rPr{b=0}", False), ("a:rPr{b=1}", True)] - ) + @pytest.fixture(params=[("a:rPr", None), ("a:rPr{b=0}", False), ("a:rPr{b=1}", True)]) def bold_get_fixture(self, request): rPr_cxml, expected_value = request.param font = Font(element(rPr_cxml)) @@ -564,9 +552,7 @@ def bold_set_fixture(self, request): expected_xml = xml(expected_rPr_cxml) return font, new_value, expected_xml - @pytest.fixture( - params=[("a:rPr", None), ("a:rPr{i=0}", False), ("a:rPr{i=1}", True)] - ) + @pytest.fixture(params=[("a:rPr", None), ("a:rPr{i=0}", False), ("a:rPr{i=1}", True)]) def italic_get_fixture(self, request): rPr_cxml, expected_value = request.param font = Font(element(rPr_cxml)) @@ -614,9 +600,7 @@ def language_id_set_fixture(self, request): expected_xml = xml(expected_rPr_cxml) return font, new_value, expected_xml - @pytest.fixture( - params=[("a:rPr", None), ("a:rPr/a:latin{typeface=Foobar}", "Foobar")] - ) + @pytest.fixture(params=[("a:rPr", None), ("a:rPr/a:latin{typeface=Foobar}", "Foobar")]) def name_get_fixture(self, request): rPr_cxml, expected_value = request.param font = Font(element(rPr_cxml)) @@ -645,9 +629,7 @@ def size_get_fixture(self, request): font = Font(element(rPr_cxml)) return font, expected_value - @pytest.fixture( - params=[("a:rPr", Pt(24), "a:rPr{sz=2400}"), ("a:rPr{sz=2400}", None, "a:rPr")] - ) + @pytest.fixture(params=[("a:rPr", Pt(24), "a:rPr{sz=2400}"), ("a:rPr{sz=2400}", None, "a:rPr")]) def size_set_fixture(self, request): rPr_cxml, new_value, expected_rPr_cxml = request.param font = Font(element(rPr_cxml)) @@ -690,6 +672,8 @@ def font(self): class Describe_Hyperlink(object): + """Unit-test suite for `pptx.text.text._Hyperlink` object.""" + def it_knows_the_target_url_of_the_hyperlink(self, hlink_with_url_): hlink, rId, url = hlink_with_url_ assert hlink.address == url @@ -701,9 +685,7 @@ def it_has_None_for_address_when_no_hyperlink_is_present(self, hlink): def it_can_set_the_target_url(self, hlink, rPr_with_hlinkClick_xml, url): hlink.address = url # verify ----------------------- - hlink.part.relate_to.assert_called_once_with( - url, RT.HYPERLINK, is_external=True - ) + hlink.part.relate_to.assert_called_once_with(url, RT.HYPERLINK, is_external=True) assert hlink._rPr.xml == rPr_with_hlinkClick_xml assert hlink.address == url @@ -713,9 +695,7 @@ def it_can_remove_the_hyperlink(self, remove_hlink_fixture_): assert hlink._rPr.xml == rPr_xml hlink.part.drop_rel.assert_called_once_with(rId) - def it_should_remove_the_hyperlink_when_url_set_to_empty_string( - self, remove_hlink_fixture_ - ): + def it_should_remove_the_hyperlink_when_url_set_to_empty_string(self, remove_hlink_fixture_): hlink, rPr_xml, rId = remove_hlink_fixture_ hlink.address = "" assert hlink._rPr.xml == rPr_xml @@ -729,16 +709,12 @@ def it_can_change_the_target_url(self, change_hlink_fixture_): # verify ----------------------- assert hlink._rPr.xml == new_rPr_xml hlink.part.drop_rel.assert_called_once_with(rId_existing) - hlink.part.relate_to.assert_called_once_with( - new_url, RT.HYPERLINK, is_external=True - ) + hlink.part.relate_to.assert_called_once_with(new_url, RT.HYPERLINK, is_external=True) # fixtures --------------------------------------------- @pytest.fixture - def change_hlink_fixture_( - self, request, hlink_with_hlinkClick, rId, rId_2, part_, url_2 - ): + def change_hlink_fixture_(self, request, hlink_with_hlinkClick, rId, rId_2, part_, url_2): hlinkClick_bldr = an_hlinkClick().with_rId(rId_2) new_rPr_xml = an_rPr().with_nsdecls("a", "r").with_child(hlinkClick_bldr).xml() part_.relate_to.return_value = rId_2 @@ -768,7 +744,7 @@ def part_(self, request, url, rId): Mock Part instance suitable for patching into _Hyperlink.part property. It returns url for target_ref() and rId for relate_to(). """ - part_ = instance_mock(request, Part) + part_ = instance_mock(request, XmlPart) part_.target_ref.return_value = url part_.relate_to.return_value = rId return part_ @@ -892,15 +868,34 @@ def it_knows_what_text_it_contains(self, text_get_fixture): text = paragraph.text assert text == expected_value - assert is_unicode(text) + assert isinstance(text, str) - def it_can_change_its_text(self, text_set_fixture): - p, value, expected_xml = text_set_fixture + @pytest.mark.parametrize( + ("p_cxml", "value", "expected_cxml"), + [ + ('a:p/(a:r/a:t"foo",a:r/a:t"bar")', "foobar", 'a:p/a:r/a:t"foobar"'), + ("a:p", "", "a:p"), + ("a:p", "foobar", 'a:p/a:r/a:t"foobar"'), + ("a:p", "foo\nbar", 'a:p/(a:r/a:t"foo",a:br,a:r/a:t"bar")'), + ("a:p", "\vfoo\n", 'a:p/(a:br,a:r/a:t"foo",a:br)'), + ("a:p", "\n\nfoo", 'a:p/(a:br,a:br,a:r/a:t"foo")'), + ("a:p", "foo\n", 'a:p/(a:r/a:t"foo",a:br)'), + ("a:p", "foo\x07\n", 'a:p/(a:r/a:t"foo_x0007_",a:br)'), + ("a:p", "ŮŦƑ-8\x1bliteral", 'a:p/a:r/a:t"ŮŦƑ-8_x001B_literal"'), + ( + "a:p", + "utf-8 unicode: Hér er texti", + 'a:p/a:r/a:t"utf-8 unicode: Hér er texti"', + ), + ], + ) + def it_can_change_its_text(self, p_cxml: str, value: str, expected_cxml: str): + p = cast("CT_TextParagraph", element(p_cxml)) paragraph = _Paragraph(p, None) paragraph.text = value - assert paragraph._element.xml == expected_xml + assert paragraph._element.xml == xml(expected_cxml) # fixtures --------------------------------------------- @@ -1124,32 +1119,6 @@ def text_get_fixture(self, request): p = element(p_cxml) return p, expected_value - @pytest.fixture( - params=[ - ('a:p/(a:r/a:t"foo",a:r/a:t"bar")', "foobar", 'a:p/a:r/a:t"foobar"'), - ("a:p", "", "a:p"), - ("a:p", "foobar", 'a:p/a:r/a:t"foobar"'), - ("a:p", "foo\nbar", 'a:p/(a:r/a:t"foo",a:br,a:r/a:t"bar")'), - ("a:p", "\vfoo\n", 'a:p/(a:br,a:r/a:t"foo",a:br)'), - ("a:p", "\n\nfoo", 'a:p/(a:br,a:br,a:r/a:t"foo")'), - ("a:p", "foo\n", 'a:p/(a:r/a:t"foo",a:br)'), - ("a:p", b"foo\x07\n", 'a:p/(a:r/a:t"foo_x0007_",a:br)'), - ("a:p", b"7-bit str", 'a:p/a:r/a:t"7-bit str"'), - ("a:p", b"8-\xc9\x93\xc3\xaf\xc8\xb6 str", 'a:p/a:r/a:t"8-ɓïȶ str"'), - ("a:p", "ŮŦƑ-8\x1bliteral", 'a:p/a:r/a:t"ŮŦƑ-8_x001B_literal"'), - ( - "a:p", - "utf-8 unicode: Hér er texti", - 'a:p/a:r/a:t"utf-8 unicode: Hér er texti"', - ), - ] - ) - def text_set_fixture(self, request): - p_cxml, value, expected_cxml = request.param - p = element(p_cxml) - expected_xml = xml(expected_cxml) - return p, value, expected_xml - # fixture components ----------------------------------- @pytest.fixture @@ -1189,17 +1158,20 @@ def it_can_get_the_text_of_the_run(self, text_get_fixture): run, expected_value = text_get_fixture text = run.text assert text == expected_value - assert is_unicode(text) - - def it_can_change_its_text(self, text_set_fixture): - r, new_value, expected_xml = text_set_fixture - run = _Run(r, None) + assert isinstance(text, str) + @pytest.mark.parametrize( + "r_cxml, new_value, expected_r_cxml", + ( + ("a:r/a:t", "barfoo", 'a:r/a:t"barfoo"'), + ("a:r/a:t", "bar\x1bfoo", 'a:r/a:t"bar_x001B_foo"'), + ("a:r/a:t", "bar\tfoo", 'a:r/a:t"bar\tfoo"'), + ), + ) + def it_can_change_its_text(self, r_cxml, new_value, expected_r_cxml): + run = _Run(element(r_cxml), None) run.text = new_value - - print("run._r.xml == %s" % repr(run._r.xml)) - print("expected_xml == %s" % repr(expected_xml)) - assert run._r.xml == expected_xml + assert run._r.xml == xml(expected_r_cxml) # fixtures --------------------------------------------- @@ -1223,19 +1195,6 @@ def text_get_fixture(self): run = _Run(r, None) return run, "foobar" - @pytest.fixture( - params=[ - ("a:r/a:t", "barfoo", 'a:r/a:t"barfoo"'), - ("a:r/a:t", "bar\x1bfoo", 'a:r/a:t"bar_x001B_foo"'), - ("a:r/a:t", "bar\tfoo", 'a:r/a:t"bar\tfoo"'), - ] - ) - def text_set_fixture(self, request): - r_cxml, new_value, expected_r_cxml = request.param - r = element(r_cxml) - expected_xml = xml(expected_r_cxml) - return r, new_value, expected_xml - # fixture components ----------------------------------- @pytest.fixture diff --git a/tests/unitdata.py b/tests/unitdata.py index 2507230af..978647865 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Shared objects for unit data builder modules.""" -""" -Shared objects for unit data builder modules -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from pptx.oxml import parse_xml from pptx.oxml.ns import nsdecls @@ -40,17 +36,6 @@ def with_xmlattr(value): return self return with_xmlattr - else: - tmpl = "'%s' object has no attribute '%s'" - raise AttributeError(tmpl % (self.__class__.__name__, name)) - - def clear(self): - """ - Reset this builder back to initial state so it can be reused within - a single test. - """ - BaseBuilder.__init__(self) - return self @property def element(self): @@ -68,15 +53,6 @@ def with_child(self, child_bldr): self._child_bldrs.append(child_bldr) return self - def with_text(self, text): - """ - 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. - """ - self._text = text - return self - def with_nsdecls(self, *nspfxs): """ Cause the element to contain namespace declarations. By default, the @@ -100,9 +76,6 @@ def xml(self, indent=0): 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") - @property def _empty_element_tag(self): return "<%s%s%s/>" % (self.__tag__, self._nsdecls, self._xmlattrs_str) @@ -118,7 +91,12 @@ def _is_empty(self): def _non_empty_element_xml(self, 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" % ( # pragma: no cover + indent_str, + self._start_tag, + self._text, + self._end_tag, + ) else: xml = "%s%s\n" % (indent_str, self._start_tag) for child_bldr in self._child_bldrs: diff --git a/tests/unitutil/__init__.py b/tests/unitutil/__init__.py index 38eca7c27..7f5a5b584 100644 --- a/tests/unitutil/__init__.py +++ b/tests/unitutil/__init__.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Helper objects for unit testing.""" -""" -Helper objects for unit testing. -""" +from __future__ import annotations def count(start=0, step=1): diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index c83fea704..79e217c20 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -1,48 +1,48 @@ -# encoding: utf-8 +"""Parser for Compact XML Expression Language (CXEL) ('see-ex-ell'). -""" -Parser for Compact XML Expression Language (CXEL) ('see-ex-ell'), a compact -XML specification language I made up that's useful for producing XML element +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 print_function +from __future__ import annotations + +from typing import TYPE_CHECKING from pyparsing import ( - alphas, - alphanums, Combine, - dblQuotedString, - delimitedList, Forward, Group, Literal, Optional, - removeQuotes, - stringEnd, Suppress, Word, + alphanums, + alphas, + dblQuotedString, + delimitedList, + removeQuotes, + stringEnd, ) from pptx.oxml import parse_xml from pptx.oxml.ns import _nsmap as nsmap +if TYPE_CHECKING: + from pptx.oxml.xmlchemy import BaseOxmlElement # ==================================================================== # api functions # ==================================================================== -def element(cxel_str): - """ - Return an oxml element parsed from the XML generated from *cxel_str*. - """ +def element(cxel_str: str) -> BaseOxmlElement: + """Return an oxml element parsed from the XML generated from `cxel_str`.""" _xml = xml(cxel_str) return parse_xml(_xml) -def xml(cxel_str): - """Return the XML generated from *cxel_str*.""" +def xml(cxel_str: str) -> str: + """Return the XML generated from `cxel_str`.""" root_node.parseWithTabs() root_token = root_node.parseString(cxel_str) xml = root_token.element.xml @@ -83,7 +83,7 @@ def __repr__(self): Provide a more meaningful repr value for an Element object, one that displays the tagname as a simple empty element, e.g. ````. """ - return "<%s/>" % self._tagname + return "<%s/>" % self._tagname # pragma: no cover def connect_children(self, child_node_list): """ @@ -134,7 +134,7 @@ def nspfx(name, is_element=False): for name, val in self._attrs: pfx = nspfx(name) if pfx is None or pfx in nspfxs: - continue + continue # pragma: no cover nspfxs.append(pfx) return nspfxs @@ -274,9 +274,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 diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 1e257d92e..938d42ef0 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,45 +1,24 @@ -# encoding: utf-8 - -""" -Utility functions for loading files for unit testing -""" +"""Utility functions for loading files for unit testing.""" import os import sys -from lxml import etree - -from pptx.oxml import oxml_parser - - _thisdir = os.path.split(__file__)[0] test_file_dir = os.path.abspath(os.path.join(_thisdir, "..", "test_files")) -def abspath(relpath): - thisdir = os.path.split(__file__)[0] - return os.path.abspath(os.path.join(thisdir, relpath)) - - -def absjoin(*paths): +def absjoin(*paths: str): return os.path.abspath(os.path.join(*paths)) -def docx_path(name): - """ - Return the absolute path to test .docx file with root name *name*. - """ - return absjoin(test_file_dir, "%s.docx" % name) - - -def parse_xml_file(file_): - """ - Return ElementTree for XML contained in *file_* - """ - return etree.parse(file_, oxml_parser) +def snippet_bytes(snippet_file_name: str): + """Return bytes read from snippet file having `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: + return f.read().strip() -def snippet_seq(name, offset=0, count=sys.maxsize): +def snippet_seq(name: str, offset: int = 0, count: int = sys.maxsize): """ Return a tuple containing the unicode text snippets read from the snippet file having *name*. Snippets are delimited by a blank line. If specified, @@ -53,21 +32,26 @@ def snippet_seq(name, offset=0, count=sys.maxsize): return tuple(snippets[start:end]) -def snippet_text(snippet_file_name): +def snippet_text(snippet_file_name: str): """ Return the unicode text read from the test snippet file having *snippet_file_name*. """ - snippet_file_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") -def testfile(name): +def testfile(name: str): """ Return the absolute path to test file having *name*. """ return absjoin(test_file_dir, name) + + +def testfile_bytes(*segments: str): + """Return bytes of file at path formed by adding `segments` to test file dir.""" + path = os.path.join(test_file_dir, *segments) + with open(path, "rb") as f: + return f.read() diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 0edbce060..3b681d983 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,39 +1,45 @@ -# encoding: utf-8 +"""Utility functions wrapping the excellent `mock` library.""" -""" -Utility functions wrapping the excellent *mock* library. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function +from typing import Any +from unittest import mock +from unittest.mock import ( + ANY, + MagicMock, + Mock, + PropertyMock, + call, + create_autospec, + mock_open, + patch, +) -import sys +from pytest import FixtureRequest, LogCaptureFixture # noqa: PT013 -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, mock_open, patch, PropertyMock +__all__ = ["ANY", "FixtureRequest", "LogCaptureFixture", "MagicMock", "call", "mock"] -def class_mock(request, q_class_name, autospec=True, **kwargs): - """ - Return a mock patching the 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 - passed through to Mock(). Patch is reversed after calling test returns. +def class_mock( + request: FixtureRequest, q_class_name: str, autospec: bool = True, **kwargs: Any +) -> Mock: + """Return a mock patching the 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 passed through to + Mock(). Patch is reversed after calling test returns. """ _patch = patch(q_class_name, autospec=autospec, **kwargs) request.addfinalizer(_patch.stop) 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 +) -> Mock: + """Return a mock for an attribute (class variable) `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) @@ -41,84 +47,84 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): return _patch.start() -def function_mock(request, q_function_name, **kwargs): - """ - Return a mock patching the function with qualified name - *q_function_name*. Patch is reversed after calling test returns. +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. """ - _patch = patch(q_function_name, **kwargs) + _patch = patch(q_function_name, autospec=autospec, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def initializer_mock(request, cls, **kwargs): - """ - Return a mock for the __init__ method on *cls* where the patch is - reversed after pytest uses it. +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__", return_value=None, **kwargs) + _patch = patch.object(cls, "__init__", autospec=autospec, return_value=None, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -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 +def instance_mock( + request: FixtureRequest, + cls: type, + name: str | None = None, + spec_set: bool = True, + **kwargs: Any, +) -> Mock: + """Return mock for instance of `cls` that draws its spec from that class. + + The mock 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. + `request.fixturename`. Additional keyword arguments are passed through to the Mock() + call that creates the mock. """ name = name if name is not None else request.fixturename return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs) -def loose_mock(request, name=None, **kwargs): - """ - Return a "loose" mock, meaning it has no spec to constrain calls on it. - Additional keyword arguments are passed through to Mock(). If called - without a name, it is assigned the name of the fixture. +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 without a name, + it is assigned the name of the fixture. """ - if name is None: - name = request.fixturename - return Mock(name=name, **kwargs) + return Mock(name=request.fixturename if name is None else name, **kwargs) -def method_mock(request, cls, method_name, **kwargs): - """ - Return a mock for method *method_name* on *cls* where the patch is - reversed after pytest uses it. +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. """ - _patch = patch.object(cls, method_name, **kwargs) + _patch = patch.object(cls, method_name, autospec=autospec, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def open_mock(request, module_name, **kwargs): - """ - Return a mock for the builtin `open()` method in *module_name*. - """ +def open_mock(request: FixtureRequest, module_name: str, **kwargs: Any): + """Return a mock for the builtin `open()` method in `module_name`.""" target = "%s.open" % module_name _patch = patch(target, mock_open(), create=True, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def property_mock(request, cls, prop_name, **kwargs): - """ - Return a mock for property *prop_name* on class *cls* where the patch is - reversed after pytest uses it. - """ +def property_mock(request: FixtureRequest, cls: type, prop_name: str, **kwargs: Any): + """Return a mock for property `prop_name` on class `cls`.""" _patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def var_mock(request, q_var_name, **kwargs): - """ - Return a mock patching the variable with qualified name *q_var_name*. - Patch is reversed after calling test returns. - """ +def var_mock(request: FixtureRequest, q_var_name: str, **kwargs: Any): + """Return mock patching the variable with qualified name *q_var_name*.""" _patch = patch(q_var_name, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() diff --git a/tox.ini b/tox.ini index d7fd79f73..37acaa5fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,37 +1,9 @@ -# -# Configuration for tox and pytest - -[flake8] -exclude = dist,docs,*.egg-info,.git,lab,ref,_scratch,spec,.tox -max-line-length = 88 - -[pytest] -norecursedirs = docs *.egg-info features .git pptx spec .tox -python_classes = Test Describe -python_functions = test_ it_ they_ but_ and_it_ - [tox] -envlist = py27, py38 +envlist = py38, py39, py310, py311, py312 [testenv] -deps = - behave==1.2.5 - lxml>=3.1.0 - Pillow>=3.3.2 - pyparsing>=2.0.1 - pytest - XlsxWriter>=0.5.7 +deps = -rrequirements-test.txt commands = py.test -qx behave --format progress --stop --tags=-wip - -[testenv:py27] -deps = - behave==1.2.5 - lxml>=3.1.0 - mock - Pillow>=3.3.2,<4.0 - pyparsing>=2.0.1 - pytest - XlsxWriter>=0.5.7 diff --git a/typings/behave/__init__.pyi b/typings/behave/__init__.pyi new file mode 100644 index 000000000..f8ffc2058 --- /dev/null +++ b/typings/behave/__init__.pyi @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Callable + +from typing_extensions import TypeAlias + +from .runner import Context + +_ThreeArgStep: TypeAlias = Callable[[Context, str, str, str], None] +_TwoArgStep: TypeAlias = Callable[[Context, str, str], None] +_OneArgStep: TypeAlias = Callable[[Context, str], None] +_NoArgStep: TypeAlias = Callable[[Context], None] +_Step: TypeAlias = _NoArgStep | _OneArgStep | _TwoArgStep | _ThreeArgStep + +def given(phrase: str) -> Callable[[_Step], _Step]: ... +def when(phrase: str) -> Callable[[_Step], _Step]: ... +def then(phrase: str) -> Callable[[_Step], _Step]: ... diff --git a/typings/behave/runner.pyi b/typings/behave/runner.pyi new file mode 100644 index 000000000..aaea74dad --- /dev/null +++ b/typings/behave/runner.pyi @@ -0,0 +1,3 @@ +from types import SimpleNamespace + +class Context(SimpleNamespace): ... diff --git a/typings/lxml/_types.pyi b/typings/lxml/_types.pyi new file mode 100644 index 000000000..34d2095db --- /dev/null +++ b/typings/lxml/_types.pyi @@ -0,0 +1,40 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Any, Callable, Collection, Literal, Mapping, Protocol, TypeVar + +from typing_extensions import TypeAlias + +from .etree import QName, _Element, _ElementTree + +_ET = TypeVar("_ET", bound=_Element, default=_Element) +_ET_co = TypeVar("_ET_co", bound=_Element, default=_Element, covariant=True) +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) + +_AttrName: TypeAlias = str + +_AttrVal: TypeAlias = _TextArg + +_ElemPathArg: TypeAlias = str | QName + +_ElementOrTree: TypeAlias = _ET | _ElementTree[_ET] + +_NSMapArg = Mapping[None, str] | Mapping[str, str] | Mapping[str | None, str] + +_NonDefaultNSMapArg = Mapping[str, str] + +_OutputMethodArg = Literal["html", "text", "xml"] + +_TagName: TypeAlias = str + +_TagSelector: TypeAlias = _TagName | Callable[..., _Element] + +# String argument also support QName in various places +_TextArg: TypeAlias = str | bytes | QName + +_XPathObject = Any + +class SupportsLaxedItems(Protocol[_KT_co, _VT_co]): + def items(self) -> Collection[tuple[_KT_co, _VT_co]]: ... diff --git a/typings/lxml/etree/__init__.pyi b/typings/lxml/etree/__init__.pyi new file mode 100644 index 000000000..e649ce0b2 --- /dev/null +++ b/typings/lxml/etree/__init__.pyi @@ -0,0 +1,18 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from ._classlookup import ElementBase as ElementBase +from ._classlookup import ElementDefaultClassLookup as ElementDefaultClassLookup +from ._cleanup import strip_elements as strip_elements +from ._element import _Element as _Element +from ._element import _ElementTree as _ElementTree +from ._module_func import fromstring as fromstring +from ._module_func import tostring as tostring +from ._module_misc import QName as QName +from ._nsclasses import ElementNamespaceClassLookup as ElementNamespaceClassLookup +from ._parser import HTMLParser as HTMLParser +from ._parser import XMLParser as XMLParser + +class CDATA: + def __init__(self, data: str) -> None: ... diff --git a/typings/lxml/etree/_classlookup.pyi b/typings/lxml/etree/_classlookup.pyi new file mode 100644 index 000000000..03313c3c4 --- /dev/null +++ b/typings/lxml/etree/_classlookup.pyi @@ -0,0 +1,75 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from ._element import _Element + +class ElementBase(_Element): + """The public Element class + + Original Docstring + ------------------ + All custom Element classes must inherit from this one. + To create an Element, use the `Element()` factory. + + BIG FAT WARNING: Subclasses *must not* override `__init__` or + `__new__` as it is absolutely undefined when these objects will be + created or destroyed. All persistent state of Elements must be + stored in the underlying XML. If you really need to initialize + the object after creation, you can implement an ``_init(self)`` + method that will be called directly after object creation. + + Subclasses of this class can be instantiated to create a new + Element. By default, the tag name will be the class name and the + namespace will be empty. You can modify this with the following + class attributes: + + * TAG - the tag name, possibly containing a namespace in Clark + notation + + * NAMESPACE - the default namespace URI, unless provided as part + of the TAG attribute. + + * HTML - flag if the class is an HTML tag, as opposed to an XML + tag. This only applies to un-namespaced tags and defaults to + false (i.e. XML). + + * PARSER - the parser that provides the configuration for the + newly created document. Providing an HTML parser here will + default to creating an HTML element. + + In user code, the latter three are commonly inherited in class + hierarchies that implement a common namespace. + """ + + def __init__( + self, + *children: object, + attrib: dict[str, str] | None = None, + **_extra: str, + ) -> None: ... + def _init(self) -> None: ... + +class ElementClassLookup: + """Superclass of Element class lookups""" + +class ElementDefaultClassLookup(ElementClassLookup): + """Element class lookup scheme that always returns the default Element + class. + + The keyword arguments ``element``, ``comment``, ``pi`` and ``entity`` + accept the respective Element classes.""" + + def __init__( + self, + element: type[ElementBase] | None = None, + ) -> None: ... + +class FallbackElementClassLookup(ElementClassLookup): + """Superclass of Element class lookups with additional fallback""" + + @property + def fallback(self) -> ElementClassLookup | None: ... + def __init__(self, fallback: ElementClassLookup | None = None) -> None: ... + def set_fallback(self, lookup: ElementClassLookup) -> None: + """Sets the fallback scheme for this lookup method""" diff --git a/typings/lxml/etree/_cleanup.pyi b/typings/lxml/etree/_cleanup.pyi new file mode 100644 index 000000000..29e6bd861 --- /dev/null +++ b/typings/lxml/etree/_cleanup.pyi @@ -0,0 +1,21 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Collection, overload + +from .._types import _ElementOrTree, _TagSelector + +@overload +def strip_elements( + __tree_or_elem: _ElementOrTree, + *tag_names: _TagSelector, + with_tail: bool = True, +) -> None: ... +@overload +def strip_elements( + __tree_or_elem: _ElementOrTree, + __tag: Collection[_TagSelector], + /, + with_tail: bool = True, +) -> None: ... diff --git a/typings/lxml/etree/_element.pyi b/typings/lxml/etree/_element.pyi new file mode 100644 index 000000000..f25a147a9 --- /dev/null +++ b/typings/lxml/etree/_element.pyi @@ -0,0 +1,96 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Collection, Generic, Iterable, Iterator, TypeVar, overload + +from typing_extensions import Self + +from .. import _types as _t +from . import CDATA + +_T = TypeVar("_T") + +# Behaves like MutableMapping but deviates a lot in details +class _Attrib: + def __bool__(self) -> bool: ... + def __contains__(self, __o: object) -> bool: ... + def __delitem__(self, __k: _t._AttrName) -> None: ... + def __getitem__(self, __k: _t._AttrName) -> str: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __setitem__(self, __k: _t._AttrName, __v: _t._AttrVal) -> None: ... + @property + def _element(self) -> _Element: ... + def get(self, key: _t._AttrName, default: _T) -> str | _T: ... + def has_key(self, key: _t._AttrName) -> bool: ... + def items(self) -> list[tuple[str, str]]: ... + def iteritems(self) -> Iterator[tuple[str, str]]: ... + def iterkeys(self) -> Iterator[str]: ... + def itervalues(self) -> Iterator[str]: ... + def keys(self) -> list[str]: ... + def values(self) -> list[str]: ... + +class _Element: + @overload + def __getitem__(self, __x: int) -> _Element: ... + @overload + def __getitem__(self, __x: slice) -> list[_Element]: ... + def __contains__(self, __o: object) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_Element]: ... + def addprevious(self, element: _Element) -> None: ... + def append(self, element: _Element) -> None: ... + @property + def attrib(self) -> _Attrib: ... + def find(self, path: _t._ElemPathArg) -> Self | None: ... + def findall( + self, path: _t._ElemPathArg, namespaces: _t._NSMapArg | None = None + ) -> list[_Element]: ... + @overload + def get(self, key: _t._AttrName) -> str | None: ... + @overload + def get(self, key: _t._AttrName, default: _T) -> str | _T: ... + def getparent(self) -> _Element | None: ... + def index(self, child: _Element, start: int | None = None, end: int | None = None) -> int: ... + def iterancestors( + self, *, tag: _t._TagSelector | Collection[_t._TagSelector] | None = None + ) -> Iterator[Self]: ... + @overload + def iterchildren( + self, *tags: _t._TagSelector, reversed: bool = False + ) -> Iterator[_Element]: ... + @overload + def iterchildren( + self, + *, + tag: _t._TagSelector | Iterable[_t._TagSelector] | None = None, + reversed: bool = False, + ) -> Iterator[_Element]: ... + @overload + def itertext(self, *tags: _t._TagSelector, with_tail: bool = True) -> Iterator[str]: ... + @overload + def itertext( + self, + *, + tag: _t._TagSelector | Collection[_t._TagSelector] | None = None, + with_tail: bool = True, + ) -> Iterator[str]: ... + def remove(self, element: _Element) -> None: ... + def set(self, key: _t._AttrName, value: _t._AttrVal) -> None: ... + @property + def tag(self) -> str: ... + @property + def tail(self) -> str | None: ... + @property + def text(self) -> str | None: ... + @text.setter + def text(self, value: str | CDATA | None) -> None: ... + def xpath( + self, + _path: str, + /, + namespaces: _t._NonDefaultNSMapArg | None = None, + ) -> _t._XPathObject: ... + +class _ElementTree(Generic[_t._ET_co]): ... diff --git a/typings/lxml/etree/_module_func.pyi b/typings/lxml/etree/_module_func.pyi new file mode 100644 index 000000000..e2910f503 --- /dev/null +++ b/typings/lxml/etree/_module_func.pyi @@ -0,0 +1,38 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Literal, overload + +from .._types import _ElementOrTree, _OutputMethodArg +from ..etree import HTMLParser, XMLParser +from ._element import _Element + +def fromstring(text: str | bytes, parser: XMLParser | HTMLParser) -> _Element: ... + +# -- Native str, no XML declaration -- +@overload +def tostring( # type: ignore[overload-overlap] + element_or_tree: _ElementOrTree, + *, + encoding: type[str] | Literal["unicode"], + method: _OutputMethodArg = "xml", + pretty_print: bool = False, + with_tail: bool = True, + standalone: bool | None = None, + doctype: str | None = None, +) -> str: ... + +# -- bytes, str encoded with `encoding`, no XML declaration -- +@overload +def tostring( + element_or_tree: _ElementOrTree, + *, + encoding: str | None = None, + method: _OutputMethodArg = "xml", + xml_declaration: bool | None = None, + pretty_print: bool = False, + with_tail: bool = True, + standalone: bool | None = None, + doctype: str | None = None, +) -> bytes: ... diff --git a/typings/lxml/etree/_module_misc.pyi b/typings/lxml/etree/_module_misc.pyi new file mode 100644 index 000000000..9da021f0c --- /dev/null +++ b/typings/lxml/etree/_module_misc.pyi @@ -0,0 +1,5 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +class QName: ... diff --git a/typings/lxml/etree/_nsclasses.pyi b/typings/lxml/etree/_nsclasses.pyi new file mode 100644 index 000000000..5118f7a80 --- /dev/null +++ b/typings/lxml/etree/_nsclasses.pyi @@ -0,0 +1,31 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Iterable, Iterator, MutableMapping, TypeVar + +from .._types import SupportsLaxedItems +from ._classlookup import ElementBase, ElementClassLookup, FallbackElementClassLookup + +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") + +class _NamespaceRegistry(MutableMapping[_KT, _VT]): + def __delitem__(self, __key: _KT) -> None: ... + def __getitem__(self, __key: _KT) -> _VT: ... + def __setitem__(self, __key: _KT, __value: _VT) -> None: ... + def __iter__(self) -> Iterator[_KT]: ... + def __len__(self) -> int: ... + def update( # type: ignore[override] + self, + class_dict_iterable: SupportsLaxedItems[_KT, _VT] | Iterable[tuple[_KT, _VT]], + ) -> None: ... + def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override] + def iteritems(self) -> Iterator[tuple[_KT, _VT]]: ... + def clear(self) -> None: ... + +class _ClassNamespaceRegistry(_NamespaceRegistry[str | None, type[ElementBase]]): ... + +class ElementNamespaceClassLookup(FallbackElementClassLookup): + def __init__(self, fallback: ElementClassLookup | None = None) -> None: ... + def get_namespace(self, ns_uri: str | None) -> _ClassNamespaceRegistry: ... diff --git a/typings/lxml/etree/_parser.pyi b/typings/lxml/etree/_parser.pyi new file mode 100644 index 000000000..2fdd144fa --- /dev/null +++ b/typings/lxml/etree/_parser.pyi @@ -0,0 +1,81 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Literal + +from ._classlookup import ElementClassLookup +from .._types import _ET_co, _NSMapArg, _TagName, SupportsLaxedItems + +class HTMLParser: + def __init__( + self, + *, + encoding: str | None = None, + remove_blank_text: bool = False, + remove_comments: bool = False, + remove_pis: bool = False, + strip_cdata: bool = True, + no_network: bool = True, + recover: bool = True, + compact: bool = True, + default_doctype: bool = True, + collect_ids: bool = True, + huge_tree: bool = False, + ) -> None: ... + def set_element_class_lookup(self, lookup: ElementClassLookup | None = None) -> None: ... + +class XMLParser: + def __init__( + self, + *, + attribute_defaults: bool = False, + collect_ids: bool = True, + compact: bool = True, + dtd_validation: bool = False, + encoding: str | None = None, + huge_tree: bool = False, + load_dtd: bool = False, + no_network: bool = True, + ns_clean: bool = False, + recover: bool = False, + remove_blank_text: bool = False, + remove_comments: bool = False, + remove_pis: bool = False, + resolve_entities: bool | Literal["internal"] = "internal", + strip_cdata: bool = True, + ) -> None: ... + def makeelement( + self, + _tag: _TagName, + /, + attrib: SupportsLaxedItems[str, str] | None = None, + nsmap: _NSMapArg | None = None, + **_extra: str, + ) -> _ET_co: ... + def set_element_class_lookup(self, lookup: ElementClassLookup | None = None) -> None: + """ + Notes + ----- + When calling this method, it is advised to also change typing + specialization of concerned parser too, because current python + typing system can't change it automatically. + + Example + ------- + Following code demonstrates how to create ``lxml.html.HTMLParser`` + manually from ``lxml.etree.HTMLParser``:: + + ```python + parser = etree.HTMLParser() + reveal_type(parser) # HTMLParser[_Element] + if TYPE_CHECKING: + parser = cast('etree.HTMLParser[HtmlElement]', parser) + else: + parser.set_element_class_lookup( + html.HtmlElementClassLookup()) + result = etree.fromstring(data, parser=parser) + reveal_type(result) # HtmlElement + ``` + """ + ...