From 86df53458d2faaca781a98b37954806eadd57f3a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 22:14:21 -0700 Subject: [PATCH 01/14] build: move pptx package under src/ This improves packaging reliability because it prevents tests from running against the current directory instead of the installed version of the package. --- .gitignore | 2 +- MANIFEST.in | 2 +- setup.py | 5 +++-- {pptx => src/pptx}/__init__.py | 0 {pptx => src/pptx}/action.py | 0 {pptx => src/pptx}/api.py | 0 {pptx => src/pptx}/chart/__init__.py | 0 {pptx => src/pptx}/chart/axis.py | 0 {pptx => src/pptx}/chart/category.py | 0 {pptx => src/pptx}/chart/chart.py | 0 {pptx => src/pptx}/chart/data.py | 0 {pptx => src/pptx}/chart/datalabel.py | 0 {pptx => src/pptx}/chart/legend.py | 0 {pptx => src/pptx}/chart/marker.py | 0 {pptx => src/pptx}/chart/plot.py | 0 {pptx => src/pptx}/chart/point.py | 0 {pptx => src/pptx}/chart/series.py | 0 {pptx => src/pptx}/chart/xlsx.py | 0 {pptx => src/pptx}/chart/xmlwriter.py | 0 {pptx => src/pptx}/compat/__init__.py | 0 {pptx => src/pptx}/compat/python2.py | 0 {pptx => src/pptx}/compat/python3.py | 0 {pptx => src/pptx}/dml/__init__.py | 0 {pptx => src/pptx}/dml/chtfmt.py | 0 {pptx => src/pptx}/dml/color.py | 0 {pptx => src/pptx}/dml/effect.py | 0 {pptx => src/pptx}/dml/fill.py | 0 {pptx => src/pptx}/dml/line.py | 0 {pptx => src/pptx}/enum/__init__.py | 0 {pptx => src/pptx}/enum/action.py | 0 {pptx => src/pptx}/enum/base.py | 0 {pptx => src/pptx}/enum/chart.py | 0 {pptx => src/pptx}/enum/dml.py | 0 {pptx => src/pptx}/enum/lang.py | 0 {pptx => src/pptx}/enum/shapes.py | 0 {pptx => src/pptx}/enum/text.py | 0 {pptx => src/pptx}/exc.py | 0 {pptx => src/pptx}/media.py | 0 {pptx => src/pptx}/opc/__init__.py | 0 {pptx => src/pptx}/opc/constants.py | 0 {pptx => src/pptx}/opc/oxml.py | 0 {pptx => src/pptx}/opc/package.py | 0 {pptx => src/pptx}/opc/packuri.py | 0 {pptx => src/pptx}/opc/serialized.py | 0 {pptx => src/pptx}/opc/shared.py | 0 {pptx => src/pptx}/opc/spec.py | 0 {pptx => src/pptx}/oxml/__init__.py | 0 {pptx => src/pptx}/oxml/action.py | 0 {pptx => src/pptx}/oxml/chart/__init__.py | 0 {pptx => src/pptx}/oxml/chart/axis.py | 0 {pptx => src/pptx}/oxml/chart/chart.py | 0 {pptx => src/pptx}/oxml/chart/datalabel.py | 0 {pptx => src/pptx}/oxml/chart/legend.py | 0 {pptx => src/pptx}/oxml/chart/marker.py | 0 {pptx => src/pptx}/oxml/chart/plot.py | 0 {pptx => src/pptx}/oxml/chart/series.py | 0 {pptx => src/pptx}/oxml/chart/shared.py | 0 {pptx => src/pptx}/oxml/coreprops.py | 0 {pptx => src/pptx}/oxml/dml/__init__.py | 0 {pptx => src/pptx}/oxml/dml/color.py | 0 {pptx => src/pptx}/oxml/dml/fill.py | 0 {pptx => src/pptx}/oxml/dml/line.py | 0 {pptx => src/pptx}/oxml/ns.py | 0 {pptx => src/pptx}/oxml/presentation.py | 0 {pptx => src/pptx}/oxml/shapes/__init__.py | 0 {pptx => src/pptx}/oxml/shapes/autoshape.py | 0 {pptx => src/pptx}/oxml/shapes/connector.py | 0 {pptx => src/pptx}/oxml/shapes/graphfrm.py | 0 {pptx => src/pptx}/oxml/shapes/groupshape.py | 0 {pptx => src/pptx}/oxml/shapes/picture.py | 0 {pptx => src/pptx}/oxml/shapes/shared.py | 0 {pptx => src/pptx}/oxml/simpletypes.py | 0 {pptx => src/pptx}/oxml/slide.py | 0 {pptx => src/pptx}/oxml/table.py | 0 {pptx => src/pptx}/oxml/text.py | 0 {pptx => src/pptx}/oxml/theme.py | 0 {pptx => src/pptx}/oxml/xmlchemy.py | 0 {pptx => src/pptx}/package.py | 0 {pptx => src/pptx}/parts/__init__.py | 0 {pptx => src/pptx}/parts/chart.py | 0 {pptx => src/pptx}/parts/coreprops.py | 0 {pptx => src/pptx}/parts/embeddedpackage.py | 0 {pptx => src/pptx}/parts/image.py | 0 {pptx => src/pptx}/parts/media.py | 0 {pptx => src/pptx}/parts/presentation.py | 0 {pptx => src/pptx}/parts/slide.py | 0 {pptx => src/pptx}/presentation.py | 0 {pptx => src/pptx}/shapes/__init__.py | 0 {pptx => src/pptx}/shapes/autoshape.py | 0 {pptx => src/pptx}/shapes/base.py | 0 {pptx => src/pptx}/shapes/connector.py | 0 {pptx => src/pptx}/shapes/freeform.py | 0 {pptx => src/pptx}/shapes/graphfrm.py | 0 {pptx => src/pptx}/shapes/group.py | 0 {pptx => src/pptx}/shapes/picture.py | 0 {pptx => src/pptx}/shapes/placeholder.py | 0 {pptx => src/pptx}/shapes/shapetree.py | 0 {pptx => src/pptx}/shared.py | 0 {pptx => src/pptx}/slide.py | 0 {pptx => src/pptx}/spec.py | 0 {pptx => src/pptx}/table.py | 0 {pptx => src/pptx}/templates/default.pptx | Bin {pptx => src/pptx}/templates/docx-icon.emf | Bin {pptx => src/pptx}/templates/generic-icon.emf | Bin {pptx => src/pptx}/templates/notes.xml | 0 {pptx => src/pptx}/templates/notesMaster.xml | 0 {pptx => src/pptx}/templates/pptx-icon.emf | Bin {pptx => src/pptx}/templates/theme.xml | 0 {pptx => src/pptx}/templates/xlsx-icon.emf | Bin {pptx => src/pptx}/text/__init__.py | 0 {pptx => src/pptx}/text/fonts.py | 0 {pptx => src/pptx}/text/layout.py | 0 {pptx => src/pptx}/text/text.py | 0 {pptx => src/pptx}/util.py | 0 tox.ini | 4 ++-- 115 files changed, 7 insertions(+), 6 deletions(-) rename {pptx => src/pptx}/__init__.py (100%) rename {pptx => src/pptx}/action.py (100%) rename {pptx => src/pptx}/api.py (100%) rename {pptx => src/pptx}/chart/__init__.py (100%) rename {pptx => src/pptx}/chart/axis.py (100%) rename {pptx => src/pptx}/chart/category.py (100%) rename {pptx => src/pptx}/chart/chart.py (100%) rename {pptx => src/pptx}/chart/data.py (100%) rename {pptx => src/pptx}/chart/datalabel.py (100%) rename {pptx => src/pptx}/chart/legend.py (100%) rename {pptx => src/pptx}/chart/marker.py (100%) rename {pptx => src/pptx}/chart/plot.py (100%) rename {pptx => src/pptx}/chart/point.py (100%) rename {pptx => src/pptx}/chart/series.py (100%) rename {pptx => src/pptx}/chart/xlsx.py (100%) rename {pptx => src/pptx}/chart/xmlwriter.py (100%) rename {pptx => src/pptx}/compat/__init__.py (100%) rename {pptx => src/pptx}/compat/python2.py (100%) rename {pptx => src/pptx}/compat/python3.py (100%) rename {pptx => src/pptx}/dml/__init__.py (100%) rename {pptx => src/pptx}/dml/chtfmt.py (100%) rename {pptx => src/pptx}/dml/color.py (100%) rename {pptx => src/pptx}/dml/effect.py (100%) rename {pptx => src/pptx}/dml/fill.py (100%) rename {pptx => src/pptx}/dml/line.py (100%) rename {pptx => src/pptx}/enum/__init__.py (100%) rename {pptx => src/pptx}/enum/action.py (100%) rename {pptx => src/pptx}/enum/base.py (100%) rename {pptx => src/pptx}/enum/chart.py (100%) rename {pptx => src/pptx}/enum/dml.py (100%) rename {pptx => src/pptx}/enum/lang.py (100%) rename {pptx => src/pptx}/enum/shapes.py (100%) rename {pptx => src/pptx}/enum/text.py (100%) rename {pptx => src/pptx}/exc.py (100%) rename {pptx => src/pptx}/media.py (100%) rename {pptx => src/pptx}/opc/__init__.py (100%) rename {pptx => src/pptx}/opc/constants.py (100%) rename {pptx => src/pptx}/opc/oxml.py (100%) rename {pptx => src/pptx}/opc/package.py (100%) rename {pptx => src/pptx}/opc/packuri.py (100%) rename {pptx => src/pptx}/opc/serialized.py (100%) rename {pptx => src/pptx}/opc/shared.py (100%) rename {pptx => src/pptx}/opc/spec.py (100%) rename {pptx => src/pptx}/oxml/__init__.py (100%) rename {pptx => src/pptx}/oxml/action.py (100%) rename {pptx => src/pptx}/oxml/chart/__init__.py (100%) rename {pptx => src/pptx}/oxml/chart/axis.py (100%) rename {pptx => src/pptx}/oxml/chart/chart.py (100%) rename {pptx => src/pptx}/oxml/chart/datalabel.py (100%) rename {pptx => src/pptx}/oxml/chart/legend.py (100%) rename {pptx => src/pptx}/oxml/chart/marker.py (100%) rename {pptx => src/pptx}/oxml/chart/plot.py (100%) rename {pptx => src/pptx}/oxml/chart/series.py (100%) rename {pptx => src/pptx}/oxml/chart/shared.py (100%) rename {pptx => src/pptx}/oxml/coreprops.py (100%) rename {pptx => src/pptx}/oxml/dml/__init__.py (100%) rename {pptx => src/pptx}/oxml/dml/color.py (100%) rename {pptx => src/pptx}/oxml/dml/fill.py (100%) rename {pptx => src/pptx}/oxml/dml/line.py (100%) rename {pptx => src/pptx}/oxml/ns.py (100%) rename {pptx => src/pptx}/oxml/presentation.py (100%) rename {pptx => src/pptx}/oxml/shapes/__init__.py (100%) rename {pptx => src/pptx}/oxml/shapes/autoshape.py (100%) rename {pptx => src/pptx}/oxml/shapes/connector.py (100%) rename {pptx => src/pptx}/oxml/shapes/graphfrm.py (100%) rename {pptx => src/pptx}/oxml/shapes/groupshape.py (100%) rename {pptx => src/pptx}/oxml/shapes/picture.py (100%) rename {pptx => src/pptx}/oxml/shapes/shared.py (100%) rename {pptx => src/pptx}/oxml/simpletypes.py (100%) rename {pptx => src/pptx}/oxml/slide.py (100%) rename {pptx => src/pptx}/oxml/table.py (100%) rename {pptx => src/pptx}/oxml/text.py (100%) rename {pptx => src/pptx}/oxml/theme.py (100%) rename {pptx => src/pptx}/oxml/xmlchemy.py (100%) rename {pptx => src/pptx}/package.py (100%) rename {pptx => src/pptx}/parts/__init__.py (100%) rename {pptx => src/pptx}/parts/chart.py (100%) rename {pptx => src/pptx}/parts/coreprops.py (100%) rename {pptx => src/pptx}/parts/embeddedpackage.py (100%) rename {pptx => src/pptx}/parts/image.py (100%) rename {pptx => src/pptx}/parts/media.py (100%) rename {pptx => src/pptx}/parts/presentation.py (100%) rename {pptx => src/pptx}/parts/slide.py (100%) rename {pptx => src/pptx}/presentation.py (100%) rename {pptx => src/pptx}/shapes/__init__.py (100%) rename {pptx => src/pptx}/shapes/autoshape.py (100%) rename {pptx => src/pptx}/shapes/base.py (100%) rename {pptx => src/pptx}/shapes/connector.py (100%) rename {pptx => src/pptx}/shapes/freeform.py (100%) rename {pptx => src/pptx}/shapes/graphfrm.py (100%) rename {pptx => src/pptx}/shapes/group.py (100%) rename {pptx => src/pptx}/shapes/picture.py (100%) rename {pptx => src/pptx}/shapes/placeholder.py (100%) rename {pptx => src/pptx}/shapes/shapetree.py (100%) rename {pptx => src/pptx}/shared.py (100%) rename {pptx => src/pptx}/slide.py (100%) rename {pptx => src/pptx}/spec.py (100%) rename {pptx => src/pptx}/table.py (100%) rename {pptx => src/pptx}/templates/default.pptx (100%) rename {pptx => src/pptx}/templates/docx-icon.emf (100%) rename {pptx => src/pptx}/templates/generic-icon.emf (100%) rename {pptx => src/pptx}/templates/notes.xml (100%) rename {pptx => src/pptx}/templates/notesMaster.xml (100%) rename {pptx => src/pptx}/templates/pptx-icon.emf (100%) rename {pptx => src/pptx}/templates/theme.xml (100%) rename {pptx => src/pptx}/templates/xlsx-icon.emf (100%) rename {pptx => src/pptx}/text/__init__.py (100%) rename {pptx => src/pptx}/text/fonts.py (100%) rename {pptx => src/pptx}/text/layout.py (100%) rename {pptx => src/pptx}/text/text.py (100%) rename {pptx => src/pptx}/util.py (100%) diff --git a/.gitignore b/.gitignore index 8f0219e06..c043a21c1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .cache .coverage /.tox/ -*.egg-info +/src/*.egg-info *.pyc /dist/ /docs/.build 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/setup.py b/setup.py index 582610e5b..6d9a0d1a5 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def ascii_bytes_from(path, *paths): # read required text from files thisdir = os.path.dirname(__file__) -init_py = ascii_bytes_from(thisdir, "pptx", "__init__.py") +init_py = ascii_bytes_from(thisdir, "src", "pptx", "__init__.py") readme = ascii_bytes_from(thisdir, "README.rst") history = ascii_bytes_from(thisdir, "HISTORY.rst") @@ -38,7 +38,7 @@ def ascii_bytes_from(path, *paths): AUTHOR_EMAIL = "python-pptx@googlegroups.com" URL = "https://github.com/scanny/python-pptx" LICENSE = "MIT" -PACKAGES = find_packages(exclude=["tests", "tests.*"]) +PACKAGES = find_packages(where="src") PACKAGE_DATA = {"pptx": ["templates/*"]} INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2", "XlsxWriter>=0.5.7"] @@ -78,6 +78,7 @@ def ascii_bytes_from(path, *paths): "license": LICENSE, "packages": PACKAGES, "package_data": PACKAGE_DATA, + "package_dir": {"": "src"}, "install_requires": INSTALL_REQUIRES, "tests_require": TESTS_REQUIRE, "test_suite": TEST_SUITE, diff --git a/pptx/__init__.py b/src/pptx/__init__.py similarity index 100% rename from pptx/__init__.py rename to src/pptx/__init__.py diff --git a/pptx/action.py b/src/pptx/action.py similarity index 100% rename from pptx/action.py rename to src/pptx/action.py diff --git a/pptx/api.py b/src/pptx/api.py similarity index 100% rename from pptx/api.py rename to src/pptx/api.py 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 100% rename from pptx/chart/axis.py rename to src/pptx/chart/axis.py diff --git a/pptx/chart/category.py b/src/pptx/chart/category.py similarity index 100% rename from pptx/chart/category.py rename to src/pptx/chart/category.py diff --git a/pptx/chart/chart.py b/src/pptx/chart/chart.py similarity index 100% rename from pptx/chart/chart.py rename to src/pptx/chart/chart.py diff --git a/pptx/chart/data.py b/src/pptx/chart/data.py similarity index 100% rename from pptx/chart/data.py rename to src/pptx/chart/data.py diff --git a/pptx/chart/datalabel.py b/src/pptx/chart/datalabel.py similarity index 100% rename from pptx/chart/datalabel.py rename to src/pptx/chart/datalabel.py diff --git a/pptx/chart/legend.py b/src/pptx/chart/legend.py similarity index 100% rename from pptx/chart/legend.py rename to src/pptx/chart/legend.py diff --git a/pptx/chart/marker.py b/src/pptx/chart/marker.py similarity index 100% rename from pptx/chart/marker.py rename to src/pptx/chart/marker.py diff --git a/pptx/chart/plot.py b/src/pptx/chart/plot.py similarity index 100% rename from pptx/chart/plot.py rename to src/pptx/chart/plot.py diff --git a/pptx/chart/point.py b/src/pptx/chart/point.py similarity index 100% rename from pptx/chart/point.py rename to src/pptx/chart/point.py diff --git a/pptx/chart/series.py b/src/pptx/chart/series.py similarity index 100% rename from pptx/chart/series.py rename to src/pptx/chart/series.py diff --git a/pptx/chart/xlsx.py b/src/pptx/chart/xlsx.py similarity index 100% rename from pptx/chart/xlsx.py rename to src/pptx/chart/xlsx.py diff --git a/pptx/chart/xmlwriter.py b/src/pptx/chart/xmlwriter.py similarity index 100% rename from pptx/chart/xmlwriter.py rename to src/pptx/chart/xmlwriter.py diff --git a/pptx/compat/__init__.py b/src/pptx/compat/__init__.py similarity index 100% rename from pptx/compat/__init__.py rename to src/pptx/compat/__init__.py diff --git a/pptx/compat/python2.py b/src/pptx/compat/python2.py similarity index 100% rename from pptx/compat/python2.py rename to src/pptx/compat/python2.py diff --git a/pptx/compat/python3.py b/src/pptx/compat/python3.py similarity index 100% rename from pptx/compat/python3.py rename to src/pptx/compat/python3.py 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 100% rename from pptx/dml/chtfmt.py rename to src/pptx/dml/chtfmt.py diff --git a/pptx/dml/color.py b/src/pptx/dml/color.py similarity index 100% rename from pptx/dml/color.py rename to src/pptx/dml/color.py diff --git a/pptx/dml/effect.py b/src/pptx/dml/effect.py similarity index 100% rename from pptx/dml/effect.py rename to src/pptx/dml/effect.py diff --git a/pptx/dml/fill.py b/src/pptx/dml/fill.py similarity index 100% rename from pptx/dml/fill.py rename to src/pptx/dml/fill.py diff --git a/pptx/dml/line.py b/src/pptx/dml/line.py similarity index 100% rename from pptx/dml/line.py rename to src/pptx/dml/line.py 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/pptx/enum/action.py b/src/pptx/enum/action.py similarity index 100% rename from pptx/enum/action.py rename to src/pptx/enum/action.py diff --git a/pptx/enum/base.py b/src/pptx/enum/base.py similarity index 100% rename from pptx/enum/base.py rename to src/pptx/enum/base.py diff --git a/pptx/enum/chart.py b/src/pptx/enum/chart.py similarity index 100% rename from pptx/enum/chart.py rename to src/pptx/enum/chart.py diff --git a/pptx/enum/dml.py b/src/pptx/enum/dml.py similarity index 100% rename from pptx/enum/dml.py rename to src/pptx/enum/dml.py diff --git a/pptx/enum/lang.py b/src/pptx/enum/lang.py similarity index 100% rename from pptx/enum/lang.py rename to src/pptx/enum/lang.py diff --git a/pptx/enum/shapes.py b/src/pptx/enum/shapes.py similarity index 100% rename from pptx/enum/shapes.py rename to src/pptx/enum/shapes.py diff --git a/pptx/enum/text.py b/src/pptx/enum/text.py similarity index 100% rename from pptx/enum/text.py rename to src/pptx/enum/text.py diff --git a/pptx/exc.py b/src/pptx/exc.py similarity index 100% rename from pptx/exc.py rename to src/pptx/exc.py diff --git a/pptx/media.py b/src/pptx/media.py similarity index 100% rename from pptx/media.py rename to src/pptx/media.py 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/pptx/opc/constants.py b/src/pptx/opc/constants.py similarity index 100% rename from pptx/opc/constants.py rename to src/pptx/opc/constants.py diff --git a/pptx/opc/oxml.py b/src/pptx/opc/oxml.py similarity index 100% rename from pptx/opc/oxml.py rename to src/pptx/opc/oxml.py diff --git a/pptx/opc/package.py b/src/pptx/opc/package.py similarity index 100% rename from pptx/opc/package.py rename to src/pptx/opc/package.py diff --git a/pptx/opc/packuri.py b/src/pptx/opc/packuri.py similarity index 100% rename from pptx/opc/packuri.py rename to src/pptx/opc/packuri.py diff --git a/pptx/opc/serialized.py b/src/pptx/opc/serialized.py similarity index 100% rename from pptx/opc/serialized.py rename to src/pptx/opc/serialized.py diff --git a/pptx/opc/shared.py b/src/pptx/opc/shared.py similarity index 100% rename from pptx/opc/shared.py rename to src/pptx/opc/shared.py diff --git a/pptx/opc/spec.py b/src/pptx/opc/spec.py similarity index 100% rename from pptx/opc/spec.py rename to src/pptx/opc/spec.py diff --git a/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py similarity index 100% rename from pptx/oxml/__init__.py rename to src/pptx/oxml/__init__.py diff --git a/pptx/oxml/action.py b/src/pptx/oxml/action.py similarity index 100% rename from pptx/oxml/action.py rename to src/pptx/oxml/action.py 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 100% rename from pptx/oxml/chart/axis.py rename to src/pptx/oxml/chart/axis.py diff --git a/pptx/oxml/chart/chart.py b/src/pptx/oxml/chart/chart.py similarity index 100% rename from pptx/oxml/chart/chart.py rename to src/pptx/oxml/chart/chart.py diff --git a/pptx/oxml/chart/datalabel.py b/src/pptx/oxml/chart/datalabel.py similarity index 100% rename from pptx/oxml/chart/datalabel.py rename to src/pptx/oxml/chart/datalabel.py diff --git a/pptx/oxml/chart/legend.py b/src/pptx/oxml/chart/legend.py similarity index 100% rename from pptx/oxml/chart/legend.py rename to src/pptx/oxml/chart/legend.py diff --git a/pptx/oxml/chart/marker.py b/src/pptx/oxml/chart/marker.py similarity index 100% rename from pptx/oxml/chart/marker.py rename to src/pptx/oxml/chart/marker.py diff --git a/pptx/oxml/chart/plot.py b/src/pptx/oxml/chart/plot.py similarity index 100% rename from pptx/oxml/chart/plot.py rename to src/pptx/oxml/chart/plot.py diff --git a/pptx/oxml/chart/series.py b/src/pptx/oxml/chart/series.py similarity index 100% rename from pptx/oxml/chart/series.py rename to src/pptx/oxml/chart/series.py diff --git a/pptx/oxml/chart/shared.py b/src/pptx/oxml/chart/shared.py similarity index 100% rename from pptx/oxml/chart/shared.py rename to src/pptx/oxml/chart/shared.py diff --git a/pptx/oxml/coreprops.py b/src/pptx/oxml/coreprops.py similarity index 100% rename from pptx/oxml/coreprops.py rename to src/pptx/oxml/coreprops.py 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 100% rename from pptx/oxml/dml/color.py rename to src/pptx/oxml/dml/color.py diff --git a/pptx/oxml/dml/fill.py b/src/pptx/oxml/dml/fill.py similarity index 100% rename from pptx/oxml/dml/fill.py rename to src/pptx/oxml/dml/fill.py diff --git a/pptx/oxml/dml/line.py b/src/pptx/oxml/dml/line.py similarity index 100% rename from pptx/oxml/dml/line.py rename to src/pptx/oxml/dml/line.py diff --git a/pptx/oxml/ns.py b/src/pptx/oxml/ns.py similarity index 100% rename from pptx/oxml/ns.py rename to src/pptx/oxml/ns.py diff --git a/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py similarity index 100% rename from pptx/oxml/presentation.py rename to src/pptx/oxml/presentation.py diff --git a/pptx/oxml/shapes/__init__.py b/src/pptx/oxml/shapes/__init__.py similarity index 100% rename from pptx/oxml/shapes/__init__.py rename to src/pptx/oxml/shapes/__init__.py diff --git a/pptx/oxml/shapes/autoshape.py b/src/pptx/oxml/shapes/autoshape.py similarity index 100% rename from pptx/oxml/shapes/autoshape.py rename to src/pptx/oxml/shapes/autoshape.py diff --git a/pptx/oxml/shapes/connector.py b/src/pptx/oxml/shapes/connector.py similarity index 100% rename from pptx/oxml/shapes/connector.py rename to src/pptx/oxml/shapes/connector.py diff --git a/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py similarity index 100% rename from pptx/oxml/shapes/graphfrm.py rename to src/pptx/oxml/shapes/graphfrm.py diff --git a/pptx/oxml/shapes/groupshape.py b/src/pptx/oxml/shapes/groupshape.py similarity index 100% rename from pptx/oxml/shapes/groupshape.py rename to src/pptx/oxml/shapes/groupshape.py diff --git a/pptx/oxml/shapes/picture.py b/src/pptx/oxml/shapes/picture.py similarity index 100% rename from pptx/oxml/shapes/picture.py rename to src/pptx/oxml/shapes/picture.py diff --git a/pptx/oxml/shapes/shared.py b/src/pptx/oxml/shapes/shared.py similarity index 100% rename from pptx/oxml/shapes/shared.py rename to src/pptx/oxml/shapes/shared.py diff --git a/pptx/oxml/simpletypes.py b/src/pptx/oxml/simpletypes.py similarity index 100% rename from pptx/oxml/simpletypes.py rename to src/pptx/oxml/simpletypes.py diff --git a/pptx/oxml/slide.py b/src/pptx/oxml/slide.py similarity index 100% rename from pptx/oxml/slide.py rename to src/pptx/oxml/slide.py diff --git a/pptx/oxml/table.py b/src/pptx/oxml/table.py similarity index 100% rename from pptx/oxml/table.py rename to src/pptx/oxml/table.py diff --git a/pptx/oxml/text.py b/src/pptx/oxml/text.py similarity index 100% rename from pptx/oxml/text.py rename to src/pptx/oxml/text.py diff --git a/pptx/oxml/theme.py b/src/pptx/oxml/theme.py similarity index 100% rename from pptx/oxml/theme.py rename to src/pptx/oxml/theme.py diff --git a/pptx/oxml/xmlchemy.py b/src/pptx/oxml/xmlchemy.py similarity index 100% rename from pptx/oxml/xmlchemy.py rename to src/pptx/oxml/xmlchemy.py diff --git a/pptx/package.py b/src/pptx/package.py similarity index 100% rename from pptx/package.py rename to src/pptx/package.py diff --git a/pptx/parts/__init__.py b/src/pptx/parts/__init__.py similarity index 100% rename from pptx/parts/__init__.py rename to src/pptx/parts/__init__.py diff --git a/pptx/parts/chart.py b/src/pptx/parts/chart.py similarity index 100% rename from pptx/parts/chart.py rename to src/pptx/parts/chart.py diff --git a/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py similarity index 100% rename from pptx/parts/coreprops.py rename to src/pptx/parts/coreprops.py diff --git a/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py similarity index 100% rename from pptx/parts/embeddedpackage.py rename to src/pptx/parts/embeddedpackage.py diff --git a/pptx/parts/image.py b/src/pptx/parts/image.py similarity index 100% rename from pptx/parts/image.py rename to src/pptx/parts/image.py diff --git a/pptx/parts/media.py b/src/pptx/parts/media.py similarity index 100% rename from pptx/parts/media.py rename to src/pptx/parts/media.py diff --git a/pptx/parts/presentation.py b/src/pptx/parts/presentation.py similarity index 100% rename from pptx/parts/presentation.py rename to src/pptx/parts/presentation.py diff --git a/pptx/parts/slide.py b/src/pptx/parts/slide.py similarity index 100% rename from pptx/parts/slide.py rename to src/pptx/parts/slide.py diff --git a/pptx/presentation.py b/src/pptx/presentation.py similarity index 100% rename from pptx/presentation.py rename to src/pptx/presentation.py diff --git a/pptx/shapes/__init__.py b/src/pptx/shapes/__init__.py similarity index 100% rename from pptx/shapes/__init__.py rename to src/pptx/shapes/__init__.py diff --git a/pptx/shapes/autoshape.py b/src/pptx/shapes/autoshape.py similarity index 100% rename from pptx/shapes/autoshape.py rename to src/pptx/shapes/autoshape.py diff --git a/pptx/shapes/base.py b/src/pptx/shapes/base.py similarity index 100% rename from pptx/shapes/base.py rename to src/pptx/shapes/base.py diff --git a/pptx/shapes/connector.py b/src/pptx/shapes/connector.py similarity index 100% rename from pptx/shapes/connector.py rename to src/pptx/shapes/connector.py diff --git a/pptx/shapes/freeform.py b/src/pptx/shapes/freeform.py similarity index 100% rename from pptx/shapes/freeform.py rename to src/pptx/shapes/freeform.py diff --git a/pptx/shapes/graphfrm.py b/src/pptx/shapes/graphfrm.py similarity index 100% rename from pptx/shapes/graphfrm.py rename to src/pptx/shapes/graphfrm.py diff --git a/pptx/shapes/group.py b/src/pptx/shapes/group.py similarity index 100% rename from pptx/shapes/group.py rename to src/pptx/shapes/group.py diff --git a/pptx/shapes/picture.py b/src/pptx/shapes/picture.py similarity index 100% rename from pptx/shapes/picture.py rename to src/pptx/shapes/picture.py diff --git a/pptx/shapes/placeholder.py b/src/pptx/shapes/placeholder.py similarity index 100% rename from pptx/shapes/placeholder.py rename to src/pptx/shapes/placeholder.py diff --git a/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py similarity index 100% rename from pptx/shapes/shapetree.py rename to src/pptx/shapes/shapetree.py diff --git a/pptx/shared.py b/src/pptx/shared.py similarity index 100% rename from pptx/shared.py rename to src/pptx/shared.py diff --git a/pptx/slide.py b/src/pptx/slide.py similarity index 100% rename from pptx/slide.py rename to src/pptx/slide.py diff --git a/pptx/spec.py b/src/pptx/spec.py similarity index 100% rename from pptx/spec.py rename to src/pptx/spec.py diff --git a/pptx/table.py b/src/pptx/table.py similarity index 100% rename from pptx/table.py rename to src/pptx/table.py 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 100% rename from pptx/text/fonts.py rename to src/pptx/text/fonts.py diff --git a/pptx/text/layout.py b/src/pptx/text/layout.py similarity index 100% rename from pptx/text/layout.py rename to src/pptx/text/layout.py diff --git a/pptx/text/text.py b/src/pptx/text/text.py similarity index 100% rename from pptx/text/text.py rename to src/pptx/text/text.py diff --git a/pptx/util.py b/src/pptx/util.py similarity index 100% rename from pptx/util.py rename to src/pptx/util.py diff --git a/tox.ini b/tox.ini index 4df2d13b1..b7b3b8434 100644 --- a/tox.ini +++ b/tox.ini @@ -24,8 +24,8 @@ filterwarnings = # -- pytest complains when pytest-xdist is not installed -- ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning -looponfailroots = pptx tests -norecursedirs = docs *.egg-info features .git pptx spec .tox +looponfailroots = src tests +norecursedirs = docs *.egg-info features .git src spec .tox python_classes = Test Describe python_functions = test_ it_ they_ but_ and_it_ From ce10c20b621aaed8f94a26fd2a5f8391287a6f74 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 21:49:05 -0700 Subject: [PATCH 02/14] dev: modernize dev environment - Add `pyproject.toml` and gather configuration there. Move `.ruff.toml` contents into `pyproject.toml`. - Add `requirements-test.txt`. --- .github/workflows/ci.yml | 36 ++++++++++++ .ruff.toml | 11 ---- pyproject.toml | 100 ++++++++++++++++++++++++++++++++++ requirements-test.txt | 7 +++ src/pptx/parts/coreprops.py | 8 +-- tests/parts/test_coreprops.py | 28 +++++----- tox.ini | 16 ------ 7 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .ruff.toml create mode 100644 pyproject.toml create mode 100644 requirements-test.txt 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/.ruff.toml b/.ruff.toml deleted file mode 100644 index 45ae2ecbc..000000000 --- a/.ruff.toml +++ /dev/null @@ -1,11 +0,0 @@ -# -- 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", -] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..9868f5fc3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[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"] diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 000000000..b542c1af7 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,7 @@ +-r requirements.txt +behave>=1.2.3 +pyparsing>=2.0.1 +pytest>=2.5 +pytest-coverage +pytest-xdist +ruff diff --git a/src/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py index 14fe583d6..e39b154d0 100644 --- a/src/pptx/parts/coreprops.py +++ b/src/pptx/parts/coreprops.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" -from datetime import datetime +from __future__ import annotations + +import datetime as dt from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import XmlPart @@ -27,7 +27,7 @@ def default(cls, 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 diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 0f1b37917..3f20ca933 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -1,10 +1,10 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.coreprops` module.""" -import pytest +from __future__ import annotations -from datetime import datetime, timedelta +import datetime as dt + +import pytest from pptx.opc.constants import CONTENT_TYPE as CT from pptx.oxml.coreprops import CT_CoreProperties @@ -55,16 +55,18 @@ def it_can_construct_a_default_core_props(self): 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) + 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 # fixtures ------------------------------------------------------- @pytest.fixture( params=[ - ("created", datetime(2012, 11, 17, 16, 37, 40)), - ("last_printed", datetime(2014, 6, 4, 4, 28)), + ("created", dt.datetime(2012, 11, 17, 16, 37, 40)), + ("last_printed", dt.datetime(2014, 6, 4, 4, 28)), ("modified", None), ] ) @@ -77,21 +79,21 @@ def date_prop_get_fixture(self, request, core_properties): ( "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"', ), @@ -145,9 +147,7 @@ def str_prop_set_fixture(self, request): 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)] - ) + @pytest.fixture(params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)]) def revision_get_fixture(self, request): str_val, expected_revision = request.param tagname = "" if str_val is None else "cp:revision" diff --git a/tox.ini b/tox.ini index b7b3b8434..223cc0ffe 100644 --- a/tox.ini +++ b/tox.ini @@ -13,22 +13,6 @@ ignore = W503 max-line-length = 88 -[pytest] -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_it_ - [tox] envlist = py27, py38, py311 requires = virtualenv<20.22.0 From 7efa08d2a6a33024dd639af9d4fddc1b57624b55 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 22:00:20 -0700 Subject: [PATCH 03/14] type: add lxml type-stubs --- typings/lxml/_types.pyi | 38 ++++++++++++ typings/lxml/etree/__init__.pyi | 18 ++++++ typings/lxml/etree/_classlookup.pyi | 75 ++++++++++++++++++++++ typings/lxml/etree/_cleanup.pyi | 21 +++++++ typings/lxml/etree/_element.pyi | 96 +++++++++++++++++++++++++++++ typings/lxml/etree/_module_func.pyi | 19 ++++++ typings/lxml/etree/_module_misc.pyi | 5 ++ typings/lxml/etree/_nsclasses.pyi | 31 ++++++++++ typings/lxml/etree/_parser.pyi | 81 ++++++++++++++++++++++++ 9 files changed, 384 insertions(+) create mode 100644 typings/lxml/_types.pyi create mode 100644 typings/lxml/etree/__init__.pyi create mode 100644 typings/lxml/etree/_classlookup.pyi create mode 100644 typings/lxml/etree/_cleanup.pyi create mode 100644 typings/lxml/etree/_element.pyi create mode 100644 typings/lxml/etree/_module_func.pyi create mode 100644 typings/lxml/etree/_module_misc.pyi create mode 100644 typings/lxml/etree/_nsclasses.pyi create mode 100644 typings/lxml/etree/_parser.pyi diff --git a/typings/lxml/_types.pyi b/typings/lxml/_types.pyi new file mode 100644 index 000000000..a16fec3dd --- /dev/null +++ b/typings/lxml/_types.pyi @@ -0,0 +1,38 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Any, Callable, Collection, 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] + +_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..067b25ce9 --- /dev/null +++ b/typings/lxml/etree/_module_func.pyi @@ -0,0 +1,19 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from .._types import _ElementOrTree +from ..etree import HTMLParser, XMLParser +from ._element import _Element + +def fromstring(text: str | bytes, parser: XMLParser | HTMLParser) -> _Element: ... + +# Under XML Canonicalization (C14N) mode, most arguments are ignored, +# some arguments would even raise exception outright if specified. +def tostring( + element_or_tree: _ElementOrTree, + *, + encoding: str | type[str] | None = None, + pretty_print: bool = False, + with_tail: bool = True, +) -> str: ... 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 + ``` + """ + ... From 01b86e64de0f6f545f77dead7f9c3368a783a791 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 22:41:34 -0700 Subject: [PATCH 04/14] rfctr(enum): modernize enumerations --- src/pptx/enum/action.py | 85 +- src/pptx/enum/base.py | 381 ++----- src/pptx/enum/chart.py | 666 +++++++----- src/pptx/enum/dml.py | 560 ++++++---- src/pptx/enum/lang.py | 1152 +++++++++++--------- src/pptx/enum/shapes.py | 1694 ++++++++++++++++------------- src/pptx/enum/text.py | 367 +++---- src/pptx/oxml/text.py | 22 +- src/pptx/parts/embeddedpackage.py | 2 +- src/pptx/parts/slide.py | 10 +- src/pptx/shapes/shapetree.py | 40 +- tests/chart/test_data.py | 66 +- tests/dml/test_line.py | 10 +- tests/enum/__init__.py | 0 tests/enum/test_base.py | 73 ++ tests/enum/test_shapes.py | 46 + tests/shapes/test_autoshape.py | 47 +- tests/test_enum.py | 120 -- 18 files changed, 2785 insertions(+), 2556 deletions(-) create mode 100644 tests/enum/__init__.py create mode 100644 tests/enum/test_base.py create mode 100644 tests/enum/test_shapes.py delete mode 100644 tests/test_enum.py diff --git a/src/pptx/enum/action.py b/src/pptx/enum/action.py index 8bbf9189a..bc447226f 100644 --- a/src/pptx/enum/action.py +++ b/src/pptx/enum/action.py @@ -1,16 +1,11 @@ -# encoding: utf-8 +"""Enumerations that describe click-action settings.""" -""" -Enumerations that describe click action settings -""" +from __future__ import annotations -from __future__ import absolute_import +from pptx.enum.base import BaseEnum -from .base import alias, Enumeration, EnumMember - -@alias("PP_ACTION") -class PP_ACTION_TYPE(Enumeration): +class PP_ACTION_TYPE(BaseEnum): """ Specifies the type of a mouse action (click or hover action). @@ -21,26 +16,56 @@ class PP_ACTION_TYPE(Enumeration): 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 """ - __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."), - ) + 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 index c57e15b33..1d49b9c19 100644 --- a/src/pptx/enum/base.py +++ b/src/pptx/enum/base.py @@ -1,40 +1,105 @@ -# encoding: utf-8 +"""Base classes and other objects used by enumerations.""" -""" -Base classes and other objects used by enumerations -""" +from __future__ import annotations -from __future__ import absolute_import, print_function - -import sys +import enum import textwrap +from typing import TYPE_CHECKING, Any, Type, TypeVar +if TYPE_CHECKING: + from typing_extensions import Self -def alias(*aliases): - """ - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to - be referenced by each of the names provided as arguments. +_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 decorator(cls): - # alias must be set in globals from caller's frame - caller = sys._getframe(1) - globals_dict = caller.f_globals - for alias in aliases: - globals_dict[alias] = cls - return cls + def __new__(cls, ms_api_value: int, docstr: str): + self = int.__new__(cls, ms_api_value) + self._value_ = ms_api_value + self.__doc__ = docstr.strip() + return self - return decorator + def __str__(self): + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" + return f"{self.name} ({self.value})" -class _DocsPageFormatter(object): - """ - Formats a RestructuredText documention page (string) for the enumeration - class parts passed to the constructor. An immutable one-shot service - object. +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. """ - def __init__(self, clsname, clsdict): + 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 @@ -69,12 +134,12 @@ def _intro_text(self): return textwrap.dedent(cls_docstring).strip() - def _member_def(self, member): - """ - Return an individual member definition formatted as an RST glossary - entry, wrapped to fit within 78 columns. + 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.docstring).strip() + member_docstring = textwrap.dedent(member.__doc__ or "").strip() member_docstring = textwrap.fill( member_docstring, width=78, @@ -90,9 +155,7 @@ def _member_defs(self): of the documentation page """ members = self._clsdict["__members__"] - member_defs = [ - self._member_def(member) for member in members if member.name is not None - ] + member_defs = [self._member_def(member) for member in members if member.name is not None] return "\n".join(member_defs) @property @@ -110,255 +173,3 @@ def _page_title(self): """ 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/src/pptx/enum/chart.py b/src/pptx/enum/chart.py index 26cd3e5fc..5e609ebd3 100644 --- a/src/pptx/enum/chart.py +++ b/src/pptx/enum/chart.py @@ -1,60 +1,39 @@ -# encoding: utf-8 +"""Enumerations used by charts and related objects.""" -""" -Enumerations used by charts and related objects -""" +from __future__ import annotations -from __future__ import absolute_import +from pptx.enum.base import BaseEnum, BaseXmlEnum -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. +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 """ - __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." - ), - ) + 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.""" -class XL_CATEGORY_TYPE(Enumeration): - """ - Specifies the type of the category axis. + 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:: @@ -62,131 +41,258 @@ class XL_CATEGORY_TYPE(Enumeration): 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 """ - __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.", - ), - ) + 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(Enumeration): - """ - Specifies the type of a chart. + +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 """ - __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.", - ), - ) + THREE_D_AREA = (-4098, "3D Area.") + """3D Area.""" + THREE_D_AREA_STACKED = (78, "3D Stacked Area.") + """3D Stacked Area.""" -@alias("XL_LABEL_POSITION") -class XL_DATA_LABEL_POSITION(XmlEnumeration): - """ - Specifies where the data label is positioned. + 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:: @@ -194,66 +300,57 @@ class XL_DATA_LABEL_POSITION(XmlEnumeration): 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 """ - __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.", - ), + 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.""" + + 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.""" -class XL_LEGEND_POSITION(XmlEnumeration): - """ - Specifies the position of the legend on a chart. + +XL_LABEL_POSITION = XL_DATA_LABEL_POSITION + + +class XL_LEGEND_POSITION(BaseXmlEnum): + """Specifies the position of the legend on a chart. Example:: @@ -261,82 +358,111 @@ class XL_LEGEND_POSITION(XmlEnumeration): 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 """ - __ms_name__ = "XlLegendPosition" + BOTTOM = (-4107, "b", "Below the chart.") + """Below the chart.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff745840.aspx" + CORNER = (2, "tr", "In the upper-right corner of the chart border.") + """In the upper-right corner of the chart border.""" - __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."), - ) + CUSTOM = (-4161, "", "A custom position.") + """A custom position.""" + 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(XmlEnumeration): - """ - Specifies the marker style for a point or series in a line chart, scatter - chart, or radar 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 """ - __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"), - ) + AUTOMATIC = (-4105, "auto", "Automatic markers") + """Automatic markers""" + CIRCLE = (8, "circle", "Circular markers") + """Circular markers""" -class XL_TICK_MARK(XmlEnumeration): - """ - Specifies a type of axis tick for a chart. + 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 """ - __ms_name__ = "XlTickMark" + CROSS = (4, "cross", "Tick mark crosses the axis") + """Tick mark crosses the axis""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff193878.aspx" + INSIDE = (2, "in", "Tick mark appears inside the axis") + """Tick mark appears inside the axis""" - __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"), - ) + 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(XmlEnumeration): - """ - Specifies the position of tick-mark labels on a chart axis. + +class XL_TICK_LABEL_POSITION(BaseXmlEnum): + """Specifies the position of tick-mark labels on a chart axis. Example:: @@ -344,20 +470,20 @@ class XL_TICK_LABEL_POSITION(XmlEnumeration): 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 """ - __ms_name__ = "XlTickLabelPosition" + HIGH = (-4127, "high", "Top or right side of the chart.") + """Top or right side of the chart.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff822561.aspx" + LOW = (-4134, "low", "Bottom or left side of the chart.") + """Bottom or left side of the chart.""" - __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."), - ) + 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 index 765fe2dea..e9a14b13f 100644 --- a/src/pptx/enum/dml.py +++ b/src/pptx/enum/dml.py @@ -1,20 +1,11 @@ -# encoding: utf-8 - """Enumerations used by DrawingML objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) +from pptx.enum.base import BaseEnum, BaseXmlEnum -class MSO_COLOR_TYPE(Enumeration): +class MSO_COLOR_TYPE(BaseEnum): """ Specifies the color specification scheme @@ -23,51 +14,35 @@ class MSO_COLOR_TYPE(Enumeration): 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 """ - __ms_name__ = "MsoColorType" + RGB = (1, "Color is specified by an |RGBColor| value.") + """Color is specified by an |RGBColor| value.""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15" ").aspx" - ) + 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""" - __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. - """, - ), + 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.""" -@alias("MSO_FILL") -class MSO_FILL_TYPE(Enumeration): +class MSO_FILL_TYPE(BaseEnum): """ Specifies the type of bitmap used for the fill of a shape. @@ -78,38 +53,46 @@ class MSO_FILL_TYPE(Enumeration): 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 """ - __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"), + 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""" -@alias("MSO_LINE") -class MSO_LINE_DASH_STYLE(XmlEnumeration): + 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`` @@ -119,36 +102,44 @@ class MSO_LINE_DASH_STYLE(XmlEnumeration): 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 """ - __ms_name__ = "MsoLineDashStyle" + DASH = (4, "dash", "Line consists of dashes only.") + """Line consists of dashes only.""" - __url__ = ( - "https://msdn.microsoft.com/en-us/vba/office-shared-vba/articles/mso" - "linedashstyle-enumeration-office" - ) + DASH_DOT = (5, "dashDot", "Line is a dash-dot pattern.") + """Line is a dash-dot pattern.""" - __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."), - ) + 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.""" -@alias("MSO_PATTERN") -class MSO_PATTERN_TYPE(XmlEnumeration): + +MSO_LINE = MSO_LINE_DASH_STYLE + + +class MSO_PATTERN_TYPE(BaseXmlEnum): """Specifies the fill pattern used in a shape. Alias: ``MSO_PATTERN`` @@ -160,99 +151,183 @@ class MSO_PATTERN_TYPE(XmlEnumeration): 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 """ - __ms_name__ = "MsoPatternType" + CROSS = (51, "cross", "Cross") + """Cross""" - __url__ = ( - "https://msdn.microsoft.com/VBA/Office-Shared-VBA/articles/msopatter" - "ntype-enumeration-office" - ) + DARK_DOWNWARD_DIAGONAL = (15, "dkDnDiag", "Dark Downward Diagonal") + """Dark Downward Diagonal""" - __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."), - ) + DARK_HORIZONTAL = (13, "dkHorz", "Dark Horizontal") + """Dark Horizontal""" + DARK_UPWARD_DIAGONAL = (16, "dkUpDiag", "Dark Upward Diagonal") + """Dark Upward Diagonal""" -@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. + 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") + """Mixed pattern""" + + +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`` @@ -262,58 +337,65 @@ class MSO_THEME_COLOR_INDEX(XmlEnumeration): 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 """ - __ms_name__ = "MsoThemeColorIndex" + NOT_THEME_COLOR = (0, "", "Indicates the color is not a theme color.") + """Indicates the color is not a theme color.""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15" ").aspx" - ) + ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.") + """Specifies the Accent 1 theme color.""" - __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.", - ), - ) + 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.") + """Indicates multiple theme colors are used, such as in a group shape.""" + + +MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX diff --git a/src/pptx/enum/lang.py b/src/pptx/enum/lang.py index f466218ec..d2da5b6a5 100644 --- a/src/pptx/enum/lang.py +++ b/src/pptx/enum/lang.py @@ -1,20 +1,11 @@ -# encoding: utf-8 +"""Enumerations used for specifying language.""" -""" -Enumerations used for specifying language. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from pptx.enum.base import BaseXmlEnum -from .base import ( - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - -class MSO_LANGUAGE_ID(XmlEnumeration): +class MSO_LANGUAGE_ID(BaseXmlEnum): """ Specifies the language identifier. @@ -23,478 +14,669 @@ class MSO_LANGUAGE_ID(XmlEnumeration): 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 """ - __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."), + 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.""" diff --git a/src/pptx/enum/shapes.py b/src/pptx/enum/shapes.py index e4758cf02..b1dec8adc 100644 --- a/src/pptx/enum/shapes.py +++ b/src/pptx/enum/shapes.py @@ -1,22 +1,14 @@ -# 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 +from __future__ import annotations +import enum + +from pptx.enum.base import BaseEnum, BaseXmlEnum -@alias("MSO_SHAPE") -class MSO_AUTO_SHAPE_TYPE(XmlEnumeration): - """ - Specifies a type of AutoShape, e.g. DOWN_ARROW + +class MSO_AUTO_SHAPE_TYPE(BaseXmlEnum): + """Specifies a type of AutoShape, e.g. DOWN_ARROW. Alias: ``MSO_SHAPE`` @@ -29,618 +21,703 @@ class MSO_AUTO_SHAPE_TYPE(XmlEnumeration): 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 """ - __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): + 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. @@ -656,28 +733,27 @@ class MSO_CONNECTOR_TYPE(XmlEnumeration): 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 """ - __ms_name__ = "MsoConnectorType" + CURVE = (3, "curvedConnector3", "Curved connector.") + """Curved connector.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff860918.aspx" + ELBOW = (2, "bentConnector3", "Elbow connector.") + """Elbow connector.""" - __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.", - ), - ) + STRAIGHT = (1, "line", "Straight line connector.") + """Straight line connector.""" -@alias("MSO") -class MSO_SHAPE_TYPE(Enumeration): - """ - Specifies the type of a shape +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`` @@ -686,47 +762,93 @@ class MSO_SHAPE_TYPE(Enumeration): 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): + MS API Name: `MsoShapeType` + + http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15).aspx """ - Indicates the OLE media type. + + 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""" + + +MSO = MSO_SHAPE_TYPE + + +class PP_MEDIA_TYPE(BaseEnum): + """Indicates the OLE media type. Example:: @@ -734,30 +856,24 @@ class PP_MEDIA_TYPE(Enumeration): 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 """ - __ms_name__ = "PpMediaType" + MOVIE = (3, "Video media such as MP4.") + """Video media such as MP4.""" - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff746008.aspx" + OTHER = (1, "Other media types") + """Other media types""" - __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.", - ), - ) + SOUND = (1, "Audio media such as MP3.") + """Audio media such as MP3.""" -@alias("PP_PLACEHOLDER") -class PP_PLACEHOLDER_TYPE(XmlEnumeration): - """ - Specifies one of the 18 distinct types of placeholder. +class PP_PLACEHOLDER_TYPE(BaseXmlEnum): + """Specifies one of the 18 distinct types of placeholder. Alias: ``PP_PLACEHOLDER`` @@ -767,48 +883,77 @@ class PP_PLACEHOLDER_TYPE(XmlEnumeration): 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" """ - __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): + 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") + """Vertical Body""" + + VERTICAL_OBJECT = (17, "", "Vertical Object") + """Vertical Object""" + + VERTICAL_TITLE = (5, "", "Vertical Title") + """Vertical Title""" + + 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. @@ -828,54 +973,41 @@ class _ProgIdEnum(object): 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 + _progId: str + _icon_filename: str + _width: int + _height: int - @property - def height(self): - return self._height + 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 icon_filename(self): - return self._icon_filename + @property + def height(self): + return self._height - @property - def progId(self): - return self._progId + @property + def icon_filename(self): + return self._icon_filename - @property - def width(self): - return self._width + @property + def progId(self): + return self._progId - 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 - ) + @property + def width(self): + return self._width - @lazyproperty - def XLSX(self): - return self.Member("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600) + 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.""" -PROG_ID = _ProgIdEnum() + 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 index 54297bbd5..892385153 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -1,85 +1,64 @@ -# encoding: utf-8 +"""Enumerations used by text and related objects.""" -""" -Enumerations used by text and related objects -""" +from __future__ import annotations -from __future__ import absolute_import +from pptx.enum.base import BaseEnum, BaseXmlEnum -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) +class MSO_AUTO_SIZE(BaseEnum): + """Determines the type of automatic sizing allowed. -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:: + 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. + 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` - 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. + http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15).aspx """ - NONE = 0 - SHAPE_TO_FIT_TEXT = 1 - TEXT_TO_FIT_SHAPE = 2 + 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. - __ms_name__ = "MsoAutoSize" + Text can freely extend beyond the horizontal and vertical edges of the shape bounding box. + """ - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15" ").aspx" + 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. - __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.", - ), + 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.""" -@alias("MSO_UNDERLINE") -class MSO_TEXT_UNDERLINE_TYPE(XmlEnumeration): +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. @@ -91,165 +70,149 @@ class MSO_TEXT_UNDERLINE_TYPE(XmlEnumeration): 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 """ - __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."), - ) + NONE = (0, "none", "Specifies no underline.") + """Specifies no underline.""" + DASH_HEAVY_LINE = (8, "dashHeavy", "Specifies a dash underline.") + """Specifies a dash underline.""" -@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. - """ + DASH_LINE = (7, "dash", "Specifies a dash line underline.") + """Specifies a dash line underline.""" - __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.", - ), + 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.""" -@alias("PP_ALIGN") -class PP_PARAGRAPH_ALIGNMENT(XmlEnumeration): + WORDS = (1, "words", "Specifies underlining words.") + """Specifies underlining words.""" + + +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 """ - Specifies the horizontal alignment for one or more paragraphs. - Alias: ``PP_ALIGN`` + 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""" + + +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 """ - __ms_name__ = "PpParagraphAlignment" + CENTER = (2, "ctr", "Center align") + """Center align""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15" ").aspx" + 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""" - __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.", - ), + 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""" + + +PP_ALIGN = PP_PARAGRAPH_ALIGNMENT diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index cf99dd0da..ced0f8088 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -69,9 +69,7 @@ def _escape_ctrl_chars(s): (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 - ) + return re.sub(r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s) class CT_TextBody(BaseOxmlElement): @@ -178,20 +176,12 @@ def unclear_content(self): @classmethod def _a_txBody_tmpl(cls): - return ( - "\n" - " \n" - " \n" - "\n" % (nsdecls("a")) - ) + return "\n" " \n" " \n" "\n" % (nsdecls("a")) @classmethod def _p_txBody_tmpl(cls): return ( - "\n" - " \n" - " \n" - "\n" % (nsdecls("p", "a")) + "\n" " \n" " \n" "\n" % (nsdecls("p", "a")) ) @classmethod @@ -237,7 +227,7 @@ def autofit(self): @autofit.setter def autofit(self, value): - if value is not None and value not in MSO_AUTO_SIZE._valid_settings: + if value is not None and value not in MSO_AUTO_SIZE: raise ValueError( "only None or a member of the MSO_AUTO_SIZE enumeration can " "be assigned to CT_TextBodyProperties.autofit, got %s" % value @@ -297,9 +287,7 @@ class CT_TextCharacterProperties(BaseOxmlElement): "a:extLst", ), ) - hlinkClick = ZeroOrOne( - "a:hlinkClick", successors=("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) diff --git a/src/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py index a04cc224a..c2d434e04 100644 --- a/src/pptx/parts/embeddedpackage.py +++ b/src/pptx/parts/embeddedpackage.py @@ -25,7 +25,7 @@ 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, diff --git a/src/pptx/parts/slide.py b/src/pptx/parts/slide.py index dfd81b4e4..5d721bb41 100644 --- a/src/pptx/parts/slide.py +++ b/src/pptx/parts/slide.py @@ -112,9 +112,7 @@ def new(cls, package, slide_part): 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 @@ -167,13 +165,11 @@ def add_chart_part(self, chart_type, chart_data): The chart depicts `chart_data` and is related to the slide contained in this part by `rId`. """ - return self.relate_to( - ChartPart.new(chart_type, chart_data, self._package), RT.CHART - ) + 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): """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 diff --git a/src/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py index 65369d51e..cebdfc91f 100644 --- a/src/pptx/shapes/shapetree.py +++ b/src/pptx/shapes/shapetree.py @@ -437,9 +437,7 @@ def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, 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 - ) + 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. @@ -564,9 +562,7 @@ def add_table(self, rows, cols, left, top, width, height): 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 - ) + graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height) graphic_frame = self._shape_factory(graphicFrame) return graphic_frame @@ -788,9 +784,7 @@ 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 - ) + 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): @@ -896,9 +890,7 @@ class is not intended to be constructed or an instance of it retained by 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 - ): + 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 @@ -917,9 +909,7 @@ def new_movie_pic( *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 cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic return @property @@ -965,9 +955,7 @@ def _poster_frame_rId(self): 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 - ) + _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file) return poster_frame_rId @property @@ -1101,9 +1089,7 @@ def _cx(self): # --- 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) + Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200) ) @lazyproperty @@ -1116,9 +1102,7 @@ def _cy(self): # --- 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) + Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600) ) @lazyproperty @@ -1132,9 +1116,7 @@ def _icon_height(self): 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) - ) + return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600) @lazyproperty def _icon_image_file(self): @@ -1150,7 +1132,7 @@ def _icon_image_file(self): # --- 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 + if isinstance(self._prog_id_arg, PROG_ID) else "generic-icon.emf" ) @@ -1195,7 +1177,7 @@ def _progId(self): # --- 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 + return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg @lazyproperty def _shape_name(self): diff --git a/tests/chart/test_data.py b/tests/chart/test_data.py index 537875569..10b325bf9 100644 --- a/tests/chart/test_data.py +++ b/tests/chart/test_data.py @@ -28,7 +28,7 @@ XySeriesData, ) 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 @@ -74,8 +74,8 @@ def ChartXmlWriter_(self, request): return ChartXmlWriter_ @pytest.fixture - def chart_type_(self, request): - return instance_mock(request, EnumValue) + def chart_type_(self): + return XL_CHART_TYPE.PIE class Describe_BaseSeriesData(object): @@ -229,15 +229,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 +241,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 +351,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 +384,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 +515,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 +523,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 +578,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 +665,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 +679,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 +714,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 +744,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 +771,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 +814,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/dml/test_line.py b/tests/dml/test_line.py index 9d6cec30a..b33e6e094 100644 --- a/tests/dml/test_line.py +++ b/tests/dml/test_line.py @@ -53,9 +53,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 @@ -80,7 +78,7 @@ def dash_style_get_fixture(self, request): @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", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=sysDot}"), ( "p:spPr/a:ln/a:prstDash", MSO_LINE.SOLID, @@ -129,9 +127,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/enum/__init__.py b/tests/enum/__init__.py new file mode 100644 index 000000000..e69de29bb 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/shapes/test_autoshape.py b/tests/shapes/test_autoshape.py index 1c4787c85..9e6173caf 100644 --- a/tests/shapes/test_autoshape.py +++ b/tests/shapes/test_autoshape.py @@ -72,17 +72,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 +143,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 +152,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 [ @@ -230,9 +219,7 @@ def _prstGeom_cases(): @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 - ) + prstGeom = a_prstGeom().with_nsdecls().with_prst(prst).with_child(an_avLst()).element return prstGeom, prst, expected_values def _effective_val_cases(): @@ -274,9 +261,7 @@ def it_xml_escapes_the_basename_when_the_name_contains_special_characters(self): assert autoshape_type.prst == "noSmoking" assert autoshape_type.basename == ""No" Symbol" - def it_knows_the_default_adj_vals_for_its_autoshape_type( - self, default_adj_vals_fixture_ - ): + def it_knows_the_default_adj_vals_for_its_autoshape_type(self, default_adj_vals_fixture_): prst, default_adj_vals = default_adj_vals_fixture_ _default_adj_vals = AutoShapeType.default_adjustment_values(prst) assert _default_adj_vals == default_adj_vals @@ -285,7 +270,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): AutoShapeType.id_from_prst("badPrst") def it_caches_autoshape_type_lookups(self): @@ -335,9 +320,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 @@ -354,9 +337,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 @@ -417,9 +398,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 @@ -508,9 +487,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/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) From c38d5f5c6850ae3aefdc3a86dbf9bd0af35cf346 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 26 Jul 2024 23:20:30 -0700 Subject: [PATCH 05/14] type: general modernization Add type-annotations broadly, but prioritizing interface objects and methods. Adjust text-width to 100, remove Python 2 support, etc. --- docs/index.rst | 2 +- features/steps/action.py | 8 +- features/steps/axis.py | 28 +- features/steps/background.py | 8 +- features/steps/category.py | 8 +- features/steps/chart.py | 13 +- features/steps/chartdata.py | 5 +- features/steps/color.py | 10 +- features/steps/coreprops.py | 16 +- features/steps/datalabel.py | 8 +- features/steps/effect.py | 8 +- features/steps/fill.py | 24 +- features/steps/font.py | 12 +- features/steps/font_color.py | 16 +- features/steps/helpers.py | 40 +- features/steps/legend.py | 12 +- features/steps/line.py | 8 +- features/steps/picture.py | 19 +- features/steps/placeholder.py | 18 +- features/steps/plot.py | 12 +- features/steps/presentation.py | 71 +- features/steps/series.py | 8 +- features/steps/shape.py | 28 +- features/steps/shapes.py | 36 +- features/steps/slide.py | 20 +- features/steps/slides.py | 10 +- features/steps/table.py | 16 +- features/steps/text.py | 18 +- features/steps/text_frame.py | 12 +- src/pptx/__init__.py | 42 +- src/pptx/action.py | 100 +-- src/pptx/api.py | 36 +- src/pptx/chart/axis.py | 4 +- src/pptx/chart/category.py | 6 +- src/pptx/chart/chart.py | 14 +- src/pptx/chart/data.py | 21 +- src/pptx/chart/datalabel.py | 12 +- src/pptx/chart/legend.py | 14 +- src/pptx/chart/marker.py | 14 +- src/pptx/chart/plot.py | 28 +- src/pptx/chart/point.py | 7 +- src/pptx/chart/series.py | 11 +- src/pptx/chart/xlsx.py | 27 +- src/pptx/chart/xmlwriter.py | 59 +- src/pptx/compat/__init__.py | 41 -- src/pptx/compat/python2.py | 41 -- src/pptx/compat/python3.py | 43 -- src/pptx/dml/chtfmt.py | 18 +- src/pptx/dml/color.py | 12 +- src/pptx/dml/effect.py | 4 +- src/pptx/dml/fill.py | 46 +- src/pptx/dml/line.py | 10 +- src/pptx/exc.py | 7 +- src/pptx/media.py | 20 +- src/pptx/opc/constants.py | 450 ++++--------- src/pptx/opc/oxml.py | 171 +++-- src/pptx/opc/package.py | 461 ++++++------- src/pptx/opc/packuri.py | 95 ++- src/pptx/opc/serialized.py | 150 +++-- src/pptx/opc/shared.py | 4 +- src/pptx/opc/spec.py | 9 +- src/pptx/oxml/__init__.py | 88 ++- src/pptx/oxml/action.py | 46 +- src/pptx/oxml/chart/axis.py | 4 +- src/pptx/oxml/chart/chart.py | 20 +- src/pptx/oxml/chart/datalabel.py | 4 +- src/pptx/oxml/chart/legend.py | 14 +- src/pptx/oxml/chart/marker.py | 14 +- src/pptx/oxml/chart/plot.py | 16 +- src/pptx/oxml/chart/series.py | 14 +- src/pptx/oxml/chart/shared.py | 9 +- src/pptx/oxml/coreprops.py | 174 +++-- src/pptx/oxml/dml/color.py | 14 +- src/pptx/oxml/dml/fill.py | 16 +- src/pptx/oxml/dml/line.py | 4 +- src/pptx/oxml/ns.py | 130 ++-- src/pptx/oxml/presentation.py | 116 ++-- src/pptx/oxml/shapes/__init__.py | 19 + src/pptx/oxml/shapes/autoshape.py | 358 +++++----- src/pptx/oxml/shapes/connector.py | 136 ++-- src/pptx/oxml/shapes/graphfrm.py | 412 ++++++------ src/pptx/oxml/shapes/groupshape.py | 138 ++-- src/pptx/oxml/shapes/picture.py | 59 +- src/pptx/oxml/shapes/shared.py | 180 ++--- src/pptx/oxml/simpletypes.py | 59 +- src/pptx/oxml/slide.py | 134 ++-- src/pptx/oxml/table.py | 374 +++++------ src/pptx/oxml/text.py | 377 ++++++----- src/pptx/oxml/theme.py | 8 +- src/pptx/oxml/xmlchemy.py | 502 ++++++-------- src/pptx/package.py | 30 +- src/pptx/parts/chart.py | 22 +- src/pptx/parts/coreprops.py | 62 +- src/pptx/parts/embeddedpackage.py | 13 +- src/pptx/parts/image.py | 252 ++++--- src/pptx/parts/media.py | 4 +- src/pptx/parts/presentation.py | 57 +- src/pptx/parts/slide.py | 81 +-- src/pptx/presentation.py | 72 +- src/pptx/shapes/__init__.py | 29 +- src/pptx/shapes/autoshape.py | 334 ++++----- src/pptx/shapes/base.py | 220 +++--- src/pptx/shapes/connector.py | 4 +- src/pptx/shapes/freeform.py | 253 +++---- src/pptx/shapes/graphfrm.py | 111 +-- src/pptx/shapes/group.py | 44 +- src/pptx/shapes/picture.py | 152 ++--- src/pptx/shapes/placeholder.py | 30 +- src/pptx/shapes/shapetree.py | 965 +++++++++++++-------------- src/pptx/shared.py | 79 +-- src/pptx/slide.py | 357 +++++----- src/pptx/spec.py | 19 +- src/pptx/table.py | 383 +++++------ src/pptx/text/fonts.py | 31 +- src/pptx/text/layout.py | 21 +- src/pptx/text/text.py | 592 ++++++++-------- src/pptx/types.py | 36 + src/pptx/util.py | 236 +++---- tests/chart/test_axis.py | 45 +- tests/chart/test_category.py | 27 +- tests/chart/test_chart.py | 44 +- tests/chart/test_data.py | 40 +- tests/chart/test_datalabel.py | 8 +- tests/chart/test_legend.py | 19 +- tests/chart/test_marker.py | 12 +- tests/chart/test_plot.py | 40 +- tests/chart/test_point.py | 16 +- tests/chart/test_series.py | 54 +- tests/chart/test_xlsx.py | 42 +- tests/chart/test_xmlwriter.py | 48 +- tests/dml/test_chtfmt.py | 8 +- tests/dml/test_color.py | 22 +- tests/dml/test_effect.py | 6 +- tests/dml/test_fill.py | 18 +- tests/dml/test_line.py | 73 +- tests/opc/test_oxml.py | 197 +++--- tests/opc/test_package.py | 173 ++--- tests/opc/test_packuri.py | 56 +- tests/opc/test_serialized.py | 196 +++--- tests/opc/unitdata/__init__.py | 0 tests/opc/unitdata/rels.py | 261 -------- tests/oxml/shapes/test_autoshape.py | 17 +- tests/oxml/shapes/test_graphfrm.py | 9 +- tests/oxml/shapes/test_groupshape.py | 20 +- tests/oxml/shapes/test_picture.py | 4 +- tests/oxml/test___init__.py | 9 +- tests/oxml/test_dml.py | 8 +- tests/oxml/test_ns.py | 16 +- tests/oxml/test_presentation.py | 44 +- tests/oxml/test_simpletypes.py | 24 +- tests/oxml/test_slide.py | 4 +- tests/oxml/test_table.py | 26 +- tests/oxml/test_theme.py | 8 +- tests/oxml/test_xmlchemy.py | 71 +- tests/oxml/unitdata/dml.py | 8 +- tests/oxml/unitdata/shape.py | 4 +- tests/oxml/unitdata/text.py | 8 +- tests/parts/test_chart.py | 19 +- tests/parts/test_coreprops.py | 221 +++--- tests/parts/test_embeddedpackage.py | 32 +- tests/parts/test_image.py | 14 +- tests/parts/test_media.py | 4 +- tests/parts/test_presentation.py | 32 +- tests/parts/test_slide.py | 71 +- tests/shapes/test_autoshape.py | 171 +++-- tests/shapes/test_base.py | 110 +-- tests/shapes/test_connector.py | 4 +- tests/shapes/test_freeform.py | 321 ++++----- tests/shapes/test_graphfrm.py | 17 +- tests/shapes/test_group.py | 4 +- tests/shapes/test_picture.py | 10 +- tests/shapes/test_placeholder.py | 29 +- tests/shapes/test_shapetree.py | 183 ++--- tests/test_action.py | 15 +- tests/test_api.py | 8 +- tests/test_media.py | 14 +- tests/test_package.py | 11 +- tests/test_presentation.py | 28 +- tests/test_shared.py | 4 +- tests/test_slide.py | 98 +-- tests/test_table.py | 33 +- tests/test_util.py | 17 +- tests/text/test_fonts.py | 45 +- tests/text/test_layout.py | 34 +- tests/text/test_text.py | 233 +++---- tests/unitdata.py | 8 +- tests/unitutil/__init__.py | 6 +- tests/unitutil/cxml.py | 38 +- tests/unitutil/file.py | 23 +- tests/unitutil/mock.py | 68 +- typings/behave/__init__.pyi | 17 + typings/behave/runner.pyi | 3 + typings/lxml/_types.pyi | 4 +- typings/lxml/etree/_module_func.pyi | 29 +- 194 files changed, 6311 insertions(+), 7751 deletions(-) delete mode 100644 src/pptx/compat/__init__.py delete mode 100644 src/pptx/compat/python2.py delete mode 100644 src/pptx/compat/python3.py create mode 100644 src/pptx/types.py delete mode 100644 tests/opc/unitdata/__init__.py delete mode 100644 tests/opc/unitdata/rels.py create mode 100644 typings/behave/__init__.pyi create mode 100644 typings/behave/runner.pyi diff --git a/docs/index.rst b/docs/index.rst index 5bb4e0e26..79ad6c369 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ 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 +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 diff --git a/features/steps/action.py b/features/steps/action.py index c3f5de0e2..2a2da135a 100644 --- a/features/steps/action.py +++ b/features/steps/action.py @@ -1,16 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for click action-related features.""" +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_file - - # given =================================================== diff --git a/features/steps/axis.py b/features/steps/axis.py index 9cffbb93a..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 @@ -122,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] @@ -210,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, @@ -233,9 +221,7 @@ 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 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 ef6dbbe75..2fce7f2ca 100644 --- a/features/steps/picture.py +++ b/features/steps/picture.py @@ -1,18 +1,17 @@ -# encoding: utf-8 - """Gherkin step implementations for picture-related features.""" -from behave import given, when, then +from __future__ import annotations + +import io + +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 =================================================== @@ -42,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)) @@ -59,9 +58,7 @@ def step_then_a_ext_image_part_appears_in_the_pptx_file(context, ext): 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 2309c7462..0c1c6ba26 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -1,51 +1,50 @@ -# encoding: utf-8 - """Gherkin step implementations for presentation-level features.""" +from __future__ import annotations + +import io import os import zipfile -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_file, test_pptx - - # 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 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 @@ -53,37 +52,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): +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) @@ -91,7 +90,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) @@ -100,15 +99,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) @@ -116,7 +115,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 @@ -128,19 +127,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) @@ -148,7 +147,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) @@ -156,25 +155,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"] @@ -184,19 +183,19 @@ def then_ext_rels_are_preserved(context): @then("the package has the expected number of .rels parts") -def then_the_package_has_the_expected_number_of_rels_parts(context): +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("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/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 48401620a..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") diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 7952f87bd..0c951298c 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -1,26 +1,20 @@ -# encoding: utf-8 - """Initialization module for python-pptx package.""" -__version__ = "0.6.23" - +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__ = "0.6.23" + +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, diff --git a/src/pptx/action.py b/src/pptx/action.py index cc55e52e1..83c6ebf19 100644 --- a/src/pptx/action.py +++ b/src/pptx/action.py @@ -1,19 +1,35 @@ -# encoding: utf-8 - """Objects related to mouse click and hover actions on a shape or text.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + 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.""" - # --- The 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 @@ -61,7 +77,7 @@ def action(self): }.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 @@ -70,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 @@ -116,11 +132,13 @@ 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_part(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 @@ -139,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 @@ -164,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. """ @@ -172,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 @@ -184,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 @@ -207,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() @@ -216,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/src/pptx/api.py b/src/pptx/api.py index 7670c886f..892f425ab 100644 --- a/src/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/src/pptx/chart/axis.py b/src/pptx/chart/axis.py index 66f325185..a9b877039 100644 --- a/src/pptx/chart/axis.py +++ b/src/pptx/chart/axis.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Axis-related chart objects.""" +from __future__ import annotations + from pptx.dml.chtfmt import ChartFormat from pptx.enum.chart import ( XL_AXIS_CROSSES, diff --git a/src/pptx/chart/category.py b/src/pptx/chart/category.py index 3d16e6f42..2c28aff5e 100644 --- a/src/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/src/pptx/chart/chart.py b/src/pptx/chart/chart.py index 14500a418..d73aa9338 100644 --- a/src/pptx/chart/chart.py +++ b/src/pptx/chart/chart.py @@ -1,13 +1,14 @@ -# encoding: utf-8 - """Chart-related objects such as Chart and ChartTitle.""" +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 @@ -88,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 diff --git a/src/pptx/chart/data.py b/src/pptx/chart/data.py index 35e2e6b64..ec6a61f31 100644 --- a/src/pptx/chart/data.py +++ b/src/pptx/chart/data.py @@ -1,8 +1,9 @@ -# encoding: utf-8 - """ChartData and related objects.""" +from __future__ import annotations + import datetime +from collections.abc import Sequence from numbers import Number from pptx.chart.xlsx import ( @@ -11,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"): diff --git a/src/pptx/chart/datalabel.py b/src/pptx/chart/datalabel.py index ec6f7cba5..af7cdf5c0 100644 --- a/src/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/src/pptx/chart/legend.py b/src/pptx/chart/legend.py index 2926fae23..9bc64dbf8 100644 --- a/src/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/src/pptx/chart/marker.py b/src/pptx/chart/marker.py index 22dc0ff19..cd4b7f024 100644 --- a/src/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): diff --git a/src/pptx/chart/plot.py b/src/pptx/chart/plot.py index ce2d1167e..6e7235855 100644 --- a/src/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/src/pptx/chart/point.py b/src/pptx/chart/point.py index 258f6ae19..2d42436cb 100644 --- a/src/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/src/pptx/chart/series.py b/src/pptx/chart/series.py index 4ae19fbc0..16112eabe 100644 --- a/src/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/src/pptx/chart/xlsx.py b/src/pptx/chart/xlsx.py index 17e1e4fc8..30b212728 100644 --- a/src/pptx/chart/xlsx.py +++ b/src/pptx/chart/xlsx.py @@ -1,13 +1,12 @@ -# encoding: utf-8 - """Chart builder and related objects.""" +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.""" @@ -19,7 +18,7 @@ def __init__(self, chart_data): @property def xlsx_blob(self): """bytes for Excel file containing chart_data.""" - xlsx_file = BytesIO() + xlsx_file = io.BytesIO() with self._open_worksheet(xlsx_file) as (workbook, worksheet): self._populate_worksheet(workbook, worksheet) return xlsx_file.getvalue() @@ -29,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}) @@ -225,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) @@ -263,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/src/pptx/chart/xmlwriter.py b/src/pptx/chart/xmlwriter.py index c485a4b88..703c53dd5 100644 --- a/src/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/src/pptx/compat/__init__.py b/src/pptx/compat/__init__.py deleted file mode 100644 index 198dc6a0e..000000000 --- a/src/pptx/compat/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# encoding: utf-8 - -"""Provides Python 2/3 compatibility objects.""" - -import sys - -try: - from collections.abc import Container, Mapping, Sequence -except ImportError: - from collections import Container, Mapping, Sequence - -if sys.version_info >= (3, 0): - from .python3 import ( # noqa - BytesIO, - Unicode, - is_integer, - is_string, - is_unicode, - to_unicode, - ) -else: - from .python2 import ( # noqa - BytesIO, - Unicode, - is_integer, - is_string, - is_unicode, - to_unicode, - ) - -__all__ = [ - "BytesIO", - "Container", - "Mapping", - "Sequence", - "Unicode", - "is_integer", - "is_string", - "is_unicode", - "to_unicode", -] diff --git a/src/pptx/compat/python2.py b/src/pptx/compat/python2.py deleted file mode 100644 index 34e2535d5..000000000 --- a/src/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/src/pptx/compat/python3.py b/src/pptx/compat/python3.py deleted file mode 100644 index 85fce2376..000000000 --- a/src/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/src/pptx/dml/chtfmt.py b/src/pptx/dml/chtfmt.py index dcecb63ab..c37e4844d 100644 --- a/src/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): diff --git a/src/pptx/dml/color.py b/src/pptx/dml/color.py index 71e619c9b..54155823d 100644 --- a/src/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/src/pptx/dml/effect.py b/src/pptx/dml/effect.py index 65753014a..9df69ce49 100644 --- a/src/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/src/pptx/dml/fill.py b/src/pptx/dml/fill.py index e84bea9c6..8212af9e8 100644 --- a/src/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 diff --git a/src/pptx/dml/line.py b/src/pptx/dml/line.py index 698c7f633..82be47a40 100644 --- a/src/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/src/pptx/exc.py b/src/pptx/exc.py index 8641fe44f..0a1e03b81 100644 --- a/src/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/src/pptx/media.py b/src/pptx/media.py index c5adf24ea..7aaf47ca1 100644 --- a/src/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/src/pptx/opc/constants.py b/src/pptx/opc/constants.py index 9eef0ee23..e1b08a93a 100644 --- a/src/pptx/opc/constants.py +++ b/src/pptx/opc/constants.py @@ -1,34 +1,24 @@ -# encoding: utf-8 - """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(object): +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_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" - ) + 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" @@ -40,9 +30,7 @@ class CONTENT_TYPE(object): 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_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml" OFC_CUSTOM_XML_PROPERTIES = ( "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" ) @@ -53,59 +41,40 @@ class CONTENT_TYPE(object): OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml" - OFC_THEME_OVERRIDE = ( - "application/vnd.openxmlformats-officedocument.themeOverride+xml" - ) + OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml" OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing" OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml" OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( "application/vnd.openxmlformats-package.digital-signature-certificate" ) - OPC_DIGITAL_SIGNATURE_ORIGIN = ( - "application/vnd.openxmlformats-package.digital-signature-origin" - ) + OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin" OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" ) OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" - PML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" - ) + PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" PML_COMMENT_AUTHORS = ( - "application/vnd.openxmlformats-officedocument.presentationml.commen" - "tAuthors+xml" + "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml" ) PML_HANDOUT_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.handou" - "tMaster+xml" + "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml" ) PML_NOTES_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesM" - "aster+xml" - ) - PML_NOTES_SLIDE = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" - ) - PML_PRESENTATION = ( - "application/vnd.openxmlformats-officedocument.presentationml.presentation" + "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.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" + "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" + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" ) PML_SLIDE_LAYOUT = ( "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" @@ -114,146 +83,88 @@ class CONTENT_TYPE(object): "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" ) PML_SLIDE_UPDATE_INFO = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo" - "+xml" + "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" + "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml" ) + PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" PNG = "image/png" - SML_CALC_CHAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" - ) - SML_CHARTSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" - ) - SML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" - ) - SML_CONNECTIONS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" - ) + SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" + SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" SML_CUSTOM_PROPERTY = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" ) - SML_DIALOGSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" - ) + SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" SML_EXTERNAL_LINK = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" ) SML_PIVOT_CACHE_DEFINITION = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefini" - "tion+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" ) SML_PIVOT_CACHE_RECORDS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecord" - "s+xml" - ) - SML_PIVOT_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml" ) + SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" SML_PRINTER_SETTINGS = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" ) - SML_QUERY_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" - ) + SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" SML_REVISION_HEADERS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+" - "xml" - ) - SML_REVISION_LOG = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml" ) + SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" SML_SHARED_STRINGS = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" ) SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - SML_SHEET_MAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - ) + SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" SML_SHEET_METADATA = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml" ) - SML_STYLES = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+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" + "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_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" + "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_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" + "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.foot" - "notes+xml" - ) - WML_HEADER = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" - ) - WML_NUMBERING = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml" - ) + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" + ) + WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" + WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml" + WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" + WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" + WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" + WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml" WML_PRINTER_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettin" - "gs" - ) - WML_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" - ) - WML_STYLES = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings" ) + WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" + WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" WML_WEB_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+x" - "ml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml" ) WMV = "video/x-ms-wmv" XML = "application/xml" @@ -264,171 +175,100 @@ class CONTENT_TYPE(object): X_WMF = "image/x-wmf" -class NAMESPACE(object): +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" - ) + 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): +class RELATIONSHIP_TARGET_MODE: """Open XML relationship target modes""" EXTERNAL = "External" INTERNAL = "Internal" -class RELATIONSHIP_TYPE(object): +class RELATIONSHIP_TYPE: AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" - A_F_CHUNK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" - ) - CALC_CHAIN = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" - ) + A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" + CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" CERTIFICATE = ( "http://schemas.openxmlformats.org/package/2006/relationships/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" - ) + 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" + "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/commentA" - "uthors" - ) - CONNECTIONS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connecti" - "ons" - ) - CONTROL = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors" ) + CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections" + CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" CORE_PROPERTIES = ( - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-p" - "roperties" + "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" ) CUSTOM_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-p" - "roperties" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" ) CUSTOM_PROPERTY = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/customProperty" - ) - CUSTOM_XML = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty" ) + CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" CUSTOM_XML_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXm" - "lProps" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps" ) DIAGRAM_COLORS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramC" - "olors" - ) - DIAGRAM_DATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramD" - "ata" + "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/diagramL" - "ayout" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout" ) 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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle" ) + DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" EXTENDED_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended" - "-properties" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" ) EXTERNAL_LINK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/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" - ) + 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" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument" ) 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" + "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/notesMas" - "ter" - ) - NOTES_SLIDE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSli" - "de" - ) - NUMBERING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numberin" - "g" - ) + 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/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" + "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" @@ -437,105 +277,55 @@ class RELATIONSHIP_TYPE(object): "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" - ) + 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/printerS" - "ettings" - ) - QUERY_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTab" - "le" + "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/revision" - "Headers" - ) - REVISION_LOG = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revision" - "Log" - ) - SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders" ) + REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog" + SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" SHARED_STRINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedSt" - "rings" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" ) SHEET_METADATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMet" - "adata" + "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/slideLay" - "out" - ) - SLIDE_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMas" - "ter" - ) + 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/slideUpd" - "ateInfo" - ) - STYLES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo" ) + STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" TABLE_SINGLE_CELLS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSin" - "gleCells" - ) - TABLE_STYLES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSty" - "les" + "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/themeOve" - "rride" - ) - THUMBNAIL = ( - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbn" - "ail" - ) - USERNAMES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/username" - "s" + "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/viewProp" - "s" - ) - VML_DRAWING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawi" - "ng" - ) + 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/webSetti" - "ngs" - ) + WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" WORKSHEET_SOURCE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/workshee" - "tSource" - ) - XML_MAPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource" ) + XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" diff --git a/src/pptx/opc/oxml.py b/src/pptx/opc/oxml.py index da774c3c9..5dd902a55 100644 --- a/src/pptx/opc/oxml.py +++ b/src/pptx/opc/oxml.py @@ -1,25 +1,30 @@ -# encoding: utf-8 - """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 .constants import NAMESPACE as NS, RELATIONSHIP_TARGET_MODE as RTM -from ..oxml import parse_xml, register_element_cls -from ..oxml.simpletypes import ( +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 ..oxml.xmlchemy import ( +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore, ) +if TYPE_CHECKING: + from pptx.opc.packuri import PackURI nsmap = { "ct": NS.OPC_CONTENT_TYPES, @@ -28,58 +33,89 @@ } -def oxml_tostring(elm, encoding=None, pretty_print=False, standalone=None): +def oxml_to_encoded_bytes( + element: BaseOxmlElement, + encoding: str = "utf-8", + pretty_print: bool = False, + standalone: bool | None = None, +) -> bytes: return etree.tostring( - elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone + element, 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 +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) -class CT_Default(BaseOxmlElement): +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. """ - ```` element, specifying the default content type to be applied - to a part with the specified extension. + 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 = RequiredAttribute("Extension", ST_Extension) - contentType = RequiredAttribute("ContentType", ST_ContentType) + extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Extension", ST_Extension + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) class CT_Override(BaseOxmlElement): - """ - ```` element, specifying the content type to be applied for a - part with the specified partname. + """`` element. + + Specifies the content type to be applied for a part with the specified partname. """ - partName = RequiredAttribute("PartName", XsdAnyUri) - contentType = RequiredAttribute("ContentType", ST_ContentType) + partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "PartName", XsdAnyUri + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) class CT_Relationship(BaseOxmlElement): - """ - ```` element, representing a single relationship from a - source to a target part. + """`` element. + + Represents 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) + 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, reltype, target, target_mode=RTM.INTERNAL): - """ - Return a new ```` element. + 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. """ - xml = '' % nsmap["pr"] - relationship = parse_xml(xml) + relationship = cast(CT_Relationship, parse_xml(f'')) relationship.rId = rId relationship.reltype = reltype - relationship.target_ref = target + relationship.target_ref = target_ref relationship.targetMode = target_mode return relationship @@ -87,62 +123,61 @@ def new(cls, rId, reltype, target, target_mode=RTM.INTERNAL): 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, reltype, target, is_external=False): - """ - Add a child ```` element with attributes set according - to parameter values. - """ + 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) - self._insert_relationship(relationship) + return self._insert_relationship(relationship) @classmethod - def new(cls): - """Return a new ```` element.""" - return parse_xml('' % nsmap["pr"]) + def new(cls) -> CT_Relationships: + """Return a new `` element.""" + return cast(CT_Relationships, parse_xml(f'')) @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. + 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_tostring(self, encoding="UTF-8", standalone=True) + 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. """ - ```` 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, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ + 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, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ + 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): - """ - Return a new ```` element. - """ - xml = '' % nsmap["ct"] - types = parse_xml(xml) - return types + def new(cls) -> CT_Types: + """Return a new `` element.""" + return cast(CT_Types, parse_xml(f'')) register_element_cls("ct:Default", CT_Default) diff --git a/src/pptx/opc/package.py b/src/pptx/opc/package.py index 0427cf347..03ee5f43b 100644 --- a/src/pptx/opc/package.py +++ b/src/pptx/opc/package.py @@ -1,15 +1,17 @@ -# 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 __future__ import annotations + import collections +from collections.abc import Mapping +from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Set, cast -from pptx.compat import is_string, Mapping -from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM, RELATIONSHIP_TYPE as RT +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 @@ -17,41 +19,49 @@ 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(object): + +class _RelatableMixin: """Provide relationship methods required by both the package and each part.""" - def part_related_by(self, reltype): + 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. + 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, reltype, is_external=False): + 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 such a relationship already exists, its rId is returned. Otherwise the relationship is + added and its new rId returned. """ - return ( - self._rels.get_or_add_ext_rel(reltype, target) - if is_external - else self._rels.get_or_add(reltype, target) - ) + 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): + 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): + 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| object containing relationships from this part to others.""" + 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__ ) @@ -60,25 +70,25 @@ def _rels(self): 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). + 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): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file @classmethod - def open(cls, pkg_file): + 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): + def drop_rel(self, rId: str) -> None: """Remove relationship identified by `rId`.""" self._rels.pop(rId) - def iter_parts(self): + def iter_parts(self) -> Iterator[Part]: """Generate exactly one reference to each part in the package.""" - visited = set() + visited: Set[Part] = set() for rel in self.iter_rels(): if rel.is_external: continue @@ -88,114 +98,109 @@ def iter_parts(self): yield part visited.add(part) - def iter_rels(self): + 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() + visited: Set[Part] = set() - def walk_rels(rels): + 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. + # -- 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 --- - for rel in walk_rels(part.rels): - yield rel + yield from walk_rels(part.rels) - for rel in walk_rels(self._rels): - yield rel + yield from walk_rels(self._rels) @property - def main_document_part(self): + 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 self.part_related_by(RT.OFFICE_DOCUMENT) + return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT)) - def next_partname(self, tmpl): + 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' + `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 = set( - p.partname for p in self.iter_parts() if p.partname.startswith(prefix) - ) + 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( # pragma: no cover - "ProgrammingError: ran out of candidate_partnames" - ) + raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover - def save(self, pkg_file): + 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): + def _load(self) -> Self: """Return the package after loading all parts and relationships.""" - pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, self) + 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): + def _rels(self) -> _Relationships: """|Relationships| object containing relationships of this package.""" return _Relationships(PACKAGE_URI.baseURI) -class _PackageLoader(object): +class _PackageLoader: """Function-object that loads a package from disk (or other store).""" - def __init__(self, pkg_file, package): + def __init__(self, pkg_file: str | IO[bytes], package: Package): self._pkg_file = pkg_file self._package = package @classmethod - def load(cls, pkg_file, package): + 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 `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. + 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): + 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["/"], parts + return xml_rels[PACKAGE_URI], parts @lazyproperty - def _content_types(self): + 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. @@ -203,18 +208,17 @@ def _content_types(self): return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI]) @lazyproperty - def _package_reader(self): + def _package_reader(self) -> PackageReader: """|PackageReader| object providing access to package-items in pkg_file.""" return PackageReader(self._pkg_file) @lazyproperty - def _parts(self): + 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. + 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 @@ -227,30 +231,30 @@ def _parts(self): package, blob=package_reader[partname], ) - for partname in (p for p in self._xml_rels.keys() if p != "/") - # --- invalid partnames can arise in some packages; ignore those rather - # --- than raise an exception. + 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): + 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 = {} - visited_partnames = set() + xml_rels: dict[PackURI, CT_Relationships] = {} + visited_partnames: Set[PackURI] = set() - def load_rels(source_partname, rels): + 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: + for rel in rels.relationship_lst: if rel.targetMode == RTM.EXTERNAL: continue target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref) @@ -261,26 +265,31 @@ def load_rels(source_partname, rels): load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI)) return xml_rels - def _xml_rels_for(self, partname): + 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. + 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 parse_xml(rels_xml) + 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. + 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, content_type, package, blob=None): + 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 @@ -288,86 +297,74 @@ def __init__(self, partname, content_type, package, blob=None): self._blob = blob @classmethod - def load(cls, partname, content_type, package, blob): + 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. + 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): + def blob(self) -> bytes: """Contents of this package part as a sequence of bytes. - May be text (XML generally) or binary. Intended to be overridden by subclasses. - Default behavior is to return the blob initial loaded during `Package.open()` - operation. + Intended to be overridden by subclasses. Default behavior is to return the blob initial + loaded during `Package.open()` operation. """ - return self._blob + return self._blob or b"" @blob.setter - def blob(self, bytes_): + 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. + In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on + demand. This works fine for binary parts though. """ - self._blob = bytes_ + self._blob = blob @lazyproperty - def content_type(self): + def content_type(self) -> str: """Content-type (MIME-type) of this part.""" return self._content_type - def drop_rel(self, rId): - """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) - - def load_rels_from_xml(self, xml_rels, parts): + 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. + 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): - """|OpcPackage| instance this part belongs to.""" + def package(self) -> Package: + """Package this part belongs to.""" return self._package @property - def partname(self): + def partname(self) -> PackURI: """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" return self._partname @partname.setter - def partname(self, partname): - if not isinstance(partname, PackURI): + 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__ + "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.""" + 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): + 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 is_string(file): + if isinstance(file, str): with open(file, "rb") as f: return f.read() @@ -377,13 +374,9 @@ def _blob_from_file(self, file): file.seek(0) return file.read() - def _rel_ref_count(self, rId): - """Return int count of references in this part's XML to `rId`.""" - return len([r for r in self._element.xpath("//@r:id") if r == rId]) - @lazyproperty - def _rels(self): - """|Relationships| object containing relationships from this part to others.""" + def _rels(self) -> _Relationships: + """Relationships from this part to others.""" return _Relationships(self._partname.baseURI) @@ -394,46 +387,65 @@ class XmlPart(Part): reserializing the XML payload and managing relationships to other parts. """ - def __init__(self, partname, content_type, package, element): + 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, content_type, package, blob): + 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=parse_xml(blob)) + return cls( + partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob)) + ) @property - def blob(self): + 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. + 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(object): +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. + Client code can register a subclass of |Part| to be used for a package blob based on its + content type. """ - part_type_for = {} + part_type_for: dict[str, type[Part]] = {} - def __new__(cls, partname, content_type, package, blob): + 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): + 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`. @@ -443,19 +455,18 @@ def _part_cls_for(cls, content_type): return Part -class _ContentTypeMap(object): +class _ContentTypeMap: """Value type providing dict semantics for looking up content type by partname.""" - def __init__(self, overrides, defaults): + def __init__(self, overrides: dict[str, str], defaults: dict[str, str]): self._overrides = overrides self._defaults = defaults - def __getitem__(self, partname): + def __getitem__(self, partname: PackURI) -> str: """Return content-type (MIME-type) for part identified by *partname*.""" - if not isinstance(partname, PackURI): + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] raise TypeError( - "_ContentTypeMap key must be , got %s" - % type(partname).__name__ + "_ContentTypeMap key must be , got %s" % type(partname).__name__ ) if partname in self._overrides: @@ -464,14 +475,13 @@ def __getitem__(self, 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 - ) + raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname) @classmethod - def from_xml(cls, content_types_xml): + def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap: """Return |_ContentTypeMap| instance populated from `content_types_xml`.""" - types_elm = parse_xml(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 ) @@ -481,57 +491,55 @@ def from_xml(cls, content_types_xml): return cls(overrides, defaults) -class _Relationships(Mapping): +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. + 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`. + 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): + def __init__(self, base_uri: str): self._base_uri = base_uri - def __contains__(self, rId): + def __contains__(self, rId: object) -> bool: """Implement 'in' operation, like `"rId7" in relationships`.""" return rId in self._rels - def __getitem__(self, rId): + 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): + def __iter__(self) -> Iterator[str]: """Implement iteration of rIds (iterating a mapping produces its keys).""" return iter(self._rels) - def __len__(self): + def __len__(self) -> int: """Return count of relationships in collection.""" return len(self._rels) - def get_or_add(self, reltype, target_part): + 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. + 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 + self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId ) - def get_or_add_ext_rel(self, reltype, target_ref): + 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. + 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 ( @@ -540,7 +548,9 @@ def get_or_add_ext_rel(self, reltype, target_ref): else existing_rId ) - def load_from_xml(self, base_uri, xml_rels, parts): + 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(): @@ -559,11 +569,11 @@ def iter_valid_rels(): self._rels.clear() self._rels.update((rel.rId, rel) for rel in iter_valid_rels()) - def part_with_reltype(self, reltype): + 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. + Raises |KeyError| if not found and |ValueError| if more than one matching relationship is + found. """ rels_of_reltype = self._rels_by_reltype[reltype] @@ -571,14 +581,12 @@ def part_with_reltype(self, reltype): 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 - ) + raise ValueError("multiple relationships of type '%s' in collection" % reltype) return rels_of_reltype[0].target_part - def pop(self, rId): - """Return |Relationship| identified by `rId` after removing it from collection. + 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. """ @@ -588,8 +596,8 @@ def pop(self, rId): 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 ` str: """Return str rId of |_Relationship| newly added to spec.""" rId = self._next_rId self._rels[rId] = _Relationship( @@ -622,7 +630,9 @@ def _add_relationship(self, reltype, target, is_external=False): ) return rId - def _get_matching(self, reltype, target, is_external=False): + 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 @@ -631,18 +641,17 @@ def _get_matching(self, reltype, target, is_external=False): if rel.is_external != is_external: continue rel_target = rel.target_ref if rel.is_external else rel.target_part - if rel_target != target: - continue - return rel.rId + if rel_target == target: + return rel.rId return None @property - def _next_rId(self): + 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 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 @@ -651,25 +660,28 @@ def _next_rId(self): 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): + def _rels(self) -> dict[str, _Relationship]: """dict {rId: _Relationship} containing relationships of this collection.""" - return dict() + return {} @property - def _rels_by_reltype(self): + def _rels_by_reltype(self) -> dict[str, list[_Relationship]]: """defaultdict {reltype: [rels]} for all relationships in collection.""" - D = collections.defaultdict(list) + D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list) for rel in self.values(): D[rel.reltype].append(rel) return D -class _Relationship(object): +class _Relationship: """Value object describing link from a part or package to another part.""" - def __init__(self, base_uri, rId, reltype, target_mode, target): + 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 @@ -677,7 +689,9 @@ def __init__(self, base_uri, rId, reltype, target_mode, target): self._target = target @classmethod - def from_xml(cls, base_uri, rel, parts): + 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 @@ -687,62 +701,63 @@ def from_xml(cls, base_uri, rel, parts): return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target) @lazyproperty - def is_external(self): + 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). + 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): + def reltype(self) -> str: """Member of RELATIONSHIP_TYPE describing relationship of target to source.""" return self._reltype @lazyproperty - def rId(self): + 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. + 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): + 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): + 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. + 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): + 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. + For internal relationships this is the relative partname, suitable for serialization + purposes. For an external relationship it is typically a URL. """ - return ( - self._target - if self.is_external - else self.target_partname.relative_ref(self._base_uri) - ) + 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 index 65a0b44ab..74ddd333f 100644 --- a/src/pptx/opc/packuri.py +++ b/src/pptx/opc/packuri.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI.""" +from __future__ import annotations + import posixpath import re @@ -9,62 +9,59 @@ class PackURI(str): """Proxy for a pack URI (partname). - Provides utility properties the baseURI and the filename slice. Behaves as |str| - otherwise. + 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): + def __new__(cls, pack_uri_str: str): if not pack_uri_str[0] == "/": - raise ValueError("PackURI must begin with slash, got '%s'" % pack_uri_str) + 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, relative_ref): - """ - Return a |PackURI| instance containing the absolute pack URI formed by - translating *relative_ref* onto *baseURI*. - """ + def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI: + """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): - """ - 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 '/'. + 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): - """ - The extension portion of this pack URI, e.g. ``'xml'`` for - ``'/ppt/slides/slide1.xml'``. Note that the period is not included. + 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 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 ''. + 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): + 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'``. + 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: @@ -78,34 +75,30 @@ def idx(self): 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 '/'. + 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): - """ - 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'. + 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 ('/') - if baseURI == "/": - relpath = self[1:] - else: - relpath = posixpath.relpath(self, baseURI) - return relpath + # relative path when `start` (second) parameter is root ("/") + return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) @property - def rels_uri(self): - """ - The pack URI of the .rels part corresponding to the current pack URI. - Only produces sensible output if the pack URI is a partname or the - package pseudo-partname '/'. + 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) diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py index 9e6ad51e0..eba628247 100644 --- a/src/pptx/opc/serialized.py +++ b/src/pptx/opc/serialized.py @@ -1,13 +1,14 @@ -# encoding: utf-8 - """API for reading/writing serialized Open Packaging Convention (OPC) package.""" +from __future__ import annotations + import os import posixpath import zipfile +from collections.abc import Container +from typing import IO, TYPE_CHECKING, Any, Sequence -from pptx.compat import Container, is_string -from pptx.exceptions import PackageNotFoundError +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 @@ -15,114 +16,123 @@ 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): +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. + 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): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file - def __contains__(self, pack_uri): + 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): + 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): + 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. + 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): + def _blob_reader(self) -> _PhysPkgReader: """|_PhysPkgReader| subtype providing read access to the package file.""" return _PhysPkgReader.factory(self._pkg_file) -class PackageWriter(object): +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. + `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. + Its single API classmethod is :meth:`write`. This class is not intended to be instantiated. """ - def __init__(self, pkg_file, pkg_rels, parts): + 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, pkg_rels, parts): + 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. + 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): + 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): + 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. + 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): + 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: + if part._rels: # pyright: ignore[reportPrivateUsage] phys_writer.write(part.partname.rels_uri, part.rels.xml) - def _write_pkg_rels(self, phys_writer): - """Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the package.""" + 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): +class _PhysPkgReader(Container[PackURI]): """Base class for physical package reader objects.""" - def __contains__(self, item): + 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): + 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 is_string(pkg_file): + if not isinstance(pkg_file, str): return _ZipPkgReader(pkg_file) # --- otherwise we treat `pkg_file` as a path --- @@ -141,14 +151,16 @@ class _DirPkgReader(_PhysPkgReader): `path` is the path to a directory containing an expanded package. """ - def __init__(self, path): + def __init__(self, path: str): self._path = os.path.abspath(path) - def __contains__(self, pack_uri): + 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): + 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: @@ -161,14 +173,14 @@ def __getitem__(self, pack_uri): class _ZipPkgReader(_PhysPkgReader): """Implements |PhysPkgReader| interface for a zip-file OPC package.""" - def __init__(self, pkg_file): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file - def __contains__(self, pack_uri): + 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): + 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. @@ -178,76 +190,80 @@ def __getitem__(self, pack_uri): return self._blobs[pack_uri] @lazyproperty - def _blobs(self): + 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(object): +class _PhysPkgWriter: """Base class for physical package writer objects.""" @classmethod - def factory(cls, pkg_file): + 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`. + 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): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file - def __enter__(self): + def __enter__(self) -> _ZipPkgWriter: """Enable use as a context-manager. Opening zip for writing happens here.""" return self - def __exit__(self, exc_type, exc_value, exc_traceback): + 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. + Closing flushes any pending physical writes and releasing any resources it's using. """ self._zipf.close() - def write(self, pack_uri, blob): + 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): + def _zipf(self) -> zipfile.ZipFile: """`ZipFile` instance open for writing.""" return zipfile.ZipFile(self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED) -class _ContentTypesItem(object): +class _ContentTypesItem: """Composes content-types "part" ([Content_Types].xml) for a collection of parts.""" - def __init__(self, parts): + def __init__(self, parts: Sequence[Part]): self._parts = parts @classmethod - def xml_for(cls, parts): + 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. + The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package. """ return cls(parts)._xml @lazyproperty - def _xml(self): + 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. + 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() @@ -260,13 +276,13 @@ def _xml(self): return _types_elm @lazyproperty - def _defaults_and_overrides(self): + 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() + overrides: dict[PackURI, str] = {} for part in self._parts: partname, content_type = part.partname, part.content_type diff --git a/src/pptx/opc/shared.py b/src/pptx/opc/shared.py index 95e379984..cc7fce8c1 100644 --- a/src/pptx/opc/shared.py +++ b/src/pptx/opc/shared.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """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. diff --git a/src/pptx/opc/spec.py b/src/pptx/opc/spec.py index 5b63f425c..a83caf8bd 100644 --- a/src/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/src/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py index 099960d72..21afaa921 100644 --- a/src/pptx/oxml/__init__.py +++ b/src/pptx/oxml/__init__.py @@ -1,64 +1,58 @@ -# 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, @@ -87,7 +81,7 @@ def register_element_cls(nsptagname, cls): 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, @@ -102,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, @@ -155,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, @@ -175,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, @@ -218,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, @@ -246,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, @@ -273,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, @@ -295,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, @@ -326,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, @@ -340,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, @@ -355,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, @@ -367,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, @@ -399,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, @@ -430,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, @@ -449,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, @@ -487,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 index baaeb923d..9b31a9e16 100644 --- a/src/pptx/oxml/action.py +++ b/src/pptx/oxml/action.py @@ -1,31 +1,26 @@ -# encoding: utf-8 +"""lxml custom element classes for text-related XML elements.""" -""" -lxml custom element classes for text-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import - -from .simpletypes import XsdString -from .xmlchemy import BaseOxmlElement, OptionalAttribute +from pptx.oxml.simpletypes import XsdString +from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute class CT_Hyperlink(BaseOxmlElement): - """ - Custom element class for elements. - """ + """Custom element class for elements.""" - rId = OptionalAttribute("r:id", XsdString) - action = OptionalAttribute("action", XsdString) + rId: str = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + action: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "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 + 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 @@ -41,12 +36,11 @@ def action_fields(self): 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. + 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 diff --git a/src/pptx/oxml/chart/axis.py b/src/pptx/oxml/chart/axis.py index c59d2440a..7129810c9 100644 --- a/src/pptx/oxml/chart/axis.py +++ b/src/pptx/oxml/chart/axis.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Axis-related oxml objects.""" -from __future__ import unicode_literals +from __future__ import annotations from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK from pptx.oxml.chart.shared import CT_Title diff --git a/src/pptx/oxml/chart/chart.py b/src/pptx/oxml/chart/chart.py index 65a0191e7..f4cd0dc7c 100644 --- a/src/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/src/pptx/oxml/chart/datalabel.py b/src/pptx/oxml/chart/datalabel.py index 091693919..b6aac2fd5 100644 --- a/src/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/src/pptx/oxml/chart/legend.py b/src/pptx/oxml/chart/legend.py index 7a2eadb8e..196ca15de 100644 --- a/src/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/src/pptx/oxml/chart/marker.py b/src/pptx/oxml/chart/marker.py index e849e3be2..34afd13d5 100644 --- a/src/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/src/pptx/oxml/chart/plot.py b/src/pptx/oxml/chart/plot.py index f917913df..9c695a43a 100644 --- a/src/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/src/pptx/oxml/chart/series.py b/src/pptx/oxml/chart/series.py index 2974a2269..9264d552d 100644 --- a/src/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/src/pptx/oxml/chart/shared.py b/src/pptx/oxml/chart/shared.py index ddea5132c..5515aa4be 100644 --- a/src/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/src/pptx/oxml/coreprops.py b/src/pptx/oxml/coreprops.py index 2993e88bc..de6b26b24 100644 --- a/src/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/src/pptx/oxml/dml/color.py b/src/pptx/oxml/dml/color.py index 4aa796d5b..dfce90aa0 100644 --- a/src/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/src/pptx/oxml/dml/fill.py b/src/pptx/oxml/dml/fill.py index a7b688a3e..2ff2255d7 100644 --- a/src/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/src/pptx/oxml/dml/line.py b/src/pptx/oxml/dml/line.py index 02d4e59c2..720ca8e07 100644 --- a/src/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 index f83c1cd0b..d900c33bf 100644 --- a/src/pptx/oxml/ns.py +++ b/src/pptx/oxml/ns.py @@ -1,66 +1,57 @@ -# encoding: utf-8 +"""Namespace related objects.""" -""" -Namespace related objects. -""" +from __future__ import annotations -from __future__ import absolute_import - -#: Maps namespace prefix to namespace name for all known PowerPoint XML -#: namespaces. +# -- 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"), + "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. - """ + """Value object that knows the semantics of an XML tag having a namespace prefix.""" - def __new__(cls, nstag, *args): + def __new__(cls, nstag: str): return super(NamespacePrefixedTag, cls).__new__(cls, nstag) - def __init__(self, nstag): + def __init__(self, nstag: str): self._pfx, self._local_part = nstag.split(":") self._ns_uri = _nsmap[self._pfx] + @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) @@ -100,40 +91,39 @@ def nsuri(self): 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'). +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'). """ - namespaces = {} - for prefix in prefixes: - namespaces[prefix] = _nsmap[prefix] - return namespaces + return {pfx: _nsmap[pfx] for pfx in prefixes} nsmap = namespaces # alias for more compact use with Element() -def nsdecls(*prefixes): +def nsdecls(*prefixes: str): 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. +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): - """ - 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'``. +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 index 17616cb4f..12c6751f1 100644 --- a/src/pptx/oxml/presentation.py +++ b/src/pptx/oxml/presentation.py @@ -1,78 +1,91 @@ -# encoding: utf-8 +"""Custom element classes for presentation-related XML elements.""" -""" -Custom element classes for presentation-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import TYPE_CHECKING, Callable -from .simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString -from .xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne, ZeroOrMore +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): - """ - ```` 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", - ), +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",) ) - 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. + """`p:sldId` element. + + Direct child of `p:sldIdLst` that contains an `rId` reference to a slide in the presentation. """ - id = RequiredAttribute("id", ST_SlideId) - rId = RequiredAttribute("r:id", XsdString) + 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. """ - ```` 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): - """ - Return a reference to a newly created child element having - its r:id attribute set to *rId*. + 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): - """ - 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. + """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. + """`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") @@ -82,14 +95,19 @@ class CT_SlideMasterIdListEntry(BaseOxmlElement): a reference to a slide master. """ - rId = RequiredAttribute("r:id", XsdString) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] class CT_SlideSize(BaseOxmlElement): - """ - ```` element, direct child of that contains the - width and height of slides in the presentation. + """`p:sldSz` 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) + 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 index e69de29bb..37f8ef60e 100644 --- a/src/pptx/oxml/shapes/__init__.py +++ 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/src/pptx/oxml/shapes/autoshape.py b/src/pptx/oxml/shapes/autoshape.py index 3da31d132..5d78f624f 100644 --- a/src/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 index ebbe1045f..91261f780 100644 --- a/src/pptx/oxml/shapes/connector.py +++ b/src/pptx/oxml/shapes/connector.py @@ -1,22 +1,23 @@ -# encoding: utf-8 +"""lxml custom element classes for XML elements related to the Connector shape.""" -""" -lxml custom element classes for shape-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import +from typing import TYPE_CHECKING, cast -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 +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 specifying a connection between - an end-point of a connector and a shape connection point. + """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) @@ -24,77 +25,68 @@ class CT_Connection(BaseShapeElement): class CT_Connector(BaseShapeElement): - """ - A line/connector shape ```` element - """ + """A line/connector shape `p:cxnSp` element""" _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst") nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr") - spPr = OneAndOnlyOne("p:spPr") + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] 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() + 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 "") - 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" - "" + 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): """ - ```` element, container for the non-visual properties of + `p:nvCxnSpPr` element, container for the non-visual properties of a connector, such as name, id, etc. """ diff --git a/src/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py index 6f65da7f4..cf32377c2 100644 --- a/src/pptx/oxml/shapes/graphfrm.py +++ b/src/pptx/oxml/shapes/graphfrm.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """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 @@ -21,159 +23,165 @@ GRAPHIC_DATA_URI_TABLE, ) +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import ( + CT_ApplicationNonVisualDrawingProps, + CT_NonVisualDrawingProps, + CT_Transform2D, + ) + class CT_GraphicalObject(BaseOxmlElement): - """ - ```` element, which is the container for the reference to or - definition of the framed graphical object (table, chart, etc.). + """`a:graphic` element. + + The container for the reference to or definition of the framed graphical object (table, chart, + etc.). """ - graphicData = OneAndOnlyOne("a:graphicData") + graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:graphicData" + ) @property - def chart(self): - """ - The ```` grandchild element, or |None| if not present. - """ + def chart(self) -> CT_Chart | None: + """The `c:chart` 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. + """`p:graphicData` 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) + 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): - """Optional "r:id" attribute value of `` descendent element. + 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. + 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): + 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. + 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. + 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 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. + 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): + 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. + 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. + 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 = self.xpath(".//p:oleObj") + oleObjs = cast(list[CT_OleObject], 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. + """`p:graphicFrame` element. + + A container for a table, a chart, or another graphical object. """ - nvGraphicFramePr = OneAndOnlyOne("p:nvGraphicFramePr") - xfrm = OneAndOnlyOne("p:xfrm") - graphic = OneAndOnlyOne("a:graphic") + 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): - """ - The ```` great-grandchild element, or |None| if not present. - """ + 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): - """ - The ``rId`` attribute of the ```` great-grandchild element, - or |None| if not present. + 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): - """ - Return the required ```` child element. Overrides version on - BaseShapeElement. + 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): - """` grandchild of this graphic-frame element.""" + def graphicData(self) -> CT_GraphicalObjectData: + """`a:graphicData` grandchild of this graphic-frame element.""" return self.graphic.graphicData @property - def graphicData_uri(self): - """str value of `uri` attribute of ` grandchild.""" + def graphicData_uri(self) -> str: + """str value of `uri` attribute of `a:graphicData` grandchild.""" return self.graphic.graphicData.uri @property - def has_oleobj(self): - """True for graphicFrame containing an OLE object, False otherwise.""" + 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): + 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. + 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. - """ + 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 @@ -181,160 +189,154 @@ def new_chart_graphicFrame(cls, id_, name, rId, x, y, cx, cy): 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. + 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. """ - xml = cls._graphicFrame_tmpl() % (id_, name, x, y, cx, cy) - graphicFrame = parse_xml(xml) - return graphicFrame + 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_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy, imgW, imgH - ): - """Return newly-created `` for embedded OLE-object. + 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. + `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). + `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, imgW, imgH - ) + 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_, name, rows, cols, x, y, cx, cy): - """ - Return a ```` element tree populated with a table - element. - """ + 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 - @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, imgW, imgH - ): - """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, - imgW=imgW, - imgH=imgH, - ) - class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement): - """`` element. + """`p:nvGraphicFramePr` element. This contains the non-visual properties of a graphic frame, such as name, id, etc. """ - cNvPr = OneAndOnlyOne("p:cNvPr") - nvPr = OneAndOnlyOne("p:nvPr") + cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvPr" + ) + nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvPr") + ) class CT_OleObject(BaseOxmlElement): - """`` element, container for an OLE object (e.g. Excel file). + """`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 = OptionalAttribute("progId", XsdString) - rId = OptionalAttribute("r:id", XsdString) - showAsIcon = OptionalAttribute("showAsIcon", XsdBoolean, default=False) + 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): + def is_embedded(self) -> bool: """True when this OLE object is embedded, False when it is linked.""" - return True if len(self.xpath("./p:embed")) > 0 else False + return len(self.xpath("./p:embed")) > 0 diff --git a/src/pptx/oxml/shapes/groupshape.py b/src/pptx/oxml/shapes/groupshape.py index e428bd79e..f62bc6662 100644 --- a/src/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/src/pptx/oxml/shapes/picture.py b/src/pptx/oxml/shapes/picture.py index 39904385d..bacc97194 100644 --- a/src/pptx/oxml/shapes/picture.py +++ b/src/pptx/oxml/shapes/picture.py @@ -1,9 +1,8 @@ -# encoding: utf-8 - """lxml custom element classes for picture-related XML elements.""" -from __future__ import division +from __future__ import annotations +from typing import TYPE_CHECKING, cast from xml.sax.saxutils import escape from pptx.oxml import parse_xml @@ -11,6 +10,10 @@ 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): """`p:pic` element. @@ -20,10 +23,10 @@ class CT_Picture(BaseShapeElement): 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. @@ -65,28 +68,38 @@ def new_ph_pic(cls, id_, name, desc, rId): @classmethod 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) - ) + 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/src/pptx/oxml/shapes/shared.py b/src/pptx/oxml/shapes/shared.py index 74eb562d3..d9f945697 100644 --- a/src/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/src/pptx/oxml/simpletypes.py b/src/pptx/oxml/simpletypes.py index 7c3ed34e5..6ceb06f7c 100644 --- a/src/pptx/oxml/simpletypes.py +++ b/src/pptx/oxml/simpletypes.py @@ -1,37 +1,35 @@ -# encoding: utf-8 - """Simple-type classes. -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. +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. +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 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)) @@ -151,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 ) @@ -231,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 @@ -349,9 +346,7 @@ def validate(cls, value): class ST_Direction(XsdTokenEnumeration): - """ - Valid values for attribute - """ + """Valid values for `` attribute.""" HORZ = "horz" VERT = "vert" @@ -419,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): @@ -471,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 ) @@ -615,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 ) @@ -636,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): @@ -661,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/src/pptx/oxml/slide.py b/src/pptx/oxml/slide.py index 36b868cf8..37a9780f6 100644 --- a/src/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/src/pptx/oxml/table.py b/src/pptx/oxml/table.py index 5b0bd5b6d..cd3e9ebc3 100644 --- a/src/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 index ced0f8088..0f9ecc152 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -1,12 +1,10 @@ -# encoding: utf-8 - """Custom element classes for text-related XML elements""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import re +from typing import TYPE_CHECKING, Callable, cast -from pptx.compat import to_unicode from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import ( MSO_AUTO_SIZE, @@ -42,27 +40,33 @@ ) 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""" - rPr = ZeroOrOne("a:rPr", successors=("a:t",)) - t = OneAndOnlyOne("a:t") + 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): - """(unicode) str containing text of (required) `a:t` child""" + 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 to_unicode(text) if text is not None else "" + # -- t.text is None when t element is empty, e.g. '' -- + return text or "" @text.setter - def text(self, str): - """*str* is unicode value to replace run text.""" - self.t.text = self._escape_ctrl_chars(str) + def text(self, value: str): # pyright: ignore[reportIncompatibleMethodOverride] + self.t.text = self._escape_ctrl_chars(value) @staticmethod - def _escape_ctrl_chars(s): + 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 @@ -78,8 +82,13 @@ class CT_TextBody(BaseOxmlElement): Also used for `c:txPr` in charts and perhaps other elements. """ - bodyPr = OneAndOnlyOne("a:bodyPr") - p = OneOrMore("a:p") + 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. @@ -90,12 +99,11 @@ def clear_content(self): 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. + 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() @@ -103,7 +111,7 @@ def defRPr(self): return defRPr @property - def is_empty(self): + def is_empty(self) -> bool: """True if only a single empty `a:p` element is present.""" ps = self.p_lst if len(ps) > 1: @@ -118,37 +126,32 @@ def is_empty(self): @classmethod def new(cls): - """ - Return a new ```` element tree - """ + """Return a new `p:txBody` 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. + 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 = parse_xml(xml) + txBody = cast(CT_TextBody, parse_xml(xml)) return txBody @classmethod def new_p_txBody(cls): - """ - Return a new ```` element tree, suitable for use in an - ```` element. - """ + """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 ```` element tree suitable for use in a chart object - like data labels or tick labels. + """Return a `c:txPr` element tree. + + Suitable for use in a chart object like data labels or tick labels. """ xml = ( "\n" @@ -167,8 +170,8 @@ def new_txPr(cls): 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). + Intuitively, reverse a ".clear_content()" operation to minimum conformance with spec + (single empty paragraph). """ if len(self.p_lst) > 0: return @@ -196,27 +199,43 @@ def _txBody_tmpl(cls): class CT_TextBodyProperties(BaseOxmlElement): - """ - custom element class - """ + """`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 = 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) + 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. - """ + """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: @@ -226,11 +245,11 @@ def autofit(self): return None @autofit.setter - def autofit(self, value): + def autofit(self, value: MSO_AUTO_SIZE | None): if value is not None and value not in MSO_AUTO_SIZE: raise ValueError( - "only None or a member of the MSO_AUTO_SIZE enumeration can " - "be assigned to CT_TextBodyProperties.autofit, got %s" % value + 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: @@ -242,12 +261,16 @@ def autofit(self, value): class CT_TextCharacterProperties(BaseOxmlElement): - """`a:rPr, a:defRPr, and `a:endParaRPr` custom element class. + """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. + '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"), @@ -275,7 +298,7 @@ class CT_TextCharacterProperties(BaseOxmlElement): "a:extLst", ), ) - latin = ZeroOrOne( + latin: CT_TextFont | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "a:latin", successors=( "a:ea", @@ -287,62 +310,73 @@ class CT_TextCharacterProperties(BaseOxmlElement): "a:extLst", ), ) - hlinkClick = ZeroOrOne("a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst")) + hlinkClick: CT_Hyperlink | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "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) + 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): - """ - Add an child element with r:id attribute set to *rId*. - """ + 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): - """ - field element, for either a slide number or date field - """ + """`a:fld` field element, for either a slide number or date field.""" - rPr = ZeroOrOne("a:rPr", successors=("a:pPr", "a:t")) - t = ZeroOrOne("a:t", successors=()) + 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): - """ - The text of the ```` child element. - """ + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] + """The text of the `a:t` child element.""" t = self.t if t is None: return "" - text = t.text - return to_unicode(text) if text is not None else "" + return t.text or "" class CT_TextFont(BaseOxmlElement): - """ - Custom element class for , , , and child - elements of CT_TextCharacterProperties, e.g. . + """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 = RequiredAttribute("typeface", ST_TextTypeface) + 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): + 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 @@ -352,9 +386,7 @@ def text(self): class CT_TextNormalAutofit(BaseOxmlElement): - """ - element specifying fit text to shape font reduction, etc. - """ + """`a:normAutofit` element specifying fit text to shape font reduction, etc.""" fontScale = OptionalAttribute( "fontScale", ST_TextFontScalePercentOrPercentString, default=100.0 @@ -364,36 +396,42 @@ class CT_TextNormalAutofit(BaseOxmlElement): class CT_TextParagraph(BaseOxmlElement): """`a:p` custom element class""" - pPr = ZeroOrOne("a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr")) + 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 = ZeroOrOne("a:endParaRPr", successors=()) + endParaRPr: CT_TextCharacterProperties | None = ZeroOrOne( + "a:endParaRPr", successors=() + ) # pyright: ignore[reportAssignmentType] - def add_br(self): - """ - Return a newly appended element. - """ + def add_br(self) -> CT_TextLineBreak: + """Return a newly appended `a:br` element.""" return self._add_br() - def add_r(self, text=None): - """ - Return a newly appended element. - """ + 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): - """Append `a:r` and `a:br` elements to *p* based on *text*. + 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). + 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--- + # ---breaks are only added _between_ items, not at start--- if idx > 0: self.add_br() # ---runs that would be empty are not added--- @@ -401,16 +439,17 @@ def append_text(self, text): self.add_r(r_str) @property - def content_children(self): + 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`. """ - text_types = {CT_RegularTextRun, CT_TextLineBreak, CT_TextField} - return tuple(elm for elm in self if type(elm) in text_types) + return tuple( + e for e in self if isinstance(e, (CT_RegularTextRun, CT_TextLineBreak, CT_TextField)) + ) @property - def text(self): + 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]) @@ -421,9 +460,15 @@ def _new_r(self): class CT_TextParagraphProperties(BaseOxmlElement): - """ - custom element class - """ + """`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", @@ -444,31 +489,43 @@ class CT_TextParagraphProperties(BaseOxmlElement): "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) + 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): - """ - 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. + 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 lnSpc.spcPct.val + return cast(CT_TextSpacingPercent, lnSpc.spcPct).val @line_spacing.setter - def line_spacing(self, value): + def line_spacing(self, value: float | Length | None): self._remove_lnSpc() if value is None: return @@ -478,11 +535,8 @@ def line_spacing(self, value): self._add_lnSpc().set_spcPct(value) @property - def space_after(self): - """ - The EMU equivalent of the centipoints value in - `./a:spcAft/a:spcPts/@val`. - """ + 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 @@ -492,17 +546,14 @@ def space_after(self): return spcPts.val @space_after.setter - def space_after(self, value): + 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`. - """ + """The EMU equivalent of the centipoints value in `./a:spcBef/a:spcPts/@val`.""" spcBef = self.spcBef if spcBef is None: return None @@ -512,54 +563,56 @@ def space_before(self): return spcPts.val @space_before.setter - def space_before(self, value): + 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 , , and elements. - """ + """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 = ZeroOrOne("a:spcPct") - spcPts = ZeroOrOne("a:spcPts") + 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): - """ - Set spacing to *value* lines, e.g. 1.75 lines. A ./a:spcPts child is - removed if present. + 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): - """ - Set spacing to *value* points. A ./a:spcPct child is removed if - present. - """ + 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): - """ - element, specifying spacing in thousandths of a percent in its - `val` attribute. - """ + """`a:spcPct` element, specifying spacing in thousandths of a percent in its `val` attribute.""" - val = RequiredAttribute("val", ST_TextSpacingPercentOrPercentString) + val: float = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "val", ST_TextSpacingPercentOrPercentString + ) class CT_TextSpacingPoint(BaseOxmlElement): - """ - element, specifying spacing in centipoints in its `val` - attribute. - """ + """`a:spcPts` element, specifying spacing in centipoints in its `val` attribute.""" - val = RequiredAttribute("val", ST_TextSpacingPoint) + val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "val", ST_TextSpacingPoint + ) diff --git a/src/pptx/oxml/theme.py b/src/pptx/oxml/theme.py index 9e3737311..19ac8dea6 100644 --- a/src/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/src/pptx/oxml/xmlchemy.py b/src/pptx/oxml/xmlchemy.py index b84ef4ddb..41fb2e171 100644 --- a/src/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/src/pptx/package.py b/src/pptx/package.py index 1d5e73cd6..79703cd6c 100644 --- a/src/pptx/package.py +++ b/src/pptx/package.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """Overall .pptx package.""" +from __future__ import annotations + +from typing import IO, Iterator + from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import OpcPackage from pptx.opc.packuri import PackURI @@ -15,7 +17,7 @@ class Package(OpcPackage): """An overall .pptx package.""" @lazyproperty - def core_properties(self): + 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). @@ -27,7 +29,7 @@ def core_properties(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, @@ -43,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. """ @@ -127,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: @@ -143,7 +143,7 @@ def __iter__(self): image_parts.append(image_part) yield image_part - def get_or_add_image_part(self, image_file): + 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 @@ -152,9 +152,9 @@ def get_or_add_image_part(self, image_file): """ image = Image.from_file(image_file) image_part = self._find_by_sha1(image.sha1) - return ImagePart.new(self._package, image) if image_part is None else 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/src/pptx/parts/chart.py b/src/pptx/parts/chart.py index 2a8a04283..7208071b4 100644 --- a/src/pptx/parts/chart.py +++ b/src/pptx/parts/chart.py @@ -1,13 +1,21 @@ -# encoding: utf-8 - """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, 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.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. @@ -18,7 +26,7 @@ class ChartPart(XmlPart): partname_template = "/ppt/charts/chart%d.xml" @classmethod - def new(cls, chart_type, chart_data, package): + 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`. @@ -74,11 +82,7 @@ def xlsx_part(self): 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) - ) + 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): diff --git a/src/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py index e39b154d0..8471cc8ef 100644 --- a/src/pptx/parts/coreprops.py +++ b/src/pptx/parts/coreprops.py @@ -3,12 +3,16 @@ from __future__ import annotations import datetime as dt +from typing import TYPE_CHECKING 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 +if TYPE_CHECKING: + from pptx.package import Package + class CorePropertiesPart(XmlPart): """Corresponds to part named `/docProps/core.xml`. @@ -16,8 +20,10 @@ class CorePropertiesPart(XmlPart): Contains the core document properties for this document package. """ + _element: CT_CoreProperties + @classmethod - def default(cls, package): + 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 @@ -31,35 +37,35 @@ def default(cls, package): 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 @@ -67,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 @@ -107,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 @@ -115,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 @@ -123,35 +129,35 @@ 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, package): + def _new(cls, package: Package) -> CorePropertiesPart: """Return new empty |CorePropertiesPart| instance.""" return CorePropertiesPart( PackURI("/docProps/core.xml"), diff --git a/src/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py index c2d434e04..7aa2cf408 100644 --- a/src/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" @@ -43,7 +48,7 @@ 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`. diff --git a/src/pptx/parts/image.py b/src/pptx/parts/image.py index db59c5fcc..9be5d02d6 100644 --- a/src/pptx/parts/image.py +++ b/src/pptx/parts/image.py @@ -1,36 +1,44 @@ -# encoding: utf-8 - """ImagePart and related objects.""" -from __future__ import division +from __future__ import annotations import hashlib +import io import os +from typing import IO, TYPE_CHECKING, Any, cast -try: - from PIL import Image as PIL_Image -except ImportError: - import Image as PIL_Image +from PIL import Image as PIL_Image -from pptx.compat import BytesIO, is_string from pptx.opc.package import Part from pptx.opc.spec import image_content_types -from pptx.util import lazyproperty +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]*.*`. + An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`. """ - def __init__(self, partname, content_type, package, blob, filename=None): + 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, image): + def new(cls, package: Package, image: Image) -> ImagePart: """Return new |ImagePart| instance containing `image`. `image` is an |Image| object. @@ -44,80 +52,76 @@ def new(cls, package, image): ) @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. + 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 + # -- return generic filename if original filename is unknown -- if self._filename is None: - return "image.%s" % self.ext + return f"image.{self.ext}" return self._filename @property - def ext(self): - """ - Return file extension for this image e.g. ``'png'``. - """ + def ext(self) -> str: + """File-name 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 image(self) -> Image: + """An |Image| object containing the image in this image part. - def scale(self, scaled_cx, scaled_cy): + Note this is a `pptx.image.Image` object, not a PIL Image. """ - 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. + 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 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: + 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 - 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): - """ - The SHA1 hash digest for the image binary of this image part, like: - ``'1be010ea47803b00e140b852765cdf84f491da47'``. + 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): - """ - A (horz_dpi, vert_dpi) 2-tuple (ints) representing the dots-per-inch - property of this image. - """ - image = Image.from_blob(self.blob) + 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): - """ - 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. + 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 @@ -126,38 +130,35 @@ def _native_size(self): width = EMU_PER_INCH * width_px / horz_dpi height = EMU_PER_INCH * height_px / vert_dpi - return width, height + return Emu(int(width)), Emu(int(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) + 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, filename): + def __init__(self, blob: bytes, filename: str | None): 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*.""" + 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): - """ - Return a new |Image| object loaded from *image_file*, which can be - either a path (string) or a file-like object. + 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 is_string(image_file): + if isinstance(image_file, str): # treat image_file as a path with open(image_file, "rb") as f: blob = f.read() @@ -173,32 +174,27 @@ def from_file(cls, image_file): return cls.from_blob(blob, filename) @property - def blob(self): - """ - The binary image bytestream of this image. - """ + def blob(self) -> bytes: + """The binary image bytestream of this image.""" return self._blob @lazyproperty - def content_type(self): - """ - MIME-type of this image, e.g. ``'image/jpeg'``. - """ + def content_type(self) -> str: + """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 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): - """ - 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. + 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))) @@ -208,12 +204,11 @@ def int_dpi(dpi): 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. + 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])) @@ -222,12 +217,11 @@ def normalize_pil_dpi(pil_dpi): 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. + 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", @@ -244,46 +238,38 @@ def ext(self): 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. + 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): - """ - SHA1 hash digest of the image blob - """ + def sha1(self) -> str: + """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. - """ + 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): - """ - The PIL Image format of this image, e.g. 'PNG'. - """ + 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): - """ - 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) + 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 = pil_image.info.get("dpi") + 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/src/pptx/parts/media.py b/src/pptx/parts/media.py index 81efb5a5d..7e8bc2f21 100644 --- a/src/pptx/parts/media.py +++ b/src/pptx/parts/media.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """MediaPart and related objects.""" +from __future__ import annotations + import hashlib from pptx.opc.package import Part diff --git a/src/pptx/parts/presentation.py b/src/pptx/parts/presentation.py index 30b4ff016..1413de457 100644 --- a/src/pptx/parts/presentation.py +++ b/src/pptx/parts/presentation.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """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 @@ -9,6 +11,10 @@ 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. @@ -16,10 +22,10 @@ class PresentationPart(XmlPart): 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*. + 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 @@ -28,14 +34,14 @@ def add_slide(self, slide_layout): return rId, slide_part.slide @property - def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties of this presentation. + 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): + 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. @@ -46,7 +52,7 @@ def get_slide(self, slide_id): return None @lazyproperty - def notes_master(self): + 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 @@ -56,12 +62,11 @@ def notes_master(self): 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. + 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) @@ -78,27 +83,27 @@ def presentation(self): """ return Presentation(self._element, self) - def related_slide(self, rId): + 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): + 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): + 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``. + 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): + 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 diff --git a/src/pptx/parts/slide.py b/src/pptx/parts/slide.py index 5d721bb41..6650564a5 100644 --- a/src/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,15 +30,17 @@ 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. + _element: CT_Slide + + 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 self.related_part(rId).image + return cast("ImagePart", self.related_part(rId)).image - def get_or_add_image_part(self, image_file): + 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 @@ -41,10 +52,8 @@ def get_or_add_image_part(self, image_file): 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 @@ -159,7 +168,7 @@ def new(cls, partname, package, slide_layout_part): slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT) return slide_part - def add_chart_part(self, chart_type, chart_data): + 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 @@ -167,7 +176,9 @@ def add_chart_part(self, chart_type, chart_data): """ 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 isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT return self.relate_to( @@ -177,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 @@ -207,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) @@ -227,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 @@ -268,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 @@ -281,11 +285,8 @@ 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*. - """ + 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 diff --git a/src/pptx/presentation.py b/src/pptx/presentation.py index eabcda72c..a41bfd59a 100644 --- a/src/pptx/presentation.py +++ b/src/pptx/presentation.py @@ -1,11 +1,19 @@ -# encoding: utf-8 - """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. @@ -14,34 +22,37 @@ class Presentation(PartElementProxy): create a presentation. """ + _element: CT_Presentation + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + @property def core_properties(self): - """ - Instance of |CoreProperties| holding the read/write Dublin Core - document properties for this presentation. + """|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): - """ - Instance of |NotesMaster| for this presentation. If the presentation - does not have a notes master, one is created from a default template + 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): - """ - Save this presentation to *file*, where *file* can be either a path - to a file (a string) or a file-like object. + 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): - """ - Height of slides in this presentation, in English Metric Units (EMU). + 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 @@ -50,18 +61,17 @@ def slide_height(self): return sldSz.cy @slide_height.setter - def slide_height(self, height): + def slide_height(self, height: Length): 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. + 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 @@ -75,10 +85,8 @@ def slide_master(self): return self.slide_masters[0] @lazyproperty - def slide_masters(self): - """ - Sequence of |SlideMaster| objects belonging to this presentation - """ + def slide_masters(self) -> SlideMasters: + """|SlideMasters| collection of slide-masters belonging to this presentation.""" return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self) @property @@ -93,15 +101,13 @@ def slide_width(self): return sldSz.cx @slide_width.setter - def slide_width(self, width): + 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. - """ + """|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]) + self.part.rename_slide_parts([cast("CT_SlideId", sldId).rId for sldId in sldIdLst]) return Slides(sldIdLst, self) diff --git a/src/pptx/shapes/__init__.py b/src/pptx/shapes/__init__.py index c8e1f24d9..332109a31 100644 --- a/src/pptx/shapes/__init__.py +++ b/src/pptx/shapes/__init__.py @@ -1,25 +1,26 @@ -# encoding: utf-8 +"""Objects used across sub-package.""" -""" -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 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. + """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): + def __init__(self, parent: ProvidesPart): super(Subshape, self).__init__() self._parent = parent @property - def part(self): - """ - The package part containing this object - """ + 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 index ead5fecb5..c7f8cd93e 100644 --- a/src/pptx/shapes/autoshape.py +++ b/src/pptx/shapes/autoshape.py @@ -1,11 +1,10 @@ -# encoding: utf-8 - """Autoshape-related objects such as Shape and Adjustment.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from numbers import Number -import xml.sax.saxutils as saxutils +from typing import TYPE_CHECKING, Iterable +from xml.sax import saxutils from pptx.dml.fill import FillFormat from pptx.dml.line import LineFormat @@ -15,110 +14,104 @@ 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(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. +class Adjustment: + """An adjustment value for an autoshape. - 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. + 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): + 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): - """ - 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 + 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): + def effective_value(self, value: float): if not isinstance(value, Number): - tmpl = "adjustment value must be numeric, got '%s'" - raise ValueError(tmpl % value) + raise ValueError(f"adjustment value must be numeric, got {repr(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. + 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): - """ - 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. + 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): - """ - Denormalized effective value (expressed in shape coordinates), - suitable for using in the XML. + 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(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``. +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): + def __init__(self, prstGeom: CT_PresetGeometry2D): super(AdjustmentCollection, self).__init__() self._adjustments_ = self._initialized_adjustments(prstGeom) self._prstGeom = prstGeom - def __getitem__(self, key): + def __getitem__(self, idx: int) -> float: """Provides indexed access, (e.g. 'adjustments[9]').""" - return self._adjustments_[key].effective_value + return self._adjustments_[idx].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. + 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_[key].effective_value = value + self._adjustments_[idx].effective_value = value self._rewrite_guides() - def _initialized_adjustments(self, prstGeom): - """ - Return an initialized list of adjustment values based on the contents - of *prstGeom* - """ + 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) @@ -127,19 +120,21 @@ def _initialized_adjustments(self, prstGeom): return adjustments def _rewrite_guides(self): - """ - Write ```` elements to the XML, one for each adjustment value. + """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, 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. + 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: @@ -153,11 +148,8 @@ def _update_adjustments_with_actuals(adjustments, guides): return @property - def _adjustments(self): - """ - Sequence containing direct references to the |Adjustment| objects - contained in collection. - """ + def _adjustments(self) -> tuple[Adjustment, ...]: + """Sequence of |Adjustment| objects contained in collection.""" return tuple(self._adjustments_) def __len__(self): @@ -165,103 +157,88 @@ def __len__(self): 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. +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``. + 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 + 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 ```` + String identifier for this auto shape type used in the `a:prstGeom` element. - .. attribute:: desc - - Informal string description of auto shape. - """ - _instances = {} + _instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {} - def __new__(cls, autoshape_type_id): - """ - Only create new instance on first call for content_type. After that, - use cached instance. + 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 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 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 + 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 + # -- 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 + "no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id ) - # otherwise initialize new instance + # -- 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 - """ + 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): + 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'). + 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): - """ - Return sequence of name, value tuples representing the adjustment - value defaults for the auto shape type identified by *prst*. - """ + 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"] - @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*. + 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) @@ -269,8 +246,8 @@ def id_from_prst(cls, prst): 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'``. + `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) @@ -278,28 +255,24 @@ def prst(self): 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 + 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, parent): + def __init__(self, sp: CT_Shape, parent: ProvidesPart): super(Shape, self).__init__(sp, parent) self._sp = sp @lazyproperty - def adjustments(self): - """ - Read-only reference to |AdjustmentCollection| instance for this - shape - """ + 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. + """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") @@ -307,49 +280,40 @@ def auto_shape_type(self): @lazyproperty def fill(self): - """ - |FillFormat| instance for this shape, providing access to fill - properties such as fill color. + """|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 ```` element containing the line format properties - XML for this shape. - """ + """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): - """ - |True| if this shape can contain text. Always |True| for an - AutoShape. - """ + 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, providing access to line - properties such as line color. + """|LineFormat| instance for this shape. + + Provides 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. + """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): - """ - Unique integer identifying the type of this shape, like - ``MSO_SHAPE_TYPE.TEXT_BOX``. - """ + 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: @@ -358,40 +322,34 @@ def shape_type(self): 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) + raise NotImplementedError("Shape instance of unrecognized shape type") @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). + 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): + 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. + Contains the text of the shape and provides access to text formatting properties. """ - txBody = self._element.get_or_add_txBody() + 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 index c9472434d..751235023 100644 --- a/src/pptx/shapes/base.py +++ b/src/pptx/shapes/base.py @@ -1,14 +1,22 @@ -# encoding: utf-8 - """Base shape-related objects such as BaseShape.""" -from __future__ import absolute_import, division, print_function, unicode_literals +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. @@ -16,158 +24,148 @@ class BaseShape(object): Subclasses include |Shape|, |Picture|, and |GraphicFrame|. """ - def __init__(self, shape_elm, parent): - super(BaseShape, self).__init__() + def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart): + super().__init__() self._element = shape_elm self._parent = parent - def __eq__(self, other): + 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. + 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): + 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): + 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. + 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 + cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage] return ActionSetting(cNvPr, self) @property - def element(self): + 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. + 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. + 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): - """ - |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. + 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): - """ - |True| if this shape can contain text. - """ + 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): - """ - Read/write. Integer distance between top and bottom extents of shape - in EMUs - """ + 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): + def height(self, value: Length): 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. + 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): - """ - Read/write. Integer distance of the left edge of this shape from the - left edge of the slide, in English Metric Units (EMU) + 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): + def left(self, value: Length): self._element.x = value @property - def name(self): - """ - Name of this shape, e.g. 'Picture 7' - """ + def name(self) -> str: + """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 + def name(self, value: str): + self._element._nvXxPr.cNvPr.name = value # pyright: ignore[reportPrivateUsage] @property - def part(self): + 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. + 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 + return cast("BaseSlidePart", 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. + 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. """ - if not self.is_placeholder: + ph = self._element.ph + if ph is None: raise ValueError("shape is not a placeholder") - return _PlaceholderFormat(self._element.ph) + return _PlaceholderFormat(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. + 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): + def rotation(self, value: float): self._element.rot = value @lazyproperty - def shadow(self): + def shadow(self) -> ShadowFormat: """|ShadowFormat| object providing access to shadow for this shape. A |ShadowFormat| object is always returned, even when no shadow is @@ -177,7 +175,7 @@ def shadow(self): return ShadowFormat(self._element.spPr) @property - def shape_id(self): + 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. @@ -185,68 +183,62 @@ def shape_id(self): 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. + 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. """ - # # 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 + raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`") @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) + 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): + def top(self, value: Length): self._element.y = value @property - def width(self): - """ - Read/write. Integer distance between left and right extents of shape - in EMUs + 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): + 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, """ - Accessed via the :attr:`~.BaseShape.placeholder_format` property of - a placeholder shape, provides properties specific to placeholders, such - as the placeholder type. - """ + + def __init__(self, element: CT_Placeholder): + super().__init__(element) + self._ph = element @property - def element(self): - """ - The `p:ph` element proxied by this object. - """ - return super(_PlaceholderFormat, self).element + def element(self) -> CT_Placeholder: + """The `p:ph` element proxied by this object.""" + return self._ph @property - def idx(self): - """ - Integer placeholder 'idx' attribute. - """ - return self._element.idx + def idx(self) -> int: + """Integer placeholder 'idx' attribute.""" + return self._ph.idx @property - def type(self): - """ - Placeholder type, a member of the :ref:`PpPlaceholderType` - enumeration, e.g. PP_PLACEHOLDER.CHART + def type(self) -> PP_PLACEHOLDER: + """Placeholder type. + + A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART """ - return self._element.type + return self._ph.type diff --git a/src/pptx/shapes/connector.py b/src/pptx/shapes/connector.py index ecd8ec9a9..070b080d5 100644 --- a/src/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 index 0168b2baf..e05b3484f 100644 --- a/src/pptx/shapes/freeform.py +++ b/src/pptx/shapes/freeform.py @@ -1,28 +1,50 @@ -# encoding: utf-8 - """Objects related to construction of freeform shapes.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Iterable, Iterator + +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 -from pptx.compat import Sequence -from pptx.util import lazyproperty +CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo" +DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close" -class FreeformBuilder(Sequence): +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. + 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. + 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): + 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 @@ -30,34 +52,41 @@ def __init__(self, shapes, start_x, start_y, x_scale, y_scale): self._x_scale = x_scale self._y_scale = y_scale - def __getitem__(self, idx): + def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride] + self, idx: int + ) -> DrawingOperation: return self._drawing_operations.__getitem__(idx) - def __iter__(self): + def __iter__(self) -> Iterator[DrawingOperation]: 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): + 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*). + (`start_x`, `start_y`). """ - return cls(shapes, int(round(start_x)), int(round(start_y)), x_scale, y_scale) + return cls(shapes, Emu(int(round(start_x))), Emu(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*. + 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*. + `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. + Returns this |FreeformBuilder| object so it can be used in chained calls. """ for x, y in vertices: self._add_line_segment(x, y) @@ -65,109 +94,109 @@ def add_line_segments(self, vertices, close=True): self._add_close() return self - def convert_to_shape(self, origin_x=0, origin_y=0): + 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. + `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. + 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) + return self._shapes._shape_factory(sp) # pyright: ignore[reportPrivateUsage] - def move_to(self, x, y): + 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. + 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): + 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. + 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 + if isinstance(drawing_operation, _Close): + continue + min_x = min(min_x, drawing_operation.x) + return Emu(min_x) @property - def shape_offset_y(self): + 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. + 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 + 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, origin_y): + 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. + `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 + 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, y): + 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): + def _drawing_operations(self) -> list[DrawingOperation]: """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.""" + 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 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 + 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): + 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 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 + 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. + 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)) @@ -175,26 +204,25 @@ def _height(self): 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. + 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): + 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. + 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) + return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y) - def _start_path(self, sp): - """Return a newly created `a:path` element added to *sp*. + 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. + 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)) @@ -204,9 +232,9 @@ def _start_path(self, sp): 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). + 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)) @@ -214,8 +242,8 @@ def _top(self): 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. + 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)) @@ -223,25 +251,24 @@ def _width(self): class _BaseDrawingOperation(object): """Base class for freeform drawing operations. - A drawing operation has at least one location (x, y) in local - coordinates. + A drawing operation has at least one location (x, y) in local coordinates. """ - def __init__(self, freeform_builder, x, y): + 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): - """Add the XML element(s) implementing this operation to *path*. + 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): + def x(self) -> Length: """Return the horizontal (x) target location of this operation. The returned value is an integer in local coordinates. @@ -249,7 +276,7 @@ def x(self): return self._x @property - def y(self): + def y(self) -> Length: """Return the vertical (y) target location of this operation. The returned value is an integer in local coordinates. @@ -261,12 +288,12 @@ class _Close(object): """Specifies adding a `` element to the current contour.""" @classmethod - def new(cls): + def new(cls) -> _Close: """Return a new _Close object.""" return cls() - def apply_operation_to(self, path): - """Add `a:close` element to *path*.""" + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose: + """Add `a:close` element to `path`.""" return path.add_close() @@ -274,21 +301,21 @@ class _LineSegment(_BaseDrawingOperation): """Specifies a straight line segment ending at the specified point.""" @classmethod - def new(cls, freeform_builder, x, y): + 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. + Both `x` and `y` are rounded to the nearest integer before use. """ - return cls(freeform_builder, int(round(x)), int(round(y))) + return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y)))) - def apply_operation_to(self, path): - """Add `a:lnTo` element to *path* for this line segment. + 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( - self._x - self._freeform_builder.shape_offset_x, - self._y - self._freeform_builder.shape_offset_y, + Emu(self._x - self._freeform_builder.shape_offset_x), + Emu(self._y - self._freeform_builder.shape_offset_y), ) @@ -296,16 +323,16 @@ 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)*. + 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. + Both `x` and `y` are rounded to the nearest integer before use. """ - return cls(freeform_builder, int(round(x)), int(round(y))) + return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y)))) - def apply_operation_to(self, path): - """Add `a:moveTo` element to *path* for this line segment.""" + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo: + """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, + Emu(self._x - self._freeform_builder.shape_offset_x), + Emu(self._y - self._freeform_builder.shape_offset_y), ) diff --git a/src/pptx/shapes/graphfrm.py b/src/pptx/shapes/graphfrm.py index de317cc5c..c0ed2bbab 100644 --- a/src/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_part(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_part(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/src/pptx/shapes/group.py b/src/pptx/shapes/group.py index 0de06f853..717375851 100644 --- a/src/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/src/pptx/shapes/picture.py b/src/pptx/shapes/picture.py index 1cb660e6f..59182860d 100644 --- a/src/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/src/pptx/shapes/placeholder.py b/src/pptx/shapes/placeholder.py index 791d91e13..c44837bef 100644 --- a/src/pptx/shapes/placeholder.py +++ b/src/pptx/shapes/placeholder.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - """Placeholder-related objects. Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors @@ -7,6 +5,10 @@ non-trivial class inheritance structure. """ +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 @@ -15,6 +17,9 @@ from pptx.shapes.picture import Picture from pptx.util import Emu +if TYPE_CHECKING: + from pptx.oxml.shapes.autoshape import CT_Shape + class _InheritsDimensions(object): """ @@ -208,13 +213,14 @@ def sz(self): class LayoutPlaceholder(_InheritsDimensions, Shape): - """ - 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. + """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. """ + element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride] + @property def _base_placeholder(self): """ @@ -241,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): @@ -299,9 +305,7 @@ 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): diff --git a/src/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py index cebdfc91f..29623f1f5 100644 --- a/src/pptx/shapes/shapetree.py +++ b/src/pptx/shapes/shapetree.py @@ -1,14 +1,16 @@ -# encoding: utf-8 - """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.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.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 @@ -33,6 +35,20 @@ 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 @@ -59,20 +75,18 @@ class _BaseShapes(ParentedElementProxy): - """ - Base class for a shape collection appearing in a slide-type object, - include Slide, SlideLayout, and SlideMaster, providing common methods. + """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, parent): + 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): - """ - Return shape at *idx* in sequence, e.g. ``shapes[2]``. - """ + 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] @@ -80,36 +94,33 @@ def __getitem__(self, idx): 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. - """ + 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): - """ - 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. + 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): - """Add a new placeholder shape based on *placeholder*.""" + 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): - """ - 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. + 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", @@ -130,60 +141,51 @@ def ph_basename(self, ph_type): }[ph_type] @property - def turbo_add_enabled(self): + 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. + 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): + 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): - """ - Return true if *shape_elm* represents a member of this collection, - False otherwise. - """ + 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): - """ - Generate each child of the ```` element that corresponds to - a shape, in the sequence they appear in the XML. + 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, 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 '``. + 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) @@ -203,12 +205,11 @@ def _next_ph_name(self, ph_type, id, orient): return name @property - def _next_shape_id(self): + 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". + 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: @@ -217,108 +218,120 @@ def _next_shape_id(self): 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*. - """ + 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.""" - def __init__(self, grpSp, parent): + 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, 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. + 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 self._shape_factory(graphicFrame) + return cast("Chart", self._shape_factory(graphicFrame)) - def add_connector(self, connector_type, begin_x, begin_y, end_x, end_y): + 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. + `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) + return cast(Connector, self._shape_factory(cxnSp)) - def add_group_shape(self, shapes=[]): + 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 + 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") + grpSp.insert_element_before( + shape._element, "p:extLst" # pyright: ignore[reportPrivateUsage] + ) if shapes: grpSp.recalculate_extents() - return self._shape_factory(grpSp) + return cast(GroupShape, self._shape_factory(grpSp)) def add_ole_object( self, - object_file, - prog_id, - left, - top, - width=None, - height=None, - icon_file=None, - icon_width=None, - icon_height=None, - ): + 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. + 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. + `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, @@ -335,85 +348,91 @@ def add_ole_object( ) 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. + 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 self._shape_factory(pic) + return cast(Picture, self._shape_factory(pic)) - def add_shape(self, autoshape_type_id, left, top, width, height): + 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_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) + return cast(Shape, self._shape_factory(sp)) - def add_textbox(self, left, top, width, height): + 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. + 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) + return cast(Shape, self._shape_factory(sp)) - def build_freeform(self, start_x=0, start_y=0, scale=1.0): + 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. + 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 + 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): - """Return the index of *shape* in this sequence. + def index(self, shape: BaseShape) -> int: + """Return the index of `shape` in this sequence. - Raises |ValueError| if *shape* is not in the collection. + 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): + 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*. + 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) @@ -423,12 +442,18 @@ def _add_chart_graphicFrame(self, 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): + 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*). + 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) @@ -439,13 +464,20 @@ def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, end_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): + 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. + 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) @@ -454,32 +486,33 @@ def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy): 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): + 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*). + `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): + 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*). + 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): + 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. + 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.--- @@ -489,15 +522,15 @@ def _recalculate_extents(self): 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). + 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): + 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. + This would typically be called when a contained shape is added, removed, or its position + or size updated. """ self._grpSp.recalculate_extents() @@ -505,38 +538,38 @@ def _recalculate_extents(self): 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. + 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, - left, - top, - width, - height, - poster_frame_image=None, - mime_type=CT.VIDEO, - ): - """Return newly added movie shape displaying video in *movie_file*. + 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. + * 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, @@ -551,120 +584,106 @@ def add_movie( ) self._spTree.append(movie_pic) self._add_video_timing(movie_pic) - return self._shape_factory(movie_pic) + return cast(GraphicFrame, 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. + 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) - graphic_frame = self._shape_factory(graphicFrame) - return graphic_frame + return cast(GraphicFrame, self._shape_factory(graphicFrame)) - 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. + 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): - """ - Instance of |SlidePlaceholders| containing sequence of placeholder - shapes in this slide. - """ + def placeholders(self) -> SlidePlaceholders: + """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. + 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 self._shape_factory(elm) + return cast(Shape, 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. - """ + 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): + 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. + 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*. - """ + 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. + """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*. - """ + 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. + """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*. - """ + 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. + """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. + 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", @@ -675,105 +694,96 @@ def ph_basename(self, ph_type): 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. - """ + 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 that differentiate behaviors for - a master, layout, and slide. By default, placeholder shapes are - constructed using |BaseShapeFactory|. Subclasses should override + """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): - """ - True if *shape_elm* is a placeholder shape, False otherwise. - """ + 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| instances representing the placeholder - shapes on a slide layout. - """ + """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout.""" - def get(self, idx, default=None): - """ - Return the first placeholder shape with matching *idx* value, or - *default* if not found. - """ + __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): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ + 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 instances representing the placeholder - shapes on a slide master. - """ + """Sequence of MasterPlaceholder 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. + __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(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return _MasterShapeFactory(shape_elm, self) + 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. - """ + """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) + __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 + """Collection of placeholder shapes on a slide. + + Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the placeholders it contains. """ - 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. + _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: @@ -781,26 +791,20 @@ def __getitem__(self, idx): raise KeyError("no placeholder on this slide with idx == %d" % idx) def __iter__(self): - """ - Generate placeholder shapes in `idx` order. - """ + """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. - """ + def __len__(self) -> int: + """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*. - """ +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 tag == qn("p:pic"): + if isinstance(shape_elm, CT_Picture): videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile") if videoFiles: return Movie(shape_elm, parent) @@ -813,46 +817,32 @@ def BaseShapeFactory(shape_elm, parent): qn("p:graphicFrame"): GraphicFrame, }.get(tag, BaseShape) - return shape_cls(shape_elm, parent) + return shape_cls(shape_elm, parent) # pyright: ignore[reportArgumentType] -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: +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, 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: +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, 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: +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, parent): - """ - Return a placeholder shape of the appropriate type for *shape_elm*. - """ +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 = { @@ -867,14 +857,11 @@ def _SlidePlaceholderFactory(shape_elm, parent): Constructor = PlaceholderPicture else: Constructor = BaseShapeFactory - return Constructor(shape_elm, parent) + return Constructor(shape_elm, parent) # pyright: ignore[reportArgumentType] -def SlideShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide. - """ +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) @@ -883,14 +870,24 @@ def SlideShapeFactory(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. + 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): + 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 @@ -901,28 +898,35 @@ def __init__(self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file @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. + 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 - return @property - def _media_rId(self): + 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. + 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): + def _pic(self) -> CT_Picture: """Return the new `p:pic` element referencing the video.""" return CT_Picture.new_video_pic( self._shape_id, @@ -937,29 +941,27 @@ def _pic(self): ) @lazyproperty - def _poster_frame_image_file(self): + 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. + 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 io.BytesIO(SPEAKER_IMAGE_BYTES) return poster_frame_file @lazyproperty - def _poster_frame_rId(self): + 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. + 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): + 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. @@ -967,31 +969,30 @@ def _shape_name(self): return self._video.filename @property - def _slide_part(self): + def _slide_part(self) -> SlidePart: """Return SlidePart object for slide containing this movie.""" return self._shapes.part @lazyproperty - def _video(self): + 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): + 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. + 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): + 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. + 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] @@ -999,26 +1000,26 @@ def _video_rId(self): 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. + 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, - icon_width, - icon_height, + 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 @@ -1035,18 +1036,18 @@ def __init__( @classmethod def graphicFrame( cls, - shapes, - shape_id, - ole_object_file, - prog_id, - x, - y, - cx, - cy, - icon_file, - icon_width, - icon_height, - ): + 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, @@ -1063,7 +1064,7 @@ def graphicFrame( )._graphicFrame @lazyproperty - def _graphicFrame(self): + def _graphicFrame(self) -> CT_GraphicalObjectFrame: """Newly-created `p:graphicFrame` element referencing embedded OLE-object.""" return CT_GraphicalObjectFrame.new_ole_object_graphicFrame( self._shape_id, @@ -1080,7 +1081,7 @@ def _graphicFrame(self): ) @lazyproperty - def _cx(self): + 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: @@ -1093,7 +1094,7 @@ def _cx(self): ) @lazyproperty - def _cy(self): + 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: @@ -1106,20 +1107,19 @@ def _cy(self): ) @lazyproperty - def _icon_height(self): + 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. + 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). + 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): + 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). @@ -1140,38 +1140,35 @@ def _icon_image_file(self): return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename)) @lazyproperty - def _icon_rId(self): + 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): + 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. + 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): + 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. + 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): + 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. + This value appears in the `progId` attribute of the `p:oleObj` element for the object. """ prog_id_arg = self._prog_id_arg @@ -1180,7 +1177,7 @@ def _progId(self): return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg @lazyproperty - def _shape_name(self): + 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. @@ -1188,6 +1185,6 @@ def _shape_name(self): return "Object %d" % (self._shape_id - 1) @lazyproperty - def _slide_part(self): + 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 index 32b529d69..da2a17182 100644 --- a/src/pptx/shared.py +++ b/src/pptx/shared.py @@ -1,87 +1,82 @@ -# encoding: utf-8 - """Objects shared by pptx modules.""" -from __future__ import unicode_literals +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. + """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): + def __init__(self, element: BaseOxmlElement): 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. + 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): + 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. - """ + """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. + """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, parent): + 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. + """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 - """ + 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 the root element of - a part such as `p:sld`. - """ + """Provides common members for proxy-objects that wrap a part's root element, e.g. `p:sld`.""" - def __init__(self, element, part): + def __init__(self, element: BaseOxmlElement, part: XmlPart): super(PartElementProxy, self).__init__(element) self._part = part @property - def part(self): - """ - The package part containing this object - """ + def part(self) -> XmlPart: + """The package part containing this object.""" return self._part diff --git a/src/pptx/slide.py b/src/pptx/slide.py index 9b93666c6..3b1b65d8e 100644 --- a/src/pptx/slide.py +++ b/src/pptx/slide.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """Slide-related objects, including masters, layouts, and notes.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator, cast + from pptx.dml.fill import FillFormat from pptx.enum.shapes import PP_PLACEHOLDER from pptx.shapes.shapetree import ( @@ -17,12 +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.""" + _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 @@ -34,31 +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. """ @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) @@ -72,9 +92,9 @@ 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. + + Provides access to shapes, the most commonly used of which are placeholders. """ @@ -85,19 +105,21 @@ class NotesSlide(_BaseSlide): page. """ - def clone_master_placeholders(self, notes_master): - """Selectively add placeholder shape elements from *notes_master*. + element: CT_NotesSlide # pyright: ignore[reportIncompatibleMethodOverride] + + 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 + 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, @@ -109,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: @@ -127,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: @@ -140,38 +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.""" - @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): @@ -188,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: @@ -305,16 +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. """ - 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. + part: SlideLayoutPart # pyright: ignore[reportIncompatibleMethodOverride] + + 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, @@ -326,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 @@ -362,56 +347,51 @@ class SlideLayouts(ParentedElementProxy): Supports indexed access, len(), iteration, index() and remove(). """ - def __init__(self, sldLayoutIdLst, parent): + part: SlideMasterPart # pyright: ignore[reportIncompatibleMethodOverride] + + 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: @@ -432,14 +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|. """ + _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) @@ -450,32 +432,27 @@ class SlideMasters(ParentedElementProxy): Has list access semantics, supporting indexed access, len(), and iteration. """ - def __init__(self, sldMasterIdLst, parent): + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + + 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) @@ -487,7 +464,7 @@ class _Background(ElementProxy): has a |_Background| object. """ - def __init__(self, cSld): + def __init__(self, cSld: CT_CommonSlideData): super(_Background, self).__init__(cSld) self._cSld = cSld diff --git a/src/pptx/spec.py b/src/pptx/spec.py index 835fde6d0..1e7bffb36 100644 --- a/src/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": (), diff --git a/src/pptx/table.py b/src/pptx/table.py index 63872eab8..3bdf54ba6 100644 --- a/src/pptx/table.py +++ b/src/pptx/table.py @@ -1,13 +1,22 @@ -# encoding: utf-8 - """Table-related objects such as Table and Cell.""" -from pptx.compat import is_integer +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 lazyproperty +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): @@ -17,66 +26,68 @@ class Table(object): :meth:`.Slide.shapes.add_table` to add a table to a slide. """ - def __init__(self, tbl, graphic_frame): + 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, col_idx): - """Return cell at *row_idx*, *col_idx*. + 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 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): + 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]``. + 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): - """ - 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. + 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): + def first_col(self, value: bool): 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. + 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): + def first_row(self, value: bool): 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. + 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): + def horz_banding(self, value: bool): self._tbl.bandRow = value - def iter_cells(self): + 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. @@ -84,185 +95,177 @@ def iter_cells(self): 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. + 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): + def last_col(self, value: bool): 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. + 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): + def last_row(self, value: bool): 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). + 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 = sum([row.height for row in self.rows]) + new_table_height = Emu(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 + 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 = sum([col.width for col in self.columns]) + new_table_width = Emu(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. - """ + 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]``. + 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): - """ - Read/write boolean property which, when true, indicates the columns - of the table should appear with alternating shading. + 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): + def vert_banding(self, value: bool): self._tbl.bandCol = value class _Cell(Subshape): """Table cell""" - def __init__(self, tc, parent): + def __init__(self, tc: CT_TableCell, parent: ProvidesPart): super(_Cell, self).__init__(parent) self._tc = tc - def __eq__(self, other): - """|True| if this object proxies the same element as *other*. + 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. + 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): + 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| instance for this cell, providing access to fill - properties such as foreground color. + 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): + 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): + 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. + 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. + 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. + 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): + def margin_left(self, margin_left: Length | None): self._validate_margin_value(margin_left) self._tc.marL = margin_left @property - def margin_right(self): - """ - Right margin of cell. - """ + def margin_right(self) -> Length: + """Right margin of cell.""" return self._tc.marR @margin_right.setter - def margin_right(self, margin_right): + def margin_right(self, margin_right: Length | None): self._validate_margin_value(margin_right) self._tc.marR = margin_right @property - def margin_top(self): - """ - Top margin of cell. - """ + def margin_top(self) -> Length: + """Top margin of cell.""" return self._tc.marT @margin_top.setter - def margin_top(self, margin_top): + def margin_top(self, margin_top: Length | None): self._validate_margin_value(margin_top) self._tc.marT = margin_top @property - def margin_bottom(self): - """ - Bottom margin of cell. - """ + def margin_bottom(self) -> Length: + """Bottom margin of cell.""" return self._tc.marB @margin_bottom.setter - def margin_bottom(self, margin_bottom): + def margin_bottom(self, margin_bottom: Length | None): 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*. + 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. + 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*. + 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) @@ -285,43 +288,38 @@ def merge(self, other_cell): tc.vMerge = True @property - def span_height(self): + 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 + 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): + 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 + 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): + 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. + 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. + 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" - ) + raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit") tc_range = TcRange.from_merge_origin(self._tc) @@ -330,64 +328,52 @@ def split(self): 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). + 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): + def text(self, text: str): self.text_frame.text = text @property - def text_frame(self): - """ - |TextFrame| instance containing the text that appears in the cell. - """ + 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): + 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. + 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. + 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): + def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None): 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: + 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) @@ -395,19 +381,18 @@ def _validate_margin_value(margin_value): class _Column(Subshape): """Table column""" - def __init__(self, gridCol, parent): + def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection): super(_Column, self).__init__(parent) + self._parent = parent self._gridCol = gridCol @property - def width(self): - """ - Width of column in EMU. - """ + def width(self) -> Length: + """Width of column in EMU.""" return self._gridCol.w @width.setter - def width(self, width): + def width(self, width: Length): self._gridCol.w = width self._parent.notify_width_changed() @@ -415,27 +400,26 @@ def width(self, width): class _Row(Subshape): """Table row""" - def __init__(self, tr, parent): + 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]``. + """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. - """ + def height(self) -> Length: + """Height of row in EMU.""" return self._tr.h @height.setter - def height(self, height): + def height(self, height: Length): self._tr.h = height self._parent.notify_height_changed() @@ -443,22 +427,23 @@ def height(self, height): class _CellCollection(Subshape): """Horizontal sequence of row cells""" - def __init__(self, tr, parent): + def __init__(self, tr: CT_TableRow, parent: _Row): super(_CellCollection, self).__init__(parent) + self._parent = parent self._tr = tr - def __getitem__(self, idx): + 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): + def __iter__(self) -> Iterator[_Cell]: """Provides iterability.""" return (_Cell(tc, self) for tc in self._tr.tc_lst) - def __len__(self): + def __len__(self) -> int: """Supports len() function (e.g. 'len(cells) == 1').""" return len(self._tr.tc_lst) @@ -466,56 +451,46 @@ def __len__(self): class _ColumnCollection(Subshape): """Sequence of table columns.""" - def __init__(self, tbl, parent): + def __init__(self, tbl: CT_Table, parent: Table): super(_ColumnCollection, self).__init__(parent) + self._parent = parent self._tbl = tbl - def __getitem__(self, idx): - """ - Provides indexed access, (e.g. 'columns[0]'). - """ + 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'). - """ + """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. - """ + """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): + def __init__(self, tbl: CT_Table, parent: Table): super(_RowCollection, self).__init__(parent) + self._parent = parent self._tbl = tbl - def __getitem__(self, idx): - """ - Provides indexed access, (e.g. 'rows[0]'). - """ + 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'). - """ + """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. - """ + """Called by a row when its height changes. Pass along to parent.""" self._parent.notify_height_changed() diff --git a/src/pptx/text/fonts.py b/src/pptx/text/fonts.py index ebc5b7d49..5ae054a83 100644 --- a/src/pptx/text/fonts.py +++ b/src/pptx/text/fonts.py @@ -1,27 +1,24 @@ -# encoding: utf-8 - """Objects related to system font file lookup.""" +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() @@ -327,9 +324,7 @@ def _iter_names(self): 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) @@ -360,12 +355,8 @@ def _read_name(self, bufr, idx, strings_offset): `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( diff --git a/src/pptx/text/layout.py b/src/pptx/text/layout.py index c230a0ec6..d2b439939 100644 --- a/src/pptx/text/layout.py +++ b/src/pptx/text/layout.py @@ -1,21 +1,26 @@ -# encoding: utf-8 - """Objects related to layout of rendered text, such as TextFitter.""" +from __future__ import annotations + +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): + 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 @@ -294,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)] diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index ba941230a..e139410c2 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -1,30 +1,49 @@ -# encoding: utf-8 - """Text-related objects such as TextFrame and Paragraph.""" -from pptx.compat import to_unicode +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 +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, lazyproperty, Pt +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 ```` element that can - appear as a child element of ````. Not intended to be constructed directly. + 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, parent): + 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): """ @@ -35,18 +54,18 @@ def add_paragraph(self): 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``. + 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): + def auto_size(self, value: MSO_AUTO_SIZE | None): self._bodyPr.autofit = value def clear(self): @@ -58,145 +77,126 @@ def clear(self): def fit_text( self, - font_family="Calibri", - max_size=18, - bold=False, - italic=False, - font_file=None, + 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). + 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 - ) + 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)``. + 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): + def margin_bottom(self, emu: Length): self._bodyPr.bIns = emu @property - def margin_left(self): - """ - Inset of text from left text frame border as |Length| value. - """ + 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): + def margin_left(self, emu: Length): self._bodyPr.lIns = emu @property - def margin_right(self): - """ - Inset of text from right text frame border as |Length| value. - """ + 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): + def margin_right(self, emu: Length): self._bodyPr.rIns = emu @property - def margin_top(self): - """ - Inset of text from top text frame border as |Length| value. - """ + 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): + def margin_top(self, emu: Length): 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. + 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): - """Unicode/str containing all text in this text-frame. + def text(self) -> str: + """All text in this text-frame as a single string. - 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. + 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. + 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. + 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). + 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): + def text(self, text: str): txBody = self._txBody txBody.clear_content() - for p_text in to_unicode(text).split("\n"): + for p_text in 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. + 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): + def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None): 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. + 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, @@ -205,7 +205,7 @@ def word_wrap(self): }[self._txBody.bodyPr.wrap] @word_wrap.setter - def word_wrap(self, value): + 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 @@ -216,7 +216,7 @@ def word_wrap(self, value): None: None, }[value] - def _apply_fit(self, font_family, font_size, is_bold, is_italic): + 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 @@ -226,49 +226,49 @@ def _apply_fit(self, font_family, font_size, is_bold, is_italic): 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*. + 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 - ) + 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. + 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 ( - self._parent.width - self.margin_left - self.margin_right, - self._parent.height - self.margin_top - self.margin_bottom, + Length(parent.width - self.margin_left - self.margin_right), + Length(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 _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): + 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, name, size, bold, italic): + 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 @@ -278,70 +278,63 @@ def set_rPr_font(rPr, name, 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. + """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): + def __init__(self, rPr: CT_TextCharacterProperties): 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. + 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): + def bold(self, value: bool | None): self._rPr.b = value @lazyproperty - def color(self): - """ - The |ColorFormat| instance that provides access to the color settings - for this font. - """ + 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| instance for this font, providing access to fill - properties such as fill color. + 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): - """ - Get or set boolean italic value of |Font| instance, with the same - behaviors as bold with respect to None values. + 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): + def italic(self, value: bool | None): 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`. + 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: @@ -349,19 +342,18 @@ def language_id(self): return self._rPr.lang @language_id.setter - def language_id(self, value): + 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): - """ - 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. + 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: @@ -369,28 +361,26 @@ def name(self): return latin.typeface @name.setter - def name(self, value): + def name(self, value: str | None): if value is None: - self._rPr._remove_latin() + self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage] 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 + 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 + >>> font.size.pt 24.0 """ sz = self._rPr.sz @@ -399,7 +389,7 @@ def size(self): return Centipoints(sz) @size.setter - def size(self, emu): + def size(self, emu: Length | None): if emu is None: self._rPr.sz = None else: @@ -407,16 +397,14 @@ def size(self, emu): 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. + 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: @@ -426,7 +414,7 @@ def underline(self): return u @underline.setter - def underline(self, value): + def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None): if value is True: value = MSO_UNDERLINE.SINGLE_LINE elif value is False: @@ -435,51 +423,51 @@ def underline(self, value): class _Hyperlink(Subshape): - """ - Text run hyperlink object. Corresponds to ```` child - element of the run's properties element (````). + """Text run hyperlink object. + + Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`). """ - def __init__(self, rPr, parent): + def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart): 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. + 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): + 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): + 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): + 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() + self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage] class _Paragraph(Subshape): """Paragraph object. Not intended to be constructed directly.""" - def __init__(self, p, parent): + def __init__(self, p: CT_TextParagraph, parent: ProvidesPart): super(_Paragraph, self).__init__(parent) self._element = self._p = p @@ -487,73 +475,67 @@ 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. - """ + 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): - """ - 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. + 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): + 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. + """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. + 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): - """ - 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. + 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): + def level(self, level: int): 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. + 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: @@ -561,27 +543,23 @@ def line_spacing(self): return pPr.line_spacing @line_spacing.setter - def line_spacing(self, value): + def line_spacing(self, value: int | float | Length | None): 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. - """ + 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| 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. + 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: @@ -589,19 +567,17 @@ def space_after(self): return pPr.space_after @space_after.setter - def space_after(self, value): + def space_after(self, value: Length | None): 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. + 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: @@ -609,88 +585,78 @@ def space_before(self): return pPr.space_before @space_before.setter - def space_before(self, value): + def space_before(self, value: Length | None): 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. + def text(self) -> str: + """Text of paragraph as a single string. - 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. + 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. - 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). + 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. - 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. + 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): + def text(self, text: str): self.clear() - self._element.append_text(to_unicode(text)) + self._element.append_text(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. + 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): - """ - The |CT_TextParagraphProperties| instance for this paragraph, the - element containing its paragraph properties. Causes the - element to be added if not present. + 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 ```` child element in a paragraph.""" + """Text run object. Corresponds to `a:r` child element in a paragraph.""" - def __init__(self, r, parent): + 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. + """|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. + 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) @@ -711,5 +677,5 @@ def text(self): return self._r.text @text.setter - def text(self, str): - self._r.text = to_unicode(str) + 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 index 5e5d92ecd..bbe8ac204 100644 --- a/src/pptx/util.py +++ b/src/pptx/util.py @@ -1,16 +1,15 @@ -# encoding: utf-8 - """Utility functions and classes.""" -from __future__ import division +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, Pt, and Px. Provides - properties for converting length values to convenient units. + """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 @@ -19,149 +18,124 @@ class Length(int): _EMUS_PER_MM = 36000 _EMUS_PER_PT = 12700 - def __new__(cls, emu): + def __new__(cls, emu: int): return int.__new__(cls, emu) @property - def inches(self): - """ - Floating point length in inches - """ + def inches(self) -> float: + """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. + 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): - """ - Floating point length in centimeters - """ + def cm(self) -> float: + """Floating point length in centimeters.""" return self / float(self._EMUS_PER_CM) @property - def emu(self): - """ - Integer length in English Metric Units - """ + def emu(self) -> int: + """Integer length in English Metric Units.""" return self @property - def mm(self): - """ - Floating point length in millimeters - """ + def mm(self) -> float: + """Floating point length in millimeters.""" return self / float(self._EMUS_PER_MM) @property - def pt(self): - """ - Floating point length in points - """ + 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 - """ + """Convenience constructor for length in inches.""" - def __new__(cls, 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 - """ + """Convenience constructor for length in hundredths of a point.""" - def __new__(cls, centipoints): + 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 - """ + """Convenience constructor for length in centimeters.""" - def __new__(cls, cm): + 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 - """ + """Convenience constructor for length in english metric units.""" - def __new__(cls, emu): + def __new__(cls, emu: int): return Length.__new__(cls, int(emu)) class Mm(Length): - """ - Convenience constructor for length in millimeters - """ + """Convenience constructor for length in millimeters.""" - def __new__(cls, mm): + 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 - """ + """Convenience value class for specifying a length in points.""" - def __new__(cls, points): + def __new__(cls, points: float): emu = int(points * Length._EMUS_PER_PT) return Length.__new__(cls, emu) -class lazyproperty(object): +_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. + 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 + 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:: + The parameter names in the methods below correspond to this usage example:: class Obj(object) @@ -171,68 +145,70 @@ def fget(self): obj = Obj() - Not suitable for wrapping a function (as opposed to a method) because it - is not callable. + Not suitable for wrapping a function (as opposed to a method) because it is not callable. """ - def __init__(self, fget): + 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. + 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 + # --- maintain a reference to the wrapped getter method self._fget = fget - # ---adopt fget's __name__, __doc__, and other attributes - functools.update_wrapper(self, 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, type=None): + 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). + *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`. + *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. + *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). + # --- 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 + return self # type: ignore - # ---when accessed on instance, start by checking instance __dict__ - value = obj.__dict__.get(self.__name__) + # --- 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, __dict__ item will absent. Evaluate fget() - # ---and store that value in the (otherwise unused) host-object - # ---__dict__ value of same name ('fget' nominally) + # --- 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 value + obj.__dict__[self._name] = value + return cast(_T, value) - def __set__(self, obj, 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. + 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") # pragma: no cover + raise AttributeError("can't set attribute") diff --git a/tests/chart/test_axis.py b/tests/chart/test_axis.py index aa0ce302f..9dbb50f51 100644 --- a/tests/chart/test_axis.py +++ b/tests/chart/test_axis.py @@ -1,25 +1,29 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Unit-test suite for pptx.chart.axis module.""" +"""Unit-test suite for `pptx.chart.axis` module.""" + +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 @@ -116,9 +120,7 @@ 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 - ): + 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) @@ -655,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): @@ -673,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): @@ -683,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): @@ -740,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"), @@ -822,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 ------------------------------------------------------- @@ -873,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 8b89c902a..667253347 100644 --- a/tests/chart/test_chart.py +++ b/tests/chart/test_chart.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.chart.chart` module.""" +from __future__ import annotations + import pytest from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis @@ -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): @@ -171,8 +171,7 @@ def cat_ax_raise_fixture(self): 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"), ( @@ -198,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) @@ -285,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) @@ -341,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): @@ -355,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): @@ -430,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): @@ -497,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"), @@ -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 10b325bf9..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.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`.""" - ChartXmlWriter_.assert_called_once_with(chart_type_, chart_data) - assert xml_bytes == expected_bytes + def it_can_generate_chart_part_XML_for_its_data(self, ChartXmlWriter_: Mock): + ChartXmlWriter_.return_value.xml = "ƒøØßår" + chart_data = _BaseChartData() + + 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): - return XL_CHART_TYPE.PIE - class Describe_BaseSeriesData(object): def it_knows_its_name(self, name_fixture): 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 ec96c9e02..dde9d4d53 100644 --- a/tests/chart/test_xlsx.py +++ b/tests/chart/test_xlsx.py @@ -1,9 +1,12 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.chart.xlsx` module.""" -import pytest +from __future__ import annotations + +import io +import pytest from xlsxwriter import Workbook from xlsxwriter.worksheet import Worksheet @@ -15,12 +18,11 @@ 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 @@ -31,9 +33,7 @@ class Describe_BaseWorkbookWriter(object): def it_can_generate_a_chart_data_Excel_blob( self, request, xlsx_file_, workbook_, worksheet_, BytesIO_ ): - _populate_worksheet_ = method_mock( - request, _BaseWorkbookWriter, "_populate_worksheet" - ) + _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_) @@ -44,9 +44,7 @@ def it_can_generate_a_chart_data_Excel_blob( 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_ - ) + _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): @@ -81,7 +79,7 @@ def populate_fixture(self): @pytest.fixture def BytesIO_(self, request): - return class_mock(request, "pptx.chart.xlsx.BytesIO") + return class_mock(request, "pptx.chart.xlsx.io.BytesIO") @pytest.fixture def Workbook_(self, request, workbook_): @@ -97,7 +95,7 @@ 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): @@ -207,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_ @@ -293,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)] @@ -330,21 +324,15 @@ 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): 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 2c2af4e03..defbaf980 100644 --- a/tests/dml/test_fill.py +++ b/tests/dml/test_fill.py @@ -1,14 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.dml.fill` module.""" +from __future__ import annotations + import pytest from pptx.dml.color import ColorFormat from pptx.dml.fill import ( + FillFormat, _BlipFill, _Fill, - FillFormat, _GradFill, _GradientStop, _GradientStops, @@ -94,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_) @@ -618,9 +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", autospec=False - ) + return method_mock(request, ColorFormat, "from_colorchoice_parent", autospec=False) @pytest.fixture def color_(self, request): @@ -662,9 +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", autospec=False - ) + 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 b33e6e094..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 @@ -75,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=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 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_ diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index f68c6857a..5dee9408b 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.opc.oxml` module.""" -from __future__ import unicode_literals +from __future__ import annotations + +from typing import cast import pytest @@ -13,141 +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 .unitdata.rels import ( - a_Default, - an_Override, - a_Relationship, - a_Relationships, - a_Types, -) +from ..unitutil.cxml import element -class DescribeCT_Default(object): +class DescribeCT_Default: """Unit-test suite for `pptx.opc.oxml.CT_Default` objects.""" 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(object): +class DescribeCT_Relationship: """Unit-test suite for `pptx.opc.oxml.CT_Relationship` objects.""" 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): +class DescribeCT_Types: """Unit-test suite for `pptx.opc.oxml.CT_Types` objects.""" - def it_provides_access_to_default_child_elements(self): - types = a_Types().element + 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(object): +class Describe_serialize_part_xml: """Unit-test suite for `pptx.opc.oxml.serialize_part_xml` function.""" - 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: --------------- @@ -156,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 d8bf20703..8c0e95809 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -1,18 +1,19 @@ -# 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.constants import ( - CONTENT_TYPE as CT, - RELATIONSHIP_TARGET_MODE as RTM, - RELATIONSHIP_TYPE as RT, -) +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, @@ -30,9 +31,11 @@ from pptx.parts.presentation import PresentationPart from ..unitutil.cxml import element -from ..unitutil.file import absjoin, snippet_bytes, testfile_bytes, 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, function_mock, @@ -43,7 +46,7 @@ ) -class Describe_RelatableMixin(object): +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 @@ -60,9 +63,7 @@ def it_can_find_a_part_related_by_reltype(self, _rels_prop_, relationships_, par 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_ - ): + 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() @@ -81,9 +82,7 @@ def and_it_can_establish_a_relationship_to_an_external_link( rId = mixin.relate_to("http://url", RT.HYPERLINK, is_external=True) - relationships_.get_or_add_ext_rel.assert_called_once_with( - RT.HYPERLINK, "http://url" - ) + 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( @@ -131,7 +130,7 @@ def _rels_prop_(self, request): return property_mock(request, _RelatableMixin, "_rels") -class DescribeOpcPackage(object): +class DescribeOpcPackage: """Unit-test suite for `pptx.opc.package.OpcPackage` objects.""" def it_can_open_a_pkg_file(self, request): @@ -153,13 +152,9 @@ def it_can_drop_a_relationship(self, _rels_prop_, relationships_): 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) - ] + 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 - ) + instance_mock(request, _Relationship, is_external=is_external, target_part=target) for is_external, target in ( (True, "http://some/url/"), (False, part_), @@ -187,9 +182,7 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): +--------> | part_1 | +--------+ """ - part_0_, part_1_ = [ - instance_mock(request, Part, name="part_%d" % i) for i in range(2) - ] + part_0_, part_1_ = [instance_mock(request, Part, name="part_%d" % i) for i in range(2)] all_rels = tuple( instance_mock( request, @@ -299,7 +292,7 @@ def _rels_prop_(self, request): return property_mock(request, OpcPackage, "_rels") -class Describe_PackageLoader(object): +class Describe_PackageLoader: """Unit-test suite for `pptx.opc.package._PackageLoader` objects.""" def it_provides_a_load_interface_classmethod(self, request, package_): @@ -328,10 +321,7 @@ def it_loads_the_package_to_help(self, request, _xml_rels_prop_): rels_ = dict( itertools.chain( (("/", instance_mock(request, _Relationships)),), - ( - ("partname_%d" % n, instance_mock(request, _Relationships)) - for n in range(1, 4) - ), + (("partname_%d" % n, instance_mock(request, _Relationships)) for n in range(1, 4)), ) ) _xml_rels_prop_.return_value = rels_ @@ -340,9 +330,7 @@ def it_loads_the_package_to_help(self, request, _xml_rels_prop_): 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_ - ) + part_.load_rels_from_xml.assert_called_once_with(rels_[part_.partname], parts_) assert pkg_xml_rels is rels_["/"] assert parts is parts_ @@ -397,7 +385,7 @@ 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, request, package_): @@ -420,19 +408,6 @@ def it_can_change_its_blob(self): def it_knows_its_content_type(self): assert Part(None, CT.PML_SLIDE, None).content_type == CT.PML_SLIDE - @pytest.mark.parametrize("ref_count, calls", ((2, []), (1, [call("rId42")]))) - def it_can_drop_a_relationship(self, request, relationships_, ref_count, calls): - _rel_ref_count_ = method_mock( - request, Part, "_rel_ref_count", return_value=ref_count - ) - property_mock(request, Part, "_rels", return_value=relationships_) - part = Part(None, None, None) - - part.drop_rel("rId42") - - _rel_ref_count_.assert_called_once_with(part, "rId42") - assert relationships_.pop.call_args_list == calls - def it_knows_the_package_it_belongs_to(self, package_): assert Part(None, None, package_).package is package_ @@ -444,9 +419,7 @@ def it_can_change_its_partname(self): 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_ - ): + 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_ @@ -484,16 +457,14 @@ def relationships_(self, request): return instance_mock(request, _Relationships) -class DescribeXmlPart(object): +class DescribeXmlPart: """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" 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_ - ) + parse_xml_ = function_mock(request, "pptx.opc.package.parse_xml", return_value=element_) _init_ = initializer_mock(request, XmlPart) part = XmlPart.load(partname, CT.PML_SLIDE, package_, b"blob") @@ -504,9 +475,7 @@ def it_can_be_constructed_by_PartFactory(self, request): def it_can_serialize_to_xml(self, request): element_ = element("p:sld") - serialize_part_xml_ = function_mock( - request, "pptx.opc.package.serialize_part_xml" - ) + serialize_part_xml_ = function_mock(request, "pptx.opc.package.serialize_part_xml") xml_part = XmlPart(None, None, None, element_) blob = xml_part.blob @@ -514,17 +483,34 @@ def it_can_serialize_to_xml(self, request): serialize_part_xml_.assert_called_once_with(element_) assert blob is serialize_part_xml_.return_value + @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) + + part.drop_rel("rId42") + + _rel_ref_count_.assert_called_once_with(part, "rId42") + assert relationships_.pop.call_args_list == calls + 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 + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def relationships_(self, request): + return instance_mock(request, _Relationships) + -class DescribePartFactory(object): +class DescribePartFactory: """Unit-test suite for `pptx.opc.package.PartFactory` objects.""" - def it_constructs_custom_part_type_for_registered_content_types( - self, request, package_, part_ - ): + 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") @@ -532,9 +518,7 @@ def it_constructs_custom_part_type_for_registered_content_types( part = PartFactory(partname, CT.PML_SLIDE, package_, b"blob") - SlidePart_.load.assert_called_once_with( - 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( @@ -546,9 +530,7 @@ def it_constructs_part_using_default_class_when_no_custom_registered( part = PartFactory(partname, CT.OFC_VML_DRAWING, package_, b"blob") - Part_.load.assert_called_once_with( - 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 ---------------------------------- @@ -562,7 +544,7 @@ def part_(self, request): return instance_mock(request, Part) -class Describe_ContentTypeMap(object): +class Describe_ContentTypeMap: """Unit-test suite for `pptx.opc.package._ContentTypeMap` objects.""" def it_can_construct_from_content_types_xml(self, request): @@ -617,8 +599,7 @@ 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"' + "\"no content-type for partname '/!blat/rhumba.1x&' " 'in [Content_Types].xml"' ) def it_raises_TypeError_on_key_not_instance_of_PackURI(self, content_type_map): @@ -630,12 +611,10 @@ def it_raises_TypeError_on_key_not_instance_of_PackURI(self, content_type_map): @pytest.fixture(scope="class") def content_type_map(self): - return _ContentTypeMap.from_xml( - testfile_bytes("expanded_pptx", "[Content_Types].xml") - ) + return _ContentTypeMap.from_xml(testfile_bytes("expanded_pptx", "[Content_Types].xml")) -class Describe_Relationships(object): +class Describe_Relationships: """Unit-test suite for `pptx.opc.package._Relationships` objects.""" @pytest.mark.parametrize("rId, expected_value", (("rId1", True), ("rId2", False))) @@ -655,9 +634,7 @@ def but_it_raises_KeyError_when_no_relationship_has_rId(self, _rels_prop_): _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_ - ): + 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) @@ -671,9 +648,7 @@ 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_ - ): + 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) @@ -684,9 +659,7 @@ def it_can_add_a_relationship_to_a_target_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_ - ): + def but_it_returns_an_existing_relationship_if_it_matches(self, part_, _get_matching_): _get_matching_.return_value = "rId3" relationships = _Relationships(None) @@ -695,9 +668,7 @@ def but_it_returns_an_existing_relationship_if_it_matches( _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_ - ): + 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) @@ -712,9 +683,7 @@ def it_can_add_an_external_relationship_to_a_URI( ) assert rId == "rId2" - def but_it_returns_an_existing_external_relationship_if_it_matches( - self, part_, _get_matching_ - ): + def but_it_returns_an_existing_external_relationship_if_it_matches(self, part_, _get_matching_): _get_matching_.return_value = "rId10" relationships = _Relationships(None) @@ -727,8 +696,7 @@ def but_it_returns_an_existing_external_relationship_if_it_matches( 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) + 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_} @@ -743,9 +711,7 @@ def it_can_load_from_the_xml_in_a_rels_part(self, request, _Relationship_, part_ ] assert relationships._rels == {"rId1": rels_[0], "rId2": rels_[1]} - def it_can_find_a_part_with_reltype( - self, _rels_by_reltype_prop_, relationship_, part_ - ): + 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_]),) @@ -852,9 +818,7 @@ def and_it_can_add_an_external_relationship_to_help( _rels_prop_.return_value = {} relationships = _Relationships("/ppt") - rId = relationships._add_relationship( - RT.HYPERLINK, "http://url", is_external=True - ) + 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" @@ -894,18 +858,14 @@ def it_can_get_a_matching_relationship_to_help( ) ] } - target = ( - target_ref if is_external else part_1 if target_ref == "part_1" else part_2 - ) + 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_ - ): + 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) @@ -979,7 +939,7 @@ def _rels_prop_(self, request): return property_mock(request, _Relationships, "_rels") -class Describe_Relationship(object): +class Describe_Relationship: """Unit-test suite for `pptx.opc.package._Relationship` objects.""" def it_can_construct_from_xml(self, request, part_): @@ -996,9 +956,7 @@ def it_can_construct_from_xml(self, request, part_): relationship = _Relationship.from_xml("/ppt", rel_elm, parts) - _init_.assert_called_once_with( - relationship, "/ppt", "rId42", RT.SLIDE, RTM.INTERNAL, part_ - ) + _init_.assert_called_once_with(relationship, "/ppt", "rId42", RT.SLIDE, RTM.INTERNAL, part_) assert isinstance(relationship, _Relationship) @pytest.mark.parametrize( @@ -1026,8 +984,7 @@ def but_it_raises_ValueError_on_target_part_for_external_rel(self): 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" + "`.target_part` property on _Relationship is undefined when " "target-mode is external" ) def it_knows_its_target_partname(self, part_): diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index f77ea68f5..5b7e64a2f 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for the `pptx.opc.packuri` module.""" +from __future__ import annotations + import pytest from pptx.opc.packuri import PackURI @@ -11,9 +11,7 @@ class DescribePackURI(object): """Unit-test suite for the `pptx.opc.packuri.PackURI` objects.""" def it_can_construct_from_relative_ref(self): - pack_uri = PackURI.from_rel_ref( - "/ppt/slides", "../slideLayouts/slideLayout1.xml" - ) + 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): @@ -21,53 +19,53 @@ def it_should_raise_on_construct_with_bad_pack_uri_str(self): PackURI("foobar") @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", "/"), ("/ppt/presentation.xml", "/ppt"), ("/ppt/slides/slide1.xml", "/ppt/slides"), - ), + ], ) - def it_knows_its_base_URI(self, uri, expected_value): + def it_knows_its_base_URI(self, uri: str, expected_value: str): assert PackURI(uri).baseURI == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", ""), ("/ppt/presentation.xml", "xml"), ("/ppt/media/image.PnG", "PnG"), - ), + ], ) - def it_knows_its_extension(self, uri, expected_value): + def it_knows_its_extension(self, uri: str, expected_value: str): assert PackURI(uri).ext == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", ""), ("/ppt/presentation.xml", "presentation.xml"), ("/ppt/media/image.png", "image.png"), - ), + ], ) - def it_knows_its_filename(self, uri, expected_value): + def it_knows_its_filename(self, uri: str, expected_value: str): assert PackURI(uri).filename == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("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, expected_value): + def it_knows_the_filename_index(self, uri: str, expected_value: str): assert PackURI(uri).idx == expected_value @pytest.mark.parametrize( - "uri, base_uri, expected_value", - ( + ("uri", "base_uri", "expected_value"), + [ ("/ppt/presentation.xml", "/", "ppt/presentation.xml"), ( "/ppt/slideMasters/slideMaster1.xml", @@ -79,18 +77,18 @@ def it_knows_the_filename_index(self, uri, expected_value): "/ppt/slides", "../slideLayouts/slideLayout1.xml", ), - ), + ], ) - def it_can_compute_its_relative_reference(self, uri, base_uri, expected_value): + 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 @pytest.mark.parametrize( - "uri, expected_value", - ( + ("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, expected_value): + 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_serialized.py b/tests/opc/test_serialized.py index 31c965904..d5b867c4e 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -1,13 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.opc.serialized` module.""" +from __future__ import annotations + import hashlib -import pytest +import io import zipfile -from pptx.compat import BytesIO -from pptx.exceptions import PackageNotFoundError +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 @@ -25,6 +28,8 @@ from ..unitutil.file import absjoin, snippet_text, test_file_dir from ..unitutil.mock import ( ANY, + FixtureRequest, + Mock, call, class_mock, function_mock, @@ -34,41 +39,40 @@ 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(object): +class DescribePackageReader: """Unit-test suite for `pptx.opc.serialized.PackageReader` objects.""" - def it_knows_whether_it_contains_a_partname(self, _blob_reader_prop_): - _blob_reader_prop_.return_value = set(("/ppt", "/docProps")) - package_reader = PackageReader(None) + 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_): + 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(None) + package_reader = PackageReader("") - assert package_reader["/ppt/slides/slide1.xml"] == b"blob" + assert package_reader[PackURI("/ppt/slides/slide1.xml")] == b"blob" - def it_can_get_the_rels_xml_for_a_partname(self, _blob_reader_prop_): + 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(None) + 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_): + 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(None) + 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): + 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_ @@ -82,25 +86,27 @@ def it_constructs_its_blob_reader_to_help(self, request): # fixture components ----------------------------------- @pytest.fixture - def _blob_reader_prop_(self, request): + def _blob_reader_prop_(self, request: FixtureRequest): return property_mock(request, PackageReader, "_blob_reader") -class DescribePackageWriter(object): +class DescribePackageWriter: """Unit-test suite for `pptx.opc.serialized.PackageWriter` objects.""" - def it_provides_a_write_interface_classmethod(self, request, relationships_): + 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_1", "part_2")) + PackageWriter.write("prs.pptx", relationships_, (part_, part_)) - _init_.assert_called_once_with( - ANY, "prs.pptx", relationships_, ("part_1", "part_2") - ) + _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, phys_writer_): + 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_ @@ -109,35 +115,35 @@ def it_can_write_a_package(self, request, phys_writer_): ) _write_pkg_rels_ = method_mock(request, PackageWriter, "_write_pkg_rels") _write_parts_ = method_mock(request, PackageWriter, "_write_parts") - package_writer = PackageWriter("prs.pptx", None, None) + 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_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, phys_writer_): - _ContentTypesItem_ = class_mock( - request, "pptx.opc.serialized._ContentTypesItem" - ) + 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(None, None, ("part_1", "part_2")) + package_writer = PackageWriter("", relationships_, (part_, part_)) package_writer._write_content_types_stream(phys_writer_) - _ContentTypesItem_.xml_for.assert_called_once_with(("part_1", "part_2")) + _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, phys_writer_): - parts_ = ( + def it_can_write_a_sequence_of_parts( + self, request: FixtureRequest, relationships_: Mock, phys_writer_: Mock + ): + parts_ = [ instance_mock( request, Part, @@ -146,8 +152,8 @@ def it_can_write_a_sequence_of_parts(self, request, phys_writer_): rels=instance_mock(request, _Relationships, xml="rels_xml_%s" % x), ) for x in ("a", "b", "c") - ) - package_writer = PackageWriter(None, None, parts_) + ] + package_writer = PackageWriter("", relationships_, parts_) package_writer._write_parts(phys_writer_) @@ -160,40 +166,44 @@ def it_can_write_a_sequence_of_parts(self, request, phys_writer_): call("/ppt/_rels/c.xml.rels", "rels_xml_c"), ] - def it_can_write_a_pkg_rels_item(self, request, phys_writer_, relationships_): + def it_can_write_a_pkg_rels_item(self, phys_writer_: Mock, relationships_: Mock): relationships_.xml = b"pkg-rels-xml" - package_writer = PackageWriter(None, relationships_, None) + package_writer = PackageWriter("", relationships_, []) package_writer._write_pkg_rels(phys_writer_) phys_writer_.write.assert_called_once_with("/_rels/.rels", b"pkg-rels-xml") - # fixture components ----------------------------------- + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) @pytest.fixture - def phys_writer_(self, request): + def phys_writer_(self, request: FixtureRequest): return instance_mock(request, _ZipPkgWriter) @pytest.fixture - def relationships_(self, request): + def relationships_(self, request: FixtureRequest): return instance_mock(request, _Relationships) -class Describe_PhysPkgReader(object): +class Describe_PhysPkgReader: """Unit-test suite for `pptx.opc.serialized._PhysPkgReader` objects.""" def it_constructs_ZipPkgReader_when_pkg_is_file_like( - self, _ZipPkgReader_, zip_pkg_reader_ + self, _ZipPkgReader_: Mock, zip_pkg_reader_: Mock ): _ZipPkgReader_.return_value = zip_pkg_reader_ - file_like_pkg = BytesIO(b"pkg-bytes") + 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): + 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_ @@ -205,7 +215,7 @@ def and_it_constructs_DirPkgReader_when_pkg_is_a_dir(self, request): assert phys_reader is dir_pkg_reader_ def and_it_constructs_ZipPkgReader_when_pkg_is_a_zip_file_path( - self, _ZipPkgReader_, zip_pkg_reader_ + self, _ZipPkgReader_: Mock, zip_pkg_reader_: Mock ): _ZipPkgReader_.return_value = zip_pkg_reader_ pkg_file_path = test_pptx_path @@ -223,29 +233,27 @@ def but_it_raises_when_pkg_path_is_not_a_package(self): # --- fixture components ------------------------------- @pytest.fixture - def zip_pkg_reader_(self, request): + def zip_pkg_reader_(self, request: FixtureRequest): return instance_mock(request, _ZipPkgReader) @pytest.fixture - def _ZipPkgReader_(self, request): + def _ZipPkgReader_(self, request: FixtureRequest): return class_mock(request, "pptx.opc.serialized._ZipPkgReader") -class Describe_DirPkgReader(object): +class Describe_DirPkgReader: """Unit-test suite for `pptx.opc.serialized._DirPkgReader` objects.""" - def it_knows_whether_it_contains_a_partname(self, dir_pkg_reader): + 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): + 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" - ) + assert hashlib.sha1(blob).hexdigest() == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" def but_it_raises_KeyError_when_requested_member_is_not_present( - self, dir_pkg_reader + self, dir_pkg_reader: _DirPkgReader ): with pytest.raises(KeyError) as e: dir_pkg_reader[PackURI("/ppt/foobar.xml")] @@ -254,31 +262,29 @@ def but_it_raises_KeyError_when_requested_member_is_not_present( # --- fixture components ------------------------------- @pytest.fixture(scope="class") - def dir_pkg_reader(self, request): + def dir_pkg_reader(self): return _DirPkgReader(dir_pkg_path) -class Describe_ZipPkgReader(object): +class Describe_ZipPkgReader: """Unit-test suite for `pptx.opc.serialized._ZipPkgReader` objects.""" - def it_knows_whether_it_contains_a_partname(self, zip_pkg_reader): + 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): + 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" - ) + assert hashlib.sha1(blob).hexdigest() == ("efa7bee0ac72464903a67a6744c1169035d52a54") def but_it_raises_KeyError_when_requested_member_is_not_present( - self, zip_pkg_reader + 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): + 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 @@ -287,14 +293,14 @@ def it_loads_the_package_blobs_on_first_access_to_help(self, zip_pkg_reader): # --- fixture components ------------------------------- @pytest.fixture(scope="class") - def zip_pkg_reader(self, request): + def zip_pkg_reader(self): return _ZipPkgReader(zip_pkg_path) -class Describe_PhysPkgWriter(object): +class Describe_PhysPkgWriter: """Unit-test suite for `pptx.opc.serialized._PhysPkgWriter` objects.""" - def it_constructs_ZipPkgWriter_unconditionally(self, request): + 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_ @@ -306,22 +312,22 @@ def it_constructs_ZipPkgWriter_unconditionally(self, request): assert phys_writer is zip_pkg_writer_ -class Describe_ZipPkgWriter(object): +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(None) + pkg_writer = _ZipPkgWriter("") assert pkg_writer.__enter__() is pkg_writer - def and_it_closes_the_zip_archive_on_context__exit__(self, _zipf_prop_): - _ZipPkgWriter(None).__exit__(None, None, None) + 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_): + 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(BytesIO(), "w") - pkg_writer = _ZipPkgWriter(None) + _zipf_prop_.return_value = zipf = zipfile.ZipFile(io.BytesIO(), "w") + pkg_writer = _ZipPkgWriter("") pkg_writer.write(pack_uri, b"blob") @@ -329,37 +335,35 @@ def it_can_write_a_blob(self, _zipf_prop_): assert len(members) == 1 assert members[pack_uri] == b"blob" - def it_provides_access_to_the_open_zip_file_to_help(self, request): + 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 - ) + ZipFile_.assert_called_once_with("prs.pptx", "w", compression=zipfile.ZIP_DEFLATED) assert zipf is ZipFile_.return_value # fixtures --------------------------------------------- @pytest.fixture - def _zipf_prop_(self, request): + def _zipf_prop_(self, request: FixtureRequest): return property_mock(request, _ZipPkgWriter, "_zipf") -class Describe_ContentTypesItem(object): +class Describe_ContentTypesItem: """Unit-test suite for `pptx.opc.serialized._ContentTypesItem` objects.""" - def it_provides_an_interface_classmethod(self, request): + 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", "zuh")) + xml = _ContentTypesItem.xml_for((part_, part_)) - _init_.assert_called_once_with(ANY, ("part", "zuh")) + _init_.assert_called_once_with(ANY, (part_, part_)) assert xml == b"xml" - def it_can_compose_content_types_xml(self, request): + 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", @@ -373,22 +377,20 @@ def it_can_compose_content_types_xml(self, request): return_value=(defaults, overrides), ) - content_types = _ContentTypesItem(None)._xml + content_types = _ContentTypesItem([])._xml assert content_types.xml == snippet_text("content-types-xml").strip() - def it_computes_defaults_and_overrides_to_help(self, request): - parts = ( - instance_mock( - request, Part, partname=PackURI(partname), content_type=content_type - ) + 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 @@ -398,3 +400,9 @@ def it_computes_defaults_and_overrides_to_help(self, request): "/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/__init__.py b/tests/opc/unitdata/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py deleted file mode 100644 index 2fd0aa947..000000000 --- a/tests/opc/unitdata/rels.py +++ /dev/null @@ -1,261 +0,0 @@ -# encoding: utf-8 - -"""Test data for relationship-related unit tests.""" - -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 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_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 66025261f..6884b06cd 100644 --- a/tests/oxml/shapes/test_groupshape.py +++ b/tests/oxml/shapes/test_groupshape.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.shapes.groupshape` module.""" +from __future__ import annotations + import pytest from pptx.oxml.shapes.autoshape import CT_Shape @@ -23,12 +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( - spTree, 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): @@ -55,9 +51,7 @@ def it_can_add_an_sp_element_for_a_placeholder(self, add_placeholder_fixt): 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 - ) + 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_ @@ -67,9 +61,7 @@ def it_can_add_an_sp_element_for_an_autoshape(self, add_autoshape_fixt): 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 - ) + 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_ diff --git a/tests/oxml/shapes/test_picture.py b/tests/oxml/shapes/test_picture.py index 9f16599ba..546d6b0fd 100644 --- a/tests/oxml/shapes/test_picture.py +++ b/tests/oxml/shapes/test_picture.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.shapes.picture` module.""" +from __future__ import annotations + import pytest from pptx.oxml.ns import nsdecls 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..1607ab5cc 100644 --- a/tests/oxml/test_presentation.py +++ b/tests/oxml/test_presentation.py @@ -1,46 +1,40 @@ -# 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 diff --git a/tests/oxml/test_simpletypes.py b/tests/oxml/test_simpletypes.py index e1a98b2ef..261edf550 100644 --- a/tests/oxml/test_simpletypes.py +++ b/tests/oxml/test_simpletypes.py @@ -1,14 +1,20 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.simpletypes` module. -`simpletypes` contains simple type class definitions. A simple type in this context -corresponds to an `` e.g. `ST_Foobar` definition in the XML schema and -provides data validation and type conversion services for use by xmlchemy. A simple-type -generally corresponds to an element attribute whereas a complex type corresponds to an -XML element (which itself can have multiple attributes and have child elements). +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 annotations + import pytest from pptx.oxml.simpletypes import ( @@ -19,13 +25,13 @@ ST_Percentage, ) -from ..unitutil.mock import method_mock, instance_mock +from ..unitutil.mock import instance_mock, method_mock class DescribeBaseSimpleType(object): """Unit-test suite for `pptx.oxml.simpletypes.BaseSimpleType` objects.""" - def it_can_convert_attr_value_to_python_type( + 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_) diff --git a/tests/oxml/test_slide.py b/tests/oxml/test_slide.py index d1b48ebc4..63b321da7 100644 --- a/tests/oxml/test_slide.py +++ b/tests/oxml/test_slide.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.slide` module.""" +from __future__ import annotations + from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide from ..unitutil.file import snippet_text 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 560657e8a..a5a39360a 100644 --- a/tests/oxml/unitdata/shape.py +++ b/tests/oxml/unitdata/shape.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Test data for autoshape-related unit tests.""" +from __future__ import annotations + from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/text.py b/tests/oxml/unitdata/text.py index 23753fdc8..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 diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index ca7fe7771..b0a41f581 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -1,13 +1,14 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.chart` module.""" +from __future__ import annotations + import pytest from pptx.chart.chart import Chart from pptx.chart.data import ChartData from pptx.enum.chart import XL_CHART_TYPE as XCT -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 OpcPackage from pptx.opc.packuri import PackURI from pptx.oxml.chart.chart import CT_ChartSpace @@ -28,9 +29,7 @@ def it_can_construct_from_chart_type_and_data(self, request): 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_ - ) + load_ = method_mock(request, ChartPart, "load", autospec=False, return_value=chart_part_) chart_part = ChartPart.new(XCT.RADAR, chart_data_, package_) @@ -39,9 +38,7 @@ def it_can_construct_from_chart_type_and_data(self, request): 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" - ) + 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, request, chartSpace_): @@ -129,9 +126,7 @@ def it_adds_an_xlsx_part_on_update_if_needed( 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_ - ): + 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") diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 3f20ca933..0983218e4 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Unit-test suite for `pptx.parts.coreprops` module.""" from __future__ import annotations @@ -14,68 +16,71 @@ class DescribeCorePropertiesPart(object): """Unit-test suite for `pptx.parts.coreprops.CorePropertiesPart` objects.""" - 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 - - 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(None) - # 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 = ( - 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 - # fixtures ------------------------------------------------------- + assert core_properties._element.xml == self.coreProperties_xml(tagname, value) - @pytest.fixture( - params=[ + @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", @@ -97,75 +102,59 @@ def date_prop_get_fixture(self, request, core_properties): "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, None, coreProperties) - 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"), - ] + 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 str_prop_set_fixture(self, request): - prop_name, tagname, value = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, None, coreProperties) - 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)]) - 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, None, coreProperties) - 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 + + 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 - @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, None, coreProperties) - expected_xml = self.coreProperties("cp:revision", str_val) - return core_properties, value, expected_xml + 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, None, xml) + return CorePropertiesPart.load(None, None, None, xml) # type: ignore diff --git a/tests/parts/test_embeddedpackage.py b/tests/parts/test_embeddedpackage.py index 1f368d557..ae2aca82f 100644 --- a/tests/parts/test_embeddedpackage.py +++ b/tests/parts/test_embeddedpackage.py @@ -1,35 +1,35 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.embeddedpackage` module.""" +from __future__ import annotations + import pytest from pptx.enum.shapes import PROG_ID 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, package_, object_blob_ - ) + 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,9 +67,7 @@ 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( xlsx_part, partname_, EmbeddedXlsxPart.content_type, package_, blob_ ) diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 8e1f68274..386e3fce9 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -1,10 +1,11 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.image` module.""" +from __future__ import annotations + +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 @@ -18,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") @@ -67,9 +67,7 @@ def it_provides_access_to_its_image(self, request, image_): (3337, 9999, 3337, 9999), ), ) - def it_can_scale_its_dimensions( - self, width, height, expected_width, expected_height - ): + 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_part = ImagePart(None, None, None, blob) @@ -211,7 +209,7 @@ def filename_fixture(self, request): 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_ diff --git a/tests/parts/test_media.py b/tests/parts/test_media.py index f183d7c47..f7095f35d 100644 --- a/tests/parts/test_media.py +++ b/tests/parts/test_media.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit test suite for `pptx.parts.media` module.""" +from __future__ import annotations + from pptx.media import Video from pptx.package import Package from pptx.parts.media import MediaPart diff --git a/tests/parts/test_presentation.py b/tests/parts/test_presentation.py index 7089e73de..edde4c44c 100644 --- a/tests/parts/test_presentation.py +++ b/tests/parts/test_presentation.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.presentation` module.""" +from __future__ import annotations + import pytest from pptx.opc.constants import RELATIONSHIP_TYPE as RT @@ -62,9 +62,7 @@ def but_it_adds_a_notes_master_part_when_needed( The notes master present case is just above. """ - NotesMasterPart_ = class_mock( - request, "pptx.parts.presentation.NotesMasterPart" - ) + 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) @@ -72,9 +70,7 @@ def but_it_adds_a_notes_master_part_when_needed( notes_master_part = prs_part.notes_master_part NotesMasterPart_.create_default.assert_called_once_with(package_) - 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, request, notes_master_part_): @@ -100,12 +96,8 @@ def it_provides_access_to_a_related_slide(self, request, slide_, related_part_): related_part_.assert_called_once_with(prs_part, "rId42") assert slide is slide_ - 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_ - ) + 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) @@ -131,14 +123,10 @@ 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") - def it_can_add_a_new_slide( - self, request, package_, slide_part_, slide_, relate_to_ - ): + 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 - ) + 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 = "rId42" @@ -181,9 +169,7 @@ def it_raises_on_slide_id_not_found(self, slide_part_, related_part_): 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_ - ): + 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})" diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index 58929d124..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.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 @@ -44,9 +45,7 @@ class DescribeBaseSlidePart(object): """Unit-test suite for `pptx.parts.slide.BaseSlidePart` objects.""" def it_knows_its_name(self): - slide_part = BaseSlidePart( - None, None, None, element("p:sld/p:cSld{name=Foobar}") - ) + slide_part = BaseSlidePart(None, None, None, element("p:sld/p:cSld{name=Foobar}")) assert slide_part.name == "Foobar" def it_can_get_a_related_image_by_rId(self, request, image_part_): @@ -65,9 +64,7 @@ def it_can_get_a_related_image_by_rId(self, request, image_part_): 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_ = method_mock( - request, BaseSlidePart, "relate_to", return_value="rId6" - ) + relate_to_ = method_mock(request, BaseSlidePart, "relate_to", return_value="rId6") slide_part = BaseSlidePart(None, None, package_, None) image_part, rId = slide_part.get_or_add_image_part("foobar.png") @@ -87,9 +84,7 @@ def image_part_(self, request): class DescribeNotesMasterPart(object): """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_ - ): + def it_can_create_a_notes_master_part(self, request, package_, notes_master_part_, theme_part_): method_mock( request, NotesMasterPart, @@ -124,9 +119,7 @@ def it_provides_access_to_its_notes_master(self, request): 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, request, package_, notes_master_part_ - ): + 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_ ) @@ -151,9 +144,7 @@ def it_creates_a_new_notes_master_part_to_help( assert notes_master_part is notes_master_part_ 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_ - ) + XmlPart_ = class_mock(request, "pptx.parts.slide.XmlPart", return_value=theme_part_) theme_elm = element("p:theme") method_mock( request, @@ -216,15 +207,11 @@ def it_can_create_a_notes_slide_part( notes_slide_part = NotesSlidePart.new(package_, slide_part_) - _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, request, notes_master_, notes_master_part_ - ): + 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_ ) @@ -237,9 +224,7 @@ def it_provides_access_to_the_notes_master( assert notes_master is notes_master_ def it_provides_access_to_its_notes_slide(self, request, notes_slide_): - NotesSlide_ = class_mock( - request, "pptx.parts.slide.NotesSlide", return_value=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) @@ -255,20 +240,14 @@ def it_adds_a_notes_slide_part_to_help( 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" - ) + 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" - ) + 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"), @@ -354,9 +333,7 @@ def it_can_add_an_embedded_ole_object_part( 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, package_, None) @@ -364,9 +341,7 @@ def it_can_add_an_embedded_ole_object_part( _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" @@ -394,9 +369,7 @@ def it_can_create_a_new_slide_part(self, request, package_, relate_to_): slide_part = SlidePart.new(partname, package_, slide_layout_part_) - _init_.assert_called_once_with( - slide_part, partname, CT.PML_SLIDE, package_, sld - ) + _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 ) @@ -559,9 +532,7 @@ class DescribeSlideLayoutPart(object): 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_ - ) + 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_ ) @@ -604,9 +575,7 @@ def it_provides_access_to_its_slide_master(self, request): 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_ - ) + slide_layout_part_ = instance_mock(request, SlideLayoutPart, slide_layout=slide_layout_) related_part_ = method_mock( request, SlideMasterPart, "related_part", return_value=slide_layout_part_ ) diff --git a/tests/shapes/test_autoshape.py b/tests/shapes/test_autoshape.py index 9e6173caf..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_ @@ -187,41 +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", ()), @@ -261,8 +259,26 @@ def it_xml_escapes_the_basename_when_the_name_contains_special_characters(self): assert autoshape_type.prst == "noSmoking" assert autoshape_type.basename == ""No" Symbol" - def it_knows_the_default_adj_vals_for_its_autoshape_type(self, default_adj_vals_fixture_): - prst, default_adj_vals = default_adj_vals_fixture_ + @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, prst: MSO_SHAPE, default_adj_vals: tuple[AdjustmentValue, ...] + ): _default_adj_vals = AutoShapeType.default_adjustment_values(prst) assert _default_adj_vals == default_adj_vals @@ -270,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(ValueError): + 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): @@ -283,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.""" diff --git a/tests/shapes/test_base.py b/tests/shapes/test_base.py index 8182323de..89632ca80 100644 --- a/tests/shapes/test_base.py +++ b/tests/shapes/test_base.py @@ -1,7 +1,11 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.shapes.base` module.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + import pytest from pptx.action import ActionSetting @@ -34,6 +38,11 @@ from ..unitutil.cxml import element, xml 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.""" @@ -57,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 @@ -272,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"), @@ -363,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): @@ -381,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 @@ -393,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 @@ -442,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 @@ -466,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 @@ -478,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 @@ -536,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 @@ -548,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 26ded32e2..dd5f53f0d 100644 --- a/tests/shapes/test_freeform.py +++ b/tests/shapes/test_freeform.py @@ -1,22 +1,27 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.shapes.freeform` module""" +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,23 +33,20 @@ class DescribeFreeformBuilder(object): """Unit-test suite for `pptx.shapes.freeform.FreeformBuilder` objects.""" - 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:] + 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) - @pytest.mark.parametrize("close", (True, False)) - def it_can_add_straight_line_segments(self, request, close): + @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) + builder = FreeformBuilder(None, None, None, None, None) # type: ignore return_value = builder.add_line_segments(((1, 2), (3, 4), (5, 6)), close) @@ -56,8 +58,10 @@ def it_can_add_straight_line_segments(self, request, close): 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) @@ -65,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) + + assert builder.shape_offset_x == expected_value - 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 + @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, sp_fixture): - builder, origin_x, origin_y, spTree, expected_xml = sp_fixture + 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() @@ -126,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 @@ -141,7 +205,9 @@ 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, request, _dx_prop_, _dy_prop_): + 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) ) @@ -154,8 +220,7 @@ def it_can_start_a_new_path_to_help(self, request, _dx_prop_, _dy_prop_): _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}" + "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] @@ -166,39 +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 - 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), @@ -206,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: @@ -226,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: @@ -239,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 @@ -256,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 @@ -266,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) @@ -340,10 +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( - 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 @@ -351,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 @@ -361,85 +322,83 @@ def width_fixture(self, request, _dx_prop_): # fixture components ----------------------------------- @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 apply_operation_to_(self, request): - return method_mock( - request, _BaseDrawingOperation, "apply_operation_to", autospec=True - ) + def apply_operation_to_(self, request: FixtureRequest): + return method_mock(request, _LineSegment, "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): + 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): + 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): + 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") @@ -508,7 +467,7 @@ 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) @@ -551,11 +510,11 @@ 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) @@ -598,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 5f2250111..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 @@ -62,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( @@ -76,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): @@ -127,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 ) 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 75b0814ca..4d9b26ea0 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.shapes.placeholder` module.""" +from __future__ import annotations + import pytest from pptx.chart.data import ChartData @@ -12,9 +12,7 @@ from pptx.parts.slide import NotesSlidePart, SlidePart from pptx.shapes.placeholder import ( BasePlaceholder, - _BaseSlidePlaceholder, ChartPlaceholder, - _InheritsDimensions, LayoutPlaceholder, MasterPlaceholder, NotesSlidePlaceholder, @@ -22,6 +20,8 @@ PlaceholderGraphicFrame, PlaceholderPicture, TablePlaceholder, + _BaseSlidePlaceholder, + _InheritsDimensions, ) from pptx.shapes.shapetree import NotesSlidePlaceholders from pptx.slide import NotesMaster, SlideLayout, SlideMaster @@ -151,9 +151,7 @@ def layout_placeholder_(self, request): @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): @@ -205,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() @@ -279,9 +275,7 @@ 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): @@ -439,9 +433,7 @@ 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): @@ -482,10 +474,7 @@ def it_creates_a_pic_element_to_help(self, request, image_size, crop_attr_names) 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})" - ), + element("p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" ",cy=99})"), None, ) diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 63c1ee290..3cf1ab225 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -1,18 +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.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 @@ -23,32 +26,32 @@ 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 @@ -218,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, ), ] @@ -244,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 @@ -264,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, ), ] @@ -319,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) @@ -347,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): @@ -622,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 @@ -766,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") @@ -782,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): @@ -792,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): @@ -854,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): @@ -1260,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 @@ -1419,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): @@ -1554,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): @@ -1679,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): @@ -1848,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] @@ -1886,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 ( @@ -1917,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 ) @@ -2021,10 +1995,6 @@ 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", autospec=False) @@ -2047,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): @@ -2077,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) @@ -2145,18 +2107,10 @@ def it_provides_a_graphicFrame_interface_method(self, request, shapes_): 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( @@ -2248,7 +2202,10 @@ def it_determines_the_shape_height_to_help(self, cy_arg, prog_id, expected_value @pytest.mark.parametrize( "icon_height_arg, expected_value", - ((Emu(666666), Emu(666666)), (None, Emu(609600)),), + ( + (Emu(666666), Emu(666666)), + (None, Emu(609600)), + ), ) def it_determines_the_icon_height_to_help(self, icon_height_arg, expected_value): element_creator = _OleObjectElementCreator( @@ -2266,9 +2223,7 @@ def it_determines_the_icon_height_to_help(self, icon_height_arg, 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 ) diff --git a/tests/test_action.py b/tests/test_action.py index 33877eeae..dd0193ca6 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -1,15 +1,13 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.action` module.""" -from __future__ import 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 Part +from pptx.opc.package import XmlPart from pptx.parts.slide import SlidePart from pptx.slide import Slide @@ -51,8 +49,7 @@ def it_can_change_its_slide_jump_target( _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}", + "p:cNvPr{a:b=c,r:s=t}/a:hlinkClick{action=ppaction://hlinksldjump,r:id=rI" "d42}", ) def but_it_clears_the_target_slide_if_None_is_assigned(self, _clear_click_action_): @@ -209,9 +206,7 @@ def target_get_fixture(self, request, action_prop_, _slide_index_prop_, part_pro return action_setting, expected_value @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 @@ -240,7 +235,7 @@ def hyperlink_(self, request): @pytest.fixture def part_(self, request): - return instance_mock(request, Part) + return instance_mock(request, XmlPart) @pytest.fixture def part_prop_(self, request): diff --git a/tests/test_api.py b/tests/test_api.py index b44573031..a48f48912 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,10 +1,6 @@ -# 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 diff --git a/tests/test_media.py b/tests/test_media.py index 9e42db9e5..be72f6e0e 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -1,16 +1,16 @@ -# encoding: utf-8 - """Unit test suite for `pptx.media` module.""" +from __future__ import annotations + +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") @@ -87,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) @@ -105,7 +103,7 @@ def from_blob_fixture(self, Video_init_): 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_ diff --git a/tests/test_package.py b/tests/test_package.py index 5e32e74ce..ee02af2d6 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.package` module.""" +from __future__ import annotations + import os import pytest @@ -11,12 +13,11 @@ 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 @@ -159,9 +160,7 @@ 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, Image_, image_, image_part_, _find_by_sha1_ - ): + 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) 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 e2d6bdc01..72a0ebc0e 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.shared` module.""" +from __future__ import annotations + import pytest from pptx.opc.package import XmlPart diff --git a/tests/test_slide.py b/tests/test_slide.py index d4a1bdeef..74b528c3b 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.slide` module.""" +from __future__ import annotations + import pytest from pptx.dml.fill import FillFormat @@ -23,9 +25,6 @@ SlideShapes, ) from pptx.slide import ( - _Background, - _BaseMaster, - _BaseSlide, NotesMaster, NotesSlide, Slide, @@ -34,6 +33,9 @@ SlideMaster, SlideMasters, Slides, + _Background, + _BaseMaster, + _BaseSlide, ) from pptx.text.text import TextFrame @@ -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) @@ -149,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_): @@ -169,9 +167,7 @@ def shapes_(self, request): class DescribeNotesSlide(object): """Unit-test suite for `pptx.slide.NotesSlide` objects.""" - def it_can_clone_the_notes_master_placeholders( - self, request, notes_master_, shapes_ - ): + 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), @@ -233,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: @@ -269,15 +263,11 @@ def notes_master_(self, request): @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_): @@ -293,9 +283,7 @@ 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): @@ -436,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_): @@ -616,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) @@ -675,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] @@ -702,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_): @@ -716,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): @@ -734,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): @@ -775,9 +751,7 @@ def it_supports_len(self, 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), @@ -795,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] @@ -805,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) @@ -871,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) @@ -964,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): @@ -1001,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" @@ -1013,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")] diff --git a/tests/test_table.py b/tests/test_table.py index 1207ff275..c53f1261f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.table` module.""" +from __future__ import annotations + import pytest from pptx.dml.fill import FillFormat @@ -10,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 @@ -68,9 +70,7 @@ def it_can_iterate_its_grid_cells(self, request, _Cell_): def it_provides_access_to_its_rows(self, request): rows_ = instance_mock(request, _RowCollection) - _RowCollection_ = class_mock( - request, "pptx.table._RowCollection", return_value=rows_ - ) + _RowCollection_ = class_mock(request, "pptx.table._RowCollection", return_value=rows_) tbl = element("a:tbl") table = Table(tbl, None) @@ -237,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) @@ -381,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) @@ -422,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) @@ -489,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) @@ -561,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] @@ -601,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) 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 275052235..995c78dd2 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -1,19 +1,18 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.text.fonts` module.""" -from __future__ import 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, @@ -85,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_, @@ -172,9 +169,7 @@ def _os_x_font_directories_(self, request): @pytest.fixture def _windows_font_directories_(self, request): - return method_mock( - request, FontFiles, "_windows_font_directories", autospec=False - ) + return method_mock(request, FontFiles, "_windows_font_directories", autospec=False) class Describe_Font(object): @@ -227,9 +222,7 @@ def it_reads_the_header_to_help_read_font(self, request): # 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 @@ -245,9 +238,7 @@ def family_fixture(self, _tables_, name_table_): name_table_.family_name = expected_name return font, expected_name - @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 @@ -468,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 @@ -503,9 +494,7 @@ def it_provides_access_to_its_names_to_help_props(self, request): _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_ - ): + 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( @@ -533,9 +522,7 @@ 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, request): @@ -555,9 +542,7 @@ def it_reads_a_name_to_help_read_names(self, request): name_str_offset, ), ) - _read_name_text_ = method_mock( - request, _NameTable, "_read_name_text", return_value=name - ) + _read_name_text_ = method_mock(request, _NameTable, "_read_name_text", return_value=name) name_table = _NameTable(None, None, None, None) actual = name_table._read_name(bufr, idx, strs_offset) @@ -591,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): diff --git a/tests/text/test_layout.py b/tests/text/test_layout.py index 2627660f2..6e2c83d6a 100644 --- a/tests/text/test_layout.py +++ b/tests/text/test_layout.py @@ -1,10 +1,12 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.text.layout` module.""" +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, @@ -31,9 +33,7 @@ def it_can_determine_the_best_fit_font_size(self, request, line_source_): ) extents, max_size = (19, 20), 42 - font_size = TextFitter.best_fit_font_size( - "Foobar", extents, max_size, "foobar.ttf" - ) + 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") @@ -46,9 +46,7 @@ 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_ @@ -70,9 +68,7 @@ def it_provides_a_fits_inside_predicate_fn( text_lines, expected_value, ): - _wrap_lines_ = method_mock( - request, TextFitter, "_wrap_lines", return_value=text_lines - ) + _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") @@ -80,9 +76,7 @@ def it_provides_a_fits_inside_predicate_fn( result = predicate(point_size) _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 - ) + _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): @@ -92,9 +86,7 @@ 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, request): @@ -114,13 +106,9 @@ def it_wraps_lines_to_help_best_fit(self, request): call(text_fitter, remainder, 21), ] - def it_breaks_off_a_line_to_help_wrap( - self, request, line_source_, _BinarySearchTree_ - ): + 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" - ) + _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 diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 28f0e65a6..3a1a7a0bb 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -1,20 +1,21 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.text.text` module.""" -from __future__ import 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,29 @@ 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", @@ -69,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 @@ -105,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 @@ -157,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): @@ -200,40 +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( params=[ ("p:txBody/a:bodyPr", None), @@ -247,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)), @@ -389,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 @@ -545,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)) @@ -566,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)) @@ -616,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)) @@ -647,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)) @@ -705,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 @@ -717,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 @@ -733,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 @@ -772,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_ @@ -896,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 --------------------------------------------- @@ -1128,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 @@ -1193,7 +1158,7 @@ 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) + assert isinstance(text, str) @pytest.mark.parametrize( "r_cxml, new_value, expected_r_cxml", diff --git a/tests/unitdata.py b/tests/unitdata.py index f40fcaf8c..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 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 d44cb51d1..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 @@ -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 0d25aac5a..938d42ef0 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,29 +1,24 @@ -# encoding: utf-8 - """Utility functions for loading files for unit testing.""" import os import sys - _thisdir = os.path.split(__file__)[0] test_file_dir = os.path.abspath(os.path.join(_thisdir, "..", "test_files")) -def absjoin(*paths): +def absjoin(*paths: str): return os.path.abspath(os.path.join(*paths)) -def snippet_bytes(snippet_file_name): +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 - ) + 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, @@ -37,27 +32,25 @@ 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): +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: diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 849a927cb..3b681d983 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,22 +1,28 @@ -# encoding: utf-8 - """Utility functions wrapping the excellent `mock` library.""" -from __future__ import absolute_import +from __future__ import annotations + +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: # pragma: no cover - 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): +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 @@ -28,8 +34,10 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): return _patch.start() -def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): # pragma: no cover - """Return a mock for attribute (class variable) `attr_name` on `cls`. +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. """ @@ -39,7 +47,9 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): # pragma: no c return _patch.start() -def function_mock(request, q_function_name, autospec=True, **kwargs): +def function_mock( + request: FixtureRequest, q_function_name: str, autospec: bool = True, **kwargs: Any +): """Return mock patching function with qualified name `q_function_name`. Patch is reversed after calling test returns. @@ -49,19 +59,23 @@ def function_mock(request, q_function_name, autospec=True, **kwargs): return _patch.start() -def initializer_mock(request, cls, autospec=True, **kwargs): +def initializer_mock(request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any): """Return mock for __init__() method on `cls`. The patch is reversed after pytest uses it. """ - _patch = patch.object( - cls, "__init__", autospec=autospec, return_value=None, **kwargs - ) + _patch = patch.object(cls, "__init__", autospec=autospec, return_value=None, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def instance_mock(request, cls, name=None, spec_set=True, **kwargs): +def instance_mock( + request: FixtureRequest, + cls: type, + name: str | None = None, + spec_set: bool = True, + **kwargs: Any, +) -> 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 @@ -73,7 +87,7 @@ def instance_mock(request, cls, name=None, spec_set=True, **kwargs): return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs) -def loose_mock(request, name=None, **kwargs): +def loose_mock(request: FixtureRequest, name: str | None = None, **kwargs: Any): """Return a "loose" mock, meaning it has no spec to constrain calls on it. Additional keyword arguments are passed through to Mock(). If called without a name, @@ -82,7 +96,9 @@ def loose_mock(request, name=None, **kwargs): return Mock(name=request.fixturename if name is None else name, **kwargs) -def method_mock(request, cls, method_name, autospec=True, **kwargs): +def method_mock( + request: FixtureRequest, cls: type, method_name: str, autospec: bool = True, **kwargs: Any +): """Return mock for method `method_name` on `cls`. The patch is reversed after pytest uses it. @@ -92,7 +108,7 @@ def method_mock(request, cls, method_name, autospec=True, **kwargs): return _patch.start() -def open_mock(request, module_name, **kwargs): +def open_mock(request: FixtureRequest, module_name: str, **kwargs: Any): """Return a mock for the builtin `open()` method in `module_name`.""" target = "%s.open" % module_name _patch = patch(target, mock_open(), create=True, **kwargs) @@ -100,14 +116,14 @@ def open_mock(request, module_name, **kwargs): return _patch.start() -def property_mock(request, cls, prop_name, **kwargs): +def property_mock(request: FixtureRequest, cls: type, prop_name: str, **kwargs: Any): """Return a mock for property `prop_name` on class `cls`.""" _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): +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) 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 index a16fec3dd..34d2095db 100644 --- a/typings/lxml/_types.pyi +++ b/typings/lxml/_types.pyi @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Callable, Collection, Mapping, Protocol, TypeVar +from typing import Any, Callable, Collection, Literal, Mapping, Protocol, TypeVar from typing_extensions import TypeAlias @@ -25,6 +25,8 @@ _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] diff --git a/typings/lxml/etree/_module_func.pyi b/typings/lxml/etree/_module_func.pyi index 067b25ce9..e2910f503 100644 --- a/typings/lxml/etree/_module_func.pyi +++ b/typings/lxml/etree/_module_func.pyi @@ -2,18 +2,37 @@ from __future__ import annotations -from .._types import _ElementOrTree +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: ... -# Under XML Canonicalization (C14N) mode, most arguments are ignored, -# some arguments would even raise exception outright if specified. -def tostring( +# -- Native str, no XML declaration -- +@overload +def tostring( # type: ignore[overload-overlap] element_or_tree: _ElementOrTree, *, - encoding: str | type[str] | None = None, + 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: ... From d5c95be6d51dbf1db6d99951bdb28efb86054de7 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 2 Aug 2024 15:25:14 -0700 Subject: [PATCH 06/14] fix: #943 Docstring implies Px subtype of Length https://github.com/scanny/python-pptx/issues/943 Remove mention of a `Px` subtype of `Length` since no such subtype exists. Turns out a pixel has various sizes on different systems so no standard unit of measure is possible. --- HISTORY.rst | 5 +++++ src/pptx/util.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1f5d4e58a..6cbff8b3b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History --------------- +0.6.24-dev0 ++++++++++++++++++++ + +- fix: #943 remove mention of a Px Length subtype + 0.6.23 (2023-11-02) +++++++++++++++++++ diff --git a/src/pptx/util.py b/src/pptx/util.py index bbe8ac204..fdec79298 100644 --- a/src/pptx/util.py +++ b/src/pptx/util.py @@ -7,7 +7,7 @@ class Length(int): - """Base class for length classes Inches, Emu, Cm, Mm, Pt, and Px. + """Base class for length classes Inches, Emu, Cm, Mm, and Pt. Provides properties for converting length values to convenient units. """ From 799b214a79ab24326ee4075265b3ed1adcf3c6cd Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 2 Aug 2024 16:17:29 -0700 Subject: [PATCH 07/14] fix: #972 next-slide-id fails when max used https://github.com/scanny/python-pptx/issues/972 Naive "max + 1" slide-id allocation algorithm assumed that slide-ids were assigned from bottom up. Apparently some client assigns slide ids from the top (2,147,483,647) down and the naive algorithm would assign an invalid slide-id in that case. Detect when the assigned id is out-of-range and fall-back to a robust algorithm for assigning a valid id based on a "first unused starting at bottom" policy. --- src/pptx/oxml/presentation.py | 25 +++++++++++++++++++++---- tests/oxml/test_presentation.py | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py index 12c6751f1..254472493 100644 --- a/src/pptx/oxml/presentation.py +++ b/src/pptx/oxml/presentation.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +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 @@ -67,14 +67,31 @@ def add_sldId(self, rId: str) -> CT_SlideId: return self._add_sldId(id=self._next_id, rId=rId) @property - def _next_id(self): + 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. """ - id_str_lst = self.xpath("./p:sldId/@id") - return max([255] + [int(id_str) for id_str in id_str_lst]) + 1 + 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): diff --git a/tests/oxml/test_presentation.py b/tests/oxml/test_presentation.py index 1607ab5cc..d2a47b27d 100644 --- a/tests/oxml/test_presentation.py +++ b/tests/oxml/test_presentation.py @@ -38,3 +38,26 @@ def it_can_add_a_sldId_element_as_a_child(self): 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 From 284fc01850391064298ad0fb6da27568199ee4a2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 2 Aug 2024 20:29:40 -0700 Subject: [PATCH 08/14] fix: #990 Turn off ZipFile strict_timestamps Accommodate system dates before 1980-01-01. This should have no effect for users with "normal" system clocks but allows the package to save files in the unusual case the system clock is set to a date prior to 1980. --- HISTORY.rst | 4 +++- src/pptx/__init__.py | 2 +- src/pptx/opc/serialized.py | 4 +++- tests/opc/test_serialized.py | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6cbff8b3b..88b6580b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,10 +3,12 @@ Release History --------------- -0.6.24-dev0 +0.6.24-dev3 +++++++++++++++++++ - 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 0.6.23 (2023-11-02) +++++++++++++++++++ diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 0c951298c..0ed160555 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "0.6.23" +__version__ = "0.6.24-dev3" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py index eba628247..0942e33cb 100644 --- a/src/pptx/opc/serialized.py +++ b/src/pptx/opc/serialized.py @@ -239,7 +239,9 @@ def write(self, pack_uri: PackURI, blob: bytes) -> None: @lazyproperty def _zipf(self) -> zipfile.ZipFile: """`ZipFile` instance open for writing.""" - return zipfile.ZipFile(self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED) + return zipfile.ZipFile( + self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) class _ContentTypesItem: diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index d5b867c4e..8dcc8cf56 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -341,7 +341,9 @@ def it_provides_access_to_the_open_zip_file_to_help(self, request: FixtureReques zipf = pkg_writer._zipf - ZipFile_.assert_called_once_with("prs.pptx", "w", compression=zipfile.ZIP_DEFLATED) + ZipFile_.assert_called_once_with( + "prs.pptx", "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) assert zipf is ZipFile_.return_value # fixtures --------------------------------------------- From af6a8f7b83c9c33b5fedfa0987d54f817f3b3cba Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 31 Jul 2024 18:06:19 -0700 Subject: [PATCH 09/14] fix: #929 raises on JPEG with image/jpg MIME-type At least one client, perhaps Adobe PDF Converter, produces a PPTX with JPEG images mapped to the non-existent `image/jpg` MIME-type rather than the correct `image/jpeg`. Accept `image/jpg` as an alias for `image/jpeg`, which is consistent with the behavior of PowerPoint which loads these files without complaint. --- HISTORY.rst | 4 +++- features/prs-open-save.feature | 4 ++++ features/steps/presentation.py | 21 ++++++++++++++++++ .../steps/test_files/test-image-jpg-mime.pptx | Bin 0 -> 33400 bytes src/pptx/__init__.py | 4 +++- 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 features/steps/test_files/test-image-jpg-mime.pptx diff --git a/HISTORY.rst b/HISTORY.rst index 88b6580b9..73c3bacef 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,13 +3,15 @@ Release History --------------- -0.6.24-dev3 +0.6.24-dev4 +++++++++++++++++++ +- 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 + 0.6.23 (2023-11-02) +++++++++++++++++++ diff --git a/features/prs-open-save.feature b/features/prs-open-save.feature index 4176adfd3..d60b2c242 100644 --- a/features/prs-open-save.feature +++ b/features/prs-open-save.feature @@ -33,3 +33,7 @@ Feature: Round-trip a presentation 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/presentation.py b/features/steps/presentation.py index 0c1c6ba26..11a259545 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -5,6 +5,7 @@ import io import os import zipfile +from typing import TYPE_CHECKING, cast from behave import given, then, when from behave.runner import Context @@ -14,6 +15,10 @@ from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.util import Inches +if TYPE_CHECKING: + from pptx import presentation + from pptx.shapes.picture import Picture + # given =================================================== @@ -38,6 +43,11 @@ 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: Context): context.prs = Presentation(test_pptx("ext-rels")) @@ -189,6 +199,17 @@ def then_the_package_has_the_expected_number_of_rels_parts(context: Context): 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: Context): presentation = context.presentation 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 0000000000000000000000000000000000000000..857bd7ca29a7f32f1f9cd751b00a7b824fb4bd36 GIT binary patch literal 33400 zcmeFZW0YiDvo2h=ZQC}w?CP>@+w8J!+qSC|T80`{#JTKDCl zB=vd?2c&@bdPoxHx0jR8lOHbA3PC~VlXDezWD-ZlyBsq)>X36}q$J4nvZkr>-b=+G zOO;FoQ6arOI#1vz?XxQ)rK$+>nC(es^)|j~5YBv_&)m2Bm4O z#e2e7_riSFbqBPnS;DSWqJ|%J_1+hliHdy!a^OqO){S&h`@(giZ3WpiyZTUz} zQ5%W89#^v~tvW5Zw~>l<&W=D~%8Hgs6qadF@PRCdhh8uT{7s_Mxih(EWd`>Cv)IzD zco&2OHqs{O_k65fYwXG(0V z)`pk3=2<$X%MxrYEz9vzru6}6e-%FQlt8Y!|63cFp93!FC@W0jDTiFO54}?D=i6?9 zNSDE$%WB^xCm&l^QoH3eL?t?Zjc<b*TUmxbdV1R-ru zabCwxgq*(}q`M+Zw`$4T%1^P=e3)|67zP$8#5Em3^Ld3|@HM#%ScWN_#T!G*q{*9& z(Ps=!!cbFl@ufKNk(CJpGSM}%R6|3uJg?1$Fq3L@?y<@~Wz3#k^QN6u=Der5#j+1O zi^FV7Fw_Ty+fZVT71^D+FJGvL4O`{ySxz`wVde%TMo<*${W zUv@+A^=)V8MDJ*2Ze;98{}1!|GhzJ8WBxIu%2*kxKLYgsBplxuw}GdcwW3#`aEpHd z4mM)TXn!Vayu4;$K#n?y5$>pdE7U(-_g>7f$~`K+s-Yp@1=U;xWLaANR;fI z#sQ{QYwfXV{khhCE8T>fG*U!_MT!EdSal3qcNE5piisS;hH3bqq?txbHk)IF$*%nv z`kW5fA5IA;D!`Wt8@d<&M1RH+EqiM@Z#tE?8T#!|(*;EIX=T%ZL7Z811%o z%m+X3=o!q<8Kfy^W5)$uTHX<~Ys27*7Wl2dl?pZ;D>%%TM5ciO0Pq1I0RNK8Kg40@ zVC?t>B>GO~wl@DqC|}+M@XNb=UGV?*t1?00UvP81H=WmBAy=?emK~xDngnDAfGmv! zE!x^$ny_WaB9BBF)j4ke^Wn`sy-xM?QnrECLtznOI35KfeO6_Z1!yyTsDfJ$rLe59 z84UDJympeFUWLBYI8=*jL!T|vM6{h^9Q2$Yt&Qdlk))-KR zF2Oy)3)XgCUJD3iB^KRbxFBR+0m`dLitCL?NM*QQF1X{*f5~8?xSUc;aUH(Jym9yO zrElU`Se%E&qMBmfRBIU_s$A6@WWez9{dNhbDPy2!+>bOm5+`X-^L}TtCW|77X9z+K z)F9~p4Pt6mp~P&Dqi!V;wU25G0lbzTfkmAf9;FEN26V&f?6nk?g{2DPe7(zj?bTHc z>c27B;byNQS8=OT^bmDliS`BSVKK2HE zkVqXLao|b}bwfuU^oC*$U=CJ92aEueG`n3lMSag9Ab*w4YvfgYI2OA9<+31;~lmW7F85$|>2HZ*W=RbDK5uwb_Rx+TtJm{`ale*t8Hu&vu~!(1mXf94## zzF{$;{#nN`YkcPPd;h3RpxwyQN%^+3Yla=x zhoEpjwPW7Z+XK{exlLo?@kkztmXe(&fDD~Z+=5-%+PhlAVl^o1LW;qyQ$7#;VK0x> z>*MA?WaNc0)KB!FoJHTz^lN|+0XVr=)1NGz@8cg|n*N`mMlC7Qs`+c>9uxoo^}mN#2Sp*eS1 zhadjy=Y*^O?U?tH1~-*a|4Yz$w!m6GufFn}E!x9n2-2@+Qq&9T_4;hOGgGSbv53p9 zNq4HX(b>5!>Fd7gq}{STafLa!G3G(@n#V@rxfZTbd_Y27@ zPapE&^PCQDeb%JKv1X5Tb9F1z)zsDCAs=>>$&Tv@tzD;|XjyHRHj6&`me)BG3yhXd zH8f0g@q1Kdw*1GW&eswx%s+eFZxym{f~AkJZnEVVkY_zDs7r&+AjhJVn9Hlo&>p1J zJ=EnAArjkxAWR^PKqbE&Kilt!7+*Z^RHV7|Cqi&hczcUSV*A*YS%&JxS1n-bZx}sh zVp+#V6pv$M(hpFDN~R9zVWJX3@S+5eP#MYzL*yZ#3Do=7BtT%@df}^aAn&EUPyn;_ ztux$+Y4DC&uG=0dI7lYexi?-fds$rQU)Nc?tu$6~PRu8Zn?H)P>e*|%e z5;ajSrEk&Gj&V3H+iljf=S-GBK6X*S?4)W>-FqwM6mCExVPt?M>^|>UCpXCmmQ#;% zjiG3$M;XhZU9z0iIon|BMagASKK1v#sZU9VSTGTf#ZQmz!5Gm|0cwyUswL|KjP{68 zOzj$9pbx@SPdvq1ekjv(c;@>zm^>w9t?r2GKm?H7^i#SN(2F&ah0WoIH|~NTv~&nq zzB7r4@X^>C-Yt7rF|OaQ>wc;6e^&H`nfU9@uNC&MfPm)TD!R13yREa+zl*H?af zVIdqt7)g;ppABqFEK^0C!1Yu(Enh9Yv;M}-JTPJ*POcfy+D-g!7*g#R4A;v;cJnM2 zxI3fHMQkb=G%&BYNU-nf+pIK(1Xyl8f41@P6(#-uAq9^Ak;0dHaQ-)!0_{I#5sVD~ z&BgG~IR^IshEg!k*Cj;#a*ada|G_=V=sP+YJN!HM_>WWnntuLTkg@n;=0LZ-^IRDk zVo15e5)y}30tIs`;_<$z?crNUi7H5XzlJ9Z!swe3=0^3z-yiH*H}ZSIdEfXgI*URN zE5JdXjDDphK%0|O5ws+eQdLH_EKsS_y?I_<%Rde1y<2~;+Bivw&&Yl|eSi9uTA_hc zC^Zqvl{r5MHyiSOtyW&;vR)wBQHQ}Z3vi!?nUfOmp|J1mmStDBUPjv$6;H+)Q|=X; zIQ5h}h~9w-9p9kHdKy}M?(}`-`x;9mfQx2xB&DW8y^QoqaJ=y|uQ}iP0OpD3q3Hz0 zkT&M0H3d0#I3+PRwZSM0!~-~X54=YOQ|S9HSk-_Yay-v?yO|09JjU10lfD21>CushqYfVBe}0D$Q4 zLXkhY=s&`8#y>8hQDscwiAc8B$|2h#7iOrP$mMLiQpR+5lNaN+8= zZOZ9Sxs!~B3Q25It2D)#1D{u_S@+pyB zhwK`@hpr6$#b=J%zBhBNErLd2DiLP4Xuce6NhKpN>#$w7G5j%x*AbJ40ZnWl#h<$uUPMxJ{#<68jy=DE+&zln z#d8}^q*c4jed-9=CSuryJ?}v_3nXMzP}>fV1D&}n^FiQ`7k(vi>>pPNMhY2-hEPp~ zgi^WBg?k-`vB|;h)&m?;MI;xL%e~g`w#w(4FrNwgBzY`{r$;>~RtfUktbB?FwBmx> zW7JT~3q(|BKe{(F6x8H_g2EJJ3)M<=`xi;Z-1#HD5{~!B$0JDBQL^-{SIDDQz=Ji4 z+r}M)8U=}(s+kkTO<++W%E3Tex2d7@)l1P_FAeFDzmsa}mo1{uo-S2c^0Z8bI!M?{ zI><8TB3<-}6fC5NF?@Qlx_L4ew4!$J8qdLVoqrw|F^o3lvDqPu)HZ` zuv*2Zjf8;`cn|D{o|Dix`Akv^VH|fj%qbrG{tS;5DO4Y(R$q;sY+z+Rw*?WreaD9 z0qoO&j=6G2ak5i<_wq-Ue2qp~rW>Ygnqp;2Nadk@X7KOKklRRxtN?Mry5J|}cqrxk zFrn!{5-7d1lk?a+$#2cyBD3jmjn01qnRt~zmxUl>5e74-rWU6ofq>-I=w$|{=2NO$ zSQh_MLNeG~-W3cF!;tED@nB4*h=S4Z+jSd==^OW{5AwctNuC;2UDp|8VkWNjPsRlg zZ(;gAH7}2D*Luie0x9}e9t})#SB6!I^Q z>89mP9U!5=svoOV0PGx38_JJM?m5@# z)}>#cY$kmsKWL5zOk7K>@V;3>UkN8`OUQ|rFuPYh*x!)wiZwIJ#n(eNG_b41$3%DE z662;V*|LF>Taqd3gy-#dJJ9qAYnZgD!BoM+&*^LtNh-$4>p-zJHcPA9R{MM$JG091 zbmXl$6hR&#SbPgvR5-QJTcgsh_b*p99sudgNU6`oYIo2#jsE~_hqm#IMqUA(2wE6V zIUJ`yyyY>r9dlA<$g?Dkw=eh8rM;DGS}k{MRF0>VGc>^`I<^K~q)Pr6H8VjZrdVS5 zt)Z&t$+d@x*!aT_Q_7x8K!9vQQ<+zMu$FZ3(ulRw4PkWE`BC$@PF^sGx{rX1_y z=Yd-lCw$vMWGi=thK}+^;<1>BFyuq-bCWxF*s6(_%GlsNmNKiuz0};1o~#Ga1JBld zRG7fFaG2xoM2_gL9`i`&_hHA~tL5Js&aBfJ_G#Ak55zpZN>sDPkm}`@Sz5oulv0H#&LV|fJ0H*OodH~ zfPq7|!UC<(>9sfV2(_Y%r>3Nbe!uYj*w|3u^c7bVRJkA#^IQE8prX=o%K$( z^}^ZX%%!Mbm|3QV7`AU&$Tc(I^dSck9TQvYXTQvPkMJeU`VppmTwO0Lm)24Y?0|cx z*^1*ekx@VP#q~oMAK3H$pNVs4_EOCF@yo6oV*O6kquX8&zi$UGIlUlT=X*4|{?wN3 z)JQ}utZn@ecF^_Mx-Eoz`#3Q##96iRcluYgHR%OaaN|!;NzduzhZd!VfLWW9qnP9e zY0bfVsnJ90uo~~@1uRhfgLUlT8}{@#oIdcoIQODDv3J{c zx1ILVR_XLA@f%f*kWT_2+ttEKdE}Y$bq+!-gV#f6@867&P@M7K! zs-O5z(4W>p7s#~@FFMgDw@$Aq;QS@#13Jzc(Y0dYH_diidC4Kf=HfCymh@A8LeZjs zdz^24zj?mY?e_XO1md*ByEmohatQX=9@+QQ*r<}YxcoLs^bq5ccpr^<5Xzrg+1ULvi>5;eWZ+@tOgW$=U8wEakS?A$2v(wY`3KhFRz_VqyY?gMBRWpo#-*W+a-FCA zGOfp#m>rF(V0 zWa}dv@5(s9Ir@k#o&lc4iQ`x}!lITTsecW)0USWcr=>*;s&(@x{dmJUbC5$Iugob@ z)EQa**=CuzjtV4*98UZN>jB6guc^{|_mhP{NI{V9A}u%W+-0Jf$%^;>^O>#h+Lebj zmrSIHA^l<_;nTzjv^h1zlkrM;@XJs9+o|+LveBY2(ch#G!2v6hP2?nRfh=zXQ`pWP zv#-?pCvI@dS&AJ?rY&@QsSr3nXl=KJ@U zpVZTp);Hj>gbo&VxkWcv5o<}LybVje)3T6muxOc=(lDaW?Ps5Vi{1|zX+?)$rX}`I zp!c6NcRW?wVT}!8Xp4J}TW{Gkcng14cv3t(34Q26coI+#sh$H+0PTkoW!B_xjdOd4 z_%onF3UZs%!jniHVFWE2pAO$2YA5>@+7lzm63VOwyCC#lAq?^?#-w;;ldl~?8Qj9= zRFb3Ey=0Twl!prgQ~X_4L*Xf^PFq$ifcSz@$G_AqH==pJqgtvc-shXA{V=T%T~z9? z^170g@CDvteH7gK-j%M>cRVuM&D+)0(@UbxCVg3*tNEnaj!|+Rh_<( zWs^(sb)5w)n>-=zK;mMJqMzVqkHI}Ng=VY|1D0WvVA5+#14qnSyT!08gLNNIY^#5v zRB%Y^xM~=y5K&QXto%%9A}OK9XQ+5_*FNBKeEKML!z{IYUogAOD1M;2RkVF=TQGX2ypT> za05;59!WpqD7f@I14K5OwjamV1LNTWmPoi1XTVWfzaHH^)gkNLgI|354iCK(ex_T9 z+Hle?x9^aC6E5L6lY$-i6r(7l)V&AqmO0a~)j0DR_bpZ~--7orGF+(Faz`$b z;_3}1{G9>s7hv+-Si;>6O*)wBVBNQCN)V0SW=5#@_BE9lTTfRx?im{)ER7ok@FC+7*R8PN7A05~xcHOeuGtpB%UxN*wv^-Z|3fihC)# zrGxJ)7#skdwmPi%eP3=Gded3aU$er)wDR#=9iEe$*)s=3;#* zv7MmEYPf>VvYE|-=aQ~23MyiWcuIr-dWUrNZW^q$@Ji+w4H27TQ4Z z?m9JQGf>}B7_Y>k7oe&=>qxGUykXKHN$wQNCL%iYUDQzpkvJxGH>8mmV;GWJ-Q^_?Wfzz+txrI>z~*vc33>vvhr`wC}I z9gIeeiVE@#1*IES+Tv)7J1qglFkyn*JxL4Y3V+2}WXg`4-P&QgqyvQNZIVz9Hy?%2 z@74XASLKH;oeY6>q?_B)+G6vb!PLC*xTrAlL_DC}yzEIphkbrq?`brmZE10M3eI(s z*`rdT_A5m%0(Czi8S{a%Zw7}H&r#j&e%;)y#{~&BXmOFv&)mi$_Jr5d+JiI-y1NL1 zJj<~^yi9Y{3cISY=bE}OQ>ZbB_bQUd`l5p<@Y&0-Yz2$qA&54$AX7%ZmCnK8@x|GU zXDJZ!4w=fkInq&v5GIwQ(wTQx#`trb^6>`0jfo%y<5@%%WQq zGD^wl=RA&~6#0`NEcTXZRzJD1=O?2<#j=-s**LuN6s=`037J*(!c37%yNBxScB@Kb zR^B^K)RU?`XXZ>s?WMjjgL=%~=0!)}%#U@w*)vR|ssNbacVd0kM)U(bS$LP^f$7 zdQM`f)t*O)dn5skYN3+VR+j2mYu`Yo{JwvEc}m5)LL>_g|3F>rVWJnwp;E&pf^K{6 z=~aNhtj#A?^Pvt${6P-6`16xTwi4wVScMC0#V|Btx-B_0A6PAlAXsH9maPNk zF%SK+!M0bn%zAY|VIR#|5t^`rJKW)FJ~$uvU7i>oJ5+H#9ywm$G44! z%6z5VahXbhWIA=_m99(%bf()7o)0L{ck-5Tan@1-xj^=Ed3g028$BOXU=ZPds8 z9fmq}B5S~k3ybC^POr(>DY4lU-S#<6d4WZyCyuOHe;R5y5(NAvP2;<48+5IS@ZG<8Qt%1Efyks+o6z`ie~w2ceSVF2ffa_|HE!b*UQG z;X5nBPB$Tw>(5FR^g~&cekU1SW`)&YcVJq&vH0w_X((+02Vmy$k8-U;12Ay{IlVNX z1sH!~WLv)N1?rKO)&H8^p)3ORoMrZD0HkgxszC!ZxI(P~G zRxO;6$FHK+1H~pm-ff+eP&h_4TNf^E9*2Bque^zS6t5$Q&JhZHFK;f}sq0(nQzOB4wWBV7_-uPyn~%QYO#;*2)M zDoy~aQt@q{6tm^095^$u2`TrYRUK%C9@P4}YQ2B89sOq}O8Y7Awfu|hKEe90E&orq z=2_Z?O@ZiFw?6efE<)Es-<3?`$mqkMXM#+A(<2}m1DjI_CRZeGhjNkk7uo$F!NvTo z7FRmduuS1JC@Ljs3YVMd2^JAzlwy7B{Le|UfNx7sa1t<# zq(=%{xq_c|_qX3KZzF()*zJT;w2)yGw0fj?cX)m>L=s8Lfkn8ohzWbKm(T=gc^N?y zn_S7)GFzz?A7f%lhv{ffy=Z+uyk!x@H*XwA*dYxhglW6_DL&g-YVMA&F)2sLaB`xZ z@7s8&fBF z1e1yTcmmfwlJZKDZh69B8kM>iZ~nFD=5GO<1)|O#(3~4s(G#yAfql`)1KrVi8``m3 z+FPv;`9*2rki2#9P?&DnvB>KRM%e;etP-qn#cJWn6#x!za7q^AhPXrY%E^yue*GH@z&PW2T_?)|qxj{@B-KIwvh%D}n`` zo+fR8vyEUU=^+pSIkt4uknR$%&5P#vx6V>55_!R(Z#;>^$uAa|M(pJ=ibxT>wBJ$n z+^GYlw3U-}seRCKPsP9EpD?(#3ukJ;)<&13FL=o&m4;ie&m(Y*2Jk5%@RUd@i8y0X z)p~E=4^oY3R~jj+xk6*B`Q7ZO{WY%X)lk_k3&f2`s)RbV)%PB{(5_?qT3F8Ql{EQ` zZ_*>J=nK|Vo4VgpVj8hPXLLu_zan|4L1%4qOe#j@&UcBl<7~2vqTC^x#ywXMwC{8o znI}P*qTZ)c-WT7m|8WV@y_Totas*U||020GHdP=R3nw9;F^+?j%682`f^B{7EDp_O z{mAB4a=ux8-ng+Ap4II6S~2&ayTpwo_)N`lEj3Pv=XDw_vS$9Mln)(s*;)niYa@UvGdI8AM}Bz zJla_$6}L|J{2A4q)yI;s%(fZTE(=l;dow4_W_=d(r#$*uJHI%dF%t(uvX}c_pwXc1 ztgQ5%Jsjn}HDW?r_Jf-}U=K#yTJZh7c2@p=$F2P%2!WGLYj?JU#N>S)OU>som%i^e zd#_vMN9xWLz9Mm9nUc{Q1`n7AuGC3)(!6kk7L?yd@<-hbi7@YNl~n(}pbqVl_}=ry z1wef5nf+$~{jJX+JZD&zB1sSfqhk>!#|f{be2iJ}YX`VB4K)BNpfn<=%jr0=jrbx%Ack z;@U%3ivIFZrn;TU>D>7y&ZAp(lc;s&q=`^WjUYj>*rRLhkcY)`jmSb6284nA&Q7Wg zHLP-&9?w}&6i`{Q?#E~+8tdI&_|^IX&|h^exHyqXVNVUo$lctLXh4ZJ3$}iShP0ms zSPG0=!&ka-M!vQZ^+@HlVRfzdGd@UN0J;&MORj$Q&0|pX0jns9kymYQ;thApiP?K- z&64ganvPXn`S)fqGTX5d7YMyNt5;I%?s33Z!wVnTJ5?8Y43%j^_(a`RldiKFJ2+F5 zng{=Zx;b52$O6{&52~(tQql*ZRD3BRAl+~oUF3sJT58xkGr`+CK=WwvWJ{;0Br>Fx z*Su@M0f_u6nlG~5%8&HJ1IC53ItV%0RA|&v>9xg0zF-4%5rmdPb|iQX1|!gVIMqCL zQ!EukS`|(Uq6n^>sDu5x`6*8Sb`m+BkNbpI>z9zOkeBegNF}BwCB5e3I4lP zFjat(vrepf_ogJ%mc^lr^EbIdDBe7fh7^~o;WPtbze87jjs7P&ob6d3_uXP1=1Cb% z9CQkcf^XEc6;R%529WG5A{GXk6bc>TPFNzzEK?BeD`<~3PGM`)pqpqvd{nWxF=!tz zoid&F+#F6s1y~&S%x7H_!th&t)+J0NrejL7gg&VfIDuV0*P5rk|Lo@Bf49jqJy+;r z5*;q;^-CJHq&eWG_R#+vCR-%g9;jmFZ^ei_0L6d*TUuP`P9`$yOa1%5NU#4&{r@yy z{~PrOarsyEFAS3}`BMK;q(AEK67hf3KXQ%*RG|kOmNOl5($iKrE9(;L#5zog)zlCr zD{z>Y=1^o}(eUosWd}aCgvA8(OZ^#Fk%K15FSoM7;_?SjH*cdJa={ddc|?;I4NyR+ zYQzH;AkhPRJL1Q{ESE=}<;34mE!bH?`>$Bc>?Y(==(pmh#_A_WK&RMY_1kHQAP2Bs zRF!EHE66q#WW@7xBPTu@NW0K~JVYC~#PDE~wQPq@gTx+EYq-I%Z>O$8>?!T4AQ`Aw zc~TBY(iBB?ZPFE~qLa0Z2?#vqLpNwPH$`lVysXk*o2iZts^#{qMIll&u}}3p4a<7E zpN_+CcDB2dveg{a2!mhz*j~v4Q(jx&vNHfb1hU+vrJR%)Fc&C9=LK}CqP8En_1tHK zn}$KAoh=DP>#oZ`DWzwD*OB;E$NgDm-` zXqXwEkMDtq1oUDEd`?k3>X>1cWbn-Iw1$hhBMI}bg%3b03AVR(57DU%u2eG!>L@0jIO;d(+JRHPW!H6+kfs8XiL^S7ux>odTO|=Z`$Hk0(&L#}v;cs?tao>e^*~ zp>q|F6M)`|mUmY%ueKS(^Yhkx@%!9LS&r~X-}l3n=_wQa-y!Donnp7WS&wkH6)+B9 z8}FCPe0*N_>RQv|^4NZmp#;}DGl~O%Z`Fn2&1{SS3Q@`2& zHd}p>;SF>7idp;riEQ=H-kM)(e;PEpFQvD7_jHgoKS0SkFbSk=*gCj_cz)D^x)6_5 z(?Ar?rFE*ja=6FijiB-CppDZpclCl_A16#cNAm$*@>eojR6@|tWp1P%Wm#hdinwRi z4@5m1b6q&e6riLIq#i`Br(!FlbS zk9~DCHn>kR(`?h~sxc02{opQ0HeB;d+DS~i_u4e^;mavOd`|tViYgyxCs_C6Jw-wz zF}=#HU&T%VC@S5%m9&nMV9o1c$jKTDYEYHo$GGM&Px|z-9!G$m{3Hki#+E`H2CCX;N(#1WQuL^a_UL}s z#G(fGJbYJi*BnY^`b-nw{wAof(0U?-ddBA1h?I@*A4?@d?qvN_9(k#`gU+Es3lS<4 zt1icWK;b$Gs@;1Q5466?O2^uf=5xo6yF zgE8I-r(I1i)XQs>8%zm-p!u ztZtIl7zC*OLj}8^ryUUUwvumJKrK~B#NSpftuAyn9w z(JBBDUJh%q0QBwPzJ|enhL1QZzyxPm{VG2UheWga0j#>tZwyH?M6>d5wlS2&irT_Y z7Un9X#S;Gm48SvWMw4rQwHgH;uB8Y%P)-5Yl-Vj3%OjG_%bn#UoJ#t*JD^|1Y0cL z0Co?Sse2AV<1S10cUw?piBNX~b6crtO)LpzYc=6+^rI5LBY;{%dXiTv+&X7f zJDrR@W4UPDa>}yH=>pOnSgKVYK=V4EYE3TH?x%l`Z$kV1qeg!to&KtS>G=BR%B%6g zp5^UxC@3cb{(h`2YmORE1D+Dfy=amH+)q6tG~fH+JZ6ZQ+C6xy7GPWi zC$Q+H({H!jleKHuue^`8@17LFN+N$s(hfJRD3Wu)pgj{&OvuEN3~Nxz*0N~pBFVUY z@U%l~kN}=-9~%f}?t|`hMO)}!nbtSQUh1F^Edw}ev$#nwzb@gzQx%eV)UTE@vcZ0) z4Av7QtpYmID8Cmo^U3wBnv<=L9fG?~`Ds6Dr}gU#u&zlxqk`!3=YVT~`%Z4(rJqIP znY|Zu)@wQZAJ6B9F}^j^4JY#NtOU@UaXeRrZ_IRfs^v#%OcilhR0_?dmfZp3aPNm> z=e~dIw1OXJ#CX1(mg7I+wEk>`>rBr$n-U3hMg-AD5eaw(AsoiL$H#e9+2Eqty z4wOs`&e!$?19sA69y^viz&SA~l0D%N)TIgMdK-4!p&zx~I&V-~k=G&P%qShi)>fu~ zzNOI52zz<@r{%bp0bq9%NM}(Dg*Ijc`Y5Js__u=tbz+C;7w_r#@s8@c(7Y6GEAE_?!l^| zq4hbXv0uHB7RzFff{+!HwWqLq9mvi4Ufz8gmSoe>WFj@?1Mf%CE3J+>p@~$Yr^*eR z^{pfQ^hJFRSKu2p>z1@sIoBG$oS#7REjmu}tehe^F(Lb*rRN@tVw&M87w|;}>b#d0 zEp2g^?rA>j#!%ZaBHn3Y;5eQDtGB82^)rQv_>_hlQK;?3%d9RH3ce*c0c%n!*U{&n zexFwU00o@29}OA2UF^2Nw}*O*i?+u|M_La{jN+>)ryN+|k?ErkA4bI@6?_(#6lfzd z1l2?(fIvZ_rc4x(>VvE<2=Y!{h@dF!h{~9~J-oGIb+@7NO^c5mbjRu?8YirW%a9E! zV@RkO?XpbGKIBR8SYxQ=o1A!0fSiK(8P`Fsk5~%HwqJTqD_)G_tOS<4GP#WQ!zvwj z)P<*!E5g(TZ$38N)VZ^#wrI}udb)m}u<&37cdC{L*mc4ETjeWSXd>1T4xqRAs`0nN z`EM=2f{3jSKf7@LI*kj+t7&opM4(`iy9F5M;-ld4^^=}$h$F7eAPIp{b4)B1?z0D6 zzt?Luj!MEn;FdC~jR=YMYF1&%*#WFwwJVoW9r*8|p*8*5tRwVu2n8)-x;2EN-xGAc zjy3II)3JJbkjK|xa6ov<^U-y-3VdOJfako|a~@brCeK`Am(T?1?jFnT zZFcBK*#MrP7twXV9fopj_BgIqS6oU^mG>n z|6TTpG<=%Oo|lqmpRgX~I3uSxtL;*k7F4E^E|?$b)W&27l%$n1N=+Ovt;S~U;P|4& z8>9e>Om7{%*0T-6j?(Y;{=W$?njkfxb&QXuXm=Qd5(;y0m~`U{^@(>`5!H(%gp<~+ zAFe3qE)eYfrrf^ez)XDK%tBwdytg`8d059)4{pL(S(?*h$vPbrZ^=3~umE{znp?09 z%Vt*FkQ#eZknYY*U_mr2g5hx29F{D7bH$}5bN1I_}u zwpk_cxn3Mn*eCG=^Sk(32GX(i>hn`Ugqx-a2^qu@a$D4zPN7n}c9}SSbREx>Vth{n zB08f=Evh~#!V}2TV5LHhie?hV+%F->XutD`na~#ySQaT;)4|T6ZQcArzN<8j*~%~x z{o3BvV*cMsE@|I}Qk!^Vk*^W}+e&zT)hb=(o-0q}sIIiBQHG{!2x#V?X8g>gBZnAt z79#J{7f?H8Z>Bp#eS`f`W~^1SboyWc$`9Pr4WDe%o>&+4VN%7srw(L1X1uv!(Xd*3 zP5OZ?^;9KWGPhoT1(rzrHJPxesMBouYmzbc;_A||ksQRHYqg@$_egKmgvGY`gGe;t z8cAcQAygU=)81tOatq|?fCliORFblT=|#9|vI#C7)`WnT{GJ;{qmF74{+2J&IQIsajey0d)0WF|T zfaq5@zpIO`{vl`mtEO@V^Tg|NnOa%`BF??RMAHj8WS>xmfrafagZk#MaJ zRK4d=GoGQqvMRx^Y@{l!x;kquW@cq9-x1!8h;A0GS$9&&@VRbQaottrOeJU@c9dHO zF4p#maxZ;uH~JHP|M=GJd(?&RarwIJsk(66j_bNtMENwa23Lv^txu1Jy!R^;7(1te`^M$v%J&$J)EEb| z5i?=0rd^}%iJa>&qFo9o+38rBUxZ*?&gK5RfJs+b`VKO-AWEg~K-?WSw^V9gstKy# zCgy>+*@aS0Ci-CPr-I-O)Mct?XimN|iqa!L2^B}p-?G#4nb8Lqqk6Q;^$f4s9x;dWHHCvUqv+RWQSH$ z&HLmWCGm%izASmI1R1`*vaOeRzo5vycl{1CZJL3GX4ZU6HF8P&t2zen-n%4L(z@@< zcQkB}W6;upnM$wf%r^I3YS73JUcaElpqwbTUK6RnF6>3W*N)0z5!X9Jk~Tw5J5}8< zUO2U6q^oVeh!LO{So$3R0zM@PlLM8ArPgNBZYjs_gS#74)# z#=*qF#3QCA#UrF5z{VzHxkg1p$H>TtOUA~|rxv_=9B1{Ms50Z-1R6ovr7 z#JXwdP}_-(cb#2XMb+`%gNRfdTnbL3@ce=<(U*mF9L9c>0EGyE!vgpCbq9a~4`2`= zz)21a5G*VR3=as*%o8eVl0MXeJ}Aq5-|=M8W+@uSWO%TV zGd0Oh#am|93@g&LH;+*bS#|!cq=2PD7E5fyu&zw{G+)anl?ca(_8~KcaM#lEJa}b! zY}IHsm2tk}<~@d+Ca<$I5A_t++aP3G#4+a$;Yrma55$VK$xA9Tq;$c8T>& z8P4^jF6&7!IU;;+Q~O@`b2kZwq7`qtQ%jpbM(nP;XkKFOI?bSWT9j2T&f?p^Vmz%t zl!9A#+cK{9j)*l>AK^H1JI9lbOLR$)%w$dYM4E)svZaW7>+l$3agb zDx1cJXTih7r3M_d_d$7YMdB1a;Uhab)>n2`eRE( zF83}xT%!D&>cqrAvzL7JmdqjfQNT9;VJE}VlZ_icufwVy7R2?z6?~#zc|<$iDH)L9 z(=)VsE3bg8*-P%br;z#|*Z5UWonPyxcY-Y+c>neU3lEpG1ZI}{NDetMuS$5x72NP9 zNN&0n!O%+Rea&w}YNhHMepxkZiBd&nToi)%-o%2l=xx2_Tt|Hp@z^RSEbQp%9Bc@! zPQv#Kcd~`c3A;_pldH83)yRf}sA^*+2PNP85gzOYN{@}OT;+8%GV~Iv8ais;W4|!nfW=5YLvJ&&CK>~hi+YsR)MS57sk@j7X*p4i+8WDQk+VYxPt1=L%qYh z#D`^5QE_ymHOps|vVvl4n=t&~_54G9ah!r)?xR{{F%DQ(nBPKvlagEt2_pG^=eBd+ z>nbvY5U#u1_D+)4ar<$%lOwV<;xZ&K6Q)zC(h{r1+dWCAdH7HgZ>}ZiNsrTa>*j@H z%oqsB>&kV$MdQAwq>>O85tBXZ?@T>9SJ5Ub)UP%?HMOtb&>Z?q$RMb)v_eIf?9296 z{VnX9$YRfjC7fNH-Bib!95lB!64LmI0|YFnvX%9`OlLk8GGvfi%{dqwCtz)IgTQ?_ zA|RNta;@X{O-1UbAj%*y!=C^6tGkNVc+i{y;Bp=dJH9W-je=dTWX+IRc(ozwn zGC@}y*x4wn04CA?Ov42q&*4uzBGo@d(mbqhmPvY5z^ONho_No(Ws4*+X&YPl&~iJI zapZc?^^#WueZzWQez89FVUc-mpe1L( zZ=|9_uj!+n-+Skz%Fw5kC`oLKo?5(3xyz=mD+F!^2DSYM)&dpy-xhPWhwqDjz0o(Y zShn}ml5$4H;#`DHc7+@yLa-rFVGD?-SrgGOl&-xL8fZZ6IC0#~ z?F$}=90S2j4vh^uxb`NYA%|shbp+Y_@D|1kqIp>-?|S-IWjpzNL)#s6A9uY~tR5?U z|K8UxtyEtd+xrqZV+Oi4Pw1*Pd}0e5+4)aFbEQrED{=U_J{H7H zC8n-pEIL_{L)x=yOmC+SqT~6Il}9X18FlPryS!z@W;)#Gn?;}dU6W8|N1`CtLZoD8 zFW|tGYW~#ROo^U;Eu15xg|mz^t>f8XyGJk$p9I4}oTL{1ShzN(G^w9qZ^E=0Rg8)s zk>DpP5NS#dpx)D4zlE)$^o^)inZuev1XmLKpxdKw>GSgU7Q`FvBa{`w(ni}|oo@tZ zc$7)6JBQb*%HmKpzbe=GyjX8Auo44~z>+jC48>|fyoY!A=zaRy1c`Wvg|el^VNsq3 zYxSEE52}v3u)8^aW1ZTI;VXBABn}%s6e$74Rl&NIl+w#Kt;JD&W@S>}Kf}sC(yGg* z!m*Z$bI2^$G!#>)R7t#K@SZHQ_}ex)S@+lEQunvE{LfmIpyRsuh+68AjP&DO5{2uY z1mSNTN3jP8!zViY$ah!hlWaZl&({Qk!AW?X5pM-L{3wKu8&ujljAk}3Bbzt0!Ai8W*2#)>6!_4@kM$^G5X`WyOc*~Q_uhw}G zuYzv;LINGcoFSoD)b1Ov_ckAe3NkhzLmrk9-rOd?q0`#YCc{ngk@2G51)xkc?O(xgW9NlAy z*j~ZbuSCM?8LaXO%e{H%n2+qbj^cr%(5;d*c`su#?tH}vJ;*JWXlBo}4_|7>7j)&4 zyYn}wipjFI?y^T3*T(pFb(PCeMiV628ZVlif~bV`g|oPpZ_CC#^E!GSO;{D*JYNHO zOYi$Vlt0$^1cq)``W84kqIyw@b)Dzt`PS7c=_2uPgQHq+N0r2 z#M+8tKc*Hrd-j`7RnV*9@?+Ye2HlX`Z_^&;cU0Ej&Lw(nDq~|@CmTB%FwZTwRaZ*D zndCL?O^@Z+#?;8H`|@4TzC-P1nvO|zS8^U%Ps=CQBV!pM`u)QBuv~{2{4GghzJYqn z5E|l2W`f*P5HsT_@nYsaQ3}kWOv-QJ27yqlD)D<}qN}@A^c(HF^0oW!+12U=4D{-6 z+uuK#IarP4aV0KC4arTXdPTQ6?l)-MLDB8OD?2)Ps-;?N*g)rArNB#dIZ z)rh|N`E+E`NAqI_tQKbppW*(`T?O5*rsL?U9|e3-X^HILn(P{4H-;;uj1jF_$TMm? zY@ym1v5IE%3FN{pC#c%yV49u6NqO(teuF70)a{~pKt<#C zpWxGhGd0*3^3V3|kBjrb6xFi@54hIE&REgW&fW>=6LYpPvNg1@{woRS4(JXb0gZ!v z#oa9Kfd>6fkBxoQMQ~)>ci}Ye?Pt^On+*ETWC0&x6$beA5pNUNuV$GS@0!U7X zNXS^3l7z>;HtuCwdd92F*IC&Gg+;|BrDf$6b@d+`8k?G10Hv-y zy?y;(2L{I{Ca0!nX6NP?);Bh{ws&^-_78wyll_neFU1ThUVyv?0}BTShJ&2N3kKHp zOeDmB1H?51%$rIOLkFzu><K96OP5DLKcf*3Y7L95b7;RDAjONp00cNV zI0*1Xzk+lH5a$0&clr$|mgv7ru*XL)18G734-5G8%l`nQ^M8QR^*_Mq{(D9`XHhH( zEqH?LnL&}=N3nHkA3p=wE&OdSk4_F}C*I*O?k}HlWa)h$Jf3lBln3Pn+;=^cI_Vi9 zSce~dBA}5cmu9IRB+z=Vq$JebFzT8=pE4gDCSsmcFIV2P56C_iK7?+Eb>3(n`fOHh zSMHc{M-*|?Olx9?p-;l!gfnr;L)?zz}V<7;ZY?ZR!-ZwTYE-U?1?>rsco`H0Oa_|ZYF*+)% zn{a42Y`R}9&T?|07|!1Dm~tlMP#iL*RH#=zBK;@|spTehghej5B)3As1cf$IThXAp z$BK{0mWnH~v$u%%J8UI!&|fV+r#*k0_~6M?&|un5VAYoVm|UuG{+@kS zt%5>GEr*3;vL6u@-eq}>Y!Y}6j3v@M1=aBR)KxAj)!iIa3f!j%d}OWLJ27>#*-S|l zaF>rJE6?gU&S$E658J0lT6haLr*H4d`n#<)<^rGPc7wINH^O&21|}Qy;nxLA8w>*a z%p?hwD=kW}Fm_^B^kf(+P$F-^CC9@hI6TmY;5d*%;C*ugKK2F%K3k|H?URg{u$su~ z&XgNjOk44K^4cx!qr<2!qetYz8wy`=0t#D$$83EE-Z!g!xy%K*>V;thG+&<7Nt$8O z;?1S@E~zfQRzAvPCq19~H?K<8onMAl@>iLy>xTAd?0b!m5*xps?NNOr-JQj(9&e*9 z-zu?uZ}24c_*wE)q?vD2O61G8ae5y<9|Q){=B*g7EB?iE2YlN5_r+kHVdrBfRp*J| zVg!E}X(mqbAn8X=S%Rh`B*eT}#wD{}E+QNDp1YncX^+_!Hdxh;<-D6tdXozqQ`eu! z=da`WSLIJIkrR?*2j|mmRy3IP^LvC>6S&LJ4e$ia7 zRXc}I>OyDZSjtK!&Zgw`-mmusiahgh2icFm;!GJYAqdB2XV!W%(135ptb>oN2JXX* z6C4TJG4;Jn(bpYAM|HWjoUH88-@Rv3qZNFA#(z;UlCDlh?h_M>^EW2-+~WNDk6J6F z_?>E6 z_ki_@csr{!fkxBqUC2mP<6_MS)q{5>b*9@AkS$)WBb_Evl9?X1-t6vFx;kYW-nTYH zcB;j4?#a0(^9su^TAFcq2bRWvTf=Epy|Xx9{`mTRjIN%7Y>Kr$Qal ztrbeNX&RCfYtpfsy6#_<9cPn6qThj>1xE|79^;@TjqVD z*KZqGkY09eFIFjdLLlQ&nCEebTVyq|+nBhIxeltshgEWh4N~>^1QB%?&eenRsU=v= zo()RTimRd{Z91E3!HG{T-5MEBAQ9F44YGPdB**WkDh+^K`OisRUyp-BfnFI<39w-O z$hUijM%Jb(_uQ>boz8L&<+C*ELsoo|@%I6)<|kfEVoH?Qb=MeN7hvck$>&AP(=aE$ zneK0dv$P4zyo}LYGT{WDj=^A7aD7XMAcT7Q8H@Bed~>iUy7LbCihIduhJb^DyVDzN zjm0mo%;W75(wv+`pV~;KmI=Plnp@t!t2As=j*_mk=h+=a`CyhLU?KvcCm^YoC;Q>G zmqj(1tNkDlr9#guD=RpkcyyP z9V_-Q!?v*I$TO^4VuWGuR1@*(Z&vDrU@`KQm=a0tZF|YX!iPPeTb&%)Um4KK9g=uS zQ7Ug9HR*cv?vp8Jljo|O z#8YF>5k;Qv#|(37gWNr0mR8Ez98YaQ4kJPwd2eJ`PqGG5$3LM_M$0bT ztQxv9$!tACz-?R(j`Kc{SB9BU1$`PFF=~ccES{cztU+4Nk+UEeKylmF^M=hqU(l5jCKX&w<5_?gDUYX(SWIXs z!YNjKeKiI9ZJFOSX~`Zp%bH$@VyVLkA;k-Sl3M;^L1UAm)^!1SiAk^t?W_6e9-nX# zZ(~;4iqgEIS=%O^(|3M!Lt|E-cD&pNcgFT%%aS?`zWDhM20B{N`eg>;^_7Xkrvg_+k#Vie&NpDxYdX*deKu?Juzg=aRgRM0g}DI78B?^hF1? zRQWSA9jps68QzH{M+>RYsyx{2Su$3`7O0nuI?hq7*rS%FW+Nr}M7;T61ujj(!NdNP zKJHD7=N-OHF2*n0tr!p8_@ z>roIM!861&`qvi<}=bMc#{EVw~Yzc?h#(b@(E5 zK#q28EbmdU`T`B?XsV#c6PI_P>BC4xr#`o?)k=nTkl2Q3;+?d8!>Mobsux2G{w8+= zP2XT^Xz>d%oDcVrr7g+MBi*n4sig!o$A<6MQr~xyxoSS}7wii_ZBxs*ne^gT##Lf> zOR9-Twp4;PVmPUKe!cIhIY71E?2>oH&sQI z-R>c)QQ0Ia=rnmjnI^5d#jU<-cPN)X3KWw6tX|`DwB*sigAn`tL3pWp8QVFU{%`#{ zuUhA;`{6egCRx!Rtum~WT_06xvWR(`(<9}}%Y=!kw`^&r?`eKDz-Bz>*@nsM=xyrq|4VUlCg%4J-oy&=Bs zde61w6m=;C;Y2nE&$O6v)>@CF-BO=P2wlN<$$)CF@zMD6zRZOsl4loldSGBl!GCAF z7XLiwfYJ?0e{i<@mzHedzgx0}vSOKYv0qUT}fE7|efzzObePO@_8fa{*5T1czUN{;3u1QWjp) zjdngz9GZN&aQvPeM?jtbMgH5u@!bZ&Kcat64lwS&LZ3B`|0Nj-_;>7|1ASpcs_&4MWV;9h2hBg^-Q8Uk+|&*z3f zgRh|x&?6QuA@2O`_yy>gKzD3jKoqRMhvUph@vj~wXz$SdYZu-jwm%Z$_ue*W0CXSL z1z_q2;I|$vXz#ywtXy~ofkNyrrNNJG7HHqU%Z- Date: Sat, 3 Aug 2024 12:18:00 -0700 Subject: [PATCH 10/14] build: modernize build process - Move to `pyproject.toml` - Update support to 3.8+ --- Makefile | 42 ++++++++------- pyproject.toml | 45 ++++++++++++++++ requirements-dev.txt | 5 ++ requirements-docs.txt | 5 ++ requirements-test.txt | 1 + setup.cfg | 0 setup.py | 89 -------------------------------- src/pptx/opc/package.py | 5 +- src/pptx/opc/serialized.py | 3 +- src/pptx/oxml/presentation.py | 2 +- src/pptx/oxml/shapes/graphfrm.py | 2 +- src/pptx/shapes/freeform.py | 3 +- src/pptx/spec.py | 2 +- tox.ini | 37 +------------ 14 files changed, 89 insertions(+), 152 deletions(-) create mode 100644 requirements-dev.txt create mode 100644 requirements-docs.txt delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/Makefile b/Makefile index 94891a89b..7a0fddb2e 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,55 @@ BEHAVE = behave MAKE = make -PYTHON = python -SETUP = $(PYTHON) ./setup.py -TWINE = $(PYTHON) -m twine - -.PHONY: accept build clean cleandocs coverage docs opendocs +.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 - $(SETUP) bdist_wheel sdist + 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 +.PHONY: test-upload test-upload: build - $(TWINE) upload --repository testpypi dist/* + twine upload --repository testpypi dist/* +.PHONY: upload upload: clean build - $(TWINE) upload dist/* + twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index 9868f5fc3..400cb6bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,45 @@ +[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 @@ -98,3 +140,6 @@ ignore = [ [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 index b542c1af7..9ddd60fd7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,3 +5,4 @@ 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 6d9a0d1a5..000000000 --- a/setup.py +++ /dev/null @@ -1,89 +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, "src", "pptx", "__init__.py") -readme = ascii_bytes_from(thisdir, "README.rst") -history = ascii_bytes_from(thisdir, "HISTORY.rst") - -# 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 = "https://github.com/scanny/python-pptx" -LICENSE = "MIT" -PACKAGES = find_packages(where="src") -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, - "long_description_content_type": "text/x-rst", - "author": AUTHOR, - "author_email": AUTHOR_EMAIL, - "url": URL, - "license": LICENSE, - "packages": PACKAGES, - "package_data": PACKAGE_DATA, - "package_dir": {"": "src"}, - "install_requires": INSTALL_REQUIRES, - "tests_require": TESTS_REQUIRE, - "test_suite": TEST_SUITE, - "classifiers": CLASSIFIERS, - "zip_safe": ZIP_SAFE, -} - -setup(**params) diff --git a/src/pptx/opc/package.py b/src/pptx/opc/package.py index 03ee5f43b..713759c54 100644 --- a/src/pptx/opc/package.py +++ b/src/pptx/opc/package.py @@ -7,8 +7,7 @@ from __future__ import annotations import collections -from collections.abc import Mapping -from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Set, cast +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 @@ -428,7 +427,7 @@ def part(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]) + return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId]) class PartFactory: diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py index 0942e33cb..92366708b 100644 --- a/src/pptx/opc/serialized.py +++ b/src/pptx/opc/serialized.py @@ -5,8 +5,7 @@ import os import posixpath import zipfile -from collections.abc import Container -from typing import IO, TYPE_CHECKING, Any, Sequence +from typing import IO, TYPE_CHECKING, Any, Container, Sequence from pptx.exc import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT diff --git a/src/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py index 254472493..17997c2b1 100644 --- a/src/pptx/oxml/presentation.py +++ b/src/pptx/oxml/presentation.py @@ -76,7 +76,7 @@ def _next_id(self) -> int: MIN_SLIDE_ID = 256 MAX_SLIDE_ID = 2147483647 - used_ids = [int(s) for s in cast(list[str], self.xpath("./p:sldId/@id"))] + 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 diff --git a/src/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py index cf32377c2..efa0b3632 100644 --- a/src/pptx/oxml/shapes/graphfrm.py +++ b/src/pptx/oxml/shapes/graphfrm.py @@ -112,7 +112,7 @@ def _oleObj(self) -> CT_OleObject | None: 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")) + oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj")) return oleObjs[-1] if oleObjs else None diff --git a/src/pptx/shapes/freeform.py b/src/pptx/shapes/freeform.py index e05b3484f..afe87385e 100644 --- a/src/pptx/shapes/freeform.py +++ b/src/pptx/shapes/freeform.py @@ -2,8 +2,7 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import TYPE_CHECKING, Iterable, Iterator +from typing import TYPE_CHECKING, Iterable, Iterator, Sequence from pptx.util import Emu, lazyproperty diff --git a/src/pptx/spec.py b/src/pptx/spec.py index 1e7bffb36..e9d3b7d58 100644 --- a/src/pptx/spec.py +++ b/src/pptx/spec.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias -AdjustmentValue: TypeAlias = tuple[str, int] +AdjustmentValue: TypeAlias = "tuple[str, int]" class ShapeSpec(TypedDict): diff --git a/tox.ini b/tox.ini index 223cc0ffe..37acaa5fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,42 +1,9 @@ -# -# Configuration for tox and pytest - -[flake8] -exclude = dist,docs,*.egg-info,.git,lab,ref,_scratch,spec,.tox -ignore = - # -- E203 - whitespace before ':'. Black disagrees for slice expressions. - E203, - - # -- W503 - line break before binary operator. Black has a different opinion about - # -- this, that binary operators should appear at the beginning of new-line - # -- expression segments. I agree because right is ragged and left lines up. - W503 -max-line-length = 88 - [tox] -envlist = py27, py38, py311 -requires = virtualenv<20.22.0 -skip_missing_interpreters = false +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 - pyparsing>=2.0.1 - pytest - XlsxWriter>=0.5.7 From 04a3e9d713c5f1306c331c4b54e4d6b408fac483 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 3 Aug 2024 12:24:14 -0700 Subject: [PATCH 11/14] release: prepare v1.0.0 release --- HISTORY.rst | 5 +++-- src/pptx/__init__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 73c3bacef..6f0835045 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,13 +3,14 @@ Release History --------------- -0.6.24-dev4 -+++++++++++++++++++ +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) diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 27d6306a1..3baec7376 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "0.6.24-dev4" +__version__ = "1.0.0" sys.modules["pptx.exceptions"] = exceptions del sys From 31955c0f4965b57ef0532f1ce97548cdfa5deea3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 3 Aug 2024 14:44:02 -0700 Subject: [PATCH 12/14] docs: update docs build --- .readthedocs.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..125538586 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +version: 2 + +# -- set the OS, Python version and other tools you might need -- +build: + os: ubuntu-22.04 + tools: + python: "3.9" + +# -- build documentation in the "docs/" directory with Sphinx -- +sphinx: + configuration: docs/conf.py + # -- fail on all warnings to avoid broken references -- + # fail_on_warning: true + +# -- package versions required to build your documentation -- +# -- see https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -- +python: + install: + - requirements: requirements-docs.txt From 0f980cd944a3b2cdf671f5907a931c755c397526 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 5 Aug 2024 10:01:49 -0700 Subject: [PATCH 13/14] fix(type): add py.typed Indicate availability of type annotations to type-checkers. --- HISTORY.rst | 6 ++++++ src/pptx/__init__.py | 2 +- src/pptx/py.typed | 0 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/pptx/py.typed diff --git a/HISTORY.rst b/HISTORY.rst index 6f0835045..b330da107 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +1.0.1 (2024-08-05) +++++++++++++++++++ + +- fix: #1000 add py.typed + + 1.0.0 (2024-08-03) ++++++++++++++++++ diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 3baec7376..aaf9cce06 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "1.0.0" +__version__ = "1.0.1" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/py.typed b/src/pptx/py.typed new file mode 100644 index 000000000..e69de29bb From 278b47b1dedd5b46ee84c286e77cdfb0bf4594be Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 6 Aug 2024 13:16:25 -0700 Subject: [PATCH 14/14] fix(enum): replace read-only enum values Some enum values like `MSO_SHAPE_TYPE.MIXED` are never produced by `python-pptx` and so were removed during the enum modernization in commit `01b86e64`. However, in at least one downstream system these values are referenced even though they can never actually occur. Replace these "read-only" values to avoid downstream breakage. --- HISTORY.rst | 5 +++++ src/pptx/__init__.py | 2 +- src/pptx/enum/chart.py | 7 +++++-- src/pptx/enum/dml.py | 12 ++++++++---- src/pptx/enum/lang.py | 3 +++ src/pptx/enum/shapes.py | 28 ++++++++++++++++++++++------ src/pptx/enum/text.py | 12 ++++++++++++ 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b330da107..e1c4e8faf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History --------------- +1.0.2 (2024-08-07) +++++++++++++++++++ + +- fix: #1003 restore read-only enum members + 1.0.1 (2024-08-05) ++++++++++++++++++ diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index aaf9cce06..fb5c2d7e4 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "1.0.1" +__version__ = "1.0.2" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/enum/chart.py b/src/pptx/enum/chart.py index 5e609ebd3..2599cf4dd 100644 --- a/src/pptx/enum/chart.py +++ b/src/pptx/enum/chart.py @@ -335,6 +335,9 @@ class XL_DATA_LABEL_POSITION(BaseXmlEnum): 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", @@ -370,8 +373,8 @@ class XL_LEGEND_POSITION(BaseXmlEnum): 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.") - """A custom position.""" + CUSTOM = (-4161, "", "A custom position (read-only).") + """A custom position (read-only).""" LEFT = (-4131, "l", "Left of the chart.") """Left of the chart.""" diff --git a/src/pptx/enum/dml.py b/src/pptx/enum/dml.py index e9a14b13f..40d5c5cdf 100644 --- a/src/pptx/enum/dml.py +++ b/src/pptx/enum/dml.py @@ -319,8 +319,8 @@ class MSO_PATTERN_TYPE(BaseXmlEnum): ZIG_ZAG = (38, "zigZag", "Zig Zag") """Zig Zag""" - MIXED = (-2, "", "Mixed pattern") - """Mixed pattern""" + MIXED = (-2, "", "Mixed pattern (read-only).") + """Mixed pattern (read-only).""" MSO_PATTERN = MSO_PATTERN_TYPE @@ -394,8 +394,12 @@ class MSO_THEME_COLOR_INDEX(BaseXmlEnum): 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.") - """Indicates multiple theme colors are used, such as in a group shape.""" + 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 index d2da5b6a5..a6bc1c8b4 100644 --- a/src/pptx/enum/lang.py +++ b/src/pptx/enum/lang.py @@ -680,3 +680,6 @@ class MSO_LANGUAGE_ID(BaseXmlEnum): 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 index b1dec8adc..86f521f40 100644 --- a/src/pptx/enum/shapes.py +++ b/src/pptx/enum/shapes.py @@ -748,6 +748,9 @@ class MSO_CONNECTOR_TYPE(BaseXmlEnum): 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 @@ -843,6 +846,9 @@ class MSO_SHAPE_TYPE(BaseEnum): WEB_VIDEO = (26, "Web video") """Web video""" + MIXED = (-2, "Multiple shape types (read-only).") + """Multiple shape types (read-only).""" + MSO = MSO_SHAPE_TYPE @@ -871,6 +877,16 @@ class PP_MEDIA_TYPE(BaseEnum): 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. @@ -937,14 +953,14 @@ class PP_PLACEHOLDER_TYPE(BaseXmlEnum): TITLE = (1, "title", "Title") """Title""" - VERTICAL_BODY = (6, "", "Vertical Body") - """Vertical Body""" + VERTICAL_BODY = (6, "", "Vertical Body (read-only).") + """Vertical Body (read-only).""" - VERTICAL_OBJECT = (17, "", "Vertical Object") - """Vertical Object""" + VERTICAL_OBJECT = (17, "", "Vertical Object (read-only).") + """Vertical Object (read-only).""" - VERTICAL_TITLE = (5, "", "Vertical Title") - """Vertical Title""" + 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.""" diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index 892385153..db266a3c9 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -57,6 +57,9 @@ class MSO_AUTO_SIZE(BaseEnum): ) """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): """ @@ -134,6 +137,9 @@ class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum): 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 @@ -161,6 +167,9 @@ class MSO_VERTICAL_ANCHOR(BaseXmlEnum): 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 @@ -214,5 +223,8 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): 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