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: ...