From 26a30f275352ed64d22b280fd6496be4e372ce09 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 10:27:01 -0700 Subject: [PATCH 01/69] rfctr: modernize docstrings and imports --- pptx/action.py | 14 +- pptx/chart/chart.py | 11 +- pptx/chart/xlsx.py | 15 +- pptx/opc/oxml.py | 17 +- pptx/opc/package.py | 239 ++++++++++++++------------- pptx/opc/packuri.py | 12 +- pptx/opc/shared.py | 15 +- pptx/package.py | 30 +--- pptx/parts/chart.py | 45 ++--- pptx/parts/coreprops.py | 26 +-- pptx/parts/image.py | 31 ++-- pptx/parts/media.py | 6 +- pptx/parts/presentation.py | 69 +++----- pptx/parts/slide.py | 41 ++--- pptx/shapes/placeholder.py | 75 ++++----- pptx/shapes/shapetree.py | 4 +- pptx/shared.py | 6 +- pptx/slide.py | 19 +-- pptx/table.py | 18 +- pptx/text/fonts.py | 43 ++--- pptx/text/layout.py | 31 ++-- pptx/text/text.py | 18 +- tests/chart/test_chart.py | 10 +- tests/chart/test_xlsx.py | 24 ++- tests/opc/test_oxml.py | 20 ++- tests/opc/test_package.py | 10 ++ tests/oxml/shapes/test_groupshape.py | 6 +- tests/parts/test_chart.py | 10 +- tests/parts/test_image.py | 8 +- tests/parts/test_presentation.py | 8 +- tests/parts/test_slide.py | 10 ++ tests/shapes/test_freeform.py | 14 +- tests/shapes/test_placeholder.py | 20 ++- tests/test_action.py | 10 +- tests/test_package.py | 12 +- tests/test_slide.py | 33 +++- tests/text/test_fonts.py | 18 +- tests/text/test_layout.py | 12 +- tests/text/test_text.py | 8 +- tests/unitutil/mock.py | 72 ++++---- 40 files changed, 535 insertions(+), 555 deletions(-) diff --git a/pptx/action.py b/pptx/action.py index 3ce6778cd..d3ea2ab4d 100644 --- a/pptx/action.py +++ b/pptx/action.py @@ -1,15 +1,11 @@ # encoding: utf-8 -""" -Objects related to mouse click and hover actions on a shape or text. -""" +"""Objects related to mouse click and hover actions on a shape or text.""" -from __future__ import absolute_import, division, print_function, unicode_literals - -from .enum.action import PP_ACTION -from .opc.constants import RELATIONSHIP_TYPE as RT -from .shapes import Subshape -from .util import lazyproperty +from pptx.enum.action import PP_ACTION +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.shapes import Subshape +from pptx.util import lazyproperty class ActionSetting(Subshape): diff --git a/pptx/chart/chart.py b/pptx/chart/chart.py index dee9c8d9f..be7edb16a 100644 --- a/pptx/chart/chart.py +++ b/pptx/chart/chart.py @@ -2,8 +2,6 @@ """Chart-related objects such as Chart and ChartTitle.""" -from __future__ import absolute_import, division, print_function, unicode_literals - from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis from pptx.chart.legend import Legend from pptx.chart.plot import PlotFactory, PlotTypeInspector @@ -79,11 +77,10 @@ def chart_title(self): @property def chart_type(self): - """ - Read-only :ref:`XlChartType` enumeration value specifying the type of - this chart. If the chart has two plots, for example, a line plot - overlayed on a bar plot, the type reported is for the first - (back-most) plot. + """Member of :ref:`XlChartType` enumeration specifying type of this chart. + + If the chart has two plots, for example, a line plot overlayed on a bar plot, + the type reported is for the first (back-most) plot. Read-only. """ first_plot = self.plots[0] return PlotTypeInspector.chart_type(first_plot) diff --git a/pptx/chart/xlsx.py b/pptx/chart/xlsx.py index 011e5a64d..17e1e4fc8 100644 --- a/pptx/chart/xlsx.py +++ b/pptx/chart/xlsx.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Chart builder and related objects. -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Chart builder and related objects.""" from contextlib import contextmanager @@ -14,9 +10,7 @@ class _BaseWorkbookWriter(object): - """ - Base class for workbook writers, providing shared members. - """ + """Base class for workbook writers, providing shared members.""" def __init__(self, chart_data): super(_BaseWorkbookWriter, self).__init__() @@ -24,10 +18,7 @@ def __init__(self, chart_data): @property def xlsx_blob(self): - """ - Return the byte stream of an Excel file formatted as chart data for - the category chart specified in the chart data object. - """ + """bytes for Excel file containing chart_data.""" xlsx_file = BytesIO() with self._open_worksheet(xlsx_file) as (workbook, worksheet): self._populate_worksheet(workbook, worksheet) diff --git a/pptx/opc/oxml.py b/pptx/opc/oxml.py index 3693b7775..321249687 100644 --- a/pptx/opc/oxml.py +++ b/pptx/opc/oxml.py @@ -1,13 +1,6 @@ # encoding: utf-8 -""" -Temporary stand-in for main oxml module that came across with the -PackageReader transplant. Probably much will get replaced with objects from -the pptx.oxml.core and then this module will either get deleted or only hold -the package related custom element classes. -""" - -from __future__ import absolute_import +"""OPC-local oxml module to handle OPC-local concerns like relationship parsing.""" from lxml import etree @@ -92,9 +85,7 @@ def new(cls, rId, reltype, target, target_mode=RTM.INTERNAL): class CT_Relationships(BaseOxmlElement): - """ - ```` element, the root element in a .rels file. - """ + """`` element, the root element in a .rels file.""" relationship = ZeroOrMore("pr:Relationship") @@ -109,9 +100,7 @@ def add_rel(self, rId, reltype, target, is_external=False): @classmethod def new(cls): - """ - Return a new ```` element. - """ + """Return a new ```` element.""" xml = '' % nsmap["pr"] relationships = parse_xml(xml) return relationships diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 564f9f112..4e5ed733f 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -17,10 +17,10 @@ class OpcPackage(object): - """ - Main API class for |python-opc|. A new instance is constructed by calling - the :meth:`open` class method with a path to a package file or file-like - object containing one. + """Main API class for |python-opc|. + + A new instance is constructed by calling the :meth:`open` classmethod with a path + to a package file or file-like object containing a package (.pptx file). """ def __init__(self): @@ -36,10 +36,7 @@ def after_unmarshal(self): pass def iter_parts(self): - """ - Generate exactly one reference to each of the parts in the package by - performing a depth-first traversal of the rels graph. - """ + """Generate exactly one reference to each part in the package.""" def walk_parts(source, visited=list()): for rel in source.rels.values(): @@ -58,22 +55,27 @@ def walk_parts(source, visited=list()): yield part def iter_rels(self): - """ - Generate exactly one reference to each relationship in the package by - performing a depth-first traversal of the rels graph. + """Generate exactly one reference to each relationship in package. + + Performs a depth-first traversal of the rels graph. """ def walk_rels(source, visited=None): visited = [] if visited is None else visited for rel in source.rels.values(): yield rel + # --- external items can have no relationships --- if rel.is_external: continue + # --- all relationships other than those for the package belong to a + # --- part. Once that part has been processed, processing it again + # --- would lead to the same relationships appearing more than once. part = rel.target_part if part in visited: continue visited.append(part) new_source = part + # --- recurse into relationships of each unvisited target-part --- for rel in walk_rels(new_source, visited): yield rel @@ -93,20 +95,18 @@ def load_rel(self, reltype, target, rId, is_external=False): @property def main_document_part(self): - """ - Return a reference to the main document part for this package. - Examples include a document part for a WordprocessingML package, a - presentation part for a PresentationML package, or a workbook part - for a SpreadsheetML package. + """Return |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) def next_partname(self, tmpl): - """ - Return a |PackURI| instance representing the next available partname - matching *tmpl*, which is a printf (%)-style template string - containing a single replacement item, a '%d' to be used to insert the - integer portion of the partname. Example: '/ppt/slides/slide%d.xml' + """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' """ partnames = [part.partname for part in self.iter_parts()] for n in range(1, len(partnames) + 2): @@ -117,20 +117,17 @@ def next_partname(self, tmpl): @classmethod def open(cls, pkg_file): - """ - Return an |OpcPackage| instance loaded with the contents of - *pkg_file*. - """ + """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" pkg_reader = PackageReader.from_file(pkg_file) package = cls() Unmarshaller.unmarshal(pkg_reader, package, PartFactory) return package def part_related_by(self, reltype): - """ - Return part to which this package has a relationship of *reltype*. - Raises |KeyError| if no such relationship is found and |ValueError| - if more than one such relationship is found. + """Return (single) part having relationship to this package of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than + one such relationship is found. """ return self.rels.part_with_reltype(reltype) @@ -143,9 +140,10 @@ def parts(self): return [part for part in self.iter_parts()] def relate_to(self, part, reltype): - """ - Return rId key of relationship to *part*, from the existing - relationship if there is one, otherwise a newly created one. + """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. """ rel = self.rels.get_or_add(reltype, part) return rel.rId @@ -159,9 +157,9 @@ def rels(self): return RelationshipCollection(PACKAGE_URI.baseURI) def save(self, pkg_file): - """ - Save this package to *pkg_file*, where *file* can be either a path to - a file (a string) or a file-like object. + """Save this package to `pkg_file`. + + `pkg_file` can be either a path to a file (a string) or a file-like object. """ for part in self.parts: part.before_marshal() @@ -169,10 +167,11 @@ def save(self, pkg_file): class Part(object): - """ - Base class for package parts. Provides common properties and methods, but - intended to be subclassed in client code to implement specific part - behaviors. + """Base class for package parts. + + Provides common properties and methods, but intended to be subclassed in client code + to implement specific part behaviors. Also serves as the default class for parts + that are not yet given specific behaviors. """ def __init__(self, partname, content_type, blob=None, package=None): @@ -206,31 +205,35 @@ def before_marshal(self): @property def blob(self): - """ - Contents of this package part as a sequence of bytes. May be text or - binary. Intended to be overridden by subclasses. Default behavior is - to return load blob. + """Contents of this package part as a sequence of bytes. + + May be text (XML generally) or binary. Intended to be overridden by subclasses. + Default behavior is to return the blob initial loaded during `Package.open()` + operation. """ return self._blob @blob.setter def blob(self, bytes_): - """ - Note that not all subclasses use the part blob as their blob source. - In particular, the |XmlPart| subclass uses its `self._element` to - serialize a blob on demand. This works find for binary parts though. + """Note that not all subclasses use the part blob as their blob source. + + In particular, the |XmlPart| subclass uses its `self._element` to serialize a + blob on demand. This works fine for binary parts though. """ self._blob = bytes_ @property def content_type(self): - """ - Content type of this part. - """ + """Content-type (MIME-type) of this part.""" return self._content_type @classmethod def load(cls, partname, content_type, blob, package): + """Return `cls` instance loaded from arguments. + + This one is a straight pass-through, but subtypes may do some pre-processing, + see XmlPart for an example. + """ return cls(partname, content_type, blob, package) def load_rel(self, reltype, target, rId, is_external=False): @@ -246,17 +249,12 @@ def load_rel(self, reltype, target, rId, is_external=False): @property def package(self): - """ - |OpcPackage| instance this part belongs to. - """ + """|OpcPackage| instance this part belongs to.""" return self._package @property def partname(self): - """ - |PackURI| instance holding partname of this part, e.g. - '/ppt/slides/slide1.xml' - """ + """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" return self._partname @partname.setter @@ -269,27 +267,27 @@ def partname(self, partname): # relationship management interface for child objects ------------ def drop_rel(self, rId): - """ - Remove the relationship identified by *rId* if its reference count - is less than 2. Relationships with a reference count of 0 are - implicit relationships. + """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: del self.rels[rId] def part_related_by(self, reltype): - """ - Return part to which this part has a relationship of *reltype*. - Raises |KeyError| if no such relationship is found and |ValueError| - if more than one such relationship is found. Provides ability to - resolve implicitly related part, such as Slide -> SlideLayout. + """Return (single) part having relationship to this part of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than + one such relationship is found. """ return self.rels.part_with_reltype(reltype) def relate_to(self, target, reltype, is_external=False): - """ - Return rId key of relationship of *reltype* to *target*, from an - existing relationship if there is one, otherwise a newly created one. + """Return rId key of relationship of `reltype` to `target`. + + If such a relationship already exists, its rId is returned. Otherwise the + relationship is added and its new rId returned. """ if is_external: return self.rels.get_or_add_ext_rel(reltype, target) @@ -308,17 +306,11 @@ def related_parts(self): @lazyproperty def rels(self): - """ - |RelationshipCollection| instance holding the relationships for this - part. - """ + """|Relationships| object containing relationships from this part to others.""" return RelationshipCollection(self._partname.baseURI) def target_ref(self, rId): - """ - Return URL contained in target ref of relationship identified by - *rId*. - """ + """Return URL contained in target ref of relationship identified by `rId`.""" rel = self.rels[rId] return rel.target_ref @@ -336,20 +328,16 @@ def _blob_from_file(self, file): return file.read() def _rel_ref_count(self, rId): - """ - Return the count of references in this part's XML to the relationship - identified by *rId*. - """ + """Return int count of references in this part's XML to `rId`.""" rIds = self._element.xpath("//@r:id") return len([_rId for _rId in rIds if _rId == rId]) class XmlPart(Part): - """ - Base class for package parts containing an XML payload, which is most of - them. Provides additional methods to the |Part| base class that take care - of parsing and reserializing the XML payload and managing relationships - to other parts. + """Base class for package parts containing an XML payload, which is most of them. + + Provides additional methods to the |Part| base class that take care of parsing and + reserializing the XML payload and managing relationships to other parts. """ def __init__(self, partname, content_type, element, package=None): @@ -358,27 +346,31 @@ def __init__(self, partname, content_type, element, package=None): @property def blob(self): + """bytes XML serialization of this part.""" return serialize_part_xml(self._element) @classmethod def load(cls, partname, content_type, blob, package): + """Return instance of `cls` loaded with parsed XML from `blob`.""" element = parse_xml(blob) return cls(partname, content_type, element, package) @property def part(self): - """ - Part of the parent protocol, "children" of the document will not know - the part that contains them so must ask their parent object. That - chain of delegation ends here for child objects. + """This part. + + This is part of the parent protocol, "children" of the document will not know + the part that contains them so must ask their parent object. That chain of + delegation ends here for child objects. """ return self class PartFactory(object): - """ - Provides a way for client code to specify a subclass of |Part| to be - constructed by |Unmarshaller| based on its content type. + """Constructs a registered subtype of |Part|. + + Client code can register a subclass of |Part| to be used for a package blob based on + its content type. """ part_type_for = {} @@ -390,10 +382,9 @@ def __new__(cls, partname, content_type, blob, package): @classmethod def _part_cls_for(cls, content_type): - """ - Return the custom part class registered for *content_type*, or the - default part class if no custom class is registered for - *content_type*. + """Return the custom part class registered for `content_type`. + + Returns |Part| if no custom class is registered for `content_type`. """ if content_type in cls.part_type_for: return cls.part_type_for[content_type] @@ -401,8 +392,14 @@ def _part_cls_for(cls, content_type): class RelationshipCollection(dict): - """ - Collection object for |_Relationship| instances, having list semantics. + """Collection of |_Relationship| instances, largely having dict semantics. + + Relationships are keyed by their rId, but may also be found in other ways, such as + by their relationship type. `rels` is a dict of |Relationship| objects keyed by + their rId. + + Note that iterating this collection generates |Relationship| references (values), + not rIds (keys) as it would for a dict. """ def __init__(self, baseURI): @@ -443,10 +440,10 @@ def get_or_add_ext_rel(self, reltype, target_ref): return rel.rId def part_with_reltype(self, reltype): - """ - Return target part of rel with matching *reltype*, raising |KeyError| - if not found and |ValueError| if more than one matching relationship - is found. + """Return target part of relationship with matching `reltype`. + + Raises |KeyError| if not found and |ValueError| if more than one matching + relationship is found. """ rel = self._get_rel_of_type(reltype) return rel.target_part @@ -461,9 +458,10 @@ def related_parts(self): @property def xml(self): - """ - Serialize this relationship collection into XML suitable for storage - as a .rels file in an OPC package. + """bytes XML serialization of this relationship collection. + + This value is suitable for storage as a .rels file in an OPC package. Includes + a `` element. + """Optional |EmbeddedXlsxPart| object containing data for this chart. + + This related part has its rId at `c:chartSpace/c:externalData/@rId`. This value + is |None| if there is no `` element. """ xlsx_part_rId = self._chartSpace.xlsx_part_rId if xlsx_part_rId is None: diff --git a/pptx/parts/coreprops.py b/pptx/parts/coreprops.py index 5cfdb4301..7d1795bff 100644 --- a/pptx/parts/coreprops.py +++ b/pptx/parts/coreprops.py @@ -1,27 +1,28 @@ # encoding: utf-8 -""" -Core properties part, corresponds to ``/docProps/core.xml`` part in package. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" from datetime import datetime -from ..opc.constants import CONTENT_TYPE as CT -from ..opc.package import XmlPart -from ..opc.packuri import PackURI -from ..oxml.coreprops import CT_CoreProperties +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 class CorePropertiesPart(XmlPart): - """ - Corresponds to part named ``/docProps/core.xml``, containing the core - document properties for this document package. + """Corresponds to part named `/docProps/core.xml`. + + Contains the core document properties for this document package. """ @classmethod def default(cls): + """Return default new |CorePropertiesPart| instance suitable as starting point. + + This provides a base for adding core-properties to a package that doesn't yet + have any. + """ core_props = cls._new() core_props.title = "PowerPoint Presentation" core_props.last_modified_by = "python-pptx" @@ -151,6 +152,7 @@ def version(self, value): @classmethod def _new(cls): + """Return new empty |CorePropertiesPart| instance.""" partname = PackURI("/docProps/core.xml") content_type = CT.OPC_CORE_PROPERTIES core_props_elm = CT_CoreProperties.new_coreProperties() diff --git a/pptx/parts/image.py b/pptx/parts/image.py index cb4c74489..1ce2f8337 100644 --- a/pptx/parts/image.py +++ b/pptx/parts/image.py @@ -2,7 +2,7 @@ """ImagePart and related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import division import hashlib import os @@ -12,16 +12,17 @@ except ImportError: import Image as PIL_Image -from ..compat import BytesIO, is_string -from ..opc.package import Part -from ..opc.spec import image_content_types -from ..util import lazyproperty +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 class ImagePart(Part): - """ - An image part, generally having a partname matching the regex - ``ppt/media/image[1-9][0-9]*.*``. + """An image part. + + An image part generally has a partname matching the regex + `ppt/media/image[1-9][0-9]*.*`. """ def __init__(self, partname, content_type, blob, package, filename=None): @@ -34,9 +35,9 @@ def load(cls, partname, content_type, blob, package): @classmethod def new(cls, package, image): - """ - Return a new |ImagePart| instance containing *image*, which is an - |Image| object. + """Return new |ImagePart| instance containing `image`. + + `image` is an |Image| object. """ partname = package.next_image_partname(image.ext) return cls(partname, image.content_type, image.blob, package, image.filename) @@ -137,9 +138,7 @@ def _px_size(self): class Image(object): - """ - Immutable value object representing an image such as a JPEG, PNG, or GIF. - """ + """Immutable value object representing an image such as a JPEG, PNG, or GIF.""" def __init__(self, blob, filename): super(Image, self).__init__() @@ -148,9 +147,7 @@ def __init__(self, blob, filename): @classmethod def from_blob(cls, blob, filename=None): - """ - Return a new |Image| object loaded from the image binary in *blob*. - """ + """Return a new |Image| object loaded from the image binary in *blob*.""" return cls(blob, filename) @classmethod diff --git a/pptx/parts/media.py b/pptx/parts/media.py index 4d4fa2300..768d890e0 100644 --- a/pptx/parts/media.py +++ b/pptx/parts/media.py @@ -2,12 +2,10 @@ """MediaPart and related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals - import hashlib -from ..opc.package import Part -from ..util import lazyproperty +from pptx.opc.package import Part +from pptx.util import lazyproperty class MediaPart(Part): diff --git a/pptx/parts/presentation.py b/pptx/parts/presentation.py index fed4b0f64..8e3a9a228 100644 --- a/pptx/parts/presentation.py +++ b/pptx/parts/presentation.py @@ -1,23 +1,19 @@ # encoding: utf-8 -""" -Presentation part, the main part in a .pptx package. -""" +"""Presentation part, the main part in a .pptx package.""" -from __future__ import absolute_import - -from ..opc.constants import RELATIONSHIP_TYPE as RT -from ..opc.package import XmlPart -from ..opc.packuri import PackURI -from ..presentation import Presentation -from .slide import NotesMasterPart, SlidePart -from ..util import lazyproperty +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.package import XmlPart +from pptx.opc.packuri import PackURI +from pptx.parts.slide import NotesMasterPart, SlidePart +from pptx.presentation import Presentation +from pptx.util import lazyproperty class PresentationPart(XmlPart): - """ - Top level class in object model, represents the contents of the /ppt - directory of a .pptx file. + """Top level class in object model. + + Represents the contents of the /ppt directory of a .pptx file. """ def add_slide(self, slide_layout): @@ -40,9 +36,9 @@ def core_properties(self): return self.package.core_properties def get_slide(self, slide_id): - """ - Return the |Slide| object identified by *slide_id* (in this - presentation), or |None| if not found. + """Return optional related |Slide| object identified by `slide_id`. + + Returns |None| if no slide with `slide_id` is related to this presentation. """ for sldId in self._element.sldIdLst: if sldId.id == slide_id: @@ -83,25 +79,19 @@ def presentation(self): return Presentation(self._element, self) def related_slide(self, rId): - """ - Return the |Slide| object for the related |SlidePart| corresponding - to relationship key *rId*. - """ + """Return |Slide| object for related |SlidePart| related by `rId`.""" return self.related_parts[rId].slide def related_slide_master(self, rId): - """ - Return the |SlideMaster| object for the related |SlideMasterPart| - corresponding to relationship key *rId*. - """ + """Return |SlideMaster| object for |SlideMasterPart| related by `rId`.""" return self.related_parts[rId].slide_master def rename_slide_parts(self, rIds): - """ - Assign incrementing partnames like ``/ppt/slides/slide9.xml`` to the - slide parts identified by *rIds*, in the order their id appears in - that sequence. The name portion is always ``slide``. The number part - forms a continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). + """Assign incrementing partnames to the slide parts identified by `rIds`. + + Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their + id appears in the `rIds` sequence. The name portion is always ``slide``. The + number part forms a continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always ``.xml``. """ for idx, rId in enumerate(rIds): @@ -109,18 +99,15 @@ def rename_slide_parts(self, rIds): slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1)) def save(self, path_or_stream): - """ - Save this presentation package to *path_or_stream*, which can be - either a path to a filesystem location (a string) or a file-like - object. + """Save this presentation package to `path_or_stream`. + + `path_or_stream` can be either a path to a filesystem location (a string) or a + file-like object. """ self.package.save(path_or_stream) def slide_id(self, slide_part): - """ - Return the slide identifier associated with *slide_part* in this - presentation. - """ + """Return the slide-id associated with `slide_part`.""" for sldId in self._element.sldIdLst: if self.related_parts[sldId.rId] is slide_part: return sldId.id @@ -128,11 +115,7 @@ def slide_id(self, slide_part): @property def _next_slide_partname(self): - """ - Return |PackURI| instance containing the partname for a slide to be - appended to this slide collection, e.g. ``/ppt/slides/slide9.xml`` - for a slide collection containing 8 slides. - """ + """Return |PackURI| instance containing next available slide partname.""" sldIdLst = self._element.get_or_add_sldIdLst() partname_str = "/ppt/slides/slide%d.xml" % (len(sldIdLst) + 1) return PackURI(partname_str) diff --git a/pptx/parts/slide.py b/pptx/parts/slide.py index 1f83c9689..faf7be18e 100644 --- a/pptx/parts/slide.py +++ b/pptx/parts/slide.py @@ -30,12 +30,11 @@ def get_image(self, rId): return self.related_parts[rId].image def get_or_add_image_part(self, image_file): - """ - Return an ``(image_part, rId)`` 2-tuple corresponding to an - |ImagePart| object containing the image in *image_file*, and related - to this slide with the key *rId*. If either the image part or - relationship already exists, they are reused, otherwise they are - newly created. + """Return `(image_part, rId)` pair corresponding to `image_file`. + + The returned |ImagePart| object contains the image in `image_file` and is + related to this slide with the key `rId`. If either the image part or + relationship already exists, they are reused, otherwise they are newly created. """ image_part = self._package.get_or_add_image_part(image_file) rId = self.relate_to(image_part, RT.IMAGE) @@ -86,10 +85,7 @@ def _new(cls, package): @classmethod def _new_theme_part(cls, package): - """ - Create and return a default theme part suitable for use with a notes - master. - """ + """Return new default theme-part suitable for use with a notes master.""" partname = package.next_partname("/ppt/theme/theme%d.xml") content_type = CT.OFC_THEME theme = CT_OfficeStyleSheet.new_default() @@ -105,10 +101,11 @@ class NotesSlidePart(BaseSlidePart): @classmethod def new(cls, package, slide_part): - """ - Create and return a new notes slide part based on the notes master - and related to both the notes master part and *slide_part*. If no - notes master is present, create one based on the default template. + """Return new |NotesSlidePart| for the slide in `slide_part`. + + The new notes-slide part is based on the (singleton) notes master and related to + both the notes-master part and `slide_part`. If no notes-master is present, + one is created based on the default template. """ notes_master_part = package.presentation_part.notes_master_part notes_slide_part = cls._add_notes_slide_part( @@ -120,17 +117,13 @@ def new(cls, package, slide_part): @lazyproperty def notes_master(self): - """ - Return the |NotesMaster| object this notes slide inherits from. - """ + """Return the |NotesMaster| object this notes slide inherits from.""" notes_master_part = self.part_related_by(RT.NOTES_MASTER) return notes_master_part.notes_master @lazyproperty def notes_slide(self): - """ - Return the |NotesSlide| object that proxies this notes slide part. - """ + """Return the |NotesSlide| object that proxies this notes slide part.""" return NotesSlide(self._element, self) @classmethod @@ -163,10 +156,10 @@ def new(cls, partname, package, slide_layout_part): return slide_part def add_chart_part(self, chart_type, chart_data): - """ - Return the rId of a new |ChartPart| object containing a chart of - *chart_type*, displaying *chart_data*, and related to the slide - contained in this part. + """Return str rId of new |ChartPart| object containing chart of `chart_type`. + + The chart depicts `chart_data` and is related to the slide contained in this + part by `rId`. """ chart_part = ChartPart.new(chart_type, chart_data, self.package) rId = self.relate_to(chart_part, RT.CHART) diff --git a/pptx/shapes/placeholder.py b/pptx/shapes/placeholder.py index 8f1bd8efa..791d91e13 100644 --- a/pptx/shapes/placeholder.py +++ b/pptx/shapes/placeholder.py @@ -1,21 +1,19 @@ # encoding: utf-8 -""" -Placeholder-related objects, specific to shapes having a `p:ph` element. -A placeholder has distinct behaviors depending on whether it appears on -a slide, layout, or master. Hence there is a non-trivial class inheritance -structure. -""" +"""Placeholder-related objects. -from __future__ import absolute_import, division, print_function, unicode_literals +Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors +depending on whether it appears on a slide, layout, or master. Hence there is a +non-trivial class inheritance structure. +""" -from .autoshape import Shape -from ..enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER -from .graphfrm import GraphicFrame -from ..oxml.shapes.graphfrm import CT_GraphicalObjectFrame -from ..oxml.shapes.picture import CT_Picture -from .picture import Picture -from ..util import Emu +from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER +from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame +from pptx.oxml.shapes.picture import CT_Picture +from pptx.shapes.autoshape import Shape +from pptx.shapes.graphfrm import GraphicFrame +from pptx.shapes.picture import Picture +from pptx.util import Emu class _InheritsDimensions(object): @@ -122,9 +120,9 @@ def _inherited_value(self, attr_name): class _BaseSlidePlaceholder(_InheritsDimensions, Shape): - """ - Base class for placeholders on slides. Provides common behaviors such as - inherited dimensions. + """Base class for placeholders on slides. + + Provides common behaviors such as inherited dimensions. """ @property @@ -274,9 +272,7 @@ class SlidePlaceholder(_BaseSlidePlaceholder): class ChartPlaceholder(_BaseSlidePlaceholder): - """ - Placeholder shape that can only accept a chart. - """ + """Placeholder shape that can only accept a chart.""" def insert_chart(self, chart_type, chart_data): """ @@ -309,18 +305,15 @@ def _new_chart_graphicFrame(self, rId, x, y, cx, cy): class PicturePlaceholder(_BaseSlidePlaceholder): - """ - Placeholder shape that can only accept a picture. - """ + """Placeholder shape that can only accept a picture.""" def insert_picture(self, image_file): - """ - Return a |PlaceholderPicture| object depicting the image in - *image_file*, which may be either a path (string) or a file-like - object. The image is cropped to fill the entire space of the - placeholder. A |PlaceholderPicture| object has all the properties and - methods of a |Picture| shape except that the value of its - :attr:`~._BaseSlidePlaceholder.shape_type` property is + """Return a |PlaceholderPicture| object depicting the image in `image_file`. + + `image_file` may be either a path (string) or a file-like object. The image is + cropped to fill the entire space of the placeholder. A |PlaceholderPicture| + object has all the properties and methods of a |Picture| shape except that the + value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`. """ pic = self._new_placeholder_pic(image_file) @@ -379,21 +372,17 @@ def _base_placeholder(self): class TablePlaceholder(_BaseSlidePlaceholder): - """ - Placeholder shape that can only accept a picture. - """ + """Placeholder shape that can only accept a table.""" def insert_table(self, rows, cols): - """ - Return a |PlaceholderGraphicFrame| object containing a table of - *rows* rows and *cols* columns. The position and width of the table - are those of the placeholder and its height is proportional to the - number of rows. A |PlaceholderGraphicFrame| object has all the - properties and methods of a |GraphicFrame| shape except that the - value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is - unconditionally `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return - value is not the new table but rather *contains* the new table. The - table can be accessed using the + """Return |PlaceholderGraphicFrame| object containing a `rows` by `cols` table. + + The position and width of the table are those of the placeholder and its height + is proportional to the number of rows. A |PlaceholderGraphicFrame| object has + all the properties and methods of a |GraphicFrame| shape except that the value + of its :attr:`~._BaseSlidePlaceholder.shape_type` property is unconditionally + `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return value is not the new table + but rather *contains* the new table. The table can be accessed using the :attr:`~.PlaceholderGraphicFrame.table` property of the returned |PlaceholderGraphicFrame| object. """ diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index 5837aa952..3153b51d3 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -97,9 +97,7 @@ def __len__(self): return len(shape_elms) def clone_placeholder(self, placeholder): - """ - Add a new placeholder shape based on *placeholder*. - """ + """Add a new placeholder shape based on *placeholder*.""" sp = placeholder.element ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx) id_ = self._next_shape_id diff --git a/pptx/shared.py b/pptx/shared.py index 27dbba5de..899438297 100644 --- a/pptx/shared.py +++ b/pptx/shared.py @@ -1,10 +1,8 @@ # encoding: utf-8 -""" -Objects shared by pptx modules. -""" +"""Objects shared by pptx modules.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import unicode_literals class ElementProxy(object): diff --git a/pptx/slide.py b/pptx/slide.py index ea0a3c246..ea1a3622f 100644 --- a/pptx/slide.py +++ b/pptx/slide.py @@ -2,8 +2,6 @@ """Slide-related objects, including masters, layouts, and notes.""" -from __future__ import absolute_import, division, print_function, unicode_literals - from pptx.dml.fill import FillFormat from pptx.enum.shapes import PP_PLACEHOLDER from pptx.shapes.shapetree import ( @@ -87,19 +85,20 @@ class NotesMaster(_BaseMaster): class NotesSlide(_BaseSlide): - """ - Notes slide object. Provides access to slide notes placeholder and other - shapes on the notes handout page. + """Notes slide object. + + Provides access to slide notes placeholder and other shapes on the notes handout + page. """ __slots__ = ("_placeholders", "_shapes") def clone_master_placeholders(self, notes_master): - """ - Selectively add placeholder shape elements from *notes_master* to the - shapes collection of this notes slide. Z-order of placeholders is - preserved. Certain placeholders (header, date, footer) are not - cloned. + """Selectively add placeholder shape elements from *notes_master*. + + Selected placeholder shape elements from *notes_master* are added to the shapes + collection of this notes slide. Z-order of placeholders is preserved. Certain + placeholders (header, date, footer) are not cloned. """ def iter_cloneable_placeholders(notes_master): diff --git a/pptx/table.py b/pptx/table.py index 2de17175f..63872eab8 100644 --- a/pptx/table.py +++ b/pptx/table.py @@ -2,8 +2,6 @@ """Table-related objects such as Table and Cell.""" -from __future__ import absolute_import, division, print_function, unicode_literals - from pptx.compat import is_integer from pptx.dml.fill import FillFormat from pptx.oxml.table import TcRange @@ -34,10 +32,10 @@ def cell(self, row_idx, col_idx): @lazyproperty def columns(self): - """ - Read-only reference to collection of |_Column| objects representing - the table's columns. |_Column| objects are accessed using list - notation, e.g. ``col = tbl.columns[0]``. + """|_ColumnCollection| instance for this table. + + Provides access to |_Column| objects representing the table's columns. |_Column| + objects are accessed using list notation, e.g. ``col = tbl.columns[0]``. """ return _ColumnCollection(self._tbl, self) @@ -137,10 +135,10 @@ def part(self): @lazyproperty def rows(self): - """ - Read-only reference to collection of |_Row| objects representing the - table's rows. |_Row| objects are accessed using list notation, e.g. - ``col = tbl.rows[0]``. + """|_RowCollection| instance for this table. + + Provides access to |_Row| objects representing the table's rows. |_Row| objects + are accessed using list notation, e.g. ``col = tbl.rows[0]``. """ return _RowCollection(self._tbl, self) diff --git a/pptx/text/fonts.py b/pptx/text/fonts.py index ceb070cc8..ebc5b7d49 100644 --- a/pptx/text/fonts.py +++ b/pptx/text/fonts.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Objects related to system font file lookup. -""" - -from __future__ import absolute_import, print_function +"""Objects related to system font file lookup.""" import os import sys @@ -156,9 +152,9 @@ def family_name(self): @lazyproperty def _fields(self): - """ - A 5-tuple containing the fields read from the font file header, also - known as the offset table. + """5-tuple containing the fields read from the font file header. + + Also known as the offset table. """ # sfnt_version, tbl_count, search_range, entry_selector, range_shift return self._stream.read_fields(">4sHHHH", 0) @@ -196,20 +192,14 @@ def _table_count(self): class _Stream(object): - """ - A thin wrapper around a file that facilitates reading C-struct values - from a binary file. - """ + """A thin wrapper around a binary file that facilitates reading C-struct values.""" def __init__(self, file): self._file = file @classmethod def open(cls, path): - """ - Return a |_Stream| providing binary access to the contents of the - file at *path*. - """ + """Return |_Stream| providing binary access to contents of file at `path`.""" return cls(open(path, "rb")) def close(self): @@ -328,9 +318,9 @@ def _decode_name(raw_name, platform_id, encoding_id): return None def _iter_names(self): - """ - Generate a key/value pair for each name in this table. The key is a - (platform_id, name_id) 2-tuple and the value is the unicode text + """Generate a key/value pair for each name in this table. + + The key is a (platform_id, name_id) 2-tuple and the value is the unicode text corresponding to that key. """ table_format, count, strings_offset = self._table_header @@ -364,11 +354,11 @@ def _raw_name_string(bufr, strings_offset, str_offset, length): return unpack_from(tmpl, bufr, offset)[0] def _read_name(self, bufr, idx, strings_offset): - """ - Return a (platform_id, name_id, name) 3-tuple like (0, 1, 'Arial') - for the name at *idx* position in *bufr*. *strings_offset* is the - index into *bufr* where actual name strings begin. The returned name - is a unicode string. + """Return a (platform_id, name_id, name) 3-tuple for name at `idx` in `bufr`. + + The triple looks like (0, 1, 'Arial'). `strings_offset` is the for the name at + `idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual + name strings begin. The returned name is a unicode string. """ platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header( bufr, idx @@ -405,10 +395,7 @@ def _table_header(self): @lazyproperty def _names(self): - """ - A mapping of (platform_id, name_id) keys to string names for this - font. - """ + """A mapping of (platform_id, name_id) keys to string names for this font.""" return dict(self._iter_names()) diff --git a/pptx/text/layout.py b/pptx/text/layout.py index 290b83a5a..69aa6f678 100644 --- a/pptx/text/layout.py +++ b/pptx/text/layout.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Objects related to layout of rendered text, such as TextFitter. -""" - -from __future__ import absolute_import, print_function +"""Objects related to layout of rendered text, such as TextFitter.""" from PIL import ImageFont @@ -20,10 +16,11 @@ def __new__(cls, line_source, extents, font_file): @classmethod def best_fit_font_size(cls, text, extents, max_size, font_file): - """ - Return the largest whole-number point size less than or equal to - *max_size* that allows *text* to fit completely within *extents* when - rendered using font defined in *font_file*. + """Return whole-number best fit point size less than or equal to `max_size`. + + The return value is the largest whole-number point size less than or equal to + `max_size` that allows `text` to fit completely within `extents` when rendered + using font defined in `font_file`. """ line_source = _LineSource(text) text_fitter = cls(line_source, extents, font_file) @@ -67,17 +64,17 @@ def predicate(line): @property def _fits_inside_predicate(self): - """ - Return a function taking an integer point size argument that returns - |True| if the text in this fitter can be wrapped to fit entirely - within its extents when rendered at that point size. + """Return function taking an integer point size argument. + + The function returns |True| if the text in this fitter can be wrapped to fit + entirely within its extents when rendered at that point size. """ def predicate(point_size): - """ - Return |True| if the text in *line_source* can be wrapped to fit - entirely within *extents* when rendered at *point_size* using the - font defined in *font_file*. + """Return |True| when text in `line_source` can be wrapped to fit. + + Fit means text can be broken into lines that fit entirely within `extents` + when rendered at `point_size` using the font defined in `font_file`. """ text_lines = self._wrap_lines(self._line_source, point_size) cy = _rendered_size("Ty", point_size, self._font_file)[1] diff --git a/pptx/text/text.py b/pptx/text/text.py index b880cf4ec..7cf3a59f1 100644 --- a/pptx/text/text.py +++ b/pptx/text/text.py @@ -2,8 +2,6 @@ """Text-related objects such as TextFrame and Paragraph.""" -from __future__ import absolute_import, division, print_function, unicode_literals - from pptx.compat import to_unicode from pptx.dml.fill import FillFormat from pptx.enum.dml import MSO_FILL @@ -18,10 +16,10 @@ 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. + """The part of a shape that contains its text. + + Not all shapes have a text frame. Corresponds to the ```` element that can + appear as a child element of ````. Not intended to be constructed directly. """ def __init__(self, txBody, parent): @@ -221,10 +219,10 @@ def word_wrap(self, value): }[value] def _apply_fit(self, font_family, font_size, is_bold, is_italic): - """ - Arrange all the text in this text frame to fit inside its extents by - setting auto size off, wrap on, and setting the font of all its text - to *font_family*, *font_size*, *is_bold*, and *is_italic*. + """Arrange text in this text frame to fit inside its extents. + + This is accomplished by setting auto size off, wrap on, and setting the font of + all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`. """ self.auto_size = MSO_AUTO_SIZE.NONE self.word_wrap = True diff --git a/tests/chart/test_chart.py b/tests/chart/test_chart.py index aa5e4cc93..b7c716c4c 100644 --- a/tests/chart/test_chart.py +++ b/tests/chart/test_chart.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Test suite for pptx.chart.chart module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.chart.chart` module.""" import pytest @@ -28,6 +26,8 @@ class DescribeChart(object): + """Unit-test suite for `pptx.chart.chart.Chart` objects.""" + def it_provides_access_to_its_font(self, font_fixture, Font_, font_): chartSpace, expected_xml = font_fixture Font_.return_value = font_ @@ -452,6 +452,8 @@ def workbook_prop_(self, request, workbook_): class DescribeChartTitle(object): + """Unit-test suite for `pptx.chart.chart.ChartTitle` objects.""" + def it_provides_access_to_its_format(self, format_fixture): chart_title, ChartFormat_, format_ = format_fixture format = chart_title.format @@ -549,6 +551,8 @@ def TextFrame_(self, request): class Describe_Plots(object): + """Unit-test suite for `pptx.chart.chart._Plots` objects.""" + def it_supports_indexed_access(self, getitem_fixture): plots, idx, PlotFactory_, plot_elm, chart_, plot_ = getitem_fixture plot = plots[idx] diff --git a/tests/chart/test_xlsx.py b/tests/chart/test_xlsx.py index 9f26d2c4b..27b5c6965 100644 --- a/tests/chart/test_xlsx.py +++ b/tests/chart/test_xlsx.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.chart.xlsx module -""" - -from __future__ import absolute_import, print_function +"""Unit-test suite for `pptx.chart.xlsx` module.""" import pytest @@ -30,10 +26,16 @@ class Describe_BaseWorkbookWriter(object): + """Unit-test suite for `pptx.chart.xlsx._BaseWorkbookWriter` objects.""" + def it_can_generate_a_chart_data_Excel_blob(self, xlsx_blob_fixture): - workbook_writer, xlsx_file_, workbook_, worksheet_, xlsx_blob = ( - xlsx_blob_fixture - ) + ( + workbook_writer, + xlsx_file_, + workbook_, + worksheet_, + xlsx_blob, + ) = xlsx_blob_fixture _xlsx_blob = workbook_writer.xlsx_blob workbook_writer._open_worksheet.assert_called_once_with(xlsx_file_) @@ -123,6 +125,8 @@ def xlsx_file_(self, request): class DescribeCategoryWorkbookWriter(object): + """Unit-test suite for `pptx.chart.xlsx.CategoryWorkbookWriter` objects.""" + def it_knows_the_categories_range_ref(self, categories_ref_fixture): workbook_writer, expected_value = categories_ref_fixture assert workbook_writer.categories_ref == expected_value @@ -368,6 +372,8 @@ def _write_series_(self, request): class DescribeBubbleWorkbookWriter(object): + """Unit-test suite for `pptx.chart.xlsx.BubbleWorkbookWriter` objects.""" + def it_can_populate_a_worksheet_with_chart_data(self, populate_fixture): workbook_writer, workbook_, worksheet_, expected_calls = populate_fixture workbook_writer._populate_worksheet(workbook_, worksheet_) @@ -413,6 +419,8 @@ def worksheet_(self, request): class DescribeXyWorkbookWriter(object): + """Unit-test suite for `pptx.chart.xlsx.XyWorkbookWriter` objects.""" + def it_can_generate_a_chart_data_Excel_blob(self, xlsx_blob_fixture): workbook_writer, _open_worksheet_, xlsx_file_ = xlsx_blob_fixture[:3] _populate_worksheet_, workbook_, worksheet_ = xlsx_blob_fixture[3:6] diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index 76131f6ea..f68c6857a 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -1,10 +1,8 @@ # encoding: utf-8 -""" -Test suite for opc.oxml module -""" +"""Unit-test suite for `pptx.opc.oxml` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import unicode_literals import pytest @@ -30,6 +28,8 @@ class DescribeCT_Default(object): + """Unit-test suite for `pptx.opc.oxml.CT_Default` objects.""" + def it_provides_read_access_to_xml_values(self): default = a_Default().element assert default.extension == "xml" @@ -37,6 +37,8 @@ def it_provides_read_access_to_xml_values(self): class DescribeCT_Override(object): + """Unit-test suite for `pptx.opc.oxml.CT_Override` objects.""" + def it_provides_read_access_to_xml_values(self): override = an_Override().element assert override.partName == "/part/name.xml" @@ -44,6 +46,8 @@ def it_provides_read_access_to_xml_values(self): class DescribeCT_Relationship(object): + """Unit-test suite for `pptx.opc.oxml.CT_Relationship` objects.""" + def it_provides_read_access_to_xml_values(self): rel = a_Relationship().element assert rel.rId == "rId9" @@ -70,6 +74,8 @@ def it_can_construct_from_attribute_values(self): class DescribeCT_Relationships(object): + """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 = ( @@ -101,6 +107,8 @@ def it_can_generate_rels_file_xml(self): class DescribeCT_Types(object): + """Unit-test suite for `pptx.opc.oxml.CT_Types` objects.""" + def it_provides_access_to_default_child_elements(self): types = a_Types().element assert len(types.default_lst) == 2 @@ -134,7 +142,9 @@ def it_can_build_types_element_incrementally(self): assert types.xml == expected_types_xml -class DescribeSerializePartXml(object): +class Describe_serialize_part_xml(object): + """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 ): diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index da09d2c2e..e35e5933a 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -39,6 +39,8 @@ class DescribeOpcPackage(object): + """Unit-test suite for `pptx.opc.package.OpcPackage` objects.""" + def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): # mockery ---------------------- pkg_file = Mock(name="pkg_file") @@ -548,6 +550,8 @@ def url_(self, request): class DescribeXmlPart(object): + """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" + def it_can_be_constructed_by_PartFactory(self, load_fixture): partname_, content_type_, blob_, package_ = load_fixture[:4] element_, parse_xml_, __init_ = load_fixture[4:] @@ -639,6 +643,8 @@ def serialize_part_xml_(self, request): class DescribePartFactory(object): + """Unit-test suite for `pptx.opc.package.PartFactory` objects.""" + def it_constructs_custom_part_type_for_registered_content_types( self, part_args_, CustomPartClass_, part_of_custom_type_ ): @@ -701,6 +707,8 @@ def part_args_2_(self, request): class Describe_Relationship(object): + """Unit-test suite for `pptx.opc.package._Relationship` objects.""" + def it_remembers_construction_values(self): # test data -------------------- rId = "rId9" @@ -737,6 +745,8 @@ def it_should_have_relative_ref_for_internal_rel(self): class DescribeRelationshipCollection(object): + """Unit-test suite for `pptx.opc.package._Relationships` objects.""" + def it_has_a_len(self): rels = RelationshipCollection(None) assert len(rels) == 0 diff --git a/tests/oxml/shapes/test_groupshape.py b/tests/oxml/shapes/test_groupshape.py index 318bc6e3d..468f482fb 100644 --- a/tests/oxml/shapes/test_groupshape.py +++ b/tests/oxml/shapes/test_groupshape.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Test suite for pptx.oxml.shapes.shapetree module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.oxml.shapes.groupshape` module.""" import pytest @@ -16,6 +14,8 @@ class DescribeCT_GroupShape(object): + """Unit-test suite for `pptx.oxml.shapes.groupshape.CT_GroupShape` objects.""" + def it_can_add_a_graphicFrame_element_containing_a_table(self, add_table_fixt): spTree, id_, name, rows, cols, x, y, cx, cy = add_table_fixt[:9] new_table_graphicFrame_ = add_table_fixt[9] diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index 8281c1685..b3339e861 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.parts.chart module. -""" - -from __future__ import absolute_import, print_function +"""Unit-test suite for `pptx.parts.chart` module.""" import pytest @@ -23,6 +19,8 @@ class DescribeChartPart(object): + """Unit-test suite for `pptx.parts.chart.ChartPart` objects.""" + def it_can_construct_from_chart_type_and_data(self, new_fixture): chart_type_, chart_data_, package_ = new_fixture[:3] partname_template, load_, partname_ = new_fixture[3:6] @@ -153,6 +151,8 @@ def xlsx_blob_(self, request): class DescribeChartWorkbook(object): + """Unit-test suite for `pptx.parts.chart.ChartWorkbook` objects.""" + def it_can_get_the_chart_xlsx_part(self, xlsx_part_get_fixture): chart_data, expected_object = xlsx_part_get_fixture assert chart_data.xlsx_part is expected_object diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 47ea1ae80..ff42961f1 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Unit test suite for pptx.parts.image module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.parts.image` module.""" import pytest @@ -29,6 +27,8 @@ class DescribeImagePart(object): + """Unit-test suite for `pptx.parts.image.ImagePart` objects.""" + def it_can_construct_from_an_image_object(self, new_fixture): package_, image_, _init_, partname_ = new_fixture @@ -109,6 +109,8 @@ def package_(self, request): class DescribeImage(object): + """Unit-test suite for `pptx.parts.image.Image` objects.""" + def it_can_construct_from_a_path(self, from_path_fixture): image_file, blob, filename, image_ = from_path_fixture image = Image.from_file(image_file) diff --git a/tests/parts/test_presentation.py b/tests/parts/test_presentation.py index 8d6d9fda3..4d820e397 100644 --- a/tests/parts/test_presentation.py +++ b/tests/parts/test_presentation.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.parts.presentation module -""" - -from __future__ import absolute_import, print_function +"""Unit-test suite for `pptx.parts.presentation` module.""" import pytest @@ -22,6 +18,8 @@ class DescribePresentationPart(object): + """Unit-test suite for `pptx.parts.presentation.PresentationPart` objects.""" + def it_provides_access_to_its_presentation(self, prs_fixture): prs_part, Presentation_, prs_elm, prs_ = prs_fixture prs = prs_part.presentation diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index aaf11e5a7..a096aae4a 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -42,6 +42,8 @@ class DescribeBaseSlidePart(object): + """Unit-test suite for `pptx.parts.slide.BaseSlidePart` objects.""" + def it_knows_its_name(self, name_fixture): base_slide, expected_value = name_fixture assert base_slide.name == expected_value @@ -113,6 +115,8 @@ def related_parts_prop_(self, request): class DescribeNotesMasterPart(object): + """Unit-test suite for `pptx.parts.slide.NotesMasterPart` objects.""" + def it_can_create_a_notes_master_part(self, create_fixture): package_, theme_part_, notes_master_part_ = create_fixture @@ -253,6 +257,8 @@ def XmlPart_(self, request, theme_part_): class DescribeNotesSlidePart(object): + """Unit-test suite for `pptx.parts.slide.NotesSlidePart` objects.""" + def it_can_create_a_notes_slide_part(self, new_fixture): package_, slide_part_, notes_master_part_ = new_fixture[:3] notes_slide_, notes_master_, notes_slide_part_ = new_fixture[3:] @@ -669,6 +675,8 @@ def video_(self, request): class DescribeSlideLayoutPart(object): + """Unit-test suite for `pptx.parts.slide.SlideLayoutPart` objects.""" + def it_provides_access_to_its_slide_master(self, master_fixture): slide_layout_part, part_related_by_, slide_master_ = master_fixture slide_master = slide_layout_part.slide_master @@ -723,6 +731,8 @@ def slide_master_part_(self, request): class DescribeSlideMasterPart(object): + """Unit-test suite for `pptx.parts.slide.SlideMasterPart` objects.""" + def it_provides_access_to_its_slide_master(self, master_fixture): slide_master_part, SlideMaster_, sldMaster, slide_master_ = master_fixture slide_master = slide_master_part.slide_master diff --git a/tests/shapes/test_freeform.py b/tests/shapes/test_freeform.py index 55a0e4d24..3e3800b33 100644 --- a/tests/shapes/test_freeform.py +++ b/tests/shapes/test_freeform.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Unit-test suite for pptx.shapes.freeform module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.shapes.freeform` module""" import pytest @@ -28,6 +26,8 @@ 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:] @@ -462,6 +462,8 @@ def _width_prop_(self, request): class Describe_BaseDrawingOperation(object): + """Unit-test suite for `pptx.shapes.freeform.BaseDrawingOperation` objects.""" + def it_knows_its_x_coordinate(self, x_fixture): drawing_operation, expected_value = x_fixture x = drawing_operation.x @@ -490,6 +492,8 @@ def y_fixture(self): class Describe_Close(object): + """Unit-test suite for `pptx.shapes.freeform._Close` objects.""" + def it_provides_a_constructor(self, new_fixture): _init_ = new_fixture @@ -527,6 +531,8 @@ def _init_(self, request): class Describe_LineSegment(object): + """Unit-test suite for `pptx.shapes.freeform._LineSegment` objects.""" + def it_provides_a_constructor(self, new_fixture): builder_, x, y, _init_, x_int, y_int = new_fixture @@ -572,6 +578,8 @@ def _init_(self, request): class Describe_MoveTo(object): + """Unit-test suite for `pptx.shapes.freeform._MoveTo` objects.""" + def it_provides_a_constructor(self, new_fixture): builder_, x, y, _init_, x_int, y_int = new_fixture diff --git a/tests/shapes/test_placeholder.py b/tests/shapes/test_placeholder.py index 0d02f21f4..3865ec81b 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.shapes.placeholder module -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Unit-test suite for `pptx.shapes.placeholder` module.""" import pytest @@ -44,6 +40,8 @@ class Describe_BaseSlidePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder._BaseSlidePlaceholder` object.""" + def it_knows_its_shape_type(self): placeholder = _BaseSlidePlaceholder(None, None) assert placeholder.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER @@ -173,6 +171,8 @@ def slide_part_(self, request): class DescribeBasePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.BasePlaceholder` object.""" + def it_knows_its_idx_value(self, idx_fixture): placeholder, idx = idx_fixture assert placeholder.idx == idx @@ -291,6 +291,8 @@ def shape_elm_factory(tagname, ph_type, idx): class DescribeChartPlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.ChartPlaceholder` object.""" + def it_can_insert_a_chart_into_itself(self, insert_fixture): chart_ph, chart_type, chart_data_, graphicFrame = insert_fixture[:4] rId, PlaceholderGraphicFrame_, ph_graphic_frame_ = insert_fixture[4:] @@ -381,6 +383,8 @@ def slide_(self, request): class DescribeLayoutPlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.LayoutPlaceholder` object.""" + def it_uses_InheritsDimensions_mixin(self): layout_placeholder = LayoutPlaceholder(None, None) assert isinstance(layout_placeholder, _InheritsDimensions) @@ -430,6 +434,8 @@ def slide_master_(self, request): class DescribeNotesSlidePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.NotesSlidePlaceholder` object.""" + def it_finds_its_base_placeholder_to_help(self, base_ph_fixture): placeholder, notes_master_, ph_type, master_placeholder_ = base_ph_fixture base_placeholder = placeholder._base_placeholder @@ -481,6 +487,8 @@ def part_prop_(self, request, notes_slide_part_): class DescribePicturePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.PicturePlaceholder` object.""" + def it_can_insert_a_picture_into_itself(self, insert_fixture): picture_ph, image_file, pic = insert_fixture[:3] PlaceholderPicture_, placeholder_picture_ = insert_fixture[3:] @@ -585,6 +593,8 @@ def slide_(self, request): class DescribeTablePlaceholder(object): + """Unit-test suite for `pptx.shapes.placeholder.TablePlaceholder` object.""" + def it_can_insert_a_table_into_itself(self, insert_fixture): table_ph, rows, cols, graphicFrame = insert_fixture[:4] PlaceholderGraphicFrame_, ph_graphic_frame_ = insert_fixture[4:] diff --git a/tests/test_action.py b/tests/test_action.py index 3d9ef020c..92313542c 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.action module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.action` module.""" import pytest @@ -19,6 +15,8 @@ class DescribeActionSetting(object): + """Unit-test suite for `pptx.action.ActionSetting` objects.""" + def it_knows_its_action_type(self, action_fixture): action_setting, expected_action = action_fixture action = action_setting.action @@ -263,6 +261,8 @@ def slide_part_(self, request): class DescribeHyperlink(object): + """Unit-test suite for `pptx.action.Hyperlink` objects.""" + def it_knows_the_target_url_of_the_hyperlink(self, address_fixture): hyperlink, rId, expected_address = address_fixture address = hyperlink.address diff --git a/tests/test_package.py b/tests/test_package.py index a4895a990..054e2da03 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.package module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.package` module.""" import pytest @@ -22,6 +18,8 @@ class DescribePackage(object): + """Unit-test suite for `pptx.package.Package` objects.""" + def it_provides_access_to_its_core_properties_part(self): pkg = Package.open("pptx/templates/default.pptx") assert isinstance(pkg.core_properties, CorePropertiesPart) @@ -149,6 +147,8 @@ def _media_parts_prop_(self, request): class Describe_ImageParts(object): + """Unit-test suite for `pptx.package._ImageParts` objects.""" + def it_can_iterate_over_the_package_image_parts(self, iter_fixture): image_parts, expected_parts = iter_fixture assert list(image_parts) == expected_parts @@ -284,6 +284,8 @@ def package_(self, request): class Describe_MediaParts(object): + """Unit-test suite for `pptx.package._MediaParts` objects.""" + def it_can_iterate_the_media_parts_in_the_package(self, iter_fixture): media_parts, expected_parts = iter_fixture assert list(media_parts) == expected_parts diff --git a/tests/test_slide.py b/tests/test_slide.py index 520acba7c..0eeb05148 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Test suite for pptx.slide module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.slide` module.""" import pytest @@ -44,6 +42,8 @@ class Describe_BaseSlide(object): + """Unit-test suite for `pptx.slide._BaseSlide` objects.""" + def it_knows_its_name(self, name_get_fixture): base_slide, expected_value = name_get_fixture assert base_slide.name == expected_value @@ -107,6 +107,8 @@ def background_(self, request): class Describe_BaseMaster(object): + """Unit-test suite for `pptx.slide._BaseMaster` objects.""" + def it_is_a_BaseSlide_subclass(self, subclass_fixture): base_master = subclass_fixture assert isinstance(base_master, _BaseSlide) @@ -165,6 +167,8 @@ def shapes_(self, request): class DescribeNotesSlide(object): + """Unit-test suite for `pptx.slide.NotesSlide` objects.""" + def it_can_clone_the_notes_master_placeholders(self, clone_fixture): notes_slide, notes_master_, clone_placeholder_, calls = clone_fixture notes_slide.clone_master_placeholders(notes_master_) @@ -177,9 +181,12 @@ def it_provides_access_to_its_shapes(self, shapes_fixture): assert shapes is shapes_ def it_provides_access_to_its_placeholders(self, placeholders_fixture): - notes_slide, NotesSlidePlaceholders_, spTree, placeholders_ = ( - placeholders_fixture - ) + ( + notes_slide, + NotesSlidePlaceholders_, + spTree, + placeholders_, + ) = placeholders_fixture placeholders = notes_slide.placeholders NotesSlidePlaceholders_.assert_called_once_with(spTree, notes_slide) assert placeholders is placeholders_ @@ -310,6 +317,8 @@ def text_frame_(self, request): class DescribeSlide(object): + """Unit-test suite for `pptx.slide.Slide` objects.""" + def it_is_a_BaseSlide_subclass(self, subclass_fixture): slide = subclass_fixture assert isinstance(slide, _BaseSlide) @@ -459,6 +468,8 @@ def slide_part_(self, request): class DescribeSlides(object): + """Unit-test suite for `pptx.slide.Slides` objects.""" + def it_supports_indexed_access(self, getitem_fixture): slides, prs_part_, rId, slide_ = getitem_fixture slide = slides[0] @@ -604,6 +615,8 @@ def slide_layout_(self, request): class DescribeSlideLayout(object): + """Unit-test suite for `pptx.slide.SlideLayout` objects.""" + def it_is_a_BaseSlide_subclass(self): slide_layout = SlideLayout(None, None) assert isinstance(slide_layout, _BaseSlide) @@ -765,6 +778,8 @@ def slide_master_(self, request): class DescribeSlideLayouts(object): + """Unit-test suite for `pptx.slide.SlideLayouts` objects.""" + def it_supports_len(self, len_fixture): slide_layouts, expected_value = len_fixture assert len(slide_layouts) == expected_value @@ -930,6 +945,8 @@ def slide_master_part_(self, request): class DescribeSlideMaster(object): + """Unit-test suite for `pptx.slide.SlideMaster` objects.""" + def it_is_a_BaseMaster_subclass(self, subclass_fixture): slide_master = subclass_fixture assert isinstance(slide_master, _BaseMaster) @@ -967,6 +984,8 @@ def slide_layouts_(self, request): class DescribeSlideMasters(object): + """Unit-test suite for `pptx.slide.SlideMasters` objects.""" + def it_knows_how_many_masters_it_contains(self, len_fixture): slide_masters, expected_value = len_fixture assert len(slide_masters) == expected_value @@ -1045,6 +1064,8 @@ def slide_master_(self, request): class Describe_Background(object): + """Unit-test suite for `pptx.slide._Background` objects.""" + def it_provides_access_to_its_fill(self, fill_fixture): background, cSld, expected_xml = fill_fixture[:3] from_fill_parent_, fill_ = fill_fixture[3:] diff --git a/tests/text/test_fonts.py b/tests/text/test_fonts.py index e3fc2eeb0..69eafd60a 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.text.fonts module -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Unit-test suite for `pptx.text.fonts` module.""" import io import pytest @@ -37,6 +33,8 @@ class DescribeFontFiles(object): + """Unit-test suite for `pptx.text.fonts.FontFiles` object.""" + def it_can_find_a_system_font_file(self, find_fixture): family_name, is_bold, is_italic, expected_path = find_fixture path = FontFiles.find(family_name, is_bold, is_italic) @@ -173,6 +171,8 @@ def _windows_font_directories_(self, request): class Describe_Font(object): + """Unit-test suite for `pptx.text.fonts._Font` object.""" + def it_can_construct_from_a_font_file_path(self, open_fixture): path, _Stream_, stream_ = open_fixture with _Font.open(path) as f: @@ -342,6 +342,8 @@ def _tables_(self, request): class Describe_Stream(object): + """Unit-test suite for `pptx.text.fonts._Stream` object.""" + def it_can_construct_from_a_path(self, open_fixture): path, open_, _init_, file_ = open_fixture stream = _Stream.open(path) @@ -413,6 +415,8 @@ def open_(self, request): class Describe_TableFactory(object): + """Unit-test suite for `pptx.text.fonts._TableFactory` object.""" + def it_constructs_the_appropriate_table_object(self, fixture): tag, stream_, offset, length, TableClass_, TableClass = fixture table = _TableFactory(tag, stream_, offset, length) @@ -441,6 +445,8 @@ def stream_(self, request): class Describe_HeadTable(object): + """Unit-test suite for `pptx.text.fonts._HeadTable` object.""" + def it_knows_whether_the_font_is_bold(self, bold_fixture): head_table, expected_value = bold_fixture assert head_table.is_bold is expected_value @@ -486,6 +492,8 @@ def _macStyle_(self, request): class Describe_NameTable(object): + """Unit-test suite for `pptx.text.fonts._NameTable` object.""" + def it_knows_the_font_family_name(self, family_fixture): name_table, expected_value = family_fixture family_name = name_table.family_name diff --git a/tests/text/test_layout.py b/tests/text/test_layout.py index fbbb274cf..9ad0a26e8 100644 --- a/tests/text/test_layout.py +++ b/tests/text/test_layout.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.text.layout module -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Unit-test suite for `pptx.text.layout` module.""" import pytest @@ -22,6 +18,8 @@ class DescribeTextFitter(object): + """Unit-test suite for `pptx.text.layout.TextFitter` object.""" + def it_can_determine_the_best_fit_font_size(self, best_fit_fixture): text, extents, max_size, font_file = best_fit_fixture[:4] _LineSource_, _init_, line_source_ = best_fit_fixture[4:7] @@ -230,6 +228,8 @@ def _wrap_lines_(self, request): class Describe_BinarySearchTree(object): + """Unit-test suite for `pptx.text.layout._BinarySearchTree` object.""" + def it_can_construct_from_an_ordered_sequence(self): bst = _BinarySearchTree.from_ordered_sequence(range(10)) @@ -271,6 +271,8 @@ def max_fixture(self, request): class Describe_LineSource(object): + """Unit-test suite for `pptx.text.layout._LineSource` object.""" + def it_generates_text_remainder_pairs(self): line_source = _LineSource("foo bar baz") expected = ( diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 8e0f0f5b3..284a2f7fb 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Test suite for pptx.text.text module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.text.text` module.""" import pytest @@ -481,6 +479,8 @@ def text_prop_(self, request): class DescribeFont(object): + """Unit-test suite for `pptx.text.text.Font` object.""" + def it_knows_its_bold_setting(self, bold_get_fixture): font, expected_value = bold_get_fixture assert font.bold == expected_value @@ -690,6 +690,8 @@ def font(self): class Describe_Hyperlink(object): + """Unit-test suite for `pptx.text.text._Hyperlink` object.""" + def it_knows_the_target_url_of_the_hyperlink(self, hlink_with_url_): hlink, rId, url = hlink_with_url_ assert hlink.address == url diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 0edbce060..ef8f3023c 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Utility functions wrapping the excellent *mock* library. -""" - -from __future__ import absolute_import, print_function +"""Utility functions wrapping the excellent `mock` library.""" import sys @@ -19,11 +15,11 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): - """ - Return a mock patching the class with qualified name *q_class_name*. - The mock is autospec'ed based on the patched class unless the optional - argument *autospec* is set to False. Any other keyword arguments are - passed through to Mock(). Patch is reversed after calling test returns. + """Return a mock patching the class with qualified name *q_class_name*. + + The mock is autospec'ed based on the patched class unless the optional argument + *autospec* is set to False. Any other keyword arguments are passed through to + Mock(). Patch is reversed after calling test returns. """ _patch = patch(q_class_name, autospec=autospec, **kwargs) request.addfinalizer(_patch.stop) @@ -31,9 +27,9 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): - """ - Return a mock for attribute *attr_name* on *cls* where the patch is - reversed after pytest uses it. + """Return a mock for attribute (class variable) `attr_name` on `cls`. + + Patch is reversed after pytest uses it. """ name = request.fixturename if name is None else name _patch = patch.object(cls, attr_name, name=name, **kwargs) @@ -42,9 +38,9 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): def function_mock(request, q_function_name, **kwargs): - """ - Return a mock patching the function with qualified name - *q_function_name*. Patch is reversed after calling test returns. + """Return mock patching function with qualified name `q_function_name`. + + Patch is reversed after calling test returns. """ _patch = patch(q_function_name, **kwargs) request.addfinalizer(_patch.stop) @@ -52,9 +48,9 @@ def function_mock(request, q_function_name, **kwargs): def initializer_mock(request, cls, **kwargs): - """ - Return a mock for the __init__ method on *cls* where the patch is - reversed after pytest uses it. + """Return mock for __init__() method on `cls`. + + The patch is reversed after pytest uses it. """ _patch = patch.object(cls, "__init__", return_value=None, **kwargs) request.addfinalizer(_patch.stop) @@ -62,22 +58,22 @@ def initializer_mock(request, cls, **kwargs): def instance_mock(request, cls, name=None, spec_set=True, **kwargs): - """ - Return a mock for an instance of *cls* that draws its spec from the class - and does not allow new attributes to be set on the instance. If *name* is + """Return mock for instance of `cls` that draws its spec from that class. + + The mock does not allow new attributes to be set on the instance. If `name` is missing or |None|, the name of the returned |Mock| instance is set to - *request.fixturename*. Additional keyword arguments are passed through to - the Mock() call that creates the mock. + `request.fixturename`. Additional keyword arguments are passed through to the Mock() + call that creates the mock. """ name = name if name is not None else request.fixturename return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs) def loose_mock(request, name=None, **kwargs): - """ - Return a "loose" mock, meaning it has no spec to constrain calls on it. - Additional keyword arguments are passed through to Mock(). If called - without a name, it is assigned the name of the fixture. + """Return a "loose" mock, meaning it has no spec to constrain calls on it. + + Additional keyword arguments are passed through to Mock(). If called without a name, + it is assigned the name of the fixture. """ if name is None: name = request.fixturename @@ -85,9 +81,9 @@ def loose_mock(request, name=None, **kwargs): def method_mock(request, cls, method_name, **kwargs): - """ - Return a mock for method *method_name* on *cls* where the patch is - reversed after pytest uses it. + """Return mock for method `method_name` on `cls`. + + The patch is reversed after pytest uses it. """ _patch = patch.object(cls, method_name, **kwargs) request.addfinalizer(_patch.stop) @@ -95,9 +91,7 @@ def method_mock(request, cls, method_name, **kwargs): def open_mock(request, module_name, **kwargs): - """ - Return a mock for the builtin `open()` method in *module_name*. - """ + """Return a mock for the builtin `open()` method in `module_name`.""" target = "%s.open" % module_name _patch = patch(target, mock_open(), create=True, **kwargs) request.addfinalizer(_patch.stop) @@ -105,20 +99,14 @@ def open_mock(request, module_name, **kwargs): def property_mock(request, cls, prop_name, **kwargs): - """ - Return a mock for property *prop_name* on class *cls* where the patch is - reversed after pytest uses it. - """ + """Return a mock for property `prop_name` on class `cls`.""" _patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() def var_mock(request, q_var_name, **kwargs): - """ - Return a mock patching the variable with qualified name *q_var_name*. - Patch is reversed after calling test returns. - """ + """Return mock patching the variable with qualified name *q_var_name*.""" _patch = patch(q_var_name, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() From b1a9488f3ee5d665869b18cb30177f965d608ff5 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 13:53:56 -0700 Subject: [PATCH 02/69] rfctr: consolidate pptx.opc.serialized modules Several of these classes are going away and we don't need this many modules to take care of the coherent serializing and deserializing responsibilities. --- pptx/opc/package.py | 3 +- pptx/opc/phys_pkg.py | 159 ------- pptx/opc/pkgwriter.py | 119 ----- pptx/opc/{pkgreader.py => serialized.py} | 269 ++++++++++- tests/opc/test_package.py | 437 +++++++++++------- tests/opc/test_phys_pkg.py | 193 -------- tests/opc/test_pkgwriter.py | 171 ------- tests/opc/test_rels.py | 268 ----------- .../{test_pkgreader.py => test_serialized.py} | 363 ++++++++++++++- 9 files changed, 878 insertions(+), 1104 deletions(-) delete mode 100644 pptx/opc/phys_pkg.py delete mode 100644 pptx/opc/pkgwriter.py rename pptx/opc/{pkgreader.py => serialized.py} (52%) delete mode 100644 tests/opc/test_phys_pkg.py delete mode 100644 tests/opc/test_pkgwriter.py delete mode 100644 tests/opc/test_rels.py rename tests/opc/{test_pkgreader.py => test_serialized.py} (54%) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 4e5ed733f..e1950bf4c 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -10,8 +10,7 @@ from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationships, serialize_part_xml from pptx.opc.packuri import PACKAGE_URI, PackURI -from pptx.opc.pkgreader import PackageReader -from pptx.opc.pkgwriter import PackageWriter +from pptx.opc.serialized import PackageReader, PackageWriter from pptx.oxml import parse_xml from pptx.util import lazyproperty diff --git a/pptx/opc/phys_pkg.py b/pptx/opc/phys_pkg.py deleted file mode 100644 index 90ca398cf..000000000 --- a/pptx/opc/phys_pkg.py +++ /dev/null @@ -1,159 +0,0 @@ -# encoding: utf-8 - -""" -Provides a general interface to a *physical* OPC package, such as a zip file. -""" - -from __future__ import absolute_import - -import os - -from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED - -from ..compat import is_string -from ..exceptions import PackageNotFoundError - -from .packuri import CONTENT_TYPES_URI - - -class PhysPkgReader(object): - """ - Factory for physical package reader objects. - """ - - def __new__(cls, pkg_file): - # if *pkg_file* is a string, treat it as a path - if is_string(pkg_file): - if os.path.isdir(pkg_file): - reader_cls = _DirPkgReader - elif is_zipfile(pkg_file): - reader_cls = _ZipPkgReader - else: - raise PackageNotFoundError("Package not found at '%s'" % pkg_file) - else: # assume it's a stream and pass it to Zip reader to sort out - reader_cls = _ZipPkgReader - - return super(PhysPkgReader, cls).__new__(reader_cls) - - -class PhysPkgWriter(object): - """ - Factory for physical package writer objects. - """ - - def __new__(cls, pkg_file): - return super(PhysPkgWriter, cls).__new__(_ZipPkgWriter) - - -class _DirPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for an OPC package extracted into a - directory. - """ - - def __init__(self, path): - """ - *path* is the path to a directory containing an expanded package. - """ - super(_DirPkgReader, self).__init__() - self._path = os.path.abspath(path) - - def blob_for(self, pack_uri): - """ - Return contents of file corresponding to *pack_uri* in package - directory. - """ - path = os.path.join(self._path, pack_uri.membername) - with open(path, "rb") as f: - blob = f.read() - return blob - - def close(self): - """ - Provides interface consistency with |ZipFileSystem|, but does - nothing, a directory file system doesn't need closing. - """ - pass - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri*, or None if the - item has no rels item. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except IOError: - rels_xml = None - return rels_xml - - -class _ZipPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for a zip file OPC package. - """ - - def __init__(self, pkg_file): - super(_ZipPkgReader, self).__init__() - self._zipf = ZipFile(pkg_file, "r") - - def blob_for(self, pack_uri): - """ - Return blob corresponding to *pack_uri*. Raises |ValueError| if no - matching member is present in zip archive. - """ - return self._zipf.read(pack_uri.membername) - - def close(self): - """ - Close the zip archive, releasing any resources it is using. - """ - self._zipf.close() - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the zip package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri* or None if no rels - item is present. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except KeyError: - rels_xml = None - return rels_xml - - -class _ZipPkgWriter(PhysPkgWriter): - """ - Implements |PhysPkgWriter| interface for a zip file OPC package. - """ - - def __init__(self, pkg_file): - super(_ZipPkgWriter, self).__init__() - self._zipf = ZipFile(pkg_file, "w", compression=ZIP_DEFLATED) - - def close(self): - """ - Close the zip archive, flushing any pending physical writes and - releasing any resources it's using. - """ - self._zipf.close() - - def write(self, pack_uri, blob): - """ - Write *blob* to this zip package with the membername corresponding to - *pack_uri*. - """ - self._zipf.writestr(pack_uri.membername, blob) diff --git a/pptx/opc/pkgwriter.py b/pptx/opc/pkgwriter.py deleted file mode 100644 index 688c21311..000000000 --- a/pptx/opc/pkgwriter.py +++ /dev/null @@ -1,119 +0,0 @@ -# encoding: utf-8 - -""" -Provides a low-level, write-only API to a serialized Open Packaging -Convention (OPC) package, essentially an implementation of OpcPackage.save() -""" - -from __future__ import absolute_import - -from .constants import CONTENT_TYPE as CT -from .oxml import CT_Types, serialize_part_xml -from .packuri import CONTENT_TYPES_URI, PACKAGE_URI -from .phys_pkg import PhysPkgWriter -from .shared import CaseInsensitiveDict -from .spec import default_content_types - - -class PackageWriter(object): - """ - Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be - either a path to a zip file (a string) or a file-like object. Its single - API method, :meth:`write`, is static, so this class is not intended to - be instantiated. - """ - - @staticmethod - def write(pkg_file, pkg_rels, parts): - """ - Write a physical package (.pptx file) to *pkg_file* containing - *pkg_rels* and *parts* and a content types stream based on the - content types of the parts. - """ - phys_writer = PhysPkgWriter(pkg_file) - PackageWriter._write_content_types_stream(phys_writer, parts) - PackageWriter._write_pkg_rels(phys_writer, pkg_rels) - PackageWriter._write_parts(phys_writer, parts) - phys_writer.close() - - @staticmethod - def _write_content_types_stream(phys_writer, parts): - """ - Write ``[Content_Types].xml`` part to the physical package with an - appropriate content type lookup target for each part in *parts*. - """ - content_types_blob = serialize_part_xml(_ContentTypesItem.xml_for(parts)) - phys_writer.write(CONTENT_TYPES_URI, content_types_blob) - - @staticmethod - def _write_parts(phys_writer, parts): - """ - Write the blob of each part in *parts* to the package, along with a - rels item for its relationships if and only if it has any. - """ - for part in parts: - phys_writer.write(part.partname, part.blob) - if len(part._rels): - phys_writer.write(part.partname.rels_uri, part._rels.xml) - - @staticmethod - def _write_pkg_rels(phys_writer, pkg_rels): - """ - Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the - package. - """ - phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) - - -class _ContentTypesItem(object): - """ - Service class that composes a content types item ([Content_Types].xml) - based on a list of parts. Not meant to be instantiated directly, its - single interface method is xml_for(), e.g. - ``_ContentTypesItem.xml_for(parts)``. - """ - - def __init__(self): - self._defaults = CaseInsensitiveDict() - self._overrides = dict() - - @classmethod - def xml_for(cls, parts): - """ - Return content types XML mapping each part in *parts* to the - appropriate content type and suitable for storage as - ``[Content_Types].xml`` in an OPC package. - """ - cti = cls() - cti._defaults["rels"] = CT.OPC_RELATIONSHIPS - cti._defaults["xml"] = CT.XML - for part in parts: - cti._add_content_type(part.partname, part.content_type) - return cti._xml() - - def _add_content_type(self, partname, content_type): - """ - Add a content type for the part with *partname* and *content_type*, - using a default or override as appropriate. - """ - ext = partname.ext - if (ext.lower(), content_type) in default_content_types: - self._defaults[ext] = content_type - else: - self._overrides[partname] = content_type - - def _xml(self): - """ - Return etree element containing the XML representation of this content - types item, suitable for serialization to the ``[Content_Types].xml`` - item for an OPC package. Although the sequence of elements is not - strictly significant, as an aid to testing and readability Default - elements are sorted by extension and Override elements are sorted by - partname. - """ - _types_elm = CT_Types.new() - for ext in sorted(self._defaults.keys()): - _types_elm.add_default(ext, self._defaults[ext]) - for partname in sorted(self._overrides.keys()): - _types_elm.add_override(partname, self._overrides[partname]) - return _types_elm diff --git a/pptx/opc/pkgreader.py b/pptx/opc/serialized.py similarity index 52% rename from pptx/opc/pkgreader.py rename to pptx/opc/serialized.py index 59c74da53..0b9517ec0 100644 --- a/pptx/opc/pkgreader.py +++ b/pptx/opc/serialized.py @@ -1,17 +1,17 @@ # encoding: utf-8 -""" -Provides a low-level, read-only API to a serialized Open Packaging Convention -(OPC) package. -""" +"""API for reading/writing serialized Open Packaging Convention (OPC) package.""" -from __future__ import absolute_import +import os +import zipfile -from .constants import RELATIONSHIP_TARGET_MODE as RTM -from .oxml import parse_xml -from .packuri import PACKAGE_URI, PackURI -from .phys_pkg import PhysPkgReader -from .shared import CaseInsensitiveDict +from pptx.compat import is_string +from pptx.exceptions import PackageNotFoundError +from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM +from pptx.opc.oxml import CT_Types, parse_xml, serialize_part_xml +from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI +from pptx.opc.shared import CaseInsensitiveDict +from pptx.opc.spec import default_content_types class PackageReader(object): @@ -30,7 +30,7 @@ def from_file(pkg_file): """ Return a |PackageReader| instance loaded with contents of *pkg_file*. """ - phys_reader = PhysPkgReader(pkg_file) + phys_reader = _PhysPkgReader(pkg_file) content_types = _ContentTypeMap.from_xml(phys_reader.content_types_xml) pkg_srels = PackageReader._srels_for(phys_reader, PACKAGE_URI) sparts = PackageReader._load_serialized_parts( @@ -163,6 +163,199 @@ def _add_override(self, partname, content_type): self._overrides[partname] = content_type +class PackageWriter(object): + """ + Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be + either a path to a zip file (a string) or a file-like object. Its single + API method, :meth:`write`, is static, so this class is not intended to + be instantiated. + """ + + @staticmethod + def write(pkg_file, pkg_rels, parts): + """ + Write a physical package (.pptx file) to *pkg_file* containing + *pkg_rels* and *parts* and a content types stream based on the + content types of the parts. + """ + phys_writer = _PhysPkgWriter(pkg_file) + PackageWriter._write_content_types_stream(phys_writer, parts) + PackageWriter._write_pkg_rels(phys_writer, pkg_rels) + PackageWriter._write_parts(phys_writer, parts) + phys_writer.close() + + @staticmethod + def _write_content_types_stream(phys_writer, parts): + """ + Write ``[Content_Types].xml`` part to the physical package with an + appropriate content type lookup target for each part in *parts*. + """ + content_types_blob = serialize_part_xml(_ContentTypesItem.xml_for(parts)) + phys_writer.write(CONTENT_TYPES_URI, content_types_blob) + + @staticmethod + def _write_parts(phys_writer, parts): + """ + Write the blob of each part in *parts* to the package, along with a + rels item for its relationships if and only if it has any. + """ + for part in parts: + phys_writer.write(part.partname, part.blob) + if len(part._rels): + phys_writer.write(part.partname.rels_uri, part._rels.xml) + + @staticmethod + def _write_pkg_rels(phys_writer, pkg_rels): + """ + Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the + package. + """ + phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) + + +class _PhysPkgReader(object): + """ + Factory for physical package reader objects. + """ + + def __new__(cls, pkg_file): + # if *pkg_file* is a string, treat it as a path + if is_string(pkg_file): + if os.path.isdir(pkg_file): + reader_cls = _DirPkgReader + elif zipfile.is_zipfile(pkg_file): + reader_cls = _ZipPkgReader + else: + raise PackageNotFoundError("Package not found at '%s'" % pkg_file) + else: # assume it's a stream and pass it to Zip reader to sort out + reader_cls = _ZipPkgReader + + return super(_PhysPkgReader, cls).__new__(reader_cls) + + +class _DirPkgReader(_PhysPkgReader): + """ + Implements |PhysPkgReader| interface for an OPC package extracted into a + directory. + """ + + def __init__(self, path): + """ + *path* is the path to a directory containing an expanded package. + """ + super(_DirPkgReader, self).__init__() + self._path = os.path.abspath(path) + + def blob_for(self, pack_uri): + """ + Return contents of file corresponding to *pack_uri* in package + directory. + """ + path = os.path.join(self._path, pack_uri.membername) + with open(path, "rb") as f: + blob = f.read() + return blob + + def close(self): + """ + Provides interface consistency with |ZipFileSystem|, but does + nothing, a directory file system doesn't need closing. + """ + pass + + @property + def content_types_xml(self): + """ + Return the `[Content_Types].xml` blob from the package. + """ + return self.blob_for(CONTENT_TYPES_URI) + + def rels_xml_for(self, source_uri): + """ + Return rels item XML for source with *source_uri*, or None if the + item has no rels item. + """ + try: + rels_xml = self.blob_for(source_uri.rels_uri) + except IOError: + rels_xml = None + return rels_xml + + +class _ZipPkgReader(_PhysPkgReader): + """ + Implements |PhysPkgReader| interface for a zip file OPC package. + """ + + def __init__(self, pkg_file): + super(_ZipPkgReader, self).__init__() + self._zipf = zipfile.ZipFile(pkg_file, "r") + + def blob_for(self, pack_uri): + """ + Return blob corresponding to *pack_uri*. Raises |ValueError| if no + matching member is present in zip archive. + """ + return self._zipf.read(pack_uri.membername) + + def close(self): + """ + Close the zip archive, releasing any resources it is using. + """ + self._zipf.close() + + @property + def content_types_xml(self): + """ + Return the `[Content_Types].xml` blob from the zip package. + """ + return self.blob_for(CONTENT_TYPES_URI) + + def rels_xml_for(self, source_uri): + """ + Return rels item XML for source with *source_uri* or None if no rels + item is present. + """ + try: + rels_xml = self.blob_for(source_uri.rels_uri) + except KeyError: + rels_xml = None + return rels_xml + + +class _PhysPkgWriter(object): + """ + Factory for physical package writer objects. + """ + + def __new__(cls, pkg_file): + return super(_PhysPkgWriter, cls).__new__(_ZipPkgWriter) + + +class _ZipPkgWriter(_PhysPkgWriter): + """ + Implements |PhysPkgWriter| interface for a zip file OPC package. + """ + + def __init__(self, pkg_file): + super(_ZipPkgWriter, self).__init__() + self._zipf = zipfile.ZipFile(pkg_file, "w", compression=zipfile.ZIP_DEFLATED) + + def close(self): + """ + Close the zip archive, flushing any pending physical writes and + releasing any resources it's using. + """ + self._zipf.close() + + def write(self, pack_uri, blob): + """ + Write *blob* to this zip package with the membername corresponding to + *pack_uri*. + """ + self._zipf.writestr(pack_uri.membername, blob) + + class _SerializedPart(object): """ Value object for an OPC package part. Provides access to the partname, @@ -291,3 +484,57 @@ def load_from_xml(baseURI, rels_item_xml): for rel_elm in rels_elm.relationship_lst: srels._srels.append(_SerializedRelationship(baseURI, rel_elm)) return srels + + +class _ContentTypesItem(object): + """ + Service class that composes a content types item ([Content_Types].xml) + based on a list of parts. Not meant to be instantiated directly, its + single interface method is xml_for(), e.g. + ``_ContentTypesItem.xml_for(parts)``. + """ + + def __init__(self): + self._defaults = CaseInsensitiveDict() + self._overrides = dict() + + @classmethod + def xml_for(cls, parts): + """ + Return content types XML mapping each part in *parts* to the + appropriate content type and suitable for storage as + ``[Content_Types].xml`` in an OPC package. + """ + cti = cls() + cti._defaults["rels"] = CT.OPC_RELATIONSHIPS + cti._defaults["xml"] = CT.XML + for part in parts: + cti._add_content_type(part.partname, part.content_type) + return cti._xml() + + def _add_content_type(self, partname, content_type): + """ + Add a content type for the part with *partname* and *content_type*, + using a default or override as appropriate. + """ + ext = partname.ext + if (ext.lower(), content_type) in default_content_types: + self._defaults[ext] = content_type + else: + self._overrides[partname] = content_type + + def _xml(self): + """ + Return etree element containing the XML representation of this content + types item, suitable for serialization to the ``[Content_Types].xml`` + item for an OPC package. Although the sequence of elements is not + strictly significant, as an aid to testing and readability Default + elements are sorted by extension and Override elements are sorted by + partname. + """ + _types_elm = CT_Types.new() + for ext in sorted(self._defaults.keys()): + _types_elm.add_default(ext, self._defaults[ext]) + for partname in sorted(self._overrides.keys()): + _types_elm.add_override(partname, self._overrides[partname]) + return _types_elm diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index e35e5933a..a9c3a9f0c 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -6,6 +6,7 @@ import pytest +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationships from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.opc.package import ( @@ -17,7 +18,7 @@ Unmarshaller, XmlPart, ) -from pptx.opc.pkgreader import PackageReader +from pptx.opc.serialized import PackageReader from pptx.oxml.xmlchemy import BaseOxmlElement from pptx.package import Package @@ -293,23 +294,6 @@ def it_can_be_constructed_by_PartFactory(self, load_fixture): __init_.assert_called_once_with(partname_, content_type_, blob_, package_) assert isinstance(part, Part) - def it_knows_its_partname(self, partname_get_fixture): - part, expected_partname = partname_get_fixture - assert part.partname == expected_partname - - def it_can_change_its_partname(self, partname_set_fixture): - part, new_partname = partname_set_fixture - part.partname = new_partname - assert part.partname == new_partname - - def it_knows_its_content_type(self, content_type_fixture): - part, expected_content_type = content_type_fixture - assert part.content_type == expected_content_type - - def it_knows_the_package_it_belongs_to(self, package_get_fixture): - part, expected_package = package_get_fixture - assert part.package == expected_package - def it_can_be_notified_after_unmarshalling_is_complete(self, part): part.after_unmarshal() @@ -325,130 +309,108 @@ def it_can_change_its_blob(self): part.blob = new_blob assert part.blob == new_blob - def it_can_load_a_blob_from_a_file_path_to_help(self): - path = absjoin(test_file_dir, "minimal.pptx") - with open(path, "rb") as f: - file_bytes = f.read() - part = Part(None, None, None, None) - - assert part._blob_from_file(path) == file_bytes - - def it_can_load_a_blob_from_a_file_like_object_to_help(self): - part = Part(None, None, None, None) - assert part._blob_from_file(io.BytesIO(b"012345")) == b"012345" - - # fixtures --------------------------------------------- - - @pytest.fixture - def blob_fixture(self, blob_): - part = Part(None, None, blob_, None) - return part, blob_ - - @pytest.fixture - def content_type_fixture(self): - content_type = "content/type" - part = Part(None, content_type, None, None) - return part, content_type - - @pytest.fixture - def load_fixture(self, request, partname_, content_type_, blob_, package_, __init_): - return (partname_, content_type_, blob_, package_, __init_) - - @pytest.fixture - def package_get_fixture(self, package_): - part = Part(None, None, None, package_) - return part, package_ + def it_knows_its_content_type(self, content_type_fixture): + part, expected_content_type = content_type_fixture + assert part.content_type == expected_content_type - @pytest.fixture - def part(self): - part = Part(None, None) - return part + def it_can_drop_a_relationship(self, drop_rel_fixture): + part, rId, rel_should_be_gone = drop_rel_fixture - @pytest.fixture - def partname_get_fixture(self): - partname = PackURI("/part/name") - part = Part(partname, None, None, None) - return part, partname + part.drop_rel(rId) - @pytest.fixture - def partname_set_fixture(self): - old_partname = PackURI("/old/part/name") - new_partname = PackURI("/new/part/name") - part = Part(old_partname, None, None, None) - return part, new_partname + if rel_should_be_gone: + assert rId not in part.rels + else: + assert rId in part.rels - # fixture components --------------------------------------------- + def it_can_load_a_relationship(self, load_rel_fixture): + part, rels_, reltype_, target_, rId_ = load_rel_fixture - @pytest.fixture - def blob_(self, request): - return instance_mock(request, bytes) + part.load_rel(reltype_, target_, rId_) - @pytest.fixture - def content_type_(self, request): - return instance_mock(request, str) + rels_.add_relationship.assert_called_once_with(reltype_, target_, rId_, False) - @pytest.fixture - def __init_(self, request): - return initializer_mock(request, Part) + def it_knows_the_package_it_belongs_to(self, package_get_fixture): + part, expected_package = package_get_fixture + assert part.package == expected_package - @pytest.fixture - def package_(self, request): - return instance_mock(request, OpcPackage) + def it_can_find_a_related_part_by_reltype(self, related_part_fixture): + part, reltype_, related_part_ = related_part_fixture - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) + related_part = part.part_related_by(reltype_) + part.rels.part_with_reltype.assert_called_once_with(reltype_) + assert related_part is related_part_ -class DescribePartRelationshipManagementInterface(object): - def it_provides_access_to_its_relationships(self, rels_fixture): - part, Relationships_, partname_, rels_ = rels_fixture - rels = part.rels - Relationships_.assert_called_once_with(partname_.baseURI) - assert rels is rels_ + def it_knows_its_partname(self, partname_get_fixture): + part, expected_partname = partname_get_fixture + assert part.partname == expected_partname - def it_can_load_a_relationship(self, load_rel_fixture): - part, rels_, reltype_, target_, rId_ = load_rel_fixture - part.load_rel(reltype_, target_, rId_) - rels_.add_relationship.assert_called_once_with(reltype_, target_, rId_, False) + def it_can_change_its_partname(self, partname_set_fixture): + part, new_partname = partname_set_fixture + part.partname = new_partname + assert part.partname == new_partname def it_can_establish_a_relationship_to_another_part(self, relate_to_part_fixture): part, target_, reltype_, rId_ = relate_to_part_fixture + rId = part.relate_to(target_, reltype_) + part.rels.get_or_add.assert_called_once_with(reltype_, target_) assert rId is rId_ def it_can_establish_an_external_relationship(self, relate_to_url_fixture): part, url_, reltype_, rId_ = relate_to_url_fixture + rId = part.relate_to(url_, reltype_, is_external=True) + part.rels.get_or_add_ext_rel.assert_called_once_with(reltype_, url_) assert rId is rId_ - def it_can_drop_a_relationship(self, drop_rel_fixture): - part, rId, rel_should_be_gone = drop_rel_fixture - part.drop_rel(rId) - if rel_should_be_gone: - assert rId not in part.rels - else: - assert rId in part.rels - - def it_can_find_a_related_part_by_reltype(self, related_part_fixture): - part, reltype_, related_part_ = related_part_fixture - related_part = part.part_related_by(reltype_) - part.rels.part_with_reltype.assert_called_once_with(reltype_) - assert related_part is related_part_ - def it_can_find_a_related_part_by_rId(self, related_parts_fixture): part, related_parts_ = related_parts_fixture assert part.related_parts is related_parts_ + def it_provides_access_to_its_relationships(self, rels_fixture): + part, Relationships_, partname_, rels_ = rels_fixture + + rels = part.rels + + Relationships_.assert_called_once_with(partname_.baseURI) + assert rels is rels_ + def it_can_find_the_uri_of_an_external_relationship(self, target_ref_fixture): part, rId_, url_ = target_ref_fixture + url = part.target_ref(rId_) + assert url == url_ + def it_can_load_a_blob_from_a_file_path_to_help(self): + path = absjoin(test_file_dir, "minimal.pptx") + with open(path, "rb") as f: + file_bytes = f.read() + part = Part(None, None, None, None) + + assert part._blob_from_file(path) == file_bytes + + def it_can_load_a_blob_from_a_file_like_object_to_help(self): + part = Part(None, None, None, None) + assert part._blob_from_file(io.BytesIO(b"012345")) == b"012345" + # fixtures --------------------------------------------- + @pytest.fixture + def blob_fixture(self, blob_): + part = Part(None, None, blob_, None) + return part, blob_ + + @pytest.fixture + def content_type_fixture(self): + content_type = "content/type" + part = Part(None, content_type, None, None) + return part, content_type + @pytest.fixture( params=[ ("p:sp", True), @@ -463,11 +425,33 @@ def drop_rel_fixture(self, request, part): part._rels = {rId: None} return part, rId, rel_should_be_dropped + @pytest.fixture + def load_fixture(self, request, partname_, content_type_, blob_, package_, __init_): + return (partname_, content_type_, blob_, package_, __init_) + @pytest.fixture def load_rel_fixture(self, part, rels_, reltype_, part_, rId_): part._rels = rels_ return part, rels_, reltype_, part_, rId_ + @pytest.fixture + def package_get_fixture(self, package_): + part = Part(None, None, None, package_) + return part, package_ + + @pytest.fixture + def partname_get_fixture(self): + partname = PackURI("/part/name") + part = Part(partname, None, None, None) + return part, partname + + @pytest.fixture + def partname_set_fixture(self): + old_partname = PackURI("/old/part/name") + new_partname = PackURI("/new/part/name") + part = Part(old_partname, None, None, None) + return part, new_partname + @pytest.fixture def relate_to_part_fixture(self, request, part, reltype_, part_, rels_, rId_): part._rels = rels_ @@ -501,6 +485,22 @@ def target_ref_fixture(self, request, part, rId_, rel_, url_): # fixture components --------------------------------------------- + @pytest.fixture + def blob_(self, request): + return instance_mock(request, bytes) + + @pytest.fixture + def content_type_(self, request): + return instance_mock(request, str) + + @pytest.fixture + def __init_(self, request): + return initializer_mock(request, Part) + + @pytest.fixture + def package_(self, request): + return instance_mock(request, OpcPackage) + @pytest.fixture def part(self): return Part(None, None) @@ -706,44 +706,6 @@ def part_args_2_(self, request): return partname_2_, content_type_2_, pkg_2_, blob_2_ -class Describe_Relationship(object): - """Unit-test suite for `pptx.opc.package._Relationship` objects.""" - - def it_remembers_construction_values(self): - # test data -------------------- - rId = "rId9" - reltype = "reltype" - target = Mock(name="target_part") - external = False - # exercise --------------------- - rel = _Relationship(rId, reltype, target, None, external) - # verify ----------------------- - assert rel.rId == rId - assert rel.reltype == reltype - assert rel.target_part == target - assert rel.is_external == external - - def it_should_raise_on_target_part_access_on_external_rel(self): - rel = _Relationship(None, None, None, None, external=True) - with pytest.raises(ValueError): - rel.target_part - - def it_should_have_target_ref_for_external_rel(self): - rel = _Relationship(None, None, "target", None, external=True) - assert rel.target_ref == "target" - - def it_should_have_relative_ref_for_internal_rel(self): - """ - Internal relationships (TargetMode == 'Internal' in the XML) should - have a relative ref, e.g. '../slideLayouts/slideLayout1.xml', for - the target_ref attribute. - """ - part = Mock(name="part", partname=PackURI("/ppt/media/image1.png")) - baseURI = "/ppt/slides" - rel = _Relationship(None, None, part, baseURI) # external=False - assert rel.target_ref == "../media/image1.png" - - class DescribeRelationshipCollection(object): """Unit-test suite for `pptx.opc.package._Relationships` objects.""" @@ -776,6 +738,16 @@ def it_can_add_a_relationship(self, _Relationship_): assert rels[rId] == rel assert rel == _Relationship_.return_value + def it_can_add_a_relationship_if_not_found( + self, rels_with_matching_rel_, rels_with_missing_rel_ + ): + + rels, reltype, part, matching_rel = rels_with_matching_rel_ + assert rels.get_or_add(reltype, part) == matching_rel + + rels, reltype, part, new_rel = rels_with_missing_rel_ + assert rels.get_or_add(reltype, part) == new_rel + def it_can_add_an_external_relationship(self, add_ext_rel_fixture_): rels, reltype, url = add_ext_rel_fixture_ rId = rels.get_or_add_ext_rel(reltype, url) @@ -792,10 +764,28 @@ def it_should_return_an_existing_one_if_it_matches( assert _rId == rId assert len(rels) == 1 + def it_can_find_a_related_part_by_reltype(self, rels_with_target_known_by_reltype): + rels, reltype, known_target_part = rels_with_target_known_by_reltype + part = rels.part_with_reltype(reltype) + assert part is known_target_part + + def it_can_find_a_related_part_by_rId(self, rels_with_known_target_part): + rels, rId, known_target_part = rels_with_known_target_part + part = rels.related_parts[rId] + assert part is known_target_part + + def it_raises_KeyError_on_part_with_rId_not_found(self): + with pytest.raises(KeyError): + RelationshipCollection(None).related_parts["rId666"] + + def it_knows_the_next_available_rId_to_help(self, rels_with_rId_gap): + rels, expected_next_rId = rels_with_rId_gap + next_rId = rels._next_rId + assert next_rId == expected_next_rId + def it_can_compose_rels_xml(self, rels, rels_elm): - # exercise --------------------- rels.xml - # verify ----------------------- + rels_elm.assert_has_calls( [ call.add_rel("rId1", "http://rt-hyperlink", "http://some/link", True), @@ -805,7 +795,7 @@ def it_can_compose_rels_xml(self, rels, rels_elm): any_order=True, ) - # fixtures --------------------------------------------- + # --- fixtures ----------------------------------------- @pytest.fixture def add_ext_rel_fixture_(self, reltype, url): @@ -819,6 +809,95 @@ def add_matching_ext_rel_fixture_(self, request, reltype, url): rels.add_relationship(reltype, url, rId, is_external=True) return rels, reltype, url, rId + @pytest.fixture + def _rel_with_known_target_part(self, _rId, _reltype, _target_part, _baseURI): + rel = _Relationship(_rId, _reltype, _target_part, _baseURI) + return rel, _rId, _target_part + + @pytest.fixture + def _rel_with_target_known_by_reltype(self, _rId, _reltype, _target_part, _baseURI): + rel = _Relationship(_rId, _reltype, _target_part, _baseURI) + return rel, _reltype, _target_part + + @pytest.fixture + def rels_elm(self, request): + """Return a rels_elm mock that will be returned from CT_Relationships.new()""" + # --- create rels_elm mock with a .xml property --- + rels_elm = Mock(name="rels_elm") + xml = PropertyMock(name="xml") + type(rels_elm).xml = xml + rels_elm.attach_mock(xml, "xml") + rels_elm.reset_mock() # to clear attach_mock call + # --- patch CT_Relationships to return that rels_elm --- + patch_ = patch.object(CT_Relationships, "new", return_value=rels_elm) + patch_.start() + request.addfinalizer(patch_.stop) + return rels_elm + + @pytest.fixture + def rels_with_known_target_part(self, rels, _rel_with_known_target_part): + rel, rId, target_part = _rel_with_known_target_part + rels.add_relationship(None, target_part, rId) + return rels, rId, target_part + + @pytest.fixture + def rels_with_matching_rel_(self, request, rels): + matching_reltype_ = instance_mock(request, str, name="matching_reltype_") + matching_part_ = instance_mock(request, Part, name="matching_part_") + matching_rel_ = instance_mock( + request, + _Relationship, + name="matching_rel_", + reltype=matching_reltype_, + target_part=matching_part_, + is_external=False, + ) + rels[1] = matching_rel_ + return rels, matching_reltype_, matching_part_, matching_rel_ + + @pytest.fixture + def rels_with_missing_rel_(self, request, rels, _Relationship_): + missing_reltype_ = instance_mock(request, str, name="missing_reltype_") + missing_part_ = instance_mock(request, Part, name="missing_part_") + new_rel_ = instance_mock( + request, + _Relationship, + name="new_rel_", + reltype=missing_reltype_, + target_part=missing_part_, + is_external=False, + ) + _Relationship_.return_value = new_rel_ + return rels, missing_reltype_, missing_part_, new_rel_ + + @pytest.fixture + def rels_with_rId_gap(self, request): + rels = RelationshipCollection(None) + + rel_with_rId1 = instance_mock( + request, _Relationship, name="rel_with_rId1", rId="rId1" + ) + rel_with_rId3 = instance_mock( + request, _Relationship, name="rel_with_rId3", rId="rId3" + ) + rels["rId1"] = rel_with_rId1 + rels["rId3"] = rel_with_rId3 + return rels, "rId2" + + @pytest.fixture + def rels_with_target_known_by_reltype( + self, rels, _rel_with_target_known_by_reltype + ): + rel, reltype, target_part = _rel_with_target_known_by_reltype + rels[1] = rel + return rels, reltype, target_part + + # --- fixture components ------------------------------- + + @pytest.fixture + def _baseURI(self): + return "/baseURI" + @pytest.fixture def _Relationship_(self, request): return class_mock(request, "pptx.opc.package._Relationship") @@ -842,32 +921,64 @@ def rels(self): return rels @pytest.fixture - def rels_elm(self, request): - """ - Return a rels_elm mock that will be returned from - CT_Relationships.new() - """ - # create rels_elm mock with a .xml property - rels_elm = Mock(name="rels_elm") - xml = PropertyMock(name="xml") - type(rels_elm).xml = xml - rels_elm.attach_mock(xml, "xml") - rels_elm.reset_mock() # to clear attach_mock call - # patch CT_Relationships to return that rels_elm - patch_ = patch.object(CT_Relationships, "new", return_value=rels_elm) - patch_.start() - request.addfinalizer(patch_.stop) - return rels_elm + def _reltype(self): + return RT.SLIDE @pytest.fixture def reltype(self): return "http://rel/type" + @pytest.fixture + def _rId(self): + return "rId6" + + @pytest.fixture + def _target_part(self, request): + return loose_mock(request) + @pytest.fixture def url(self): return "https://github.com/scanny/python-pptx" +class Describe_Relationship(object): + """Unit-test suite for `pptx.opc.package._Relationship` objects.""" + + def it_remembers_construction_values(self): + # test data -------------------- + rId = "rId9" + reltype = "reltype" + target = Mock(name="target_part") + external = False + # exercise --------------------- + rel = _Relationship(rId, reltype, target, None, external) + # verify ----------------------- + assert rel.rId == rId + assert rel.reltype == reltype + assert rel.target_part == target + assert rel.is_external == external + + def it_should_raise_on_target_part_access_on_external_rel(self): + rel = _Relationship(None, None, None, None, external=True) + with pytest.raises(ValueError): + rel.target_part + + def it_should_have_target_ref_for_external_rel(self): + rel = _Relationship(None, None, "target", None, external=True) + assert rel.target_ref == "target" + + def it_should_have_relative_ref_for_internal_rel(self): + """ + Internal relationships (TargetMode == 'Internal' in the XML) should + have a relative ref, e.g. '../slideLayouts/slideLayout1.xml', for + the target_ref attribute. + """ + part = Mock(name="part", partname=PackURI("/ppt/media/image1.png")) + baseURI = "/ppt/slides" + rel = _Relationship(None, None, part, baseURI) # external=False + assert rel.target_ref == "../media/image1.png" + + class DescribeUnmarshaller(object): def it_can_unmarshal_from_a_pkg_reader( self, diff --git a/tests/opc/test_phys_pkg.py b/tests/opc/test_phys_pkg.py deleted file mode 100644 index aa6db8bae..000000000 --- a/tests/opc/test_phys_pkg.py +++ /dev/null @@ -1,193 +0,0 @@ -# encoding: utf-8 - -""" -Test suite for pptx.opc.packaging module -""" - -from __future__ import absolute_import - -try: - from io import BytesIO # Python 3 -except ImportError: - from StringIO import StringIO as BytesIO - -import hashlib -import pytest - -from zipfile import ZIP_DEFLATED, ZipFile - -from pptx.exceptions import PackageNotFoundError -from pptx.opc.packuri import PACKAGE_URI, PackURI -from pptx.opc.phys_pkg import ( - _DirPkgReader, - PhysPkgReader, - PhysPkgWriter, - _ZipPkgReader, - _ZipPkgWriter, -) - -from ..unitutil.file import absjoin, test_file_dir -from ..unitutil.mock import class_mock, loose_mock, Mock - - -test_pptx_path = absjoin(test_file_dir, "test.pptx") -dir_pkg_path = absjoin(test_file_dir, "expanded_pptx") -zip_pkg_path = test_pptx_path - - -class DescribeDirPkgReader(object): - def it_is_used_by_PhysPkgReader_when_pkg_is_a_dir(self): - phys_reader = PhysPkgReader(dir_pkg_path) - assert isinstance(phys_reader, _DirPkgReader) - - def it_doesnt_mind_being_closed_even_though_it_doesnt_need_it(self, dir_reader): - dir_reader.close() - - def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_reader): - pack_uri = PackURI("/ppt/presentation.xml") - blob = dir_reader.blob_for(pack_uri) - sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" - - def it_can_get_the_content_types_xml(self, dir_reader): - sha1 = hashlib.sha1(dir_reader.content_types_xml).hexdigest() - assert sha1 == "a68cf138be3c4eb81e47e2550166f9949423c7df" - - def it_can_retrieve_the_rels_xml_for_a_source_uri(self, dir_reader): - rels_xml = dir_reader.rels_xml_for(PACKAGE_URI) - sha1 = hashlib.sha1(rels_xml).hexdigest() - assert sha1 == "64ffe86bb2bbaad53c3c1976042b907f8e10c5a3" - - def it_returns_none_when_part_has_no_rels_xml(self, dir_reader): - partname = PackURI("/ppt/viewProps.xml") - rels_xml = dir_reader.rels_xml_for(partname) - assert rels_xml is None - - # fixtures --------------------------------------------- - - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) - - @pytest.fixture(scope="class") - def dir_reader(self): - return _DirPkgReader(dir_pkg_path) - - -class DescribePhysPkgReader(object): - def it_raises_when_pkg_path_is_not_a_package(self): - with pytest.raises(PackageNotFoundError): - PhysPkgReader("foobar") - - -class DescribeZipPkgReader(object): - def it_is_used_by_PhysPkgReader_when_pkg_is_a_zip(self): - phys_reader = PhysPkgReader(zip_pkg_path) - assert isinstance(phys_reader, _ZipPkgReader) - - def it_is_used_by_PhysPkgReader_when_pkg_is_a_stream(self): - with open(zip_pkg_path, "rb") as stream: - phys_reader = PhysPkgReader(stream) - assert isinstance(phys_reader, _ZipPkgReader) - - def it_opens_pkg_file_zip_on_construction(self, ZipFile_, pkg_file_): - _ZipPkgReader(pkg_file_) - ZipFile_.assert_called_once_with(pkg_file_, "r") - - def it_can_be_closed(self, ZipFile_): - # mockery ---------------------- - zipf = ZipFile_.return_value - zip_pkg_reader = _ZipPkgReader(None) - # exercise --------------------- - zip_pkg_reader.close() - # verify ----------------------- - zipf.close.assert_called_once_with() - - def it_can_retrieve_the_blob_for_a_pack_uri(self, phys_reader): - pack_uri = PackURI("/ppt/presentation.xml") - blob = phys_reader.blob_for(pack_uri) - sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "efa7bee0ac72464903a67a6744c1169035d52a54" - - def it_has_the_content_types_xml(self, phys_reader): - sha1 = hashlib.sha1(phys_reader.content_types_xml).hexdigest() - assert sha1 == "ab762ac84414fce18893e18c3f53700c01db56c3" - - def it_can_retrieve_rels_xml_for_source_uri(self, phys_reader): - rels_xml = phys_reader.rels_xml_for(PACKAGE_URI) - sha1 = hashlib.sha1(rels_xml).hexdigest() - assert sha1 == "e31451d4bbe7d24adbe21454b8e9fdae92f50de5" - - def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): - partname = PackURI("/ppt/viewProps.xml") - rels_xml = phys_reader.rels_xml_for(partname) - assert rels_xml is None - - # fixtures --------------------------------------------- - - @pytest.fixture(scope="class") - def phys_reader(self, request): - phys_reader = _ZipPkgReader(zip_pkg_path) - request.addfinalizer(phys_reader.close) - return phys_reader - - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) - - -class DescribeZipPkgWriter(object): - def it_is_used_by_PhysPkgWriter_unconditionally(self, tmp_pptx_path): - phys_writer = PhysPkgWriter(tmp_pptx_path) - assert isinstance(phys_writer, _ZipPkgWriter) - - def it_opens_pkg_file_zip_on_construction(self, ZipFile_): - pkg_file = Mock(name="pkg_file") - _ZipPkgWriter(pkg_file) - ZipFile_.assert_called_once_with(pkg_file, "w", compression=ZIP_DEFLATED) - - def it_can_be_closed(self, ZipFile_): - # mockery ---------------------- - zipf = ZipFile_.return_value - zip_pkg_writer = _ZipPkgWriter(None) - # exercise --------------------- - zip_pkg_writer.close() - # verify ----------------------- - zipf.close.assert_called_once_with() - - def it_can_write_a_blob(self, pkg_file): - # setup ------------------------ - pack_uri = PackURI("/part/name.xml") - blob = "".encode("utf-8") - # exercise --------------------- - pkg_writer = PhysPkgWriter(pkg_file) - pkg_writer.write(pack_uri, blob) - pkg_writer.close() - # verify ----------------------- - written_blob_sha1 = hashlib.sha1(blob).hexdigest() - zipf = ZipFile(pkg_file, "r") - retrieved_blob = zipf.read(pack_uri.membername) - zipf.close() - retrieved_blob_sha1 = hashlib.sha1(retrieved_blob).hexdigest() - assert retrieved_blob_sha1 == written_blob_sha1 - - # fixtures --------------------------------------------- - - @pytest.fixture - def pkg_file(self, request): - pkg_file = BytesIO() - request.addfinalizer(pkg_file.close) - return pkg_file - - -# fixtures ------------------------------------------------- - - -@pytest.fixture -def tmp_pptx_path(tmpdir): - return str(tmpdir.join("test_python-pptx.pptx")) - - -@pytest.fixture -def ZipFile_(request): - return class_mock(request, "pptx.opc.phys_pkg.ZipFile") diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py deleted file mode 100644 index 72d2b09ac..000000000 --- a/tests/opc/test_pkgwriter.py +++ /dev/null @@ -1,171 +0,0 @@ -# encoding: utf-8 - -""" -Test suite for opc.pkgwriter module -""" - -import pytest - -from pptx.opc.constants import CONTENT_TYPE as CT -from pptx.opc.package import Part -from pptx.opc.packuri import PackURI -from pptx.opc.pkgwriter import _ContentTypesItem, PackageWriter - -from .unitdata.types import a_Default, a_Types, an_Override -from ..unitutil.mock import ( - call, - function_mock, - instance_mock, - MagicMock, - method_mock, - Mock, - patch, -) - - -class DescribePackageWriter(object): - def it_can_write_a_package(self, PhysPkgWriter_, _write_methods): - # mockery ---------------------- - pkg_file = Mock(name="pkg_file") - pkg_rels = Mock(name="pkg_rels") - parts = Mock(name="parts") - phys_writer = PhysPkgWriter_.return_value - # exercise --------------------- - PackageWriter.write(pkg_file, pkg_rels, parts) - # verify ----------------------- - expected_calls = [ - call._write_content_types_stream(phys_writer, parts), - call._write_pkg_rels(phys_writer, pkg_rels), - call._write_parts(phys_writer, parts), - ] - PhysPkgWriter_.assert_called_once_with(pkg_file) - assert _write_methods.mock_calls == expected_calls - phys_writer.close.assert_called_once_with() - - def it_can_write_a_content_types_stream(self, xml_for, serialize_part_xml_): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - parts = Mock(name="parts") - # exercise --------------------- - PackageWriter._write_content_types_stream(phys_writer, parts) - # verify ----------------------- - xml_for.assert_called_once_with(parts) - serialize_part_xml_.assert_called_once_with(xml_for.return_value) - phys_writer.write.assert_called_once_with( - "/[Content_Types].xml", serialize_part_xml_.return_value - ) - - def it_can_write_a_pkg_rels_item(self): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - pkg_rels = Mock(name="pkg_rels") - # exercise --------------------- - PackageWriter._write_pkg_rels(phys_writer, pkg_rels) - # verify ----------------------- - phys_writer.write.assert_called_once_with("/_rels/.rels", pkg_rels.xml) - - def it_can_write_a_list_of_parts(self): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - rels = MagicMock(name="rels") - rels.__len__.return_value = 1 - part1 = Mock(name="part1", _rels=rels) - part2 = Mock(name="part2", _rels=[]) - # exercise --------------------- - PackageWriter._write_parts(phys_writer, [part1, part2]) - # verify ----------------------- - expected_calls = [ - call(part1.partname, part1.blob), - call(part1.partname.rels_uri, part1._rels.xml), - call(part2.partname, part2.blob), - ] - assert phys_writer.write.mock_calls == expected_calls - - # fixtures --------------------------------------------- - - @pytest.fixture - def PhysPkgWriter_(self, request): - _patch = patch("pptx.opc.pkgwriter.PhysPkgWriter") - request.addfinalizer(_patch.stop) - return _patch.start() - - @pytest.fixture - def serialize_part_xml_(self, request): - return function_mock(request, "pptx.opc.pkgwriter.serialize_part_xml") - - @pytest.fixture - def _write_methods(self, request): - """Mock that patches all the _write_* methods of PackageWriter""" - root_mock = Mock(name="PackageWriter") - patch1 = patch.object(PackageWriter, "_write_content_types_stream") - patch2 = patch.object(PackageWriter, "_write_pkg_rels") - patch3 = patch.object(PackageWriter, "_write_parts") - root_mock.attach_mock(patch1.start(), "_write_content_types_stream") - root_mock.attach_mock(patch2.start(), "_write_pkg_rels") - root_mock.attach_mock(patch3.start(), "_write_parts") - - def fin(): - patch1.stop() - patch2.stop() - patch3.stop() - - request.addfinalizer(fin) - return root_mock - - @pytest.fixture - def xml_for(self, request): - return method_mock(request, _ContentTypesItem, "xml_for") - - -class Describe_ContentTypesItem(object): - def it_can_compose_content_types_xml(self, xml_for_fixture): - parts, expected_xml = xml_for_fixture - types_elm = _ContentTypesItem.xml_for(parts) - assert types_elm.xml == expected_xml - - # fixtures --------------------------------------------- - - def _mock_part(self, request, name, partname_str, content_type): - partname = PackURI(partname_str) - return instance_mock( - request, Part, name=name, partname=partname, content_type=content_type - ) - - @pytest.fixture( - params=[ - ("Default", "/ppt/MEDIA/image.PNG", CT.PNG), - ("Default", "/ppt/media/image.xml", CT.XML), - ("Default", "/ppt/media/image.rels", CT.OPC_RELATIONSHIPS), - ("Default", "/ppt/media/image.jpeg", CT.JPEG), - ("Override", "/docProps/core.xml", "app/vnd.core"), - ("Override", "/ppt/slides/slide1.xml", "app/vnd.ct_sld"), - ("Override", "/zebra/foo.bar", "app/vnd.foobar"), - ] - ) - def xml_for_fixture(self, request): - elm_type, partname_str, content_type = request.param - part_ = self._mock_part(request, "part_", partname_str, content_type) - # expected_xml ----------------- - types_bldr = a_Types().with_nsdecls() - ext = partname_str.split(".")[-1].lower() - if elm_type == "Default" and ext not in ("rels", "xml"): - default_bldr = a_Default() - default_bldr.with_Extension(ext) - default_bldr.with_ContentType(content_type) - types_bldr.with_child(default_bldr) - - types_bldr.with_child( - a_Default().with_Extension("rels").with_ContentType(CT.OPC_RELATIONSHIPS) - ) - types_bldr.with_child( - a_Default().with_Extension("xml").with_ContentType(CT.XML) - ) - - if elm_type == "Override": - override_bldr = an_Override() - override_bldr.with_PartName(partname_str) - override_bldr.with_ContentType(content_type) - types_bldr.with_child(override_bldr) - - expected_xml = types_bldr.xml() - return [part_], expected_xml diff --git a/tests/opc/test_rels.py b/tests/opc/test_rels.py deleted file mode 100644 index 489cba27a..000000000 --- a/tests/opc/test_rels.py +++ /dev/null @@ -1,268 +0,0 @@ -# encoding: utf-8 - -"""Test suite for pptx.part module.""" - -from __future__ import absolute_import - -import pytest - -from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.oxml import CT_Relationships -from pptx.opc.package import Part, _Relationship, RelationshipCollection -from pptx.opc.packuri import PackURI - -from ..unitutil.mock import ( - call, - class_mock, - instance_mock, - loose_mock, - Mock, - patch, - PropertyMock, -) - - -class Describe_Relationship(object): - def it_remembers_construction_values(self): - # test data -------------------- - rId = "rId9" - reltype = "reltype" - target = Mock(name="target_part") - external = False - # exercise --------------------- - rel = _Relationship(rId, reltype, target, None, external) - # verify ----------------------- - assert rel.rId == rId - assert rel.reltype == reltype - assert rel.target_part == target - assert rel.is_external == external - - def it_should_raise_on_target_part_access_on_external_rel(self): - rel = _Relationship(None, None, None, None, external=True) - with pytest.raises(ValueError): - rel.target_part - - def it_should_have_target_ref_for_external_rel(self): - rel = _Relationship(None, None, "target", None, external=True) - assert rel.target_ref == "target" - - def it_should_have_relative_ref_for_internal_rel(self): - """ - Internal relationships (TargetMode == 'Internal' in the XML) should - have a relative ref, e.g. '../slideLayouts/slideLayout1.xml', for - the target_ref attribute. - """ - part = Mock(name="part", partname=PackURI("/ppt/media/image1.png")) - baseURI = "/ppt/slides" - rel = _Relationship(None, None, part, baseURI) # external=False - assert rel.target_ref == "../media/image1.png" - - -class DescribeRelationshipCollection(object): - def it_also_has_dict_style_get_rel_by_rId(self, rels_with_known_rel): - rels, rId, known_rel = rels_with_known_rel - assert rels[rId] == known_rel - - def it_should_raise_on_failed_lookup_by_rId(self, rels): - with pytest.raises(KeyError): - rels["rId666"] - - def it_has_a_len(self, rels): - assert len(rels) == 0 - - def it_can_add_a_relationship(self, _Relationship_): - baseURI, rId, reltype, target, is_external = ( - "baseURI", - "rId9", - "reltype", - "target", - False, - ) - rels = RelationshipCollection(baseURI) - rel = rels.add_relationship(reltype, target, rId, is_external) - _Relationship_.assert_called_once_with( - rId, reltype, target, baseURI, is_external - ) - assert rels[rId] == rel - assert rel == _Relationship_.return_value - - def it_can_add_a_relationship_if_not_found( - self, rels_with_matching_rel_, rels_with_missing_rel_ - ): - - rels, reltype, part, matching_rel = rels_with_matching_rel_ - assert rels.get_or_add(reltype, part) == matching_rel - - rels, reltype, part, new_rel = rels_with_missing_rel_ - assert rels.get_or_add(reltype, part) == new_rel - - def it_knows_the_next_available_rId(self, rels_with_rId_gap): - rels, expected_next_rId = rels_with_rId_gap - next_rId = rels._next_rId - assert next_rId == expected_next_rId - - def it_can_find_a_related_part_by_reltype(self, rels_with_target_known_by_reltype): - rels, reltype, known_target_part = rels_with_target_known_by_reltype - part = rels.part_with_reltype(reltype) - assert part is known_target_part - - def it_can_find_a_related_part_by_rId(self, rels_with_known_target_part): - rels, rId, known_target_part = rels_with_known_target_part - part = rels.related_parts[rId] - assert part is known_target_part - - def it_raises_KeyError_on_part_with_rId_not_found(self, rels): - with pytest.raises(KeyError): - rels.related_parts["rId666"] - - def it_can_compose_rels_xml(self, rels_with_known_rels, rels_elm): - rels_with_known_rels.xml - rels_elm.assert_has_calls( - [ - call.add_rel("rId1", "http://rt-hyperlink", "http://some/link", True), - call.add_rel("rId2", "http://rt-image", "../media/image1.png", False), - call.xml(), - ], - any_order=True, - ) - - # def it_raises_on_add_rel_with_duplicate_rId(self, rels, rel): - # with pytest.raises(ValueError): - # rels.add_rel(rel) - - # fixtures --------------------------------------------- - - @pytest.fixture - def _Relationship_(self, request): - return class_mock(request, "pptx.opc.package._Relationship") - - @pytest.fixture - def rel(self, _rId, _reltype, _target_part, _baseURI): - return _Relationship(_rId, _reltype, _target_part, _baseURI) - - @pytest.fixture - def rels(self, _baseURI): - return RelationshipCollection(_baseURI) - - @pytest.fixture - def rels_elm(self, request): - """ - Return a rels_elm mock that will be returned from - CT_Relationships.new() - """ - # create rels_elm mock with a .xml property - rels_elm = Mock(name="rels_elm") - xml = PropertyMock(name="xml") - type(rels_elm).xml = xml - rels_elm.attach_mock(xml, "xml") - rels_elm.reset_mock() # to clear attach_mock call - # patch CT_Relationships to return that rels_elm - patch_ = patch.object(CT_Relationships, "new", return_value=rels_elm) - patch_.start() - request.addfinalizer(patch_.stop) - return rels_elm - - @pytest.fixture - def rels_with_known_rel(self, rels, _rId, rel): - rels[_rId] = rel - return rels, _rId, rel - - @pytest.fixture - def rels_with_known_rels(self): - """ - Populated RelationshipCollection instance that will exercise the - rels.xml property. - """ - rels = RelationshipCollection("/baseURI") - rels.add_relationship( - reltype="http://rt-hyperlink", - target="http://some/link", - rId="rId1", - is_external=True, - ) - part = Mock(name="part") - part.partname.relative_ref.return_value = "../media/image1.png" - rels.add_relationship(reltype="http://rt-image", target=part, rId="rId2") - return rels - - @pytest.fixture - def rels_with_known_target_part(self, rels, _rel_with_known_target_part): - rel, rId, target_part = _rel_with_known_target_part - rels.add_relationship(None, target_part, rId) - return rels, rId, target_part - - @pytest.fixture - def rels_with_matching_rel_(self, request, rels): - matching_reltype_ = instance_mock(request, str, name="matching_reltype_") - matching_part_ = instance_mock(request, Part, name="matching_part_") - matching_rel_ = instance_mock( - request, - _Relationship, - name="matching_rel_", - reltype=matching_reltype_, - target_part=matching_part_, - is_external=False, - ) - rels[1] = matching_rel_ - return rels, matching_reltype_, matching_part_, matching_rel_ - - @pytest.fixture - def rels_with_missing_rel_(self, request, rels, _Relationship_): - missing_reltype_ = instance_mock(request, str, name="missing_reltype_") - missing_part_ = instance_mock(request, Part, name="missing_part_") - new_rel_ = instance_mock( - request, - _Relationship, - name="new_rel_", - reltype=missing_reltype_, - target_part=missing_part_, - is_external=False, - ) - _Relationship_.return_value = new_rel_ - return rels, missing_reltype_, missing_part_, new_rel_ - - @pytest.fixture - def rels_with_rId_gap(self, request, rels): - rel_with_rId1 = instance_mock( - request, _Relationship, name="rel_with_rId1", rId="rId1" - ) - rel_with_rId3 = instance_mock( - request, _Relationship, name="rel_with_rId3", rId="rId3" - ) - rels["rId1"] = rel_with_rId1 - rels["rId3"] = rel_with_rId3 - return rels, "rId2" - - @pytest.fixture - def rels_with_target_known_by_reltype( - self, rels, _rel_with_target_known_by_reltype - ): - rel, reltype, target_part = _rel_with_target_known_by_reltype - rels[1] = rel - return rels, reltype, target_part - - @pytest.fixture - def _baseURI(self): - return "/baseURI" - - @pytest.fixture - def _rel_with_known_target_part(self, _rId, _reltype, _target_part, _baseURI): - rel = _Relationship(_rId, _reltype, _target_part, _baseURI) - return rel, _rId, _target_part - - @pytest.fixture - def _rel_with_target_known_by_reltype(self, _rId, _reltype, _target_part, _baseURI): - rel = _Relationship(_rId, _reltype, _target_part, _baseURI) - return rel, _reltype, _target_part - - @pytest.fixture - def _reltype(self): - return RT.SLIDE - - @pytest.fixture - def _rId(self): - return "rId6" - - @pytest.fixture - def _target_part(self, request): - return loose_mock(request) diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_serialized.py similarity index 54% rename from tests/opc/test_pkgreader.py rename to tests/opc/test_serialized.py index b561e6421..1cf78d745 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_serialized.py @@ -1,37 +1,58 @@ # encoding: utf-8 -""" -Test suite for opc.pkgreader module -""" +"""Unit-test suite for `pptx.opc.serialized` module.""" -from __future__ import absolute_import, print_function, unicode_literals +try: + from io import BytesIO # Python 3 +except ImportError: + from StringIO import StringIO as BytesIO +import hashlib import pytest +from zipfile import ZIP_DEFLATED, ZipFile + +from pptx.exceptions import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM from pptx.opc.oxml import CT_Relationship -from pptx.opc.packuri import PackURI -from pptx.opc.phys_pkg import _ZipPkgReader -from pptx.opc.pkgreader import ( - _ContentTypeMap, +from pptx.opc.package import Part +from pptx.opc.packuri import PACKAGE_URI, PackURI +from pptx.opc.serialized import ( PackageReader, + PackageWriter, + _ContentTypeMap, + _ContentTypesItem, + _DirPkgReader, + _PhysPkgReader, + _PhysPkgWriter, _SerializedPart, _SerializedRelationship, _SerializedRelationshipCollection, + _ZipPkgReader, + _ZipPkgWriter, ) from .unitdata.types import a_Default, a_Types, an_Override +from ..unitutil.file import absjoin, test_file_dir from ..unitutil.mock import ( + MagicMock, + Mock, call, class_mock, function_mock, initializer_mock, + instance_mock, + loose_mock, method_mock, - Mock, patch, ) +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): @pytest.fixture def from_xml(self, request): @@ -46,19 +67,19 @@ def _load_serialized_parts(self, request): return method_mock(request, PackageReader, "_load_serialized_parts") @pytest.fixture - def PhysPkgReader_(self, request): - _patch = patch("pptx.opc.pkgreader.PhysPkgReader", spec_set=_ZipPkgReader) + def _PhysPkgReader_(self, request): + _patch = patch("pptx.opc.serialized._PhysPkgReader", spec_set=_ZipPkgReader) request.addfinalizer(_patch.stop) return _patch.start() @pytest.fixture def _SerializedPart_(self, request): - return class_mock(request, "pptx.opc.pkgreader._SerializedPart") + return class_mock(request, "pptx.opc.serialized._SerializedPart") @pytest.fixture def _SerializedRelationshipCollection_(self, request): return class_mock( - request, "pptx.opc.pkgreader._SerializedRelationshipCollection" + request, "pptx.opc.serialized._SerializedRelationshipCollection" ) @pytest.fixture @@ -70,10 +91,10 @@ def _walk_phys_parts(self, request): return method_mock(request, PackageReader, "_walk_phys_parts") def it_can_construct_from_pkg_file( - self, init, PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts + self, init, _PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts ): # mockery ---------------------- - phys_reader = PhysPkgReader_.return_value + phys_reader = _PhysPkgReader_.return_value content_types = from_xml.return_value pkg_srels = _srels_for.return_value sparts = _load_serialized_parts.return_value @@ -81,7 +102,7 @@ def it_can_construct_from_pkg_file( # exercise --------------------- pkg_reader = PackageReader.from_file(pkg_file) # verify ----------------------- - PhysPkgReader_.assert_called_once_with(pkg_file) + _PhysPkgReader_.assert_called_once_with(pkg_file) from_xml.assert_called_once_with(phys_reader.content_types_xml) _srels_for.assert_called_once_with(phys_reader, "/") _load_serialized_parts.assert_called_once_with( @@ -319,6 +340,245 @@ def _xml_from(self, entries): return types_bldr.xml() +class DescribePackageWriter(object): + def it_can_write_a_package(self, _PhysPkgWriter_, _write_methods): + # mockery ---------------------- + pkg_file = Mock(name="pkg_file") + pkg_rels = Mock(name="pkg_rels") + parts = Mock(name="parts") + phys_writer = _PhysPkgWriter_.return_value + # exercise --------------------- + PackageWriter.write(pkg_file, pkg_rels, parts) + # verify ----------------------- + expected_calls = [ + call._write_content_types_stream(phys_writer, parts), + call._write_pkg_rels(phys_writer, pkg_rels), + call._write_parts(phys_writer, parts), + ] + _PhysPkgWriter_.assert_called_once_with(pkg_file) + assert _write_methods.mock_calls == expected_calls + phys_writer.close.assert_called_once_with() + + def it_can_write_a_content_types_stream(self, xml_for, serialize_part_xml_): + # mockery ---------------------- + phys_writer = Mock(name="phys_writer") + parts = Mock(name="parts") + # exercise --------------------- + PackageWriter._write_content_types_stream(phys_writer, parts) + # verify ----------------------- + xml_for.assert_called_once_with(parts) + serialize_part_xml_.assert_called_once_with(xml_for.return_value) + phys_writer.write.assert_called_once_with( + "/[Content_Types].xml", serialize_part_xml_.return_value + ) + + def it_can_write_a_pkg_rels_item(self): + # mockery ---------------------- + phys_writer = Mock(name="phys_writer") + pkg_rels = Mock(name="pkg_rels") + # exercise --------------------- + PackageWriter._write_pkg_rels(phys_writer, pkg_rels) + # verify ----------------------- + phys_writer.write.assert_called_once_with("/_rels/.rels", pkg_rels.xml) + + def it_can_write_a_list_of_parts(self): + # mockery ---------------------- + phys_writer = Mock(name="phys_writer") + rels = MagicMock(name="rels") + rels.__len__.return_value = 1 + part1 = Mock(name="part1", _rels=rels) + part2 = Mock(name="part2", _rels=[]) + # exercise --------------------- + PackageWriter._write_parts(phys_writer, [part1, part2]) + # verify ----------------------- + expected_calls = [ + call(part1.partname, part1.blob), + call(part1.partname.rels_uri, part1._rels.xml), + call(part2.partname, part2.blob), + ] + assert phys_writer.write.mock_calls == expected_calls + + # fixtures --------------------------------------------- + + @pytest.fixture + def _PhysPkgWriter_(self, request): + _patch = patch("pptx.opc.serialized._PhysPkgWriter") + request.addfinalizer(_patch.stop) + return _patch.start() + + @pytest.fixture + def serialize_part_xml_(self, request): + return function_mock(request, "pptx.opc.serialized.serialize_part_xml") + + @pytest.fixture + def _write_methods(self, request): + """Mock that patches all the _write_* methods of PackageWriter""" + root_mock = Mock(name="PackageWriter") + patch1 = patch.object(PackageWriter, "_write_content_types_stream") + patch2 = patch.object(PackageWriter, "_write_pkg_rels") + patch3 = patch.object(PackageWriter, "_write_parts") + root_mock.attach_mock(patch1.start(), "_write_content_types_stream") + root_mock.attach_mock(patch2.start(), "_write_pkg_rels") + root_mock.attach_mock(patch3.start(), "_write_parts") + + def fin(): + patch1.stop() + patch2.stop() + patch3.stop() + + request.addfinalizer(fin) + return root_mock + + @pytest.fixture + def xml_for(self, request): + return method_mock(request, _ContentTypesItem, "xml_for") + + +class Describe_DirPkgReader(object): + def it_is_used_by_PhysPkgReader_when_pkg_is_a_dir(self): + phys_reader = _PhysPkgReader(dir_pkg_path) + assert isinstance(phys_reader, _DirPkgReader) + + def it_doesnt_mind_being_closed_even_though_it_doesnt_need_it(self, dir_reader): + dir_reader.close() + + def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_reader): + pack_uri = PackURI("/ppt/presentation.xml") + blob = dir_reader.blob_for(pack_uri) + sha1 = hashlib.sha1(blob).hexdigest() + assert sha1 == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" + + def it_can_get_the_content_types_xml(self, dir_reader): + sha1 = hashlib.sha1(dir_reader.content_types_xml).hexdigest() + assert sha1 == "a68cf138be3c4eb81e47e2550166f9949423c7df" + + def it_can_retrieve_the_rels_xml_for_a_source_uri(self, dir_reader): + rels_xml = dir_reader.rels_xml_for(PACKAGE_URI) + sha1 = hashlib.sha1(rels_xml).hexdigest() + assert sha1 == "64ffe86bb2bbaad53c3c1976042b907f8e10c5a3" + + def it_returns_none_when_part_has_no_rels_xml(self, dir_reader): + partname = PackURI("/ppt/viewProps.xml") + rels_xml = dir_reader.rels_xml_for(partname) + assert rels_xml is None + + # fixtures --------------------------------------------- + + @pytest.fixture + def pkg_file_(self, request): + return loose_mock(request) + + @pytest.fixture(scope="class") + def dir_reader(self): + return _DirPkgReader(dir_pkg_path) + + +class Describe_PhysPkgReader(object): + def it_raises_when_pkg_path_is_not_a_package(self): + with pytest.raises(PackageNotFoundError): + _PhysPkgReader("foobar") + + +class Describe_ZipPkgReader(object): + def it_is_used_by_PhysPkgReader_when_pkg_is_a_zip(self): + phys_reader = _PhysPkgReader(zip_pkg_path) + assert isinstance(phys_reader, _ZipPkgReader) + + def it_is_used_by_PhysPkgReader_when_pkg_is_a_stream(self): + with open(zip_pkg_path, "rb") as stream: + phys_reader = _PhysPkgReader(stream) + assert isinstance(phys_reader, _ZipPkgReader) + + def it_opens_pkg_file_zip_on_construction(self, ZipFile_, pkg_file_): + _ZipPkgReader(pkg_file_) + ZipFile_.assert_called_once_with(pkg_file_, "r") + + def it_can_be_closed(self, ZipFile_): + # mockery ---------------------- + zipf = ZipFile_.return_value + zip_pkg_reader = _ZipPkgReader(None) + # exercise --------------------- + zip_pkg_reader.close() + # verify ----------------------- + zipf.close.assert_called_once_with() + + def it_can_retrieve_the_blob_for_a_pack_uri(self, phys_reader): + pack_uri = PackURI("/ppt/presentation.xml") + blob = phys_reader.blob_for(pack_uri) + sha1 = hashlib.sha1(blob).hexdigest() + assert sha1 == "efa7bee0ac72464903a67a6744c1169035d52a54" + + def it_has_the_content_types_xml(self, phys_reader): + sha1 = hashlib.sha1(phys_reader.content_types_xml).hexdigest() + assert sha1 == "ab762ac84414fce18893e18c3f53700c01db56c3" + + def it_can_retrieve_rels_xml_for_source_uri(self, phys_reader): + rels_xml = phys_reader.rels_xml_for(PACKAGE_URI) + sha1 = hashlib.sha1(rels_xml).hexdigest() + assert sha1 == "e31451d4bbe7d24adbe21454b8e9fdae92f50de5" + + def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): + partname = PackURI("/ppt/viewProps.xml") + rels_xml = phys_reader.rels_xml_for(partname) + assert rels_xml is None + + # fixtures --------------------------------------------- + + @pytest.fixture(scope="class") + def phys_reader(self, request): + phys_reader = _ZipPkgReader(zip_pkg_path) + request.addfinalizer(phys_reader.close) + return phys_reader + + @pytest.fixture + def pkg_file_(self, request): + return loose_mock(request) + + +class Describe_ZipPkgWriter(object): + def it_is_used_by_PhysPkgWriter_unconditionally(self, tmp_pptx_path): + phys_writer = _PhysPkgWriter(tmp_pptx_path) + assert isinstance(phys_writer, _ZipPkgWriter) + + def it_opens_pkg_file_zip_on_construction(self, ZipFile_): + pkg_file = Mock(name="pkg_file") + _ZipPkgWriter(pkg_file) + ZipFile_.assert_called_once_with(pkg_file, "w", compression=ZIP_DEFLATED) + + def it_can_be_closed(self, ZipFile_): + # mockery ---------------------- + zipf = ZipFile_.return_value + zip_pkg_writer = _ZipPkgWriter(None) + # exercise --------------------- + zip_pkg_writer.close() + # verify ----------------------- + zipf.close.assert_called_once_with() + + def it_can_write_a_blob(self, pkg_file): + # setup ------------------------ + pack_uri = PackURI("/part/name.xml") + blob = "".encode("utf-8") + # exercise --------------------- + pkg_writer = _PhysPkgWriter(pkg_file) + pkg_writer.write(pack_uri, blob) + pkg_writer.close() + # verify ----------------------- + written_blob_sha1 = hashlib.sha1(blob).hexdigest() + zipf = ZipFile(pkg_file, "r") + retrieved_blob = zipf.read(pack_uri.membername) + zipf.close() + retrieved_blob_sha1 = hashlib.sha1(retrieved_blob).hexdigest() + assert retrieved_blob_sha1 == written_blob_sha1 + + # fixtures --------------------------------------------- + + @pytest.fixture + def pkg_file(self, request): + pkg_file = BytesIO() + request.addfinalizer(pkg_file.close) + return pkg_file + + class Describe_SerializedPart(object): def it_remembers_construction_values(self): # test data -------------------- @@ -425,8 +685,75 @@ def it_should_be_iterable(self): @pytest.fixture def parse_xml(self, request): - return function_mock(request, "pptx.opc.pkgreader.parse_xml") + return function_mock(request, "pptx.opc.serialized.parse_xml") @pytest.fixture def _SerializedRelationship_(self, request): - return class_mock(request, "pptx.opc.pkgreader._SerializedRelationship") + return class_mock(request, "pptx.opc.serialized._SerializedRelationship") + + +class Describe_ContentTypesItem(object): + def it_can_compose_content_types_xml(self, xml_for_fixture): + parts, expected_xml = xml_for_fixture + types_elm = _ContentTypesItem.xml_for(parts) + assert types_elm.xml == expected_xml + + # fixtures --------------------------------------------- + + def _mock_part(self, request, name, partname_str, content_type): + partname = PackURI(partname_str) + return instance_mock( + request, Part, name=name, partname=partname, content_type=content_type + ) + + @pytest.fixture( + params=[ + ("Default", "/ppt/MEDIA/image.PNG", CT.PNG), + ("Default", "/ppt/media/image.xml", CT.XML), + ("Default", "/ppt/media/image.rels", CT.OPC_RELATIONSHIPS), + ("Default", "/ppt/media/image.jpeg", CT.JPEG), + ("Override", "/docProps/core.xml", "app/vnd.core"), + ("Override", "/ppt/slides/slide1.xml", "app/vnd.ct_sld"), + ("Override", "/zebra/foo.bar", "app/vnd.foobar"), + ] + ) + def xml_for_fixture(self, request): + elm_type, partname_str, content_type = request.param + part_ = self._mock_part(request, "part_", partname_str, content_type) + # expected_xml ----------------- + types_bldr = a_Types().with_nsdecls() + ext = partname_str.split(".")[-1].lower() + if elm_type == "Default" and ext not in ("rels", "xml"): + default_bldr = a_Default() + default_bldr.with_Extension(ext) + default_bldr.with_ContentType(content_type) + types_bldr.with_child(default_bldr) + + types_bldr.with_child( + a_Default().with_Extension("rels").with_ContentType(CT.OPC_RELATIONSHIPS) + ) + types_bldr.with_child( + a_Default().with_Extension("xml").with_ContentType(CT.XML) + ) + + if elm_type == "Override": + override_bldr = an_Override() + override_bldr.with_PartName(partname_str) + override_bldr.with_ContentType(content_type) + types_bldr.with_child(override_bldr) + + expected_xml = types_bldr.xml() + return [part_], expected_xml + + +# fixtures ------------------------------------------------- + + +@pytest.fixture +def tmp_pptx_path(tmpdir): + return str(tmpdir.join("test_python-pptx.pptx")) + + +@pytest.fixture +def ZipFile_(request): + return class_mock(request, "pptx.opc.serialized.zipfile.ZipFile") From 1f8a3979d82aa517feb0d7f316a9879991fcf094 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 22:04:32 -0700 Subject: [PATCH 03/69] rfctr: default mocks to autospec --- pptx/action.py | 3 +- pptx/package.py | 15 +- tests/chart/test_xlsx.py | 119 ++--------- tests/opc/test_package.py | 94 ++------- tests/opc/test_serialized.py | 82 +++---- tests/oxml/shapes/test_groupshape.py | 20 +- tests/parts/test_image.py | 39 ++-- tests/parts/test_slide.py | 43 ++-- tests/shapes/test_freeform.py | 70 +++--- tests/shapes/test_placeholder.py | 305 ++++++++++----------------- tests/test_action.py | 68 +++--- tests/test_package.py | 57 ++--- tests/test_slide.py | 34 ++- tests/text/test_fonts.py | 212 ++++++++----------- tests/text/test_layout.py | 185 ++++++---------- tests/text/test_text.py | 76 +++---- tests/unitutil/mock.py | 14 +- 17 files changed, 520 insertions(+), 916 deletions(-) diff --git a/pptx/action.py b/pptx/action.py index d3ea2ab4d..ea3cc5c8e 100644 --- a/pptx/action.py +++ b/pptx/action.py @@ -123,8 +123,7 @@ def target_slide(self, slide): return hlink = self._element.get_or_add_hlinkClick() hlink.action = "ppaction://hlinksldjump" - this_part, target_part = self.part, slide.part - hlink.rId = this_part.relate_to(target_part, RT.SLIDE) + hlink.rId = self.part.relate_to(slide.part, RT.SLIDE) def _clear_click_action(self): """Remove any existing click action.""" diff --git a/pptx/package.py b/pptx/package.py index 5e09133eb..30bc9baf5 100644 --- a/pptx/package.py +++ b/pptx/package.py @@ -143,18 +143,15 @@ def __iter__(self): yield image_part def get_or_add_image_part(self, image_file): - """ - Return an |ImagePart| object containing the image in *image_file*, - which is either a path to an image file or a file-like object - containing an image. If an image part containing this same image - already exists, that instance is returned, otherwise a new image part - is created. + """Return |ImagePart| object containing the image in `image_file`. + + `image_file` can be either a path to an image file or a file-like object + containing an image. If an image part containing this same image already exists, + that instance is returned, otherwise a new image part is created. """ image = Image.from_file(image_file) image_part = self._find_by_sha1(image.sha1) - if image_part is None: - image_part = ImagePart.new(self._package, image) - return image_part + return ImagePart.new(self._package, image) if image_part is None else image_part def _find_by_sha1(self, sha1): """ diff --git a/tests/chart/test_xlsx.py b/tests/chart/test_xlsx.py index 27b5c6965..ec96c9e02 100644 --- a/tests/chart/test_xlsx.py +++ b/tests/chart/test_xlsx.py @@ -28,21 +28,26 @@ class Describe_BaseWorkbookWriter(object): """Unit-test suite for `pptx.chart.xlsx._BaseWorkbookWriter` objects.""" - def it_can_generate_a_chart_data_Excel_blob(self, xlsx_blob_fixture): - ( - workbook_writer, - xlsx_file_, - workbook_, - worksheet_, - xlsx_blob, - ) = xlsx_blob_fixture - _xlsx_blob = workbook_writer.xlsx_blob - - workbook_writer._open_worksheet.assert_called_once_with(xlsx_file_) - workbook_writer._populate_worksheet.assert_called_once_with( + def it_can_generate_a_chart_data_Excel_blob( + self, request, xlsx_file_, workbook_, worksheet_, BytesIO_ + ): + _populate_worksheet_ = method_mock( + request, _BaseWorkbookWriter, "_populate_worksheet" + ) + _open_worksheet_ = method_mock(request, _BaseWorkbookWriter, "_open_worksheet") + # --- to make context manager behavior work --- + _open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) + BytesIO_.return_value = xlsx_file_ + xlsx_file_.getvalue.return_value = b"xlsx-blob" + workbook_writer = _BaseWorkbookWriter(None) + + xlsx_blob = workbook_writer.xlsx_blob + + _open_worksheet_.assert_called_once_with(workbook_writer, xlsx_file_) + _populate_worksheet_.assert_called_once_with( workbook_writer, workbook_, worksheet_ ) - assert _xlsx_blob is xlsx_blob + assert xlsx_blob == b"xlsx-blob" def it_can_open_a_worksheet_in_a_context(self, open_fixture): wb_writer, xlsx_file_, workbook_, worksheet_, Workbook_ = open_fixture @@ -72,41 +77,12 @@ def populate_fixture(self): workbook_writer = _BaseWorkbookWriter(None) return workbook_writer - @pytest.fixture - def xlsx_blob_fixture( - self, - request, - xlsx_file_, - workbook_, - worksheet_, - _populate_worksheet_, - _open_worksheet_, - BytesIO_, - ): - workbook_writer = _BaseWorkbookWriter(None) - xlsx_blob = "fooblob" - BytesIO_.return_value = xlsx_file_ - # to make context manager behavior work - _open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) - xlsx_file_.getvalue.return_value = xlsx_blob - return (workbook_writer, xlsx_file_, workbook_, worksheet_, xlsx_blob) - # fixture components --------------------------------------------- @pytest.fixture def BytesIO_(self, request): return class_mock(request, "pptx.chart.xlsx.BytesIO") - @pytest.fixture - def _open_worksheet_(self, request): - return method_mock(request, _BaseWorkbookWriter, "_open_worksheet") - - @pytest.fixture - def _populate_worksheet_(self, request): - return method_mock( - request, _BaseWorkbookWriter, "_populate_worksheet", autospec=True - ) - @pytest.fixture def Workbook_(self, request, workbook_): return class_mock(request, "pptx.chart.xlsx.Workbook", return_value=workbook_) @@ -421,17 +397,6 @@ def worksheet_(self, request): class DescribeXyWorkbookWriter(object): """Unit-test suite for `pptx.chart.xlsx.XyWorkbookWriter` objects.""" - def it_can_generate_a_chart_data_Excel_blob(self, xlsx_blob_fixture): - workbook_writer, _open_worksheet_, xlsx_file_ = xlsx_blob_fixture[:3] - _populate_worksheet_, workbook_, worksheet_ = xlsx_blob_fixture[3:6] - xlsx_blob_ = xlsx_blob_fixture[6] - - xlsx_blob = workbook_writer.xlsx_blob - - _open_worksheet_.assert_called_once_with(xlsx_file_) - _populate_worksheet_.assert_called_once_with(workbook_, worksheet_) - assert xlsx_blob is xlsx_blob_ - def it_can_populate_a_worksheet_with_chart_data(self, populate_fixture): workbook_writer, workbook_, worksheet_, expected_calls = populate_fixture workbook_writer._populate_worksheet(workbook_, worksheet_) @@ -461,46 +426,8 @@ def populate_fixture(self, workbook_, worksheet_): ] return workbook_writer, workbook_, worksheet_, expected_calls - @pytest.fixture - def xlsx_blob_fixture( - self, - request, - xlsx_file_, - BytesIO_, - _open_worksheet_, - workbook_, - worksheet_, - _populate_worksheet_, - xlsx_blob_, - ): - workbook_writer = XyWorkbookWriter(None) - return ( - workbook_writer, - _open_worksheet_, - xlsx_file_, - _populate_worksheet_, - workbook_, - worksheet_, - xlsx_blob_, - ) - # fixture components --------------------------------------------- - @pytest.fixture - def BytesIO_(self, request, xlsx_file_): - return class_mock(request, "pptx.chart.xlsx.BytesIO", return_value=xlsx_file_) - - @pytest.fixture - def _open_worksheet_(self, request, workbook_, worksheet_): - open_worksheet_ = method_mock(request, XyWorkbookWriter, "_open_worksheet") - # to make context manager behavior work - open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) - return open_worksheet_ - - @pytest.fixture - def _populate_worksheet_(self, request): - return method_mock(request, XyWorkbookWriter, "_populate_worksheet") - @pytest.fixture def workbook_(self, request): return instance_mock(request, Workbook) @@ -508,13 +435,3 @@ def workbook_(self, request): @pytest.fixture def worksheet_(self, request): return instance_mock(request, Worksheet) - - @pytest.fixture - def xlsx_blob_(self, request): - return instance_mock(request, bytes) - - @pytest.fixture - def xlsx_file_(self, request, xlsx_blob_): - xlsx_file_ = instance_mock(request, BytesIO) - xlsx_file_.getvalue.return_value = xlsx_blob_ - return xlsx_file_ diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index a9c3a9f0c..5c22fb2e5 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -6,7 +6,7 @@ import pytest -from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationships from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.opc.package import ( @@ -288,10 +288,13 @@ def Unmarshaller_(self, request): class DescribePart(object): """Unit-test suite for `pptx.opc.package.Part` objects.""" - def it_can_be_constructed_by_PartFactory(self, load_fixture): - partname_, content_type_, blob_, package_, __init_ = load_fixture - part = Part.load(partname_, content_type_, blob_, package_) - __init_.assert_called_once_with(partname_, content_type_, blob_, package_) + def it_can_be_constructed_by_PartFactory(self, request, package_): + partname_ = PackURI("/ppt/slides/slide1.xml") + _init_ = initializer_mock(request, Part) + + part = Part.load(partname_, CT.PML_SLIDE, b"blob", package_) + + _init_.assert_called_once_with(part, partname_, CT.PML_SLIDE, b"blob", package_) assert isinstance(part, Part) def it_can_be_notified_after_unmarshalling_is_complete(self, part): @@ -425,10 +428,6 @@ def drop_rel_fixture(self, request, part): part._rels = {rId: None} return part, rId, rel_should_be_dropped - @pytest.fixture - def load_fixture(self, request, partname_, content_type_, blob_, package_, __init_): - return (partname_, content_type_, blob_, package_, __init_) - @pytest.fixture def load_rel_fixture(self, part, rels_, reltype_, part_, rId_): part._rels = rels_ @@ -489,14 +488,6 @@ def target_ref_fixture(self, request, part, rId_, rel_, url_): def blob_(self, request): return instance_mock(request, bytes) - @pytest.fixture - def content_type_(self, request): - return instance_mock(request, str) - - @pytest.fixture - def __init_(self, request): - return initializer_mock(request, Part) - @pytest.fixture def package_(self, request): return instance_mock(request, OpcPackage) @@ -552,14 +543,19 @@ def url_(self, request): class DescribeXmlPart(object): """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" - def it_can_be_constructed_by_PartFactory(self, load_fixture): - partname_, content_type_, blob_, package_ = load_fixture[:4] - element_, parse_xml_, __init_ = load_fixture[4:] - # exercise --------------------- - part = XmlPart.load(partname_, content_type_, blob_, package_) - # verify ----------------------- - parse_xml_.assert_called_once_with(blob_) - __init_.assert_called_once_with(partname_, content_type_, element_, package_) + def it_can_be_constructed_by_PartFactory(self, request): + partname = PackURI("/ppt/slides/slide1.xml") + element_ = element("p:sld") + package_ = instance_mock(request, OpcPackage) + parse_xml_ = function_mock( + request, "pptx.opc.package.parse_xml", return_value=element_ + ) + _init_ = initializer_mock(request, XmlPart) + + part = XmlPart.load(partname, CT.PML_SLIDE, b"blob", package_) + + parse_xml_.assert_called_once_with(b"blob") + _init_.assert_called_once_with(part, partname, CT.PML_SLIDE, element_, package_) assert isinstance(part, XmlPart) def it_can_serialize_to_xml(self, blob_fixture): @@ -579,64 +575,16 @@ def blob_fixture(self, request, element_, serialize_part_xml_): xml_part = XmlPart(None, None, element_, None) return xml_part, element_, serialize_part_xml_ - @pytest.fixture - def load_fixture( - self, - request, - partname_, - content_type_, - blob_, - package_, - element_, - parse_xml_, - __init_, - ): - return ( - partname_, - content_type_, - blob_, - package_, - element_, - parse_xml_, - __init_, - ) - @pytest.fixture def part_fixture(self): return XmlPart(None, None, None, None) # fixture components --------------------------------------------- - @pytest.fixture - def blob_(self, request): - return instance_mock(request, str) - - @pytest.fixture - def content_type_(self, request): - return instance_mock(request, str) - @pytest.fixture def element_(self, request): return instance_mock(request, BaseOxmlElement) - @pytest.fixture - def __init_(self, request): - return initializer_mock(request, XmlPart) - - @pytest.fixture - def package_(self, request): - return instance_mock(request, OpcPackage) - - @pytest.fixture - def parse_xml_(self, request, element_): - return function_mock( - request, "pptx.opc.package.parse_xml", return_value=element_ - ) - - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - @pytest.fixture def serialize_part_xml_(self, request): return function_mock(request, "pptx.opc.package.serialize_part_xml") diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 1cf78d745..9f00f70f5 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -54,54 +54,20 @@ class DescribePackageReader(object): - @pytest.fixture - def from_xml(self, request): - return method_mock(request, _ContentTypeMap, "from_xml") - - @pytest.fixture - def init(self, request): - return initializer_mock(request, PackageReader) - - @pytest.fixture - def _load_serialized_parts(self, request): - return method_mock(request, PackageReader, "_load_serialized_parts") - - @pytest.fixture - def _PhysPkgReader_(self, request): - _patch = patch("pptx.opc.serialized._PhysPkgReader", spec_set=_ZipPkgReader) - request.addfinalizer(_patch.stop) - return _patch.start() - - @pytest.fixture - def _SerializedPart_(self, request): - return class_mock(request, "pptx.opc.serialized._SerializedPart") - - @pytest.fixture - def _SerializedRelationshipCollection_(self, request): - return class_mock( - request, "pptx.opc.serialized._SerializedRelationshipCollection" - ) - - @pytest.fixture - def _srels_for(self, request): - return method_mock(request, PackageReader, "_srels_for") - - @pytest.fixture - def _walk_phys_parts(self, request): - return method_mock(request, PackageReader, "_walk_phys_parts") + """Unit-test suite for `pptx.opc.serialized.PackageReader` objects.""" def it_can_construct_from_pkg_file( self, init, _PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts ): - # mockery ---------------------- + phys_reader = _PhysPkgReader_.return_value content_types = from_xml.return_value pkg_srels = _srels_for.return_value sparts = _load_serialized_parts.return_value pkg_file = Mock(name="pkg_file") - # exercise --------------------- + pkg_reader = PackageReader.from_file(pkg_file) - # verify ----------------------- + _PhysPkgReader_.assert_called_once_with(pkg_file) from_xml.assert_called_once_with(phys_reader.content_types_xml) _srels_for.assert_called_once_with(phys_reader, "/") @@ -109,7 +75,7 @@ def it_can_construct_from_pkg_file( phys_reader, pkg_srels, content_types ) phys_reader.close.assert_called_once_with() - init.assert_called_once_with(content_types, pkg_srels, sparts) + init.assert_called_once_with(pkg_reader, content_types, pkg_srels, sparts) assert isinstance(pkg_reader, PackageReader) def it_can_iterate_over_the_serialized_parts(self): @@ -235,6 +201,44 @@ def it_can_retrieve_srels_for_a_source_uri( load_from_xml.assert_called_once_with(source_uri.baseURI, rels_xml) assert retval == srels + # fixture components ----------------------------------- + + @pytest.fixture + def from_xml(self, request): + return method_mock(request, _ContentTypeMap, "from_xml") + + @pytest.fixture + def init(self, request): + return initializer_mock(request, PackageReader) + + @pytest.fixture + def _load_serialized_parts(self, request): + return method_mock(request, PackageReader, "_load_serialized_parts") + + @pytest.fixture + def _PhysPkgReader_(self, request): + _patch = patch("pptx.opc.serialized._PhysPkgReader", spec_set=_ZipPkgReader) + request.addfinalizer(_patch.stop) + return _patch.start() + + @pytest.fixture + def _SerializedPart_(self, request): + return class_mock(request, "pptx.opc.serialized._SerializedPart") + + @pytest.fixture + def _SerializedRelationshipCollection_(self, request): + return class_mock( + request, "pptx.opc.serialized._SerializedRelationshipCollection" + ) + + @pytest.fixture + def _srels_for(self, request): + return method_mock(request, PackageReader, "_srels_for") + + @pytest.fixture + def _walk_phys_parts(self, request): + return method_mock(request, PackageReader, "_walk_phys_parts") + class Describe_ContentTypeMap(object): def it_can_construct_from_ct_item_xml(self, from_xml_fixture): diff --git a/tests/oxml/shapes/test_groupshape.py b/tests/oxml/shapes/test_groupshape.py index 468f482fb..66025261f 100644 --- a/tests/oxml/shapes/test_groupshape.py +++ b/tests/oxml/shapes/test_groupshape.py @@ -26,7 +26,9 @@ def it_can_add_a_graphicFrame_element_containing_a_table(self, add_table_fixt): new_table_graphicFrame_.assert_called_once_with( id_, name, rows, cols, x, y, cx, cy ) - insert_element_before_.assert_called_once_with(graphicFrame_, "p:extLst") + insert_element_before_.assert_called_once_with( + spTree, graphicFrame_, "p:extLst" + ) assert graphicFrame is graphicFrame_ def it_can_add_a_grpSp_element(self, add_grpSp_fixture): @@ -40,37 +42,45 @@ def it_can_add_a_grpSp_element(self, add_grpSp_fixture): def it_can_add_a_pic_element_representing_a_picture(self, add_pic_fixt): spTree, id_, name, desc, rId, x, y, cx, cy = add_pic_fixt[:9] CT_Picture_, insert_element_before_, pic_ = add_pic_fixt[9:] + pic = spTree.add_pic(id_, name, desc, rId, x, y, cx, cy) + CT_Picture_.new_pic.assert_called_once_with(id_, name, desc, rId, x, y, cx, cy) - insert_element_before_.assert_called_once_with(pic_, "p:extLst") + insert_element_before_.assert_called_once_with(spTree, pic_, "p:extLst") assert pic is pic_ def it_can_add_an_sp_element_for_a_placeholder(self, add_placeholder_fixt): spTree, id_, name, ph_type, orient, sz, idx = add_placeholder_fixt[:7] CT_Shape_, insert_element_before_, sp_ = add_placeholder_fixt[7:] + sp = spTree.add_placeholder(id_, name, ph_type, orient, sz, idx) + CT_Shape_.new_placeholder_sp.assert_called_once_with( id_, name, ph_type, orient, sz, idx ) - insert_element_before_.assert_called_once_with(sp_, "p:extLst") + insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ def it_can_add_an_sp_element_for_an_autoshape(self, add_autoshape_fixt): spTree, id_, name, prst, x, y, cx, cy = add_autoshape_fixt[:8] CT_Shape_, insert_element_before_, sp_ = add_autoshape_fixt[8:] + sp = spTree.add_autoshape(id_, name, prst, x, y, cx, cy) + CT_Shape_.new_autoshape_sp.assert_called_once_with( id_, name, prst, x, y, cx, cy ) - insert_element_before_.assert_called_once_with(sp_, "p:extLst") + insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ def it_can_add_a_textbox_sp_element(self, add_textbox_fixt): spTree, id_, name, x, y, cx, cy, CT_Shape_ = add_textbox_fixt[:8] insert_element_before_, sp_ = add_textbox_fixt[8:] + sp = spTree.add_textbox(id_, name, x, y, cx, cy) + CT_Shape_.new_textbox_sp.assert_called_once_with(id_, name, x, y, cx, cy) - insert_element_before_.assert_called_once_with(sp_, "p:extLst") + insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ def it_can_recalculate_its_pos_and_size(self, recalc_fixture): diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index ff42961f1..035c9f2c4 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -29,14 +29,21 @@ class DescribeImagePart(object): """Unit-test suite for `pptx.parts.image.ImagePart` objects.""" - def it_can_construct_from_an_image_object(self, new_fixture): - package_, image_, _init_, partname_ = new_fixture + def it_can_construct_from_an_image_object(self, request, image_): + package_ = instance_mock(request, Package) + _init_ = initializer_mock(request, ImagePart) + partname_ = package_.next_image_partname.return_value image_part = ImagePart.new(package_, image_) package_.next_image_partname.assert_called_once_with(image_.ext) _init_.assert_called_once_with( - partname_, image_.content_type, image_.blob, package_, image_.filename + image_part, + partname_, + image_.content_type, + image_.blob, + package_, + image_.filename, ) assert isinstance(image_part, ImagePart) @@ -62,11 +69,6 @@ def image_fixture(self, Image_, image_): image_part = ImagePart(None, None, blob, None, filename) return image_part, Image_, blob, filename, image_ - @pytest.fixture - def new_fixture(self, request, package_, image_, _init_): - partname_ = package_.next_image_partname.return_value - return package_, image_, _init_, partname_ - @pytest.fixture( params=[ (None, None, Emu(2590800), Emu(2590800)), @@ -99,14 +101,6 @@ def Image_(self, request, image_): def image_(self, request): return instance_mock(request, Image) - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, ImagePart) - - @pytest.fixture - def package_(self, request): - return instance_mock(request, Package) - class DescribeImage(object): """Unit-test suite for `pptx.parts.image.Image` objects.""" @@ -123,10 +117,10 @@ def it_can_construct_from_a_stream(self, from_stream_fixture): Image.from_blob.assert_called_once_with(blob, None) assert image is image_ - def it_can_construct_from_a_blob(self, from_blob_fixture): - blob, filename = from_blob_fixture - image = Image.from_blob(blob, filename) - Image.__init__.assert_called_once_with(blob, filename) + def it_can_construct_from_a_blob(self, _init_): + image = Image.from_blob(b"blob", "foo.png") + + _init_.assert_called_once_with(image, b"blob", "foo.png") assert isinstance(image, Image) def it_knows_its_blob(self, blob_fixture): @@ -221,11 +215,6 @@ def filename_fixture(self, request): image = Image(None, filename) return image, filename - @pytest.fixture - def from_blob_fixture(self, _init_): - blob, filename = b"foobar", "foo.png" - return blob, filename - @pytest.fixture def from_path_fixture(self, from_blob_, image_): image_file = test_image_path diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index a096aae4a..8450e8912 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -52,15 +52,20 @@ def it_can_get_a_related_image_by_rId(self, get_image_fixture): slide, rId, image_ = get_image_fixture assert slide.get_image(rId) is image_ - def it_can_add_an_image_part(self, image_part_fixture): - slide, image_file, image_part_, rId_ = image_part_fixture + 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" + ) + slide_part = BaseSlidePart(None, None, None, package_) - image_part, rId = slide.get_or_add_image_part(image_file) + image_part, rId = slide_part.get_or_add_image_part("foobar.png") - slide._package.get_or_add_image_part.assert_called_once_with(image_file) - slide.relate_to.assert_called_once_with(image_part_, RT.IMAGE) + package_.get_or_add_image_part.assert_called_once_with("foobar.png") + relate_to_.assert_called_once_with(slide_part, image_part_, RT.IMAGE) assert image_part is image_part_ - assert rId is rId_ + assert rId == "rId6" # fixtures ------------------------------------------------------- @@ -72,14 +77,6 @@ def get_image_fixture(self, related_parts_prop_, image_part_, image_): image_part_.image = image_ return slide, rId, image_ - @pytest.fixture - def image_part_fixture(self, partname_, package_, image_part_, relate_to_): - slide = BaseSlidePart(partname_, None, None, package_) - image_file, rId = "foobar.png", "rId6" - package_.get_or_add_image_part.return_value = image_part_ - relate_to_.return_value = rId - return slide, image_file, image_part_, rId - @pytest.fixture def name_fixture(self): sld_cxml, expected_value = "p:sld/p:cSld{name=Foobar}", "Foobar" @@ -97,18 +94,6 @@ def image_(self, request): def image_part_(self, request): return instance_mock(request, ImagePart) - @pytest.fixture - def package_(self, request): - return instance_mock(request, Package) - - @pytest.fixture - def partname_(self): - return PackURI("/foo/bar.xml") - - @pytest.fixture - def relate_to_(self, request): - return method_mock(request, BaseSlidePart, "relate_to") - @pytest.fixture def related_parts_prop_(self, request): return property_mock(request, BaseSlidePart, "related_parts") @@ -509,14 +494,16 @@ def it_can_get_or_add_a_video_part(self, package_, video_, relate_to_, media_par def it_can_create_a_new_slide_part(self, request, package_, relate_to_): partname = PackURI("/foobar.xml") - SlidePart_init_ = initializer_mock(request, SlidePart) + _init_ = initializer_mock(request, SlidePart) slide_layout_part_ = instance_mock(request, SlideLayoutPart) CT_Slide_ = class_mock(request, "pptx.parts.slide.CT_Slide") CT_Slide_.new.return_value = sld = element("c:sld") slide_part = SlidePart.new(partname, package_, slide_layout_part_) - SlidePart_init_.assert_called_once_with(partname, CT.PML_SLIDE, sld, package_) + _init_.assert_called_once_with( + slide_part, partname, CT.PML_SLIDE, sld, package_ + ) slide_part.relate_to.assert_called_once_with( slide_part, slide_layout_part_, RT.SLIDE_LAYOUT ) diff --git a/tests/shapes/test_freeform.py b/tests/shapes/test_freeform.py index 3e3800b33..0f23c755c 100644 --- a/tests/shapes/test_freeform.py +++ b/tests/shapes/test_freeform.py @@ -39,13 +39,21 @@ def it_provides_a_constructor(self, new_fixture): ) assert isinstance(builder, FreeformBuilder) - def it_can_add_straight_line_segments(self, add_segs_fixture): - builder, vertices, close, add_calls, close_calls = add_segs_fixture + @pytest.mark.parametrize("close", (True, False)) + def it_can_add_straight_line_segments(self, request, close): + _add_line_segment_ = method_mock(request, FreeformBuilder, "_add_line_segment") + _add_close_ = method_mock(request, FreeformBuilder, "_add_close") - return_value = builder.add_line_segments(vertices, close) + builder = FreeformBuilder(None, None, None, None, None) + + return_value = builder.add_line_segments(((1, 2), (3, 4), (5, 6)), close) - assert builder._add_line_segment.call_args_list == add_calls - assert builder._add_close.call_args_list == close_calls + assert _add_line_segment_.call_args_list == [ + call(builder, 1, 2), + call(builder, 3, 4), + call(builder, 5, 6), + ] + assert _add_close_.call_args_list == ([call(builder)] if close else []) assert return_value is builder def it_can_move_the_pen_location(self, move_to_fixture): @@ -133,14 +141,22 @@ def it_knows_the_local_coordinate_height_to_help(self, dy_fixture): dy = builder._dy assert dy == expected_value - def it_can_start_a_new_path_to_help(self, start_path_fixture): - builder, sp, _local_to_shape_ = start_path_fixture[:3] - start_x, start_y, expected_xml = start_path_fixture[3:] + def it_can_start_a_new_path_to_help(self, request, _dx_prop_, _dy_prop_): + _local_to_shape_ = method_mock( + request, FreeformBuilder, "_local_to_shape", return_value=(101, 202) + ) + sp = element("p:sp/p:spPr/a:custGeom") + start_x, start_y = 42, 24 + _dx_prop_.return_value, _dy_prop_.return_value = 1001, 2002 + builder = FreeformBuilder(None, start_x, start_y, None, None) path = builder._start_path(sp) - _local_to_shape_.assert_called_once_with(start_x, start_y) - assert sp.xml == expected_xml + _local_to_shape_.assert_called_once_with(builder, start_x, start_y) + assert sp.xml == xml( + "p:sp/p:spPr/a:custGeom/a:pathLst/a:path{w=1001,h=2002}/a:moveTo" + "/a:pt{x=101,y=202}" + ) assert path is sp.xpath(".//a:path")[-1] def it_translates_local_to_shape_coordinates_to_help(self, local_fixture): @@ -164,14 +180,6 @@ def add_seg_fixture(self, _LineSegment_new_, line_segment_): builder = FreeformBuilder(None, None, None, None, None) return builder, x, y, _LineSegment_new_, line_segment_ - @pytest.fixture(params=[(True, [call()]), (False, [])]) - def add_segs_fixture(self, request, _add_line_segment_, _add_close_): - close, close_calls = request.param - vertices = ((1, 2), (3, 4), (5, 6)) - builder = FreeformBuilder(None, None, None, None, None) - add_calls = [call(1, 2), call(3, 4), call(5, 6)] - return builder, vertices, close, add_calls, close_calls - @pytest.fixture def convert_fixture( self, shapes_, apply_operation_to_, _add_freeform_sp_, _start_path_, shape_ @@ -332,20 +340,6 @@ def sp_fixture(self, _left_prop_, _top_prop_, _width_prop_, _height_prop_): expected_xml = snippet_seq("freeform")[0] return builder, origin_x, origin_y, spTree, expected_xml - @pytest.fixture - def start_path_fixture(self, _dx_prop_, _dy_prop_, _local_to_shape_): - sp = element("p:sp/p:spPr/a:custGeom") - start_x, start_y = 42, 24 - _dx_prop_.return_value, _dy_prop_.return_value = 1001, 2002 - _local_to_shape_.return_value = 101, 202 - - builder = FreeformBuilder(None, start_x, start_y, None, None) - expected_xml = xml( - "p:sp/p:spPr/a:custGeom/a:pathLst/a:path{w=1001,h=2002}/a:moveTo" - "/a:pt{x=101,y=202}" - ) - return builder, sp, _local_to_shape_, start_x, start_y, expected_xml - @pytest.fixture( params=[(0, 11.0, 0), (100, 10.36, 1036), (914242, 943.1, 862221630)] ) @@ -366,18 +360,10 @@ def width_fixture(self, request, _dx_prop_): # fixture components ----------------------------------- - @pytest.fixture - def _add_close_(self, request): - return method_mock(request, FreeformBuilder, "_add_close") - @pytest.fixture def _add_freeform_sp_(self, request): return method_mock(request, FreeformBuilder, "_add_freeform_sp", autospec=True) - @pytest.fixture - def _add_line_segment_(self, request): - return method_mock(request, FreeformBuilder, "_add_line_segment") - @pytest.fixture def apply_operation_to_(self, request): return method_mock( @@ -420,10 +406,6 @@ def line_segment_(self, request): def _LineSegment_new_(self, request): return method_mock(request, _LineSegment, "new") - @pytest.fixture - def _local_to_shape_(self, request): - return method_mock(request, FreeformBuilder, "_local_to_shape") - @pytest.fixture def move_to_(self, request): return instance_mock(request, _MoveTo) diff --git a/tests/shapes/test_placeholder.py b/tests/shapes/test_placeholder.py index 3865ec81b..04a7d9e70 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -5,6 +5,7 @@ import pytest from pptx.chart.data import ChartData +from pptx.enum.chart import XL_CHART_TYPE as XCT from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.oxml.shapes.shared import ST_Direction, ST_PlaceholderSize from pptx.parts.image import ImagePart @@ -50,11 +51,15 @@ def it_provides_override_dimensions_when_present(self, override_fixture): placeholder, prop_name, expected_value = override_fixture assert getattr(placeholder, prop_name) == expected_value - def it_provides_inherited_dims_when_no_override(self, inherited_fixture): - placeholder, prop_name, expected_value = inherited_fixture + @pytest.mark.parametrize("prop_name", ("left", "top", "width", "height")) + def it_provides_inherited_dims_when_no_override(self, request, prop_name): + method_mock(request, _BaseSlidePlaceholder, "_inherited_value", return_value=42) + placeholder = _BaseSlidePlaceholder(element("p:sp/p:spPr"), None) + value = getattr(placeholder, prop_name) - placeholder._inherited_value.assert_called_once_with(prop_name) - assert value == expected_value + + placeholder._inherited_value.assert_called_once_with(placeholder, prop_name) + assert value == 42 def it_gets_an_inherited_dim_value_to_help(self, base_val_fixture): placeholder, attr_name, expected_value = base_val_fixture @@ -113,13 +118,6 @@ def dim_set_fixture(self, request): expected_xml = xml(expected_cxml) return placeholder, prop_name, value, expected_xml - @pytest.fixture(params=["left", "top", "width", "height"]) - def inherited_fixture(self, request, _inherited_value_): - prop_name = request.param - placeholder = _BaseSlidePlaceholder(element("p:sp/p:spPr"), None) - _inherited_value_.return_value = expected_value = 42 - return placeholder, prop_name, expected_value - @pytest.fixture( params=[ ("left", "p:sp/p:spPr/a:xfrm/a:off{x=12}", 12), @@ -147,10 +145,6 @@ def replace_fixture(self): def _base_placeholder_prop_(self, request): return property_mock(request, _BaseSlidePlaceholder, "_base_placeholder") - @pytest.fixture - def _inherited_value_(self, request): - return method_mock(request, _BaseSlidePlaceholder, "_inherited_value") - @pytest.fixture def layout_placeholder_(self, request): return instance_mock(request, LayoutPlaceholder) @@ -293,19 +287,38 @@ def shape_elm_factory(tagname, ph_type, idx): class DescribeChartPlaceholder(object): """Unit-test suite for `pptx.shapes.placeholder.ChartPlaceholder` object.""" - def it_can_insert_a_chart_into_itself(self, insert_fixture): - chart_ph, chart_type, chart_data_, graphicFrame = insert_fixture[:4] - rId, PlaceholderGraphicFrame_, ph_graphic_frame_ = insert_fixture[4:] + def it_can_insert_a_chart_into_itself(self, request, part_prop_): + slide_part_ = instance_mock(request, SlidePart) + slide_part_.add_chart_part.return_value = "rId6" + part_prop_.return_value = slide_part_ + graphicFrame = element("p:graphicFrame") + _new_chart_graphicFrame_ = method_mock( + request, + ChartPlaceholder, + "_new_chart_graphicFrame", + return_value=graphicFrame, + ) + _replace_placeholder_with_ = method_mock( + request, ChartPlaceholder, "_replace_placeholder_with" + ) + placeholder_graphic_frame_ = instance_mock(request, PlaceholderGraphicFrame) + PlaceholderGraphicFrame_ = class_mock( + request, + "pptx.shapes.placeholder.PlaceholderGraphicFrame", + return_value=placeholder_graphic_frame_, + ) + chart_data_ = instance_mock(request, ChartData) + chart_ph = ChartPlaceholder( + element("p:sp/p:spPr/a:xfrm/(a:off{x=1,y=2},a:ext{cx=3,cy=4})"), "parent" + ) - ph_graphic_frame = chart_ph.insert_chart(chart_type, chart_data_) + ph_graphic_frame = chart_ph.insert_chart(XCT.PIE, chart_data_) - chart_ph.part.add_chart_part.assert_called_once_with(chart_type, chart_data_) - chart_ph._new_chart_graphicFrame.assert_called_once_with( - rId, chart_ph.left, chart_ph.top, chart_ph.width, chart_ph.height - ) - chart_ph._replace_placeholder_with.assert_called_once_with(graphicFrame) + slide_part_.add_chart_part.assert_called_once_with(XCT.PIE, chart_data_) + _new_chart_graphicFrame_.assert_called_once_with(chart_ph, "rId6", 1, 2, 3, 4) + _replace_placeholder_with_.assert_called_once_with(chart_ph, graphicFrame) PlaceholderGraphicFrame_.assert_called_once_with(graphicFrame, chart_ph._parent) - assert ph_graphic_frame is ph_graphic_frame_ + assert ph_graphic_frame is placeholder_graphic_frame_ def it_creates_a_graphicFrame_element_to_help(self, new_fixture): chart_ph, rId, x, y, cx, cy, expected_xml = new_fixture @@ -314,31 +327,6 @@ def it_creates_a_graphicFrame_element_to_help(self, new_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def insert_fixture( - self, - part_prop_, - chart_data_, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - _new_chart_graphicFrame_, - _replace_placeholder_with_, - ): - sp_cxml = "p:sp/p:spPr/a:xfrm/(a:off{x=1,y=2},a:ext{cx=3,cy=4})" - chart_ph = ChartPlaceholder(element(sp_cxml), "parent") - chart_type, rId, graphicFrame = 42, "rId6", element("p:graphicFrame") - part_prop_.return_value.add_chart_part.return_value = rId - _new_chart_graphicFrame_.return_value = graphicFrame - return ( - chart_ph, - chart_type, - chart_data_, - graphicFrame, - rId, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - ) - @pytest.fixture def new_fixture(self): sp_cxml = "p:sp/p:nvSpPr/p:cNvPr{id=4,name=bar}" @@ -350,36 +338,8 @@ def new_fixture(self): # fixture components --------------------------------------------- @pytest.fixture - def chart_data_(self, request): - return instance_mock(request, ChartData) - - @pytest.fixture - def _new_chart_graphicFrame_(self, request): - return method_mock(request, ChartPlaceholder, "_new_chart_graphicFrame") - - @pytest.fixture - def part_prop_(self, request, slide_): - return property_mock(request, ChartPlaceholder, "part", return_value=slide_) - - @pytest.fixture - def PlaceholderGraphicFrame_(self, request, placeholder_graphic_frame_): - return class_mock( - request, - "pptx.shapes.placeholder.PlaceholderGraphicFrame", - return_value=placeholder_graphic_frame_, - ) - - @pytest.fixture - def placeholder_graphic_frame_(self, request): - return instance_mock(request, PlaceholderGraphicFrame) - - @pytest.fixture - def _replace_placeholder_with_(self, request): - return method_mock(request, ChartPlaceholder, "_replace_placeholder_with") - - @pytest.fixture - def slide_(self, request): - return instance_mock(request, SlidePart) + def part_prop_(self, request): + return property_mock(request, ChartPlaceholder, "part") class DescribeLayoutPlaceholder(object): @@ -489,22 +449,57 @@ def part_prop_(self, request, notes_slide_part_): class DescribePicturePlaceholder(object): """Unit-test suite for `pptx.shapes.placeholder.PicturePlaceholder` object.""" - def it_can_insert_a_picture_into_itself(self, insert_fixture): - picture_ph, image_file, pic = insert_fixture[:3] - PlaceholderPicture_, placeholder_picture_ = insert_fixture[3:] + def it_can_insert_a_picture_into_itself(self, request): + pic = element("p:pic") + _new_placeholder_pic_ = method_mock( + request, PicturePlaceholder, "_new_placeholder_pic", return_value=pic + ) + _replace_placeholder_with_ = method_mock( + request, PicturePlaceholder, "_replace_placeholder_with" + ) + placeholder_picture_ = instance_mock(request, PlaceholderPicture) + PlaceholderPicture_ = class_mock( + request, + "pptx.shapes.placeholder.PlaceholderPicture", + return_value=placeholder_picture_, + ) + picture_ph = PicturePlaceholder(None, "parent") - placeholder_picture = picture_ph.insert_picture(image_file) + placeholder_picture = picture_ph.insert_picture("foobar.png") - picture_ph._new_placeholder_pic.assert_called_once_with(image_file) - picture_ph._replace_placeholder_with.assert_called_once_with(pic) + _new_placeholder_pic_.assert_called_once_with(picture_ph, "foobar.png") + _replace_placeholder_with_.assert_called_once_with(picture_ph, pic) PlaceholderPicture_.assert_called_once_with(pic, picture_ph._parent) assert placeholder_picture is placeholder_picture_ - def it_creates_a_pic_element_to_help(self, pic_fixture): - picture_ph, image_file, expected_xml = pic_fixture - pic = picture_ph._new_placeholder_pic(image_file) - picture_ph._get_or_add_image.assert_called_once_with(image_file) - assert pic.xml == expected_xml + @pytest.mark.parametrize( + "image_size, crop_attr_names", + (((444, 333), ("l", "r")), ((333, 444), ("t", "b"))), + ) + def it_creates_a_pic_element_to_help(self, request, image_size, crop_attr_names): + _get_or_add_image_ = method_mock( + request, + PicturePlaceholder, + "_get_or_add_image", + return_value=(42, "bar", image_size), + ) + picture_ph = PicturePlaceholder( + element( + "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" + ",cy=99})" + ), + None, + ) + + pic = picture_ph._new_placeholder_pic("foobar.png") + + _get_or_add_image_.assert_called_once_with(picture_ph, "foobar.png") + assert pic.xml == xml( + "p:pic/(p:nvPicPr/(p:cNvPr{id=2,name=foo,descr=bar},p:cNvPicPr/a" + ":picLocks{noGrp=1,noChangeAspect=1},p:nvPr),p:blipFill/(a:blip{" + "r:embed=42},a:srcRect{%s=12500,%s=12500},a:stretch/a:fillRect)," + "p:spPr)" % crop_attr_names + ) def it_adds_an_image_to_help(self, get_or_add_fixture): placeholder, image_file, expected_value = get_or_add_fixture @@ -525,86 +520,45 @@ def get_or_add_fixture(self, part_prop_, image_part_): expected_value = rId, desc, image_size return placeholder, image_file, expected_value - @pytest.fixture - def insert_fixture( - self, - PlaceholderPicture_, - placeholder_picture_, - _new_placeholder_pic_, - _replace_placeholder_with_, - ): - picture_ph = PicturePlaceholder(None, "parent") - image_file, pic = "foobar.png", element("p:pic") - _new_placeholder_pic_.return_value = pic - PlaceholderPicture_.return_value = placeholder_picture_ - return (picture_ph, image_file, pic, PlaceholderPicture_, placeholder_picture_) - - @pytest.fixture(params=[((444, 333), ("l", "r")), ((333, 444), ("t", "b"))]) - def pic_fixture(self, request, _get_or_add_image_): - image_size, crop_attr_names = request.param - sp_cxml = ( - "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" ",cy=99})" - ) - sp = element(sp_cxml) - picture_ph = PicturePlaceholder(sp, None) - image_file = "foobar.png" - _get_or_add_image_.return_value = 42, "bar", image_size - expected_xml = xml( - "p:pic/(p:nvPicPr/(p:cNvPr{id=2,name=foo,descr=bar},p:cNvPicPr/a" - ":picLocks{noGrp=1,noChangeAspect=1},p:nvPr),p:blipFill/(a:blip{" - "r:embed=42},a:srcRect{%s=12500,%s=12500},a:stretch/a:fillRect)," - "p:spPr)" % crop_attr_names - ) - return picture_ph, image_file, expected_xml - # fixture components --------------------------------------------- - @pytest.fixture - def _get_or_add_image_(self, request): - return method_mock(request, PicturePlaceholder, "_get_or_add_image") - @pytest.fixture def image_part_(self, request): return instance_mock(request, ImagePart) @pytest.fixture - def _new_placeholder_pic_(self, request): - return method_mock(request, PicturePlaceholder, "_new_placeholder_pic") - - @pytest.fixture - def part_prop_(self, request, slide_): - return property_mock(request, PicturePlaceholder, "part", return_value=slide_) - - @pytest.fixture - def PlaceholderPicture_(self, request): - return class_mock(request, "pptx.shapes.placeholder.PlaceholderPicture") - - @pytest.fixture - def placeholder_picture_(self, request): - return instance_mock(request, PlaceholderPicture) - - @pytest.fixture - def _replace_placeholder_with_(self, request): - return method_mock(request, PicturePlaceholder, "_replace_placeholder_with") - - @pytest.fixture - def slide_(self, request): - return instance_mock(request, SlidePart) + def part_prop_(self, request): + return property_mock(request, PicturePlaceholder, "part") class DescribeTablePlaceholder(object): """Unit-test suite for `pptx.shapes.placeholder.TablePlaceholder` object.""" - def it_can_insert_a_table_into_itself(self, insert_fixture): - table_ph, rows, cols, graphicFrame = insert_fixture[:4] - PlaceholderGraphicFrame_, ph_graphic_frame_ = insert_fixture[4:] + def it_can_insert_a_table_into_itself(self, request): + graphicFrame = element("p:graphicFrame") + _new_placeholder_table_ = method_mock( + request, + TablePlaceholder, + "_new_placeholder_table", + return_value=graphicFrame, + ) + _replace_placeholder_with_ = method_mock( + request, TablePlaceholder, "_replace_placeholder_with" + ) + placeholder_graphic_frame_ = instance_mock(request, PlaceholderGraphicFrame) + PlaceholderGraphicFrame_ = class_mock( + request, + "pptx.shapes.placeholder.PlaceholderGraphicFrame", + return_value=placeholder_graphic_frame_, + ) + table_ph = TablePlaceholder(None, "parent") - ph_graphic_frame = table_ph.insert_table(rows, cols) + ph_graphic_frame = table_ph.insert_table(4, 2) - table_ph._new_placeholder_table.assert_called_once_with(rows, cols) - table_ph._replace_placeholder_with.assert_called_once_with(graphicFrame) + _new_placeholder_table_.assert_called_once_with(table_ph, 4, 2) + _replace_placeholder_with_.assert_called_once_with(table_ph, graphicFrame) PlaceholderGraphicFrame_.assert_called_once_with(graphicFrame, table_ph._parent) - assert ph_graphic_frame is ph_graphic_frame_ + assert ph_graphic_frame is placeholder_graphic_frame_ def it_creates_a_graphicFrame_element_to_help(self, new_fixture): table_ph, rows, cols, expected_xml = new_fixture @@ -613,27 +567,6 @@ def it_creates_a_graphicFrame_element_to_help(self, new_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def insert_fixture( - self, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - _new_placeholder_table_, - _replace_placeholder_with_, - ): - table_ph = TablePlaceholder(None, "parent") - rows, cols, graphicFrame = 4, 2, element("p:graphicFrame") - _new_placeholder_table_.return_value = graphicFrame - PlaceholderGraphicFrame_.return_value = placeholder_graphic_frame_ - return ( - table_ph, - rows, - cols, - graphicFrame, - PlaceholderGraphicFrame_, - placeholder_graphic_frame_, - ) - @pytest.fixture def new_fixture(self): sp_cxml = ( @@ -644,21 +577,3 @@ def new_fixture(self): rows, cols = 1, 1 expected_xml = snippet_seq("placeholders")[0] return table_ph, rows, cols, expected_xml - - # fixture components --------------------------------------------- - - @pytest.fixture - def _new_placeholder_table_(self, request): - return method_mock(request, TablePlaceholder, "_new_placeholder_table") - - @pytest.fixture - def PlaceholderGraphicFrame_(self, request): - return class_mock(request, "pptx.shapes.placeholder.PlaceholderGraphicFrame") - - @pytest.fixture - def placeholder_graphic_frame_(self, request): - return instance_mock(request, PlaceholderGraphicFrame) - - @pytest.fixture - def _replace_placeholder_with_(self, request): - return method_mock(request, TablePlaceholder, "_replace_placeholder_with") diff --git a/tests/test_action.py b/tests/test_action.py index 92313542c..073d0df79 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -7,6 +7,7 @@ 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.parts.slide import SlidePart from pptx.slide import Slide @@ -34,15 +35,31 @@ def it_can_find_its_slide_jump_target(self, target_get_fixture): target_slide = action_setting.target_slide assert target_slide == expected_value - def it_can_change_its_slide_jump_target(self, target_set_fixture): - action_setting, value, expected_xml = target_set_fixture[:3] - slide_part_, calls = target_set_fixture[3:] + def it_can_change_its_slide_jump_target( + self, request, _clear_click_action_, slide_, part_prop_, part_ + ): + part_prop_.return_value = part_ + part_.relate_to.return_value = "rId42" + slide_part_ = instance_mock(request, SlidePart) + slide_.part = slide_part_ + action_setting = ActionSetting(element("p:cNvPr{a:b=c,r:s=t}"), None) - action_setting.target_slide = value + action_setting.target_slide = slide_ - action_setting._clear_click_action.assert_called_once_with() - assert action_setting._element.xml == expected_xml - assert slide_part_.relate_to.call_args_list == calls + _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}", + ) + + def but_it_clears_the_target_slide_if_None_is_assigned(self, _clear_click_action_): + action_setting = ActionSetting(element("p:cNvPr{a:b=c,r:s=t}"), None) + + action_setting.target_slide = None + + _clear_click_action_.assert_called_once_with(action_setting) + assert action_setting._element.xml == xml("p:cNvPr{a:b=c,r:s=t}") def it_raises_on_no_next_prev_slide(self, target_raise_fixture): action_setting = target_raise_fixture @@ -130,11 +147,11 @@ def action_fixture(self, request): ), ] ) - def clear_fixture(self, request, part_prop_, slide_part_): + def clear_fixture(self, request, part_prop_, part_): xPr_cxml, rId, expected_cxml = request.param action_setting = ActionSetting(element(xPr_cxml), None) - part_prop_.return_value = slide_part_ + part_prop_.return_value = part_ calls = [call(rId)] if rId else [] expected_xml = xml(expected_cxml) @@ -188,31 +205,6 @@ def target_get_fixture(self, request, action_prop_, _slide_index_prop_, part_pro related_parts_.__getitem__.return_value.slide = 4 return action_setting, expected_value - @pytest.fixture( - params=[ - (None, "p:cNvPr{a:b=c,r:s=t}"), - ( - "slide_", - "p:cNvPr{a:b=c,r:s=t}/a:hlinkClick{action=ppaction://hlinksldjump,r" - ":id=rId42}", - ), - ] - ) - def target_set_fixture( - self, request, slide_, _clear_click_action_, part_prop_, slide_part_ - ): - value_key, expected_cxml = request.param - action_setting = ActionSetting(element("p:cNvPr{a:b=c,r:s=t}"), None) - value = None if value_key is None else slide_ - - part_prop_.return_value = slide_part_ - slide_part_.relate_to.return_value = "rId42" - slide_.part = slide_part_ - - expected_xml = xml(expected_cxml) - calls = [] if value is None else [call(slide_part_, RT.SLIDE)] - return action_setting, value, expected_xml, slide_part_, calls - @pytest.fixture(params=[(PP_ACTION.NEXT_SLIDE, 2), (PP_ACTION.PREVIOUS_SLIDE, 0)]) def target_raise_fixture( self, request, action_prop_, part_prop_, _slide_index_prop_ @@ -243,6 +235,10 @@ def Hyperlink_(self, request, hyperlink_): def hyperlink_(self, request): return instance_mock(request, Hyperlink) + @pytest.fixture + def part_(self, request): + return instance_mock(request, Part) + @pytest.fixture def part_prop_(self, request): return property_mock(request, ActionSetting, "part") @@ -255,10 +251,6 @@ def slide_(self, request): def _slide_index_prop_(self, request): return property_mock(request, ActionSetting, "_slide_index") - @pytest.fixture - def slide_part_(self, request): - return instance_mock(request, SlidePart) - class DescribeHyperlink(object): """Unit-test suite for `pptx.action.Hyperlink` objects.""" diff --git a/tests/test_package.py b/tests/test_package.py index 054e2da03..2bdf78ce1 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -153,23 +153,31 @@ def it_can_iterate_over_the_package_image_parts(self, iter_fixture): image_parts, expected_parts = iter_fixture assert list(image_parts) == expected_parts - def it_can_get_a_matching_image_part(self, get_fixture): - image_parts, image_file, Image_, image_, image_part_ = get_fixture + def it_can_get_a_matching_image_part( + self, Image_, image_, image_part_, _find_by_sha1_ + ): + Image_.from_file.return_value = image_ + _find_by_sha1_.return_value = image_part_ + image_parts = _ImageParts(None) - image_part = image_parts.get_or_add_image_part(image_file) + image_part = image_parts.get_or_add_image_part("image.png") - Image_.from_file.assert_called_once_with(image_file) - image_parts._find_by_sha1.assert_called_once_with(image_.sha1) + Image_.from_file.assert_called_once_with("image.png") + _find_by_sha1_.assert_called_once_with(image_parts, image_.sha1) assert image_part is image_part_ - def it_can_add_an_image_part(self, add_fixture): - image_parts, image_file, Image_, image_ = add_fixture[:4] - ImagePart_, package_, image_part_ = add_fixture[4:] + def it_can_add_an_image_part( + self, package_, Image_, image_, _find_by_sha1_, ImagePart_, image_part_ + ): + Image_.from_file.return_value = image_ + _find_by_sha1_.return_value = None + ImagePart_.new.return_value = image_part_ + image_parts = _ImageParts(package_) - image_part = image_parts.get_or_add_image_part(image_file) + image_part = image_parts.get_or_add_image_part("image.png") - Image_.from_file.assert_called_once_with(image_file) - image_parts._find_by_sha1.assert_called_once_with(image_.sha1) + Image_.from_file.assert_called_once_with("image.png") + _find_by_sha1_.assert_called_once_with(image_parts, image_.sha1) ImagePart_.new.assert_called_once_with(package_, image_) assert image_part is image_part_ @@ -192,25 +200,6 @@ def but_it_skips_unsupported_image_types(self, request, _iter_): # fixtures --------------------------------------------- - @pytest.fixture - def add_fixture( - self, package_, Image_, image_, _find_by_sha1_, ImagePart_, image_part_ - ): - image_parts = _ImageParts(package_) - image_file = "foobar.png" - Image_.from_file.return_value = image_ - _find_by_sha1_.return_value = None - ImagePart_.new.return_value = image_part_ - return ( - image_parts, - image_file, - Image_, - image_, - ImagePart_, - package_, - image_part_, - ) - @pytest.fixture(params=[True, False]) def find_fixture(self, request, _iter_, image_part_): image_part_is_present = request.param @@ -225,14 +214,6 @@ def find_fixture(self, request, _iter_, image_part_): expected_value = None return image_parts, sha1, expected_value - @pytest.fixture - def get_fixture(self, Image_, image_, image_part_, _find_by_sha1_): - image_parts = _ImageParts(None) - image_file = "foobar.png" - Image_.from_file.return_value = image_ - _find_by_sha1_.return_value = image_part_ - return image_parts, image_file, Image_, image_, image_part_ - @pytest.fixture def iter_fixture(self, request, package_): def rel(is_external, reltype): diff --git a/tests/test_slide.py b/tests/test_slide.py index 0eeb05148..7e139068b 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -169,10 +169,19 @@ def shapes_(self, request): class DescribeNotesSlide(object): """Unit-test suite for `pptx.slide.NotesSlide` objects.""" - def it_can_clone_the_notes_master_placeholders(self, clone_fixture): - notes_slide, notes_master_, clone_placeholder_, calls = clone_fixture + def it_can_clone_the_notes_master_placeholders( + self, request, notes_master_, shapes_ + ): + placeholders = notes_master_.placeholders = ( + BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=body}"), None), + BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=dt}"), None), + ) + property_mock(request, NotesSlide, "shapes", return_value=shapes_) + notes_slide = NotesSlide(None, None) + notes_slide.clone_master_placeholders(notes_master_) - assert clone_placeholder_.call_args_list == calls + + assert shapes_.clone_placeholder.call_args_list == [call(placeholders[0])] def it_provides_access_to_its_shapes(self, shapes_fixture): notes_slide, NotesSlideShapes_, spTree, shapes_ = shapes_fixture @@ -203,17 +212,6 @@ def it_provides_access_to_its_notes_text_frame(self, notes_tf_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def clone_fixture(self, notes_master_, clone_placeholder_, shapes_prop_, shapes_): - notes_slide = NotesSlide(None, None) - placeholders = notes_master_.placeholders = ( - BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=body}"), None), - BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=dt}"), None), - ) - calls = [call(placeholders[0])] - shapes_.clone_placeholder = clone_placeholder_ - return notes_slide, notes_master_, clone_placeholder_, calls - @pytest.fixture( params=[ (("SLIDE_IMAGE", "BODY", "SLIDE_NUMBER"), 1), @@ -265,10 +263,6 @@ def shapes_fixture(self, NotesSlideShapes_, shapes_): # fixture components --------------------------------------------- - @pytest.fixture - def clone_placeholder_(self, request): - return method_mock(request, NotesSlideShapes, "clone_placeholder") - @pytest.fixture def notes_master_(self, request): return instance_mock(request, NotesMaster) @@ -307,10 +301,6 @@ def placeholders_prop_(self, request, placeholders_): def shapes_(self, request): return instance_mock(request, NotesSlideShapes) - @pytest.fixture - def shapes_prop_(self, request, shapes_): - return property_mock(request, NotesSlide, "shapes", return_value=shapes_) - @pytest.fixture def text_frame_(self, request): return instance_mock(request, TextFrame) diff --git a/tests/text/test_fonts.py b/tests/text/test_fonts.py index 69eafd60a..4aba411c0 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -63,9 +63,8 @@ def it_knows_windows_font_dirs_to_help_find(self, win_dirs_fixture): def it_iterates_over_fonts_in_dir_to_help_find(self, iter_fixture): directory, _Font_, expected_calls, expected_paths = iter_fixture - paths = list(FontFiles._iter_font_files_in(directory)) - print(directory) + paths = list(FontFiles._iter_font_files_in(directory)) assert _Font_.open.call_args_list == expected_calls assert paths == expected_paths @@ -208,11 +207,15 @@ def it_knows_the_table_count_to_help_read(self, table_count_fixture): font, expected_value = table_count_fixture assert font._table_count == expected_value - def it_reads_the_header_to_help_read_font(self, fields_fixture): - font, expected_values = fields_fixture + def it_reads_the_header_to_help_read_font(self, request): + stream_ = instance_mock(request, _Stream) + stream_.read_fields.return_value = ("foob", 42, 64, 7, 16) + font = _Font(stream_) + fields = font._fields - font._stream.read_fields.assert_called_once_with(">4sHHHH", 0) - assert fields == expected_values + + stream_.read_fields.assert_called_once_with(">4sHHHH", 0) + assert fields == ("foob", 42, 64, 7, 16) # fixtures --------------------------------------------- @@ -234,13 +237,6 @@ def family_fixture(self, _tables_, name_table_): name_table_.family_name = expected_name return font, expected_name - @pytest.fixture - def fields_fixture(self, read_fields_): - stream = _Stream(None) - font = _Font(stream) - read_fields_.return_value = expected_values = ("foob", 42, 64, 7, 16) - return font, expected_values - @pytest.fixture( params=[("head", True, True), ("head", False, False), ("foob", True, False)] ) @@ -312,10 +308,6 @@ def _iter_table_records_(self, request): def name_table_(self, request): return instance_mock(request, _NameTable) - @pytest.fixture - def read_fields_(self, request): - return method_mock(request, _Stream, "read_fields") - @pytest.fixture def _Stream_(self, request): return class_mock(request, "pptx.text.fonts._Stream") @@ -344,11 +336,15 @@ def _tables_(self, request): class Describe_Stream(object): """Unit-test suite for `pptx.text.fonts._Stream` object.""" - def it_can_construct_from_a_path(self, open_fixture): - path, open_, _init_, file_ = open_fixture - stream = _Stream.open(path) - open_.assert_called_once_with(path, "rb") - _init_.assert_called_once_with(file_) + def it_can_construct_from_a_path(self, request): + open_ = open_mock(request, "pptx.text.fonts") + _init_ = initializer_mock(request, _Stream) + file_ = open_.return_value + + stream = _Stream.open("foobar.ttf") + + open_.assert_called_once_with("foobar.ttf", "rb") + _init_.assert_called_once_with(stream, file_) assert isinstance(stream, _Stream) def it_can_be_closed(self, close_fixture): @@ -377,12 +373,6 @@ def close_fixture(self, file_): stream = _Stream(file_) return stream, file_ - @pytest.fixture - def open_fixture(self, open_, _init_): - path = "foobar.ttf" - file_ = open_.return_value - return path, open_, _init_, file_ - @pytest.fixture def read_fixture(self, file_): stream = _Stream(file_) @@ -405,14 +395,6 @@ def read_flds_fixture(self, file_): def file_(self, request): return instance_mock(request, io.RawIOBase) - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, _Stream) - - @pytest.fixture - def open_(self, request): - return open_mock(request, "pptx.text.fonts") - class Describe_TableFactory(object): """Unit-test suite for `pptx.text.fonts._TableFactory` object.""" @@ -499,17 +481,41 @@ def it_knows_the_font_family_name(self, family_fixture): family_name = name_table.family_name assert family_name == expected_value - def it_provides_access_to_its_names_to_help_props(self, names_fixture): - name_table, names_dict = names_fixture + def it_provides_access_to_its_names_to_help_props(self, request): + _iter_names_ = method_mock( + request, + _NameTable, + "_iter_names", + return_value=iter([((0, 1), "Foobar"), ((3, 1), "Barfoo")]), + ) + name_table = _NameTable(None, None, None, None) + names = name_table._names - name_table._iter_names.assert_called_once_with() - assert names == names_dict - def it_iterates_over_its_names_to_help_read_names(self, iter_fixture): - name_table, expected_calls, expected_names = iter_fixture + _iter_names_.assert_called_once_with(name_table) + assert names == {(0, 1): "Foobar", (3, 1): "Barfoo"} + + def it_iterates_over_its_names_to_help_read_names( + self, request, _table_bytes_prop_ + ): + property_mock(request, _NameTable, "_table_header", return_value=(0, 3, 42)) + _table_bytes_prop_.return_value = "xXx" + _read_name_ = method_mock( + request, + _NameTable, + "_read_name", + side_effect=iter([(0, 1, "Foobar"), (3, 1, "Barfoo"), (9, 9, None)]), + ) + name_table = _NameTable(None, None, None, None) + names = list(name_table._iter_names()) - assert name_table._read_name.call_args_list == expected_calls - assert names == expected_names + + assert _read_name_.call_args_list == [ + call(name_table, "xXx", 0, 42), + call(name_table, "xXx", 1, 42), + call(name_table, "xXx", 2, 42), + ] + assert names == [((0, 1), "Foobar"), ((3, 1), "Barfoo")] def it_reads_the_table_header_to_help_read_names(self, header_fixture): names_table, expected_value = header_fixture @@ -524,18 +530,40 @@ def it_buffers_the_table_bytes_to_help_read_names(self, bytes_fixture): ) assert table_bytes == expected_value - def it_reads_a_name_to_help_read_names(self, read_fixture): - name_table, bufr, idx, strs_offset, platform_id = read_fixture[:5] - encoding_id, name_str_offset, length = read_fixture[5:8] - expected_value = read_fixture[8] + def it_reads_a_name_to_help_read_names(self, request): + bufr, idx, strs_offset, platform_id, name_id = "buffer", 3, 47, 0, 1 + encoding_id, name_str_offset, length, name = 7, 36, 12, "Arial" + _name_header_ = method_mock( + request, + _NameTable, + "_name_header", + return_value=( + platform_id, + encoding_id, + 666, + name_id, + length, + name_str_offset, + ), + ) + _read_name_text_ = method_mock( + request, _NameTable, "_read_name_text", return_value=name + ) + name_table = _NameTable(None, None, None, None) - name = name_table._read_name(bufr, idx, strs_offset) + actual = name_table._read_name(bufr, idx, strs_offset) - name_table._name_header.assert_called_once_with(bufr, idx) - name_table._read_name_text.assert_called_once_with( - bufr, platform_id, encoding_id, strs_offset, name_str_offset, length + _name_header_.assert_called_once_with(bufr, idx) + _read_name_text_.assert_called_once_with( + name_table, + bufr, + platform_id, + encoding_id, + strs_offset, + name_str_offset, + length, ) - assert name == expected_value + assert actual == (platform_id, name_id, name) def it_reads_a_name_header_to_help_read_names(self, name_hdr_fixture): name_table, bufr, idx, expected_value = name_hdr_fixture @@ -602,29 +630,19 @@ def decode_fixture(self, request): ({(9, 1): "Foobar", (6, 1): "Barfoo"}, None), ] ) - def family_fixture(self, request, _names_): + def family_fixture(self, request, _names_prop_): names, expected_value = request.param name_table = _NameTable(None, None, None, None) - _names_.return_value = names + _names_prop_.return_value = names return name_table, expected_value @pytest.fixture - def header_fixture(self, _table_bytes_): + def header_fixture(self, _table_bytes_prop_): name_table = _NameTable(None, None, None, None) - _table_bytes_.return_value = b"\x00\x00\x00\x02\x00\x2A" + _table_bytes_prop_.return_value = b"\x00\x00\x00\x02\x00\x2A" expected_value = (0, 2, 42) return name_table, expected_value - @pytest.fixture - def iter_fixture(self, _table_header_, _table_bytes_, _read_name): - name_table = _NameTable(None, None, None, None) - _table_header_.return_value = (0, 3, 42) - _table_bytes_.return_value = "xXx" - _read_name.side_effect = [(0, 1, "Foobar"), (3, 1, "Barfoo"), (9, 9, None)] - expected_calls = [call("xXx", 0, 42), call("xXx", 1, 42), call("xXx", 2, 42)] - expected_names = [((0, 1), "Foobar"), ((3, 1), "Barfoo")] - return name_table, expected_calls, expected_names - @pytest.fixture def name_hdr_fixture(self): name_table = _NameTable(None, None, None, None) @@ -642,13 +660,6 @@ def name_hdr_fixture(self): expected_value = (0, 1, 2, 3, 4, 5) return name_table, bufr, idx, expected_value - @pytest.fixture - def names_fixture(self, _iter_names_): - name_table = _NameTable(None, None, None, None) - _iter_names_.return_value = iter([((0, 1), "Foobar"), ((3, 1), "Barfoo")]) - names_dict = {(0, 1): "Foobar", (3, 1): "Barfoo"} - return name_table, names_dict - @pytest.fixture def name_text_fixture(self, _raw_name_string_, _decode_name_): name_table = _NameTable(None, None, None, None) @@ -676,33 +687,6 @@ def raw_fixture(self): expected_bytes = b"Foobar" return (name_table, bufr, strings_offset, str_offset, length, expected_bytes) - @pytest.fixture - def read_fixture(self, _name_header, _read_name_text): - name_table = _NameTable(None, None, None, None) - bufr, idx, strs_offset, platform_id, name_id = "buffer", 3, 47, 0, 1 - encoding_id, name_str_offset, length, name = 7, 36, 12, "Arial" - _name_header.return_value = ( - platform_id, - encoding_id, - 666, - name_id, - length, - name_str_offset, - ) - _read_name_text.return_value = name - expected_value = (platform_id, name_id, name) - return ( - name_table, - bufr, - idx, - strs_offset, - platform_id, - encoding_id, - name_str_offset, - length, - expected_value, - ) - # fixture components ----------------------------------- @pytest.fixture @@ -710,37 +694,17 @@ def _decode_name_(self, request): return method_mock(request, _NameTable, "_decode_name") @pytest.fixture - def _iter_names_(self, request): - return method_mock(request, _NameTable, "_iter_names") - - @pytest.fixture - def _name_header(self, request): - return method_mock(request, _NameTable, "_name_header") + def _names_prop_(self, request): + return property_mock(request, _NameTable, "_names") @pytest.fixture def _raw_name_string_(self, request): return method_mock(request, _NameTable, "_raw_name_string") - @pytest.fixture - def _read_name(self, request): - return method_mock(request, _NameTable, "_read_name") - - @pytest.fixture - def _read_name_text(self, request): - return method_mock(request, _NameTable, "_read_name_text") - @pytest.fixture def stream_(self, request): return instance_mock(request, _Stream) @pytest.fixture - def _table_bytes_(self, request): + def _table_bytes_prop_(self, request): return property_mock(request, _NameTable, "_table_bytes") - - @pytest.fixture - def _table_header_(self, request): - return property_mock(request, _NameTable, "_table_header") - - @pytest.fixture - def _names_(self, request): - return property_mock(request, _NameTable, "_names") diff --git a/tests/text/test_layout.py b/tests/text/test_layout.py index 9ad0a26e8..2627660f2 100644 --- a/tests/text/test_layout.py +++ b/tests/text/test_layout.py @@ -7,6 +7,7 @@ from pptx.text.layout import _BinarySearchTree, _Line, _LineSource, TextFitter from ..unitutil.mock import ( + ANY, call, class_mock, function_mock, @@ -20,17 +21,24 @@ class DescribeTextFitter(object): """Unit-test suite for `pptx.text.layout.TextFitter` object.""" - def it_can_determine_the_best_fit_font_size(self, best_fit_fixture): - text, extents, max_size, font_file = best_fit_fixture[:4] - _LineSource_, _init_, line_source_ = best_fit_fixture[4:7] - _best_fit_font_size_, font_size_ = best_fit_fixture[7:] + def it_can_determine_the_best_fit_font_size(self, request, line_source_): + _LineSource_ = class_mock( + request, "pptx.text.layout._LineSource", return_value=line_source_ + ) + _init_ = initializer_mock(request, TextFitter) + _best_fit_font_size_ = method_mock( + request, TextFitter, "_best_fit_font_size", return_value=36 + ) + extents, max_size = (19, 20), 42 - font_size = TextFitter.best_fit_font_size(text, extents, max_size, font_file) + font_size = TextFitter.best_fit_font_size( + "Foobar", extents, max_size, "foobar.ttf" + ) - _LineSource_.assert_called_once_with(text) - _init_.assert_called_once_with(line_source_, extents, font_file) - _best_fit_font_size_.assert_called_once_with(max_size) - assert font_size is font_size_ + _LineSource_.assert_called_once_with("Foobar") + _init_.assert_called_once_with(line_source_, extents, "foobar.ttf") + _best_fit_font_size_.assert_called_once_with(ANY, max_size) + assert font_size == 36 def it_finds_best_fit_font_size_to_help_best_fit(self, _best_fit_fixture): text_fitter, max_size, _BinarySearchTree_ = _best_fit_fixture[:3] @@ -44,20 +52,38 @@ def it_finds_best_fit_font_size_to_help_best_fit(self, _best_fit_fixture): sizes_.find_max.assert_called_once_with(predicate_) assert font_size is font_size_ - def it_provides_a_fits_inside_predicate_fn(self, fits_pred_fixture): - text_fitter, point_size = fits_pred_fixture[:2] - _rendered_size_, expected_bool_value = fits_pred_fixture[2:] + @pytest.mark.parametrize( + "extents, point_size, text_lines, expected_value", + ( + ((66, 99), 6, ("foo", "bar"), False), + ((66, 100), 6, ("foo", "bar"), True), + ((66, 101), 6, ("foo", "bar"), True), + ), + ) + def it_provides_a_fits_inside_predicate_fn( + self, + request, + line_source_, + _rendered_size_, + extents, + point_size, + text_lines, + expected_value, + ): + _wrap_lines_ = method_mock( + request, TextFitter, "_wrap_lines", return_value=text_lines + ) + _rendered_size_.return_value = (None, 50) + text_fitter = TextFitter(line_source_, extents, "foobar.ttf") predicate = text_fitter._fits_inside_predicate result = predicate(point_size) - text_fitter._wrap_lines.assert_called_once_with( - text_fitter._line_source, point_size - ) + _wrap_lines_.assert_called_once_with(text_fitter, line_source_, point_size) _rendered_size_.assert_called_once_with( "Ty", point_size, text_fitter._font_file ) - assert result is expected_bool_value + assert result is expected_value def it_provides_a_fits_in_width_predicate_fn(self, fits_cx_pred_fixture): text_fitter, point_size, line = fits_cx_pred_fixture[:3] @@ -71,48 +97,44 @@ def it_provides_a_fits_in_width_predicate_fn(self, fits_cx_pred_fixture): ) assert result is expected_value - def it_wraps_lines_to_help_best_fit(self, wrap_fixture): - text_fitter, line_source, point_size, remainder = wrap_fixture + def it_wraps_lines_to_help_best_fit(self, request): + line_source, remainder = _LineSource("foo bar"), _LineSource("bar") + _break_line_ = method_mock( + request, + TextFitter, + "_break_line", + side_effect=[("foo", remainder), ("bar", _LineSource(""))], + ) + text_fitter = TextFitter(None, (None, None), None) - text_fitter._wrap_lines(line_source, point_size) + text_fitter._wrap_lines(line_source, 21) - assert text_fitter._break_line.call_args_list == [ - call(line_source, point_size), - call(remainder, point_size), + assert _break_line_.call_args_list == [ + call(text_fitter, line_source, 21), + call(text_fitter, remainder, 21), ] - def it_breaks_off_a_line_to_help_wrap(self, break_fixture): - text_fitter, line_source_, point_size = break_fixture[:3] - _BinarySearchTree_, bst_, predicate_ = break_fixture[3:6] - max_value_ = break_fixture[6] + def it_breaks_off_a_line_to_help_wrap( + self, request, line_source_, _BinarySearchTree_ + ): + bst_ = instance_mock(request, _BinarySearchTree) + _fits_in_width_predicate_ = method_mock( + request, TextFitter, "_fits_in_width_predicate" + ) + _BinarySearchTree_.from_ordered_sequence.return_value = bst_ + predicate_ = _fits_in_width_predicate_.return_value + max_value_ = bst_.find_max.return_value + text_fitter = TextFitter(None, (None, None), None) - value = text_fitter._break_line(line_source_, point_size) + value = text_fitter._break_line(line_source_, 21) _BinarySearchTree_.from_ordered_sequence.assert_called_once_with(line_source_) - text_fitter._fits_in_width_predicate.assert_called_once_with(point_size) + text_fitter._fits_in_width_predicate.assert_called_once_with(text_fitter, 21) bst_.find_max.assert_called_once_with(predicate_) assert value is max_value_ # fixtures --------------------------------------------- - @pytest.fixture - def best_fit_fixture(self, _LineSource_, _init_, _best_fit_font_size_): - text, extents, max_size = "Foobar", (19, 20), 42 - font_file = "foobar.ttf" - line_source_ = _LineSource_.return_value - font_size_ = _best_fit_font_size_.return_value - return ( - text, - extents, - max_size, - font_file, - _LineSource_, - _init_, - line_source_, - _best_fit_font_size_, - font_size_, - ) - @pytest.fixture def _best_fit_fixture(self, _BinarySearchTree_, _fits_inside_predicate_): text_fitter = TextFitter(None, (None, None), None) @@ -129,25 +151,6 @@ def _best_fit_fixture(self, _BinarySearchTree_, _fits_inside_predicate_): font_size_, ) - @pytest.fixture - def break_fixture( - self, line_source_, _BinarySearchTree_, bst_, _fits_in_width_predicate_ - ): - text_fitter = TextFitter(None, (None, None), None) - point_size = 21 - _BinarySearchTree_.from_ordered_sequence.return_value = bst_ - predicate_ = _fits_in_width_predicate_.return_value - max_value_ = bst_.find_max.return_value - return ( - text_fitter, - line_source_, - point_size, - _BinarySearchTree_, - bst_, - predicate_, - max_value_, - ) - @pytest.fixture(params=[(49, True), (50, True), (51, False)]) def fits_cx_pred_fixture(self, request, _rendered_size_): rendered_width, expected_value = request.param @@ -156,64 +159,16 @@ def fits_cx_pred_fixture(self, request, _rendered_size_): _rendered_size_.return_value = (rendered_width, None) return (text_fitter, point_size, line, _rendered_size_, expected_value) - @pytest.fixture( - params=[ - ((66, 99), 6, ("foo", "bar"), False), - ((66, 100), 6, ("foo", "bar"), True), - ((66, 101), 6, ("foo", "bar"), True), - ] - ) - def fits_pred_fixture(self, request, line_source_, _wrap_lines_, _rendered_size_): - extents, point_size, text_lines, expected_value = request.param - text_fitter = TextFitter(line_source_, extents, "foobar.ttf") - _wrap_lines_.return_value = text_lines - _rendered_size_.return_value = (None, 50) - return text_fitter, point_size, _rendered_size_, expected_value - - @pytest.fixture - def wrap_fixture(self, _break_line_): - text_fitter = TextFitter(None, (None, None), None) - point_size = 21 - line_source, remainder = _LineSource("foo bar"), _LineSource("bar") - _break_line_.side_effect = [("foo", remainder), ("bar", _LineSource(""))] - return text_fitter, line_source, point_size, remainder - # fixture components ----------------------------------- - @pytest.fixture - def _best_fit_font_size_(self, request): - return method_mock(request, TextFitter, "_best_fit_font_size") - @pytest.fixture def _BinarySearchTree_(self, request): return class_mock(request, "pptx.text.layout._BinarySearchTree") - @pytest.fixture - def _break_line_(self, request): - return method_mock(request, TextFitter, "_break_line") - - @pytest.fixture - def bst_(self, request): - return instance_mock(request, _BinarySearchTree) - - @pytest.fixture - def _fits_in_width_predicate_(self, request): - return method_mock(request, TextFitter, "_fits_in_width_predicate") - @pytest.fixture def _fits_inside_predicate_(self, request): return property_mock(request, TextFitter, "_fits_inside_predicate") - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, TextFitter) - - @pytest.fixture - def _LineSource_(self, request, line_source_): - return class_mock( - request, "pptx.text.layout._LineSource", return_value=line_source_ - ) - @pytest.fixture def line_source_(self, request): return instance_mock(request, _LineSource) @@ -222,10 +177,6 @@ def line_source_(self, request): def _rendered_size_(self, request): return function_mock(request, "pptx.text.layout._rendered_size") - @pytest.fixture - def _wrap_lines_(self, request): - return method_mock(request, TextFitter, "_wrap_lines") - class Describe_BinarySearchTree(object): """Unit-test suite for `pptx.text.layout._BinarySearchTree` object.""" diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 284a2f7fb..26695df4b 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -110,9 +110,7 @@ def it_can_replace_the_text_it_contains(self, text_set_fixture): assert text_frame._element.xml == expected_xml - def it_can_resize_its_text_to_best_fit( - self, text_prop_, _best_fit_font_size_, _apply_fit_ - ): + def it_can_resize_its_text_to_best_fit(self, request, text_prop_): family, max_size, bold, italic, font_file, font_size = ( "Family", 42, @@ -122,15 +120,18 @@ def it_can_resize_its_text_to_best_fit( 21, ) text_prop_.return_value = "some text" - _best_fit_font_size_.return_value = font_size + _best_fit_font_size_ = method_mock( + request, TextFrame, "_best_fit_font_size", return_value=font_size + ) + _apply_fit_ = method_mock(request, TextFrame, "_apply_fit") text_frame = TextFrame(None, None) text_frame.fit_text(family, max_size, bold, italic, font_file) - text_frame._best_fit_font_size.assert_called_once_with( - family, max_size, bold, italic, font_file + _best_fit_font_size_.assert_called_once_with( + text_frame, family, max_size, bold, italic, font_file ) - text_frame._apply_fit.assert_called_once_with(family, font_size, bold, italic) + _apply_fit_.assert_called_once_with(text_frame, family, font_size, bold, italic) def it_calculates_its_best_fit_font_size_to_help_fit_text(self, size_font_fixture): text_frame, family, max_size, bold, italic = size_font_fixture[:5] @@ -153,12 +154,16 @@ def it_calculates_its_effective_size_to_help_fit_text(self): text_frame = Shape(element(sp_cxml), None).text_frame assert text_frame._extents == (731520, 822960) - def it_applies_fit_to_help_fit_text(self, apply_fit_fixture): - text_frame, family, font_size, bold, italic = apply_fit_fixture + def it_applies_fit_to_help_fit_text(self, request): + family, font_size, bold, italic = "Family", 42, True, False + _set_font_ = method_mock(request, TextFrame, "_set_font") + text_frame = TextFrame(element("p:txBody/a:bodyPr"), None) + text_frame._apply_fit(family, font_size, bold, italic) + assert text_frame.auto_size is MSO_AUTO_SIZE.NONE assert text_frame.word_wrap is True - text_frame._set_font.assert_called_once_with(family, font_size, bold, italic) + _set_font_.assert_called_once_with(text_frame, family, font_size, bold, italic) def it_sets_its_font_to_help_fit_text(self, set_font_fixture): text_frame, family, size, bold, italic, expected_xml = set_font_fixture @@ -213,13 +218,6 @@ def anchor_set_fixture(self, request): expected_xml = xml(expected_cxml) return text_frame, new_value, expected_xml - @pytest.fixture - def apply_fit_fixture(self, _set_font_): - txBody = element("p:txBody/a:bodyPr") - text_frame = TextFrame(txBody, None) - family, font_size, bold, italic = "Family", 42, True, False - return text_frame, family, font_size, bold, italic - @pytest.fixture( params=[ ("p:txBody/a:bodyPr", None), @@ -439,14 +437,6 @@ def wrap_set_fixture(self, request): # fixture components ----------------------------------- - @pytest.fixture - def _apply_fit_(self, request): - return method_mock(request, TextFrame, "_apply_fit") - - @pytest.fixture - def _best_fit_font_size_(self, request): - return method_mock(request, TextFrame, "_best_fit_font_size") - @pytest.fixture def _extents_prop_(self, request): return property_mock(request, TextFrame, "_extents") @@ -459,10 +449,6 @@ def FontFiles_(self, request): def paragraphs_prop_(self, request): return property_mock(request, TextFrame, "paragraphs") - @pytest.fixture - def _set_font_(self, request): - return method_mock(request, TextFrame, "_set_font") - @pytest.fixture def TextFitter_(self, request): return class_mock(request, "pptx.text.text.TextFitter") @@ -1193,15 +1179,18 @@ def it_can_get_the_text_of_the_run(self, text_get_fixture): assert text == expected_value assert is_unicode(text) - def it_can_change_its_text(self, text_set_fixture): - r, new_value, expected_xml = text_set_fixture - run = _Run(r, None) - + @pytest.mark.parametrize( + "r_cxml, new_value, expected_r_cxml", + ( + ("a:r/a:t", "barfoo", 'a:r/a:t"barfoo"'), + ("a:r/a:t", "bar\x1bfoo", 'a:r/a:t"bar_x001B_foo"'), + ("a:r/a:t", "bar\tfoo", 'a:r/a:t"bar\tfoo"'), + ), + ) + def it_can_change_its_text(self, r_cxml, new_value, expected_r_cxml): + run = _Run(element(r_cxml), None) run.text = new_value - - print("run._r.xml == %s" % repr(run._r.xml)) - print("expected_xml == %s" % repr(expected_xml)) - assert run._r.xml == expected_xml + assert run._r.xml == xml(expected_r_cxml) # fixtures --------------------------------------------- @@ -1225,19 +1214,6 @@ def text_get_fixture(self): run = _Run(r, None) return run, "foobar" - @pytest.fixture( - params=[ - ("a:r/a:t", "barfoo", 'a:r/a:t"barfoo"'), - ("a:r/a:t", "bar\x1bfoo", 'a:r/a:t"bar_x001B_foo"'), - ("a:r/a:t", "bar\tfoo", 'a:r/a:t"bar\tfoo"'), - ] - ) - def text_set_fixture(self, request): - r_cxml, new_value, expected_r_cxml = request.param - r = element(r_cxml) - expected_xml = xml(expected_r_cxml) - return r, new_value, expected_xml - # fixture components ----------------------------------- @pytest.fixture diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index ef8f3023c..efbe6b17f 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -37,22 +37,24 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): return _patch.start() -def function_mock(request, q_function_name, **kwargs): +def function_mock(request, q_function_name, autospec=True, **kwargs): """Return mock patching function with qualified name `q_function_name`. Patch is reversed after calling test returns. """ - _patch = patch(q_function_name, **kwargs) + _patch = patch(q_function_name, autospec=autospec, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def initializer_mock(request, cls, **kwargs): +def initializer_mock(request, cls, autospec=True, **kwargs): """Return mock for __init__() method on `cls`. The patch is reversed after pytest uses it. """ - _patch = patch.object(cls, "__init__", return_value=None, **kwargs) + _patch = patch.object( + cls, "__init__", autospec=autospec, return_value=None, **kwargs + ) request.addfinalizer(_patch.stop) return _patch.start() @@ -80,12 +82,12 @@ def loose_mock(request, name=None, **kwargs): return Mock(name=name, **kwargs) -def method_mock(request, cls, method_name, **kwargs): +def method_mock(request, cls, method_name, autospec=True, **kwargs): """Return mock for method `method_name` on `cls`. The patch is reversed after pytest uses it. """ - _patch = patch.object(cls, method_name, **kwargs) + _patch = patch.object(cls, method_name, autospec=autospec, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() From f6e994d8fb682f8462d6406b3ee263748d0f74c9 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 15:59:07 -0700 Subject: [PATCH 04/69] rfctr: normalize method ordering --- pptx/opc/package.py | 74 +++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index e1950bf4c..c6bd4b094 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -25,6 +25,14 @@ class OpcPackage(object): def __init__(self): super(OpcPackage, self).__init__() + @classmethod + def open(cls, pkg_file): + """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" + pkg_reader = PackageReader.from_file(pkg_file) + package = cls() + Unmarshaller.unmarshal(pkg_reader, package, PartFactory) + return package + def after_unmarshal(self): """ Called by loading code after all parts and relationships have been @@ -114,14 +122,6 @@ def next_partname(self, tmpl): return PackURI(candidate_partname) raise Exception("ProgrammingError: ran out of candidate_partnames") - @classmethod - def open(cls, pkg_file): - """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" - pkg_reader = PackageReader.from_file(pkg_file) - package = cls() - Unmarshaller.unmarshal(pkg_reader, package, PartFactory) - return package - def part_related_by(self, reltype): """Return (single) part having relationship to this package of `reltype`. @@ -180,7 +180,14 @@ def __init__(self, partname, content_type, blob=None, package=None): self._blob = blob self._package = package - # load/save interface to OpcPackage ------------------------------ + @classmethod + def load(cls, partname, content_type, blob, package): + """Return `cls` instance loaded from arguments. + + This one is a straight pass-through, but subtypes may do some pre-processing, + see XmlPart for an example. + """ + return cls(partname, content_type, blob, package) def after_unmarshal(self): """ @@ -226,14 +233,14 @@ def content_type(self): """Content-type (MIME-type) of this part.""" return self._content_type - @classmethod - def load(cls, partname, content_type, blob, package): - """Return `cls` instance loaded from arguments. + def drop_rel(self, rId): + """Remove relationship identified by `rId` if its reference count is under 2. - This one is a straight pass-through, but subtypes may do some pre-processing, - see XmlPart for an example. + Relationships with a reference count of 0 are implicit relationships. Note that + only XML parts can drop relationships. """ - return cls(partname, content_type, blob, package) + if self._rel_ref_count(rId) < 2: + del self.rels[rId] def load_rel(self, reltype, target, rId, is_external=False): """ @@ -251,6 +258,14 @@ def package(self): """|OpcPackage| instance this part belongs to.""" return self._package + def part_related_by(self, reltype): + """Return (single) part having relationship to this part of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than + one such relationship is found. + """ + return self.rels.part_with_reltype(reltype) + @property def partname(self): """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" @@ -263,25 +278,6 @@ def partname(self, partname): raise TypeError(tmpl % type(partname).__name__) self._partname = partname - # relationship management interface for child objects ------------ - - 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: - del self.rels[rId] - - def part_related_by(self, reltype): - """Return (single) part having relationship to this part of `reltype`. - - Raises |KeyError| if no such relationship is found and |ValueError| if more than - one such relationship is found. - """ - return self.rels.part_with_reltype(reltype) - def relate_to(self, target, reltype, is_external=False): """Return rId key of relationship of `reltype` to `target`. @@ -343,17 +339,17 @@ def __init__(self, partname, content_type, element, package=None): super(XmlPart, self).__init__(partname, content_type, package=package) self._element = element - @property - def blob(self): - """bytes XML serialization of this part.""" - return serialize_part_xml(self._element) - @classmethod def load(cls, partname, content_type, blob, package): """Return instance of `cls` loaded with parsed XML from `blob`.""" element = parse_xml(blob) return cls(partname, content_type, element, package) + @property + def blob(self): + """bytes XML serialization of this part.""" + return serialize_part_xml(self._element) + @property def part(self): """This part. From 7b20c7489bcd06781535d78e77bdb37baef1b291 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 23:18:24 -0700 Subject: [PATCH 05/69] rfctr: rename `pptx.opc.package._Relationships` --- docs/conf.py | 75 ++++++++++++---------- docs/dev/resources/about_relationships.rst | 3 +- pptx/opc/package.py | 12 ++-- tests/opc/test_package.py | 44 ++++++------- tests/opc/unitdata/rels.py | 16 ++--- 5 files changed, 74 insertions(+), 76 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e1117d803..536221211 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) from pptx import __version__ # noqa: E402 @@ -30,8 +30,8 @@ def _warn_node(self, msg, node, **kwargs): - if not msg.startswith('nonlocal image URI found:'): - self._warnfunc(msg, '%s:%s' % get_source_line(node), **kwargs) + if not msg.startswith("nonlocal image URI found:"): + self._warnfunc(msg, "%s:%s" % get_source_line(node), **kwargs) sphinx.environment.BuildEnvironment.warn_node = _warn_node @@ -45,31 +45,31 @@ def _warn_node(self, msg, node, **kwargs): # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.inheritance_diagram', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode' + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-pptx' -copyright = u'2012, 2013, Steve Canny' +project = u"python-pptx" +copyright = u"2012, 2013, Steve Canny" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -300,7 +300,7 @@ def _warn_node(self, msg, node, **kwargs): .. |_Relationship| replace:: :class:`._Relationship` -.. |RelationshipCollection| replace:: :class:`RelationshipCollection` +.. |_Relationships| replace:: :class:`_Relationships` .. |RGBColor| replace:: :class:`.RGBColor` @@ -381,7 +381,7 @@ def _warn_node(self, msg, node, **kwargs): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['.build'] +exclude_patterns = [".build"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -399,7 +399,7 @@ def _warn_node(self, msg, node, **kwargs): # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -409,7 +409,7 @@ def _warn_node(self, msg, node, **kwargs): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'armstrong' +html_theme = "armstrong" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -417,7 +417,7 @@ def _warn_node(self, msg, node, **kwargs): # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['.themes'] +html_theme_path = [".themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -438,7 +438,7 @@ def _warn_node(self, msg, node, **kwargs): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -450,8 +450,7 @@ def _warn_node(self, msg, node, **kwargs): # Custom sidebar templates, maps document names to template names. html_sidebars = { - '**': ['localtoc.html', 'relations.html', 'sidebarlinks.html', - 'searchbox.html'] + "**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"] } # Additional templates that should be rendered to pages, maps page names to @@ -485,7 +484,7 @@ def _warn_node(self, msg, node, **kwargs): # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-pptxdoc' +htmlhelp_basename = "python-pptxdoc" # -- Options for LaTeX output ----------------------------------------------- @@ -493,10 +492,8 @@ def _warn_node(self, msg, node, **kwargs): latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -505,8 +502,13 @@ def _warn_node(self, msg, node, **kwargs): # (source start file, target name, title, author, # documentclass [howto/manual]). latex_documents = [ - ('index', 'python-pptx.tex', u'python-pptx Documentation', - u'Steve Canny', 'manual'), + ( + "index", + "python-pptx.tex", + u"python-pptx Documentation", + u"Steve Canny", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -535,8 +537,7 @@ def _warn_node(self, msg, node, **kwargs): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'python-pptx', u'python-pptx Documentation', - [u'Steve Canny'], 1) + ("index", "python-pptx", u"python-pptx Documentation", [u"Steve Canny"], 1) ] # If true, show URL addresses after external links. @@ -549,9 +550,15 @@ def _warn_node(self, msg, node, **kwargs): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-pptx', u'python-pptx Documentation', - u'Steve Canny', 'python-pptx', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "python-pptx", + u"python-pptx Documentation", + u"Steve Canny", + "python-pptx", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. diff --git a/docs/dev/resources/about_relationships.rst b/docs/dev/resources/about_relationships.rst index 99f57f305..e6e12c445 100644 --- a/docs/dev/resources/about_relationships.rst +++ b/docs/dev/resources/about_relationships.rst @@ -151,7 +151,8 @@ How will dynamic parts (like Slide) interact with its relationship list? ? Should it just add items to the relationship list when it creates new things? -? Does it need some sort of lookup capability in order to delete? Or just have a delete relationship method on RelationshipCollection or something like that. +? Does it need some sort of lookup capability in order to delete? Or just have a delete +relationship method on _Relationships or something like that. Need to come up with a plausible set of use cases to think about a design. Right now the only use case is loading a template into a presentation and diff --git a/pptx/opc/package.py b/pptx/opc/package.py index c6bd4b094..c7b7db381 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -149,11 +149,8 @@ def relate_to(self, part, reltype): @lazyproperty def rels(self): - """ - Return a reference to the |RelationshipCollection| holding the - relationships for this package. - """ - return RelationshipCollection(PACKAGE_URI.baseURI) + """The |_Relationships| object containing the relationships for this package.""" + return _Relationships(PACKAGE_URI.baseURI) def save(self, pkg_file): """Save this package to `pkg_file`. @@ -302,7 +299,7 @@ def related_parts(self): @lazyproperty def rels(self): """|Relationships| object containing relationships from this part to others.""" - return RelationshipCollection(self._partname.baseURI) + return _Relationships(self._partname.baseURI) def target_ref(self, rId): """Return URL contained in target ref of relationship identified by `rId`.""" @@ -386,7 +383,7 @@ def _part_cls_for(cls, content_type): return cls.default_part_type -class RelationshipCollection(dict): +class _Relationships(dict): """Collection of |_Relationship| instances, largely having dict semantics. Relationships are keyed by their rId, but may also be found in other ways, such as @@ -398,7 +395,6 @@ class RelationshipCollection(dict): """ def __init__(self, baseURI): - super(RelationshipCollection, self).__init__() self._baseURI = baseURI self._target_parts_by_rId = {} diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 5c22fb2e5..62d811b21 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -14,7 +14,7 @@ Part, PartFactory, _Relationship, - RelationshipCollection, + _Relationships, Unmarshaller, XmlPart, ) @@ -53,13 +53,11 @@ def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, PartFactory_) assert isinstance(pkg, OpcPackage) - def it_initializes_its_rels_collection_on_first_reference( - self, RelationshipCollection_ - ): + def it_initializes_its_rels_collection_on_first_reference(self, _Relationships_): pkg = OpcPackage() rels = pkg.rels - RelationshipCollection_.assert_called_once_with(PACKAGE_URI.baseURI) - assert rels == RelationshipCollection_.return_value + _Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) + assert rels == _Relationships_.return_value def it_can_add_a_relationship_to_a_part(self, pkg_with_rels_, rel_attrs_): reltype, target, rId = rel_attrs_ @@ -248,8 +246,8 @@ def pkg_with_rels_(self, request, rels_): return pkg @pytest.fixture - def RelationshipCollection_(self, request): - return class_mock(request, "pptx.opc.package.RelationshipCollection") + def _Relationships_(self, request): + return class_mock(request, "pptx.opc.package._Relationships") @pytest.fixture def rel_attrs_(self, request): @@ -268,13 +266,13 @@ def rel(self, request, is_external, target_part, name): ) def rels(self, request, values): - rels = instance_mock(request, RelationshipCollection) + rels = instance_mock(request, _Relationships) rels.values.return_value = values return rels @pytest.fixture def rels_(self, request): - return instance_mock(request, RelationshipCollection) + return instance_mock(request, _Relationships) @pytest.fixture def reltype(self, request): @@ -507,7 +505,7 @@ def partname_(self, request): @pytest.fixture def Relationships_(self, request, rels_): return class_mock( - request, "pptx.opc.package.RelationshipCollection", return_value=rels_ + request, "pptx.opc.package._Relationships", return_value=rels_ ) @pytest.fixture @@ -516,7 +514,7 @@ def rel_(self, request, rId_, url_): @pytest.fixture def rels_(self, request, part_, rel_, rId_, related_parts_): - rels_ = instance_mock(request, RelationshipCollection) + rels_ = instance_mock(request, _Relationships) rels_.part_with_reltype.return_value = part_ rels_.get_or_add.return_value = rel_ rels_.get_or_add_ext_rel.return_value = rId_ @@ -654,21 +652,21 @@ def part_args_2_(self, request): return partname_2_, content_type_2_, pkg_2_, blob_2_ -class DescribeRelationshipCollection(object): +class Describe_Relationships(object): """Unit-test suite for `pptx.opc.package._Relationships` objects.""" def it_has_a_len(self): - rels = RelationshipCollection(None) + rels = _Relationships(None) assert len(rels) == 0 def it_has_dict_style_lookup_of_rel_by_rId(self): rel = Mock(name="rel", rId="foobar") - rels = RelationshipCollection(None) + rels = _Relationships(None) rels["foobar"] = rel assert rels["foobar"] == rel def it_should_raise_on_failed_lookup_by_rId(self): - rels = RelationshipCollection(None) + rels = _Relationships(None) with pytest.raises(KeyError): rels["barfoo"] @@ -680,7 +678,7 @@ def it_can_add_a_relationship(self, _Relationship_): "target", False, ) - rels = RelationshipCollection(baseURI) + rels = _Relationships(baseURI) rel = rels.add_relationship(reltype, target, rId, external) _Relationship_.assert_called_once_with(rId, reltype, target, baseURI, external) assert rels[rId] == rel @@ -724,7 +722,7 @@ def it_can_find_a_related_part_by_rId(self, rels_with_known_target_part): def it_raises_KeyError_on_part_with_rId_not_found(self): with pytest.raises(KeyError): - RelationshipCollection(None).related_parts["rId666"] + _Relationships(None).related_parts["rId666"] def it_knows_the_next_available_rId_to_help(self, rels_with_rId_gap): rels, expected_next_rId = rels_with_rId_gap @@ -747,13 +745,13 @@ def it_can_compose_rels_xml(self, rels, rels_elm): @pytest.fixture def add_ext_rel_fixture_(self, reltype, url): - rels = RelationshipCollection(None) + rels = _Relationships(None) return rels, reltype, url @pytest.fixture def add_matching_ext_rel_fixture_(self, request, reltype, url): rId = "rId369" - rels = RelationshipCollection(None) + rels = _Relationships(None) rels.add_relationship(reltype, url, rId, is_external=True) return rels, reltype, url, rId @@ -820,7 +818,7 @@ def rels_with_missing_rel_(self, request, rels, _Relationship_): @pytest.fixture def rels_with_rId_gap(self, request): - rels = RelationshipCollection(None) + rels = _Relationships(None) rel_with_rId1 = instance_mock( request, _Relationship, name="rel_with_rId1", rId="rId1" @@ -853,10 +851,10 @@ def _Relationship_(self, request): @pytest.fixture def rels(self): """ - Populated RelationshipCollection instance that will exercise the + Populated _Relationships instance that will exercise the rels.xml property. """ - rels = RelationshipCollection("/baseURI") + rels = _Relationships("/baseURI") rels.add_relationship( reltype="http://rt-hyperlink", target="http://some/link", diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index a28788126..6412cba22 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -1,13 +1,9 @@ # encoding: utf-8 -""" -Test data for relationship-related unit tests. -""" - -from __future__ import absolute_import +"""Test data for relationship-related unit tests.""" from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.package import RelationshipCollection +from pptx.opc.package import _Relationships from pptx.opc.constants import NAMESPACE as NS from pptx.oxml import parse_xml @@ -29,8 +25,8 @@ def with_indent(self, indent): return self -class RelationshipCollectionBuilder(object): - """Builder class for test RelationshipCollections""" +class RelationshipsBuilder(object): + """Builder class for test _Relationships""" partname_tmpls = { RT.SLIDE_MASTER: "/ppt/slideMasters/slideMaster%d.xml", @@ -61,7 +57,7 @@ def _next_tuple_partname(self, reltype): return partname_tmpl % partnum def build(self): - rels = RelationshipCollection() + rels = _Relationships() for rel in self.relationships: rels.add_rel(rel) return rels @@ -299,7 +295,7 @@ def a_Relationships(): def a_rels(): - return RelationshipCollectionBuilder() + return RelationshipsBuilder() def a_Types(): From f1c3cc0a089c43bf063ac3c36899d59653f4c17e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 15:21:28 -0700 Subject: [PATCH 06/69] rfctr: remove before/after marshalling hooks These were YAGNI from the start, get rid of these to simplify getting rid of Unmarshaller. --- pptx/opc/package.py | 34 ----------------------- tests/opc/test_package.py | 57 +++++++++------------------------------ 2 files changed, 12 insertions(+), 79 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index c7b7db381..f6d101d6b 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -33,15 +33,6 @@ def open(cls, pkg_file): Unmarshaller.unmarshal(pkg_reader, package, PartFactory) return package - def after_unmarshal(self): - """ - Called by loading code after all parts and relationships have been - loaded, to afford the opportunity for any required post-processing. - This one does nothing other than catch the call if a subclass - doesn't. - """ - pass - def iter_parts(self): """Generate exactly one reference to each part in the package.""" @@ -157,8 +148,6 @@ def save(self, pkg_file): `pkg_file` can be either a path to a file (a string) or a file-like object. """ - for part in self.parts: - part.before_marshal() PackageWriter.write(pkg_file, self.rels, self.parts) @@ -186,26 +175,6 @@ def load(cls, partname, content_type, blob, package): """ return cls(partname, content_type, blob, package) - def after_unmarshal(self): - """ - Entry point for post-unmarshaling processing, for example to parse - the part XML. May be overridden by subclasses without forwarding call - to super. - """ - # don't place any code here, just catch call if not overridden by - # subclass - pass - - def before_marshal(self): - """ - Entry point for pre-serialization processing, for example to finalize - part naming if necessary. May be overridden by subclasses without - forwarding call to super. - """ - # don't place any code here, just catch call if not overridden by - # subclass - pass - @property def blob(self): """Contents of this package part as a sequence of bytes. @@ -523,9 +492,6 @@ def unmarshal(pkg_reader, package, part_factory): """ parts = Unmarshaller._unmarshal_parts(pkg_reader, package, part_factory) Unmarshaller._unmarshal_relationships(pkg_reader, package, parts) - for part in parts.values(): - part.after_unmarshal() - package.after_unmarshal() @staticmethod def _unmarshal_parts(pkg_reader, package, part_factory): diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 62d811b21..974c07f61 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -35,6 +35,7 @@ method_mock, Mock, patch, + property_mock, PropertyMock, ) @@ -103,15 +104,17 @@ def it_can_find_the_next_available_vector_partname(self, next_partname_fixture): assert isinstance(partname, PackURI) assert partname == expected_partname - def it_can_save_to_a_pkg_file(self, pkg_file_, PackageWriter_, parts, parts_): - pkg = OpcPackage() - pkg.save(pkg_file_) - for part in parts_: - part.before_marshal.assert_called_once_with() - PackageWriter_.write.assert_called_once_with(pkg_file_, pkg._rels, parts_) + def it_can_save_to_a_pkg_file(self, request, rels_): + PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") + property_mock(request, OpcPackage, "rels", return_value=rels_) + property_mock(request, OpcPackage, "parts", return_value=["parts"]) + package = OpcPackage() + + package.save("prs.pptx") - def it_can_be_notified_after_unmarshalling_is_complete(self, pkg): - pkg.after_unmarshal() + PackageWriter_.write.assert_called_once_with("prs.pptx", rels_, ["parts"]) + + # assert False # fixtures --------------------------------------------- @@ -197,10 +200,6 @@ def iter_parts_(self, request): def PackageReader_(self, request): return class_mock(request, "pptx.opc.package.PackageReader") - @pytest.fixture - def PackageWriter_(self, request): - return class_mock(request, "pptx.opc.package.PackageWriter") - @pytest.fixture def PartFactory_(self, request): return class_mock(request, "pptx.opc.package.PartFactory") @@ -213,32 +212,10 @@ def part_1_(self, request): def part_2_(self, request): return instance_mock(request, Part) - @pytest.fixture - def parts(self, request, parts_): - """ - Return a mock patching property OpcPackage.parts, reversing the - patch after each use. - """ - _patch = patch.object( - OpcPackage, "parts", new_callable=PropertyMock, return_value=parts_ - ) - request.addfinalizer(_patch.stop) - return _patch.start() - - @pytest.fixture - def parts_(self, request): - part_ = instance_mock(request, Part, name="part_") - part_2_ = instance_mock(request, Part, name="part_2_") - return [part_, part_2_] - @pytest.fixture def pkg(self, request): return OpcPackage() - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) - @pytest.fixture def pkg_with_rels_(self, request, rels_): pkg = OpcPackage() @@ -295,12 +272,6 @@ def it_can_be_constructed_by_PartFactory(self, request, package_): _init_.assert_called_once_with(part, partname_, CT.PML_SLIDE, b"blob", package_) assert isinstance(part, Part) - def it_can_be_notified_after_unmarshalling_is_complete(self, part): - part.after_unmarshal() - - def it_can_be_notified_before_marshalling_is_started(self, part): - part.before_marshal() - def it_uses_the_load_blob_as_its_blob(self, blob_fixture): part, load_blob = blob_fixture assert part.blob is load_blob @@ -935,14 +906,10 @@ def it_can_unmarshal_from_a_pkg_reader( _unmarshal_relationships, parts_dict_, ): - # exercise --------------------- Unmarshaller.unmarshal(pkg_reader_, pkg_, part_factory_) - # verify ----------------------- + _unmarshal_parts.assert_called_once_with(pkg_reader_, pkg_, part_factory_) _unmarshal_relationships.assert_called_once_with(pkg_reader_, pkg_, parts_dict_) - for part in parts_dict_.values(): - part.after_unmarshal.assert_called_once_with() - pkg_.after_unmarshal.assert_called_once_with() def it_can_unmarshal_parts( self, From 4f44afbeabb8ed76436dd76fe3e4e09cf59a3ba0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 21 Aug 2021 16:24:58 -0700 Subject: [PATCH 07/69] rfctr: replace Part.related_part() Was `Part.related_parts -> dict`. --- docs/dev/analysis/cht-access-xlsx.rst | 54 --------- pptx/action.py | 2 +- pptx/opc/package.py | 19 +-- pptx/parts/chart.py | 2 +- pptx/parts/presentation.py | 10 +- pptx/parts/slide.py | 4 +- pptx/shapes/graphfrm.py | 4 +- tests/opc/test_package.py | 55 ++++----- tests/parts/test_chart.py | 125 ++++++++------------ tests/parts/test_presentation.py | 159 ++++++++++++-------------- tests/parts/test_slide.py | 65 ++++------- tests/shapes/test_graphfrm.py | 39 +++---- tests/test_action.py | 4 +- 13 files changed, 200 insertions(+), 342 deletions(-) diff --git a/docs/dev/analysis/cht-access-xlsx.rst b/docs/dev/analysis/cht-access-xlsx.rst index 20706b460..646b14285 100644 --- a/docs/dev/analysis/cht-access-xlsx.rst +++ b/docs/dev/analysis/cht-access-xlsx.rst @@ -55,60 +55,6 @@ Workbook chart. Read-only Object. -Code sketches -------------- - -``ChartPart.xlsx_blob = blob``:: - - @xlsx_blob.setter - def xlsx_blob(self, blob): - xlsx_part = self.xlsx_part - if xlsx_part: - xlsx_part.blob = blob - else: - xlsx_part = EmbeddedXlsxPart.new(blob, self.package) - rId = self.relate_to(xlsx_part, RT.PACKAGE) - externalData = self._element.get_or_add_externalData - externalData.rId = rId - -``@classmethod EmbeddedXlsxPart.new(cls, blob, package)``:: - - partname = cls.next_partname(package) - content_type = CT.SML_SHEET - xlsx_part = EmbeddedXlsxPart(partname, content_type, blob, package) - return xlsx_part - - -``ChartPart.add_or_replace_xlsx(xlsx_stream)``:: - - xlsx_part = self.get_or_add_xlsx_part() - xlsx_stream.seek(0) - xlsx_bytes = xlsx_stream.read() - xlsx_part.blob = xlsx_bytes - - -``ChartPart.xlsx_part``:: - - externalData = self._element.externalData - if externalData is None: - raise ValueError("chart has no embedded worksheet") - rId = externalData.rId - xlsx_part = self.related_parts[rId] - return xlsx_part - - # later ... - - xlsx_stream = BytesIO(xlsx_part.blob) - xlsx_package = OpcPackage.open(xlsx_stream) - workbook_part = xlsx_package.main_document - - -* Maybe can implement just a few Excel parts, enough to access and manipulate - the data necessary. Like Workbook (start part I think) and Worksheet. - -* What about linked rather than embedded Worksheet? - - XML specimens ------------- diff --git a/pptx/action.py b/pptx/action.py index ea3cc5c8e..256b3e1e4 100644 --- a/pptx/action.py +++ b/pptx/action.py @@ -114,7 +114,7 @@ def target_slide(self): return self._slides[prev_slide_idx] elif self.action == PP_ACTION.NAMED_SLIDE: rId = self._hlink.rId - return self.part.related_parts[rId].slide + return self.part.related_part(rId).slide @target_slide.setter def target_slide(self, slide): diff --git a/pptx/opc/package.py b/pptx/opc/package.py index f6d101d6b..c13346d9b 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -256,14 +256,9 @@ def relate_to(self, target, reltype, is_external=False): rel = self.rels.get_or_add(reltype, target) return rel.rId - @property - def related_parts(self): - """ - Dictionary mapping related parts by rId, so child objects can resolve - explicit relationships present in the part XML, e.g. sldIdLst to a - specific |Slide| instance. - """ - return self.rels.related_parts + def related_part(self, rId): + """Return related |Part| subtype identified by `rId`.""" + return self.rels[rId].target_part @lazyproperty def rels(self): @@ -408,14 +403,6 @@ def part_with_reltype(self, reltype): rel = self._get_rel_of_type(reltype) return rel.target_part - @property - def related_parts(self): - """ - dict mapping rIds to target parts for all the internal relationships - in the collection. - """ - return self._target_parts_by_rId - @property def xml(self): """bytes XML serialization of this relationship collection. diff --git a/pptx/parts/chart.py b/pptx/parts/chart.py index 0e54674a0..202992233 100644 --- a/pptx/parts/chart.py +++ b/pptx/parts/chart.py @@ -75,7 +75,7 @@ def xlsx_part(self): xlsx_part_rId = self._chartSpace.xlsx_part_rId if xlsx_part_rId is None: return None - return self._chart_part.related_parts[xlsx_part_rId] + return self._chart_part.related_part(xlsx_part_rId) @xlsx_part.setter def xlsx_part(self, xlsx_part): diff --git a/pptx/parts/presentation.py b/pptx/parts/presentation.py index 8e3a9a228..30b4ff016 100644 --- a/pptx/parts/presentation.py +++ b/pptx/parts/presentation.py @@ -42,7 +42,7 @@ def get_slide(self, slide_id): """ for sldId in self._element.sldIdLst: if sldId.id == slide_id: - return self.related_parts[sldId.rId].slide + return self.related_part(sldId.rId).slide return None @lazyproperty @@ -80,11 +80,11 @@ def presentation(self): def related_slide(self, rId): """Return |Slide| object for related |SlidePart| related by `rId`.""" - return self.related_parts[rId].slide + return self.related_part(rId).slide def related_slide_master(self, rId): """Return |SlideMaster| object for |SlideMasterPart| related by `rId`.""" - return self.related_parts[rId].slide_master + return self.related_part(rId).slide_master def rename_slide_parts(self, rIds): """Assign incrementing partnames to the slide parts identified by `rIds`. @@ -95,7 +95,7 @@ def rename_slide_parts(self, rIds): The extension is always ``.xml``. """ for idx, rId in enumerate(rIds): - slide_part = self.related_parts[rId] + slide_part = self.related_part(rId) slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1)) def save(self, path_or_stream): @@ -109,7 +109,7 @@ def save(self, path_or_stream): def slide_id(self, slide_part): """Return the slide-id associated with `slide_part`.""" for sldId in self._element.sldIdLst: - if self.related_parts[sldId.rId] is slide_part: + if self.related_part(sldId.rId) is slide_part: return sldId.id raise ValueError("matching slide_part not found") diff --git a/pptx/parts/slide.py b/pptx/parts/slide.py index faf7be18e..c91b27a35 100644 --- a/pptx/parts/slide.py +++ b/pptx/parts/slide.py @@ -27,7 +27,7 @@ def get_image(self, rId): by *rId*. Raises |KeyError| if no image is related by that id, which would generally indicate a corrupted .pptx file. """ - return self.related_parts[rId].image + return self.related_part(rId).image def get_or_add_image_part(self, image_file): """Return `(image_part, rId)` pair corresponding to `image_file`. @@ -284,7 +284,7 @@ def related_slide_layout(self, rId): Return the |SlideLayout| object of the related |SlideLayoutPart| corresponding to relationship key *rId*. """ - return self.related_parts[rId].slide_layout + return self.related_part(rId).slide_layout @lazyproperty def slide_master(self): diff --git a/pptx/shapes/graphfrm.py b/pptx/shapes/graphfrm.py index f7f817a1e..de317cc5c 100644 --- a/pptx/shapes/graphfrm.py +++ b/pptx/shapes/graphfrm.py @@ -36,7 +36,7 @@ def chart(self): @property def chart_part(self): """The |ChartPart| object containing the chart in this graphic frame.""" - return self.part.related_parts[self._element.chart_rId] + return self.part.related_part(self._element.chart_rId) @property def has_chart(self): @@ -127,7 +127,7 @@ def blob(self): This value is None if the embedded object does not represent a "file". """ - return self.part.related_parts[self._graphicData.blob_rId].blob + return self.part.related_part(self._graphicData.blob_rId).blob @property def prog_id(self): diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 974c07f61..dc1eeada3 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -339,9 +339,18 @@ def it_can_establish_an_external_relationship(self, relate_to_url_fixture): part.rels.get_or_add_ext_rel.assert_called_once_with(reltype_, url_) assert rId is rId_ - def it_can_find_a_related_part_by_rId(self, related_parts_fixture): - part, related_parts_ = related_parts_fixture - assert part.related_parts is related_parts_ + def it_can_find_a_related_part_by_rId( + self, request, rels_prop_, relationships_, relationship_, part_ + ): + relationship_.target_part = part_ + relationships_.__getitem__.return_value = relationship_ + rels_prop_.return_value = relationships_ + part = Part(None, None, None) + + related_part = part.related_part("rId17") + + relationships_.__getitem__.assert_called_once_with("rId17") + assert related_part is part_ def it_provides_access_to_its_relationships(self, rels_fixture): part, Relationships_, partname_, rels_ = rels_fixture @@ -436,11 +445,6 @@ def related_part_fixture(self, request, part, rels_, reltype_, part_): part._rels = rels_ return part, reltype_, part_ - @pytest.fixture - def related_parts_fixture(self, request, part, rels_, related_parts_): - part._rels = rels_ - return part, related_parts_ - @pytest.fixture def rels_fixture(self, Relationships_, partname_, rels_): part = Part(partname_, None) @@ -484,17 +488,24 @@ def rel_(self, request, rId_, url_): return instance_mock(request, _Relationship, rId=rId_, target_ref=url_) @pytest.fixture - def rels_(self, request, part_, rel_, rId_, related_parts_): + def relationship_(self, request): + return instance_mock(request, _Relationship) + + @pytest.fixture + def relationships_(self, request): + return instance_mock(request, _Relationships) + + @pytest.fixture + def rels_(self, request, part_, rel_, rId_): rels_ = instance_mock(request, _Relationships) rels_.part_with_reltype.return_value = part_ rels_.get_or_add.return_value = rel_ rels_.get_or_add_ext_rel.return_value = rId_ - rels_.related_parts = related_parts_ return rels_ @pytest.fixture - def related_parts_(self, request): - return instance_mock(request, dict) + def rels_prop_(self, request): + return property_mock(request, Part, "rels") @pytest.fixture def reltype_(self, request): @@ -686,15 +697,6 @@ def it_can_find_a_related_part_by_reltype(self, rels_with_target_known_by_reltyp part = rels.part_with_reltype(reltype) assert part is known_target_part - def it_can_find_a_related_part_by_rId(self, rels_with_known_target_part): - rels, rId, known_target_part = rels_with_known_target_part - part = rels.related_parts[rId] - assert part is known_target_part - - def it_raises_KeyError_on_part_with_rId_not_found(self): - with pytest.raises(KeyError): - _Relationships(None).related_parts["rId666"] - def it_knows_the_next_available_rId_to_help(self, rels_with_rId_gap): rels, expected_next_rId = rels_with_rId_gap next_rId = rels._next_rId @@ -726,11 +728,6 @@ def add_matching_ext_rel_fixture_(self, request, reltype, url): rels.add_relationship(reltype, url, rId, is_external=True) return rels, reltype, url, rId - @pytest.fixture - def _rel_with_known_target_part(self, _rId, _reltype, _target_part, _baseURI): - rel = _Relationship(_rId, _reltype, _target_part, _baseURI) - return rel, _rId, _target_part - @pytest.fixture def _rel_with_target_known_by_reltype(self, _rId, _reltype, _target_part, _baseURI): rel = _Relationship(_rId, _reltype, _target_part, _baseURI) @@ -751,12 +748,6 @@ def rels_elm(self, request): request.addfinalizer(patch_.stop) return rels_elm - @pytest.fixture - def rels_with_known_target_part(self, rels, _rel_with_known_target_part): - rel, rId, target_part = _rel_with_known_target_part - rels.add_relationship(None, target_part, rId) - return rels, rId, target_part - @pytest.fixture def rels_with_matching_rel_(self, request, rels): matching_reltype_ = instance_mock(request, str, name="matching_reltype_") diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index b3339e861..aaa7e39e7 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -153,23 +153,57 @@ def xlsx_blob_(self, request): class DescribeChartWorkbook(object): """Unit-test suite for `pptx.parts.chart.ChartWorkbook` objects.""" - def it_can_get_the_chart_xlsx_part(self, xlsx_part_get_fixture): - chart_data, expected_object = xlsx_part_get_fixture - assert chart_data.xlsx_part is expected_object + def it_can_get_the_chart_xlsx_part(self, chart_part_, xlsx_part_): + chart_part_.related_part.return_value = xlsx_part_ + chart_workbook = ChartWorkbook( + element("c:chartSpace/c:externalData{r:id=rId42}"), chart_part_ + ) + + xlsx_part = chart_workbook.xlsx_part + + chart_part_.related_part.assert_called_once_with("rId42") + assert xlsx_part is xlsx_part_ + + def but_it_returns_None_when_the_chart_has_no_xlsx_part(self): + chart_workbook = ChartWorkbook(element("c:chartSpace"), None) + assert chart_workbook.xlsx_part is None + + @pytest.mark.parametrize( + "chartSpace_cxml, expected_cxml", + ( + ( + "c:chartSpace{r:a=b}", + "c:chartSpace{r:a=b}/c:externalData{r:id=rId" "42}/c:autoUpdate{val=0}", + ), + ( + "c:chartSpace/c:externalData{r:id=rId66}", + "c:chartSpace/c:externalData{r:id=rId42}", + ), + ), + ) + def it_can_change_the_chart_xlsx_part( + self, chart_part_, xlsx_part_, chartSpace_cxml, expected_cxml + ): + chart_part_.relate_to.return_value = "rId42" + chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - def it_can_change_the_chart_xlsx_part(self, xlsx_part_set_fixture): - chart_data, xlsx_part_, expected_xml = xlsx_part_set_fixture chart_data.xlsx_part = xlsx_part_ - chart_data._chart_part.relate_to.assert_called_once_with(xlsx_part_, RT.PACKAGE) - assert chart_data._chartSpace.xml == expected_xml - def it_adds_an_xlsx_part_on_update_if_needed(self, add_part_fixture): - chart_data, xlsx_blob_, EmbeddedXlsxPart_ = add_part_fixture[:3] - package_, xlsx_part_prop_, xlsx_part_ = add_part_fixture[3:] + chart_part_.relate_to.assert_called_once_with(xlsx_part_, RT.PACKAGE) + assert chart_data._chartSpace.xml == xml(expected_cxml) - chart_data.update_from_xlsx_blob(xlsx_blob_) + def it_adds_an_xlsx_part_on_update_if_needed( + self, request, chart_part_, package_, xlsx_part_, xlsx_part_prop_ + ): + EmbeddedXlsxPart_ = class_mock(request, "pptx.parts.chart.EmbeddedXlsxPart") + EmbeddedXlsxPart_.new.return_value = xlsx_part_ + chart_part_.package = package_ + xlsx_part_prop_.return_value = None + chart_data = ChartWorkbook(element("c:chartSpace"), chart_part_) - EmbeddedXlsxPart_.new.assert_called_once_with(xlsx_blob_, package_) + chart_data.update_from_xlsx_blob(b"xlsx-blob") + + EmbeddedXlsxPart_.new.assert_called_once_with(b"xlsx-blob", package_) xlsx_part_prop_.assert_called_with(xlsx_part_) def but_replaces_xlsx_blob_when_part_exists(self, update_blob_fixture): @@ -179,79 +213,16 @@ def but_replaces_xlsx_blob_when_part_exists(self, update_blob_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def add_part_fixture( - self, - request, - chart_part_, - xlsx_blob_, - EmbeddedXlsxPart_, - package_, - xlsx_part_, - xlsx_part_prop_, - ): - chartSpace_cxml = "c:chartSpace" - chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - xlsx_part_prop_.return_value = None - return ( - chart_data, - xlsx_blob_, - EmbeddedXlsxPart_, - package_, - xlsx_part_prop_, - xlsx_part_, - ) - @pytest.fixture def update_blob_fixture(self, request, xlsx_blob_, xlsx_part_prop_): chart_data = ChartWorkbook(None, None) return chart_data, xlsx_blob_ - @pytest.fixture( - params=[ - ("c:chartSpace", None), - ("c:chartSpace/c:externalData{r:id=rId42}", "rId42"), - ] - ) - def xlsx_part_get_fixture(self, request, chart_part_, xlsx_part_): - chartSpace_cxml, xlsx_part_rId = request.param - chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - expected_object = xlsx_part_ if xlsx_part_rId else None - return chart_data, expected_object - - @pytest.fixture( - params=[ - ( - "c:chartSpace{r:a=b}", - "c:chartSpace{r:a=b}/c:externalData{r:id=rId" "42}/c:autoUpdate{val=0}", - ), - ( - "c:chartSpace/c:externalData{r:id=rId66}", - "c:chartSpace/c:externalData{r:id=rId42}", - ), - ] - ) - def xlsx_part_set_fixture(self, request, chart_part_, xlsx_part_): - chartSpace_cxml, expected_cxml = request.param - chart_data = ChartWorkbook(element(chartSpace_cxml), chart_part_) - expected_xml = xml(expected_cxml) - return chart_data, xlsx_part_, expected_xml - # fixture components --------------------------------------------- @pytest.fixture - def chart_part_(self, request, package_, xlsx_part_): - chart_part_ = instance_mock(request, ChartPart) - chart_part_.package = package_ - chart_part_.related_parts = {"rId42": xlsx_part_} - chart_part_.relate_to.return_value = "rId42" - return chart_part_ - - @pytest.fixture - def EmbeddedXlsxPart_(self, request, xlsx_part_): - EmbeddedXlsxPart_ = class_mock(request, "pptx.parts.chart.EmbeddedXlsxPart") - EmbeddedXlsxPart_.new.return_value = xlsx_part_ - return EmbeddedXlsxPart_ + def chart_part_(self, request): + return instance_mock(request, ChartPart) @pytest.fixture def package_(self, request): diff --git a/tests/parts/test_presentation.py b/tests/parts/test_presentation.py index 4d820e397..3d42788d2 100644 --- a/tests/parts/test_presentation.py +++ b/tests/parts/test_presentation.py @@ -9,7 +9,7 @@ from pptx.package import Package from pptx.parts.coreprops import CorePropertiesPart from pptx.parts.presentation import PresentationPart -from pptx.parts.slide import NotesMasterPart, SlidePart +from pptx.parts.slide import NotesMasterPart, SlideMasterPart, SlidePart from pptx.presentation import Presentation from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster @@ -63,24 +63,42 @@ def it_provides_access_to_its_notes_master(self, notes_master_fixture): notes_master = prs_part.notes_master assert notes_master is notes_master_ - def it_provides_access_to_a_related_slide(self, slide_fixture): - prs_part, rId, slide_ = slide_fixture - slide = prs_part.related_slide(rId) - prs_part.related_parts.__getitem__.assert_called_once_with(rId) + def it_provides_access_to_a_related_slide(self, request, slide_, related_part_): + slide_part_ = instance_mock(request, SlidePart, slide=slide_) + related_part_.return_value = slide_part_ + prs_part = PresentationPart(None, None, None, None) + + slide = prs_part.related_slide("rId42") + + related_part_.assert_called_once_with(prs_part, "rId42") assert slide is slide_ - def it_provides_access_to_a_related_master(self, master_fixture): - prs_part, rId, slide_master_ = master_fixture - slide_master = prs_part.related_slide_master(rId) - prs_part.related_parts.__getitem__.assert_called_once_with(rId) + 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) + + slide_master = prs_part.related_slide_master("rId42") + + related_part_.assert_called_once_with(prs_part, "rId42") assert slide_master is slide_master_ - def it_can_rename_related_slide_parts(self, rename_fixture): - prs_part, rIds, getitem_ = rename_fixture[:3] - calls, slide_parts, expected_names = rename_fixture[3:] + def it_can_rename_related_slide_parts(self, request, related_part_): + rIds = tuple("rId%d" % n for n in range(5, 0, -1)) + slide_parts = tuple(instance_mock(request, SlidePart) for _ in range(5)) + related_part_.side_effect = iter(slide_parts) + prs_part = PresentationPart(None, None, None, None) + prs_part.rename_slide_parts(rIds) - assert getitem_.call_args_list == calls - assert [sp.partname for sp in slide_parts] == expected_names + + assert related_part_.call_args_list == [call(prs_part, rId) for rId in rIds] + assert [s.partname for s in slide_parts] == [ + PackURI("/ppt/slides/slide%d.xml" % (i + 1)) for i in range(len(rIds)) + ] def it_can_save_the_package_to_a_file(self, save_fixture): prs_part, file_, package_ = save_fixture @@ -99,19 +117,49 @@ def it_can_add_a_new_slide(self, add_slide_fixture): assert rId is rId_ assert slide is slide_ - def it_finds_the_slide_id_of_a_slide_part(self, slide_id_fixture): - prs_part, slide_part_, expected_value = slide_id_fixture + def it_finds_the_slide_id_of_a_slide_part(self, slide_part_, related_part_): + prs_elm = element( + "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" + "b,id=257},p:sldId{r:id=c,id=258})" + ) + related_part_.side_effect = iter((None, slide_part_, None)) + prs_part = PresentationPart(None, None, prs_elm, None) + _slide_id = prs_part.slide_id(slide_part_) - assert _slide_id == expected_value - def it_raises_on_slide_id_not_found(self, slide_id_raises_fixture): - prs_part, slide_part_ = slide_id_raises_fixture + assert related_part_.call_args_list == [ + call(prs_part, "a"), + call(prs_part, "b"), + ] + assert _slide_id == 257 + + def it_raises_on_slide_id_not_found(self, slide_part_, related_part_): + prs_elm = element( + "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" + "b,id=257},p:sldId{r:id=c,id=258})" + ) + related_part_.return_value = "not the slide you're looking for" + prs_part = PresentationPart(None, None, prs_elm, None) + with pytest.raises(ValueError): prs_part.slide_id(slide_part_) - def it_finds_a_slide_by_slide_id(self, get_slide_fixture): - prs_part, slide_id, expected_value = get_slide_fixture + @pytest.mark.parametrize("is_present", (True, False)) + def it_finds_a_slide_by_slide_id( + self, is_present, slide_, slide_part_, related_part_ + ): + prs_elm = element( + "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" + "b,id=257},p:sldId{r:id=c,id=258})" + ) + slide_id = 257 if is_present else 666 + expected_value = slide_ if is_present else None + related_part_.return_value = slide_part_ + slide_part_.slide = slide_ + prs_part = PresentationPart(None, None, prs_elm, None) + slide = prs_part.get_slide(slide_id) + assert slide == expected_value def it_knows_the_next_slide_partname_to_help(self, next_fixture): @@ -156,28 +204,6 @@ def core_props_fixture(self, package_, core_properties_): package_.core_properties = core_properties_ return prs_part, core_properties_ - @pytest.fixture(params=[True, False]) - def get_slide_fixture(self, request, slide_, slide_part_, related_parts_prop_): - is_present = request.param - prs_elm = element( - "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" - "b,id=257},p:sldId{r:id=c,id=258})" - ) - prs_part = PresentationPart(None, None, prs_elm) - slide_id = 257 if is_present else 666 - expected_value = slide_ if is_present else None - related_parts_prop_.return_value = {"a": None, "b": slide_part_, "c": None} - slide_part_.slide = slide_ - return prs_part, slide_id, expected_value - - @pytest.fixture - def master_fixture(self, slide_master_, related_parts_prop_): - prs_part = PresentationPart(None, None, None, None) - rId = "rId42" - related_parts_ = related_parts_prop_.return_value - related_parts_.__getitem__.return_value.slide_master = slide_master_ - return prs_part, rId, slide_master_ - @pytest.fixture def next_fixture(self): prs_elm = element("p:presentation/p:sldIdLst/(p:sldId,p:sldId)") @@ -220,55 +246,12 @@ def prs_fixture(self, Presentation_, prs_): prs_part = PresentationPart(None, None, prs_elm) return prs_part, Presentation_, prs_elm, prs_ - @pytest.fixture - def rename_fixture(self, related_parts_prop_): - prs_part = PresentationPart(None, None, None) - rIds = ("rId1", "rId2") - getitem_ = related_parts_prop_.return_value.__getitem__ - calls = [call("rId1"), call("rId2")] - slide_parts = [SlidePart(None, None, None), SlidePart(None, None, None)] - expected_names = [ - PackURI("/ppt/slides/slide1.xml"), - PackURI("/ppt/slides/slide2.xml"), - ] - getitem_.side_effect = slide_parts - return (prs_part, rIds, getitem_, calls, slide_parts, expected_names) - @pytest.fixture def save_fixture(self, package_): prs_part = PresentationPart(None, None, None, package_) file_ = "foobar.docx" return prs_part, file_, package_ - @pytest.fixture - def slide_fixture(self, slide_, related_parts_prop_): - prs_part = PresentationPart(None, None, None, None) - rId = "rId42" - related_parts_ = related_parts_prop_.return_value - related_parts_.__getitem__.return_value.slide = slide_ - return prs_part, rId, slide_ - - @pytest.fixture - def slide_id_fixture(self, slide_part_, related_parts_prop_): - prs_elm = element( - "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" - "b,id=257},p:sldId{r:id=c,id=258})" - ) - prs_part = PresentationPart(None, None, prs_elm) - expected_value = 257 - related_parts_prop_.return_value = {"a": None, "b": slide_part_, "c": None} - return prs_part, slide_part_, expected_value - - @pytest.fixture - def slide_id_raises_fixture(self, slide_part_, related_parts_prop_): - prs_elm = element( - "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" - "b,id=257},p:sldId{r:id=c,id=258})" - ) - prs_part = PresentationPart(None, None, prs_elm) - related_parts_prop_.return_value = {"a": None, "b": None, "c": None} - return prs_part, slide_part_ - # fixture components --------------------------------------------- @pytest.fixture @@ -318,8 +301,8 @@ def relate_to_(self, request): return method_mock(request, PresentationPart, "relate_to", autospec=True) @pytest.fixture - def related_parts_prop_(self, request): - return property_mock(request, PresentationPart, "related_parts") + def related_part_(self, request): + return method_mock(request, PresentationPart, "related_part") @pytest.fixture def slide_(self, request): diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index 8450e8912..099fde94f 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -37,7 +37,6 @@ initializer_mock, instance_mock, method_mock, - property_mock, ) @@ -48,9 +47,18 @@ def it_knows_its_name(self, name_fixture): base_slide, expected_value = name_fixture assert base_slide.name == expected_value - def it_can_get_a_related_image_by_rId(self, get_image_fixture): - slide, rId, image_ = get_image_fixture - assert slide.get_image(rId) is image_ + def it_can_get_a_related_image_by_rId(self, request, image_part_): + image_ = instance_mock(request, Image) + image_part_.image = image_ + related_part_ = method_mock( + request, BaseSlidePart, "related_part", return_value=image_part_ + ) + slide_part = BaseSlidePart(None, None, None, None) + + image = slide_part.get_image("rId42") + + related_part_.assert_called_once_with(slide_part, "rId42") + assert image is image_ def it_can_add_an_image_part(self, request, image_part_): package_ = instance_mock(request, Package) @@ -69,14 +77,6 @@ def it_can_add_an_image_part(self, request, image_part_): # fixtures ------------------------------------------------------- - @pytest.fixture - def get_image_fixture(self, related_parts_prop_, image_part_, image_): - slide = BaseSlidePart(None, None, None, None) - rId = "rId42" - related_parts_prop_.return_value = {rId: image_part_} - image_part_.image = image_ - return slide, rId, image_ - @pytest.fixture def name_fixture(self): sld_cxml, expected_value = "p:sld/p:cSld{name=Foobar}", "Foobar" @@ -86,18 +86,10 @@ def name_fixture(self): # fixture components --------------------------------------------- - @pytest.fixture - def image_(self, request): - return instance_mock(request, Image) - @pytest.fixture def image_part_(self, request): return instance_mock(request, ImagePart) - @pytest.fixture - def related_parts_prop_(self, request): - return property_mock(request, BaseSlidePart, "related_parts") - class DescribeNotesMasterPart(object): """Unit-test suite for `pptx.parts.slide.NotesMasterPart` objects.""" @@ -726,10 +718,19 @@ def it_provides_access_to_its_slide_master(self, master_fixture): SlideMaster_.assert_called_once_with(sldMaster, slide_master_part) assert slide_master is slide_master_ - def it_provides_access_to_a_related_slide_layout(self, related_fixture): - slide_master_part, rId, getitem_, slide_layout_ = related_fixture - slide_layout = slide_master_part.related_slide_layout(rId) - getitem_.assert_called_once_with(rId) + def it_provides_access_to_a_related_slide_layout(self, request): + slide_layout_ = instance_mock(request, SlideLayout) + slide_layout_part_ = instance_mock( + request, SlideLayoutPart, slide_layout=slide_layout_ + ) + related_part_ = method_mock( + request, SlideMasterPart, "related_part", return_value=slide_layout_part_ + ) + slide_master_part = SlideMasterPart(None, None, None, None) + + slide_layout = slide_master_part.related_slide_layout("rId42") + + related_part_.assert_called_once_with(slide_master_part, "rId42") assert slide_layout is slide_layout_ # fixtures ------------------------------------------------------- @@ -740,24 +741,8 @@ def master_fixture(self, SlideMaster_, slide_master_): slide_master_part = SlideMasterPart(None, None, sldMaster) return slide_master_part, SlideMaster_, sldMaster, slide_master_ - @pytest.fixture - def related_fixture(self, slide_layout_, related_parts_prop_): - slide_master_part = SlideMasterPart(None, None, None, None) - rId = "rId42" - getitem_ = related_parts_prop_.return_value.__getitem__ - getitem_.return_value.slide_layout = slide_layout_ - return slide_master_part, rId, getitem_, slide_layout_ - # fixture components --------------------------------------------- - @pytest.fixture - def related_parts_prop_(self, request): - return property_mock(request, SlideMasterPart, "related_parts") - - @pytest.fixture - def slide_layout_(self, request): - return instance_mock(request, SlideLayout) - @pytest.fixture def SlideMaster_(self, request, slide_master_): return class_mock( diff --git a/tests/shapes/test_graphfrm.py b/tests/shapes/test_graphfrm.py index 0b19e3bad..5f2250111 100644 --- a/tests/shapes/test_graphfrm.py +++ b/tests/shapes/test_graphfrm.py @@ -41,20 +41,17 @@ def but_it_raises_on_chart_if_there_isnt_one(self, has_chart_prop_): assert str(e.value) == "shape does not contain a chart" def it_provides_access_to_its_chart_part(self, request, chart_part_): - graphicFrame = element( - "p:graphicFrame/a:graphic/a:graphicData/c:chart{r:id=rId42}" - ) - property_mock( - request, - GraphicFrame, - "part", - return_value=instance_mock( - request, SlidePart, related_parts={"rId42": chart_part_} - ), + slide_part_ = instance_mock(request, SlidePart) + slide_part_.related_part.return_value = chart_part_ + property_mock(request, GraphicFrame, "part", return_value=slide_part_) + graphic_frame = GraphicFrame( + element("p:graphicFrame/a:graphic/a:graphicData/c:chart{r:id=rId42}"), None ) - graphic_frame = GraphicFrame(graphicFrame, None) - assert graphic_frame.chart_part is chart_part_ + chart_part = graphic_frame.chart_part + + slide_part_.related_part.assert_called_once_with("rId42") + assert chart_part is chart_part_ @pytest.mark.parametrize( "graphicData_uri, expected_value", @@ -159,17 +156,15 @@ class Describe_OleFormat(object): def it_provides_access_to_the_OLE_object_blob(self, request): ole_obj_part_ = instance_mock(request, EmbeddedPackagePart, blob=b"0123456789") - property_mock( - request, - _OleFormat, - "part", - return_value=instance_mock( - request, SlidePart, related_parts={"rId7": ole_obj_part_} - ), - ) - graphicData = element("a:graphicData/p:oleObj{r:id=rId7}") + slide_part_ = instance_mock(request, SlidePart) + slide_part_.related_part.return_value = ole_obj_part_ + property_mock(request, _OleFormat, "part", return_value=slide_part_) + ole_format = _OleFormat(element("a:graphicData/p:oleObj{r:id=rId7}"), None) + + blob = ole_format.blob - assert _OleFormat(graphicData, None).blob == b"0123456789" + slide_part_.related_part.assert_called_once_with("rId7") + assert blob == b"0123456789" def it_knows_the_OLE_object_prog_id(self): graphicData = element("a:graphicData/p:oleObj{progId=Excel.Sheet.12}") diff --git a/tests/test_action.py b/tests/test_action.py index 073d0df79..f11eebc73 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -201,8 +201,8 @@ def target_get_fixture(self, request, action_prop_, _slide_index_prop_, part_pro # this becomes the return value of ActionSetting._slides prs_part_ = part_prop_.return_value.package.presentation_part prs_part_.presentation.slides = [0, 1, 2, 3, 4, 5] - related_parts_ = part_prop_.return_value.related_parts - related_parts_.__getitem__.return_value.slide = 4 + related_part_ = part_prop_.return_value.related_part + related_part_.return_value.slide = 4 return action_setting, expected_value @pytest.fixture(params=[(PP_ACTION.NEXT_SLIDE, 2), (PP_ACTION.PREVIOUS_SLIDE, 0)]) From 8c5a96b1a707c24b2c2b95c11d90dd29e25d07fe Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Aug 2021 17:36:45 -0700 Subject: [PATCH 08/69] rfctr: upgrade lazyproperty and remove __slots__ Implement @lazyproperty as a descriptor rather than a "hidden" instance variable like `._propname`. This invalidates a lot of the __slots__ defined to accommodate those hidden instance variables and since __slots__ was really a mis-step, just get rid of all of them. Fix instances that took advantage of the presence of that hidden `._propname` instance variable, particularly very old tests. --- pptx/chart/axis.py | 4 - pptx/chart/chart.py | 2 - pptx/chart/data.py | 27 +++---- pptx/chart/marker.py | 2 - pptx/dml/chtfmt.py | 2 - pptx/dml/fill.py | 2 - pptx/opc/package.py | 17 ++-- pptx/presentation.py | 21 ++--- pptx/shapes/shapetree.py | 2 - pptx/shared.py | 6 -- pptx/slide.py | 20 ----- pptx/util.py | 131 ++++++++++++++++++++++++++----- tests/chart/test_chart.py | 20 ++--- tests/opc/test_package.py | 161 +++++++++++++++++--------------------- tests/test_shared.py | 13 +-- tests/test_table.py | 58 +++++++------- 16 files changed, 255 insertions(+), 233 deletions(-) diff --git a/pptx/chart/axis.py b/pptx/chart/axis.py index 465676d96..4285c36f4 100644 --- a/pptx/chart/axis.py +++ b/pptx/chart/axis.py @@ -230,8 +230,6 @@ def visible(self, value): class AxisTitle(ElementProxy): """Provides properties for manipulating axis title.""" - __slots__ = ("_title", "_format") - def __init__(self, title): super(AxisTitle, self).__init__(title) self._title = title @@ -314,8 +312,6 @@ class MajorGridlines(ElementProxy): axis. """ - __slots__ = ("_xAx", "_format") - def __init__(self, xAx): super(MajorGridlines, self).__init__(xAx) self._xAx = xAx # axis element, catAx or valAx diff --git a/pptx/chart/chart.py b/pptx/chart/chart.py index be7edb16a..14500a418 100644 --- a/pptx/chart/chart.py +++ b/pptx/chart/chart.py @@ -211,8 +211,6 @@ class ChartTitle(ElementProxy): # actually differ in certain fuller behaviors, but at present they're # essentially identical. - __slots__ = ("_title", "_format") - def __init__(self, title): super(ChartTitle, self).__init__(title) self._title = title diff --git a/pptx/chart/data.py b/pptx/chart/data.py index c95adffbe..35e2e6b64 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -ChartData and related objects. -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""ChartData and related objects.""" import datetime from numbers import Number @@ -298,19 +294,20 @@ def add_series(self, name, values=(), number_format=None): series_data.add_data_point(value) return series_data - @lazyproperty + @property def categories(self): - """ - A |data.Categories| object providing access to the hierarchy of - category objects for this chart data. Assigning an iterable of - category labels (strings, numbers, or dates) replaces the - |data.Categories| object with a new one containing a category for - each label in the sequence. + """|data.Categories| object providing access to category-object hierarchy. - Creating a chart from chart data having date categories will cause - the chart to have a |DateAxis| for its category axis. + Assigning an iterable of category labels (strings, numbers, or dates) replaces + the |data.Categories| object with a new one containing a category for each label + in the sequence. + + Creating a chart from chart data having date categories will cause the chart to + have a |DateAxis| for its category axis. """ - return Categories() + if not getattr(self, "_categories", False): + self._categories = Categories() + return self._categories @categories.setter def categories(self, category_labels): diff --git a/pptx/chart/marker.py b/pptx/chart/marker.py index 738083cf2..22dc0ff19 100644 --- a/pptx/chart/marker.py +++ b/pptx/chart/marker.py @@ -18,8 +18,6 @@ class Marker(ElementProxy): a line-type chart. """ - __slots__ = ("_format",) - @lazyproperty def format(self): """ diff --git a/pptx/dml/chtfmt.py b/pptx/dml/chtfmt.py index ed03ad783..dcecb63ab 100644 --- a/pptx/dml/chtfmt.py +++ b/pptx/dml/chtfmt.py @@ -23,8 +23,6 @@ class ChartFormat(ElementProxy): provided by the :attr:`format` property on the target axis, series, etc. """ - __slots__ = ("_fill", "_line") - @lazyproperty def fill(self): """ diff --git a/pptx/dml/fill.py b/pptx/dml/fill.py index ddc5ef963..e84bea9c6 100644 --- a/pptx/dml/fill.py +++ b/pptx/dml/fill.py @@ -375,8 +375,6 @@ class _GradientStop(ElementProxy): A gradient stop defines a color and a position. """ - __slots__ = ("_gs", "_color") - def __init__(self, gs): super(_GradientStop, self).__init__(gs) self._gs = gs diff --git a/pptx/opc/package.py b/pptx/opc/package.py index c13346d9b..3a11aa56b 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -206,7 +206,7 @@ def drop_rel(self, rId): only XML parts can drop relationships. """ if self._rel_ref_count(rId) < 2: - del self.rels[rId] + self._rels.pop(rId) def load_rel(self, reltype, target, rId, is_external=False): """ @@ -217,7 +217,7 @@ def load_rel(self, reltype, target, rId, is_external=False): methods exist for adding a new relationship to a part when manipulating a part. """ - return self.rels.add_relationship(reltype, target, rId, is_external) + return self._rels.add_relationship(reltype, target, rId, is_external) @property def package(self): @@ -230,7 +230,7 @@ def part_related_by(self, reltype): Raises |KeyError| if no such relationship is found and |ValueError| if more than one such relationship is found. """ - return self.rels.part_with_reltype(reltype) + return self._rels.part_with_reltype(reltype) @property def partname(self): @@ -258,12 +258,12 @@ def relate_to(self, target, reltype, is_external=False): def related_part(self, rId): """Return related |Part| subtype identified by `rId`.""" - return self.rels[rId].target_part + return self._rels[rId].target_part @lazyproperty def rels(self): - """|Relationships| object containing relationships from this part to others.""" - return _Relationships(self._partname.baseURI) + """|Relationships| collection of relationships from this part to other parts.""" + return self._rels def target_ref(self, rId): """Return URL contained in target ref of relationship identified by `rId`.""" @@ -288,6 +288,11 @@ def _rel_ref_count(self, rId): rIds = self._element.xpath("//@r:id") return len([_rId for _rId in rIds if _rId == rId]) + @lazyproperty + def _rels(self): + """|Relationships| collection of relationships from this part to other parts.""" + return _Relationships(self._partname.baseURI) + class XmlPart(Part): """Base class for package parts containing an XML payload, which is most of them. diff --git a/pptx/presentation.py b/pptx/presentation.py index 4f01e71bc..eabcda72c 100644 --- a/pptx/presentation.py +++ b/pptx/presentation.py @@ -1,23 +1,18 @@ # encoding: utf-8 -""" -Main presentation object. -""" +"""Main presentation object.""" -from __future__ import absolute_import, division, print_function, unicode_literals - -from .shared import PartElementProxy -from .slide import SlideMasters, Slides -from .util import lazyproperty +from pptx.shared import PartElementProxy +from pptx.slide import SlideMasters, Slides +from pptx.util import lazyproperty class Presentation(PartElementProxy): - """ - PresentationML (PML) presentation. Not intended to be constructed - directly. Use :func:`pptx.Presentation` to open or create a presentation. - """ + """PresentationML (PML) presentation. - __slots__ = ("_slide_masters", "_slides") + Not intended to be constructed directly. Use :func:`pptx.Presentation` to open or + create a presentation. + """ @property def core_properties(self): diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index 3153b51d3..e4eaf55af 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -753,8 +753,6 @@ class SlidePlaceholders(ParentedElementProxy): placeholders it contains. """ - __slots__ = () - def __getitem__(self, idx): """ Access placeholder shape having *idx*. Note that while this looks diff --git a/pptx/shared.py b/pptx/shared.py index 899438297..32b529d69 100644 --- a/pptx/shared.py +++ b/pptx/shared.py @@ -13,8 +13,6 @@ class ElementProxy(object): type of class in python-pptx other than custom element (oxml) classes. """ - __slots__ = ("_element",) - def __init__(self, element): self._element = element @@ -51,8 +49,6 @@ class ParentedElementProxy(ElementProxy): attribute to subclasses and the public :attr:`parent` read-only property. """ - __slots__ = ("_parent",) - def __init__(self, element, parent): super(ParentedElementProxy, self).__init__(element) self._parent = parent @@ -79,8 +75,6 @@ class PartElementProxy(ElementProxy): a part such as `p:sld`. """ - __slots__ = ("_part",) - def __init__(self, element, part): super(PartElementProxy, self).__init__(element) self._part = part diff --git a/pptx/slide.py b/pptx/slide.py index ea1a3622f..9b93666c6 100644 --- a/pptx/slide.py +++ b/pptx/slide.py @@ -21,8 +21,6 @@ class _BaseSlide(PartElementProxy): """Base class for slide objects, including masters, layouts and notes.""" - __slots__ = ("_background",) - @lazyproperty def background(self): """|_Background| object providing slide background properties. @@ -56,8 +54,6 @@ class _BaseMaster(_BaseSlide): Provides access to placeholders and regular shapes. """ - __slots__ = ("_placeholders", "_shapes") - @lazyproperty def placeholders(self): """ @@ -81,8 +77,6 @@ class NotesMaster(_BaseMaster): most commonly used of which are placeholders. """ - __slots__ = () - class NotesSlide(_BaseSlide): """Notes slide object. @@ -91,8 +85,6 @@ class NotesSlide(_BaseSlide): page. """ - __slots__ = ("_placeholders", "_shapes") - def clone_master_placeholders(self, notes_master): """Selectively add placeholder shape elements from *notes_master*. @@ -167,8 +159,6 @@ def shapes(self): class Slide(_BaseSlide): """Slide object. Provides access to shapes and slide-level properties.""" - __slots__ = ("_placeholders", "_shapes") - @property def background(self): """|_Background| object providing slide background properties. @@ -320,8 +310,6 @@ class SlideLayout(_BaseSlide): slide layout-level properties. """ - __slots__ = ("_placeholders", "_shapes") - def iter_cloneable_placeholders(self): """ Generate a reference to each layout placeholder on this slide layout @@ -374,8 +362,6 @@ class SlideLayouts(ParentedElementProxy): Supports indexed access, len(), iteration, index() and remove(). """ - __slots__ = ("_sldLayoutIdLst",) - def __init__(self, sldLayoutIdLst, parent): super(SlideLayouts, self).__init__(sldLayoutIdLst, parent) self._sldLayoutIdLst = sldLayoutIdLst @@ -452,8 +438,6 @@ class SlideMaster(_BaseMaster): inherited from |_BaseMaster|. """ - __slots__ = ("_slide_layouts",) - @lazyproperty def slide_layouts(self): """|SlideLayouts| object providing access to this slide-master's layouts.""" @@ -466,8 +450,6 @@ class SlideMasters(ParentedElementProxy): Has list access semantics, supporting indexed access, len(), and iteration. """ - __slots__ = ("_sldMasterIdLst",) - def __init__(self, sldMasterIdLst, parent): super(SlideMasters, self).__init__(sldMasterIdLst, parent) self._sldMasterIdLst = sldMasterIdLst @@ -505,8 +487,6 @@ class _Background(ElementProxy): has a |_Background| object. """ - __slots__ = ("_cSld", "_fill") - def __init__(self, cSld): super(_Background, self).__init__(cSld) self._cSld = cSld diff --git a/pptx/util.py b/pptx/util.py index 76e39000a..77deabd3a 100644 --- a/pptx/util.py +++ b/pptx/util.py @@ -2,7 +2,9 @@ """Utility functions and classes.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import division + +import functools class Length(int): @@ -123,21 +125,114 @@ def __new__(cls, points): return Length.__new__(cls, emu) -def lazyproperty(f): - """ - @lazyprop decorator. Decorated method will be called only on first access - to calculate a cached property value. After that, the cached value is - returned. +class lazyproperty(object): + """Decorator like @property, but evaluated only on first access. + + Like @property, this can only be used to decorate methods having only + a `self` parameter, and is accessed like an attribute on an instance, + i.e. trailing parentheses are not used. Unlike @property, the decorated + method is only evaluated on first access; the resulting value is cached + and that same value returned on second and later access without + re-evaluation of the method. + + Like @property, this class produces a *data descriptor* object, which is + stored in the __dict__ of the *class* under the name of the decorated + method ('fget' nominally). The cached value is stored in the __dict__ of + the *instance* under that same name. + + Because it is a data descriptor (as opposed to a *non-data descriptor*), + its `__get__()` method is executed on each access of the decorated + attribute; the __dict__ item of the same name is "shadowed" by the + descriptor. + + While this may represent a performance improvement over a property, its + greater benefit may be its other characteristics. One common use is to + construct collaborator objects, removing that "real work" from the + constructor, while still only executing once. It also de-couples client + code from any sequencing considerations; if it's accessed from more than + one location, it's assured it will be ready whenever needed. + + Loosely based on: https://stackoverflow.com/a/6849299/1902513. + + A lazyproperty is read-only. There is no counterpart to the optional + "setter" (or deleter) behavior of an @property. This is critically + important to maintaining its immutability and idempotence guarantees. + Attempting to assign to a lazyproperty raises AttributeError + unconditionally. + + The parameter names in the methods below correspond to this usage + example:: + + class Obj(object) + + @lazyproperty + def fget(self): + return 'some result' + + obj = Obj() + + Not suitable for wrapping a function (as opposed to a method) because it + is not callable. """ - cache_attr_name = "_%s" % f.__name__ # like '_foobar' for prop 'foobar' - docstring = f.__doc__ - - def get_prop_value(obj): - try: - return getattr(obj, cache_attr_name) - except AttributeError: - value = f(obj) - setattr(obj, cache_attr_name, value) - return value - - return property(get_prop_value, doc=docstring) + + def __init__(self, fget): + """*fget* is the decorated method (a "getter" function). + + A lazyproperty is read-only, so there is only an *fget* function (a + regular @property can also have an fset and fdel function). This name + was chosen for consistency with Python's `property` class which uses + this name for the corresponding parameter. + """ + # ---maintain a reference to the wrapped getter method + self._fget = fget + # ---adopt fget's __name__, __doc__, and other attributes + functools.update_wrapper(self, fget) + + def __get__(self, obj, type=None): + """Called on each access of 'fget' attribute on class or instance. + + *self* is this instance of a lazyproperty descriptor "wrapping" the + property method it decorates (`fget`, nominally). + + *obj* is the "host" object instance when the attribute is accessed + from an object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None + when accessed on the class, e.g. `Obj.fget`. + + *type* is the class hosting the decorated getter method (`fget`) on + both class and instance attribute access. + """ + # ---when accessed on class, e.g. Obj.fget, just return this + # ---descriptor instance (patched above to look like fget). + if obj is None: + return self + + # ---when accessed on instance, start by checking instance __dict__ + 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) + value = self._fget(obj) + obj.__dict__[self.__name__] = value + return value + + def __set__(self, obj, value): + """Raises unconditionally, to preserve read-only behavior. + + This decorator is intended to implement immutable (and idempotent) + object attributes. For that reason, assignment to this property must + be explicitly prevented. + + If this __set__ method was not present, this descriptor would become + a *non-data descriptor*. That would be nice because the cached value + would be accessed directly once set (__dict__ attrs have precedence + over non-data descriptors on instance attribute lookup). The problem + is, there would be nothing to stop assignment to the cached value, + which would overwrite the result of `fget()` and break both the + immutability and idempotence guarantees of this decorator. + + The performance with this __set__() method in place was roughly 0.4 + usec per access when measured on a 2.8GHz development machine; so + quite snappy and probably not a rich target for optimization efforts. + """ + raise AttributeError("can't set attribute") diff --git a/tests/chart/test_chart.py b/tests/chart/test_chart.py index b7c716c4c..8b89c902a 100644 --- a/tests/chart/test_chart.py +++ b/tests/chart/test_chart.py @@ -108,11 +108,15 @@ def it_provides_access_to_its_legend(self, legend_fixture): assert Legend_.call_args_list == expected_calls assert legend is expected_value - def it_knows_its_chart_type(self, chart_type_fixture): - chart, PlotTypeInspector_, plot_, chart_type = chart_type_fixture - _chart_type = chart.chart_type + def it_knows_its_chart_type(self, request, PlotTypeInspector_, plot_): + property_mock(request, Chart, "plots", return_value=[plot_]) + PlotTypeInspector_.chart_type.return_value = XL_CHART_TYPE.PIE + chart = Chart(None, None) + + chart_type = chart.chart_type + PlotTypeInspector_.chart_type.assert_called_once_with(plot_) - assert _chart_type is chart_type + assert chart_type == XL_CHART_TYPE.PIE def it_knows_its_style(self, style_get_fixture): chart, expected_value = style_get_fixture @@ -163,14 +167,6 @@ def cat_ax_raise_fixture(self): chart = Chart(element("c:chartSpace/c:chart/c:plotArea"), None) return chart - @pytest.fixture - def chart_type_fixture(self, PlotTypeInspector_, plot_): - chart = Chart(None, None) - chart._plots = [plot_] - chart_type = XL_CHART_TYPE.PIE - PlotTypeInspector_.chart_type.return_value = chart_type - return chart, PlotTypeInspector_, plot_, chart_type - @pytest.fixture( params=[ ( diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index dc1eeada3..a63392627 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -60,19 +60,34 @@ def it_initializes_its_rels_collection_on_first_reference(self, _Relationships_) _Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) assert rels == _Relationships_.return_value - def it_can_add_a_relationship_to_a_part(self, pkg_with_rels_, rel_attrs_): - reltype, target, rId = rel_attrs_ - pkg = pkg_with_rels_ - # exercise --------------------- - pkg.load_rel(reltype, target, rId) - # verify ----------------------- - pkg._rels.add_relationship.assert_called_once_with(reltype, target, rId, False) + def it_can_add_a_relationship_to_a_part(self, request, rels_prop_, relationships_): + rels_prop_.return_value = relationships_ + relationship_ = instance_mock(request, _Relationship) + relationships_.add_relationship.return_value = relationship_ + target_ = instance_mock(request, Part, name="target_part") + package = OpcPackage() - def it_can_establish_a_relationship_to_another_part(self, relate_to_part_fixture_): - pkg, part_, reltype, rId = relate_to_part_fixture_ - _rId = pkg.relate_to(part_, reltype) - pkg.rels.get_or_add.assert_called_once_with(reltype, part_) - assert _rId == rId + relationship = package.load_rel(RT.SLIDE, target_, "rId99") + + relationships_.add_relationship.assert_called_once_with( + RT.SLIDE, target_, "rId99", False + ) + assert relationship is relationship_ + + def it_can_establish_a_relationship_to_another_part( + self, request, rels_prop_, relationships_ + ): + rels_prop_.return_value = relationships_ + relationship_ = instance_mock(request, _Relationship) + relationships_.get_or_add.return_value = relationship_ + relationship_.rId = "rId99" + part_ = instance_mock(request, Part) + package = OpcPackage() + + rId = package.relate_to(part_, "http://rel/type") + + relationships_.get_or_add.assert_called_once_with("http://rel/type", part_) + assert rId == "rId99" def it_can_provide_a_list_of_the_parts_it_contains(self): # mockery ---------------------- @@ -92,10 +107,17 @@ def it_can_iterate_over_its_relationships(self, iter_rels_fixture): rels = list(package.iter_rels()) assert rels == expected_rels - def it_can_find_a_part_related_by_reltype(self, related_part_fixture_): - pkg, reltype, related_part_ = related_part_fixture_ - related_part = pkg.part_related_by(reltype) - pkg.rels.part_with_reltype.assert_called_once_with(reltype) + def it_can_find_a_part_related_by_reltype( + self, request, rels_prop_, relationships_ + ): + related_part_ = instance_mock(request, Part, name="related_part_") + relationships_.part_with_reltype.return_value = related_part_ + rels_prop_.return_value = relationships_ + package = OpcPackage() + + related_part = package.part_related_by(RT.SLIDE) + + relationships_.part_with_reltype.assert_called_once_with(RT.SLIDE) assert related_part is related_part_ def it_can_find_the_next_available_vector_partname(self, next_partname_fixture): @@ -146,24 +168,7 @@ def next_partname_fixture(self, request, iter_parts_): return package, partname_template, expected_partname @pytest.fixture - def relate_to_part_fixture_(self, request, pkg, rels_, reltype): - rId = "rId99" - rel_ = instance_mock(request, _Relationship, name="rel_", rId=rId) - rels_.get_or_add.return_value = rel_ - pkg._rels = rels_ - part_ = instance_mock(request, Part, name="part_") - return pkg, part_, reltype, rId - - @pytest.fixture - def related_part_fixture_(self, request, rels_, reltype): - related_part_ = instance_mock(request, Part, name="related_part_") - rels_.part_with_reltype.return_value = related_part_ - pkg = OpcPackage() - pkg._rels = rels_ - return pkg, reltype, related_part_ - - @pytest.fixture - def rels_fixture(self, request, part_1_, part_2_): + def rels_fixture(self, request, rels_prop_, part_1_, part_2_): """ +----------+ +--------+ | pkg_rels |-- r1 --> | part_1 | @@ -184,7 +189,7 @@ def rels_fixture(self, request, part_1_, part_2_): package = OpcPackage() - package._rels = self.rels(request, (r1, r4, r5)) + rels_prop_.return_value = self.rels(request, (r1, r4, r5)) part_1_.rels = self.rels(request, (r2,)) part_2_.rels = self.rels(request, (r3,)) @@ -212,27 +217,10 @@ def part_1_(self, request): def part_2_(self, request): return instance_mock(request, Part) - @pytest.fixture - def pkg(self, request): - return OpcPackage() - - @pytest.fixture - def pkg_with_rels_(self, request, rels_): - pkg = OpcPackage() - pkg._rels = rels_ - return pkg - @pytest.fixture def _Relationships_(self, request): return class_mock(request, "pptx.opc.package._Relationships") - @pytest.fixture - def rel_attrs_(self, request): - reltype = "http://rel/type" - target_ = instance_mock(request, Part, name="target_") - rId = "rId99" - return reltype, target_, rId - def rel(self, request, is_external, target_part, name): return instance_mock( request, @@ -242,6 +230,10 @@ def rel(self, request, is_external, target_part, name): name=name, ) + @pytest.fixture + def relationships_(self, request): + return instance_mock(request, _Relationships) + def rels(self, request, values): rels = instance_mock(request, _Relationships) rels.values.return_value = values @@ -252,8 +244,8 @@ def rels_(self, request): return instance_mock(request, _Relationships) @pytest.fixture - def reltype(self, request): - return "http://rel/type" + def rels_prop_(self, request): + return property_mock(request, OpcPackage, "rels") @pytest.fixture def Unmarshaller_(self, request): @@ -285,15 +277,20 @@ def it_knows_its_content_type(self, content_type_fixture): part, expected_content_type = content_type_fixture assert part.content_type == expected_content_type - def it_can_drop_a_relationship(self, drop_rel_fixture): - part, rId, rel_should_be_gone = drop_rel_fixture + @pytest.mark.parametrize("ref_count, calls", ((2, []), (1, [call("rId42")]))) + def it_can_drop_a_relationship( + self, request, _rels_prop_, relationships_, ref_count, calls + ): + _rel_ref_count_ = method_mock( + request, Part, "_rel_ref_count", return_value=ref_count + ) + _rels_prop_.return_value = relationships_ + part = Part(None, None, None) - part.drop_rel(rId) + part.drop_rel("rId42") - if rel_should_be_gone: - assert rId not in part.rels - else: - assert rId in part.rels + _rel_ref_count_.assert_called_once_with(part, "rId42") + assert relationships_.pop.call_args_list == calls def it_can_load_a_relationship(self, load_rel_fixture): part, rels_, reltype_, target_, rId_ = load_rel_fixture @@ -340,11 +337,11 @@ def it_can_establish_an_external_relationship(self, relate_to_url_fixture): assert rId is rId_ def it_can_find_a_related_part_by_rId( - self, request, rels_prop_, relationships_, relationship_, part_ + self, request, _rels_prop_, relationships_, relationship_, part_ ): relationship_.target_part = part_ relationships_.__getitem__.return_value = relationship_ - rels_prop_.return_value = relationships_ + _rels_prop_.return_value = relationships_ part = Part(None, None, None) related_part = part.related_part("rId17") @@ -392,23 +389,9 @@ def content_type_fixture(self): part = Part(None, content_type, None, None) return part, content_type - @pytest.fixture( - params=[ - ("p:sp", True), - ("p:sp/r:a{r:id=rId42}", True), - ("p:sp/r:a{r:id=rId42}/r:b{r:id=rId42}", False), - ] - ) - def drop_rel_fixture(self, request, part): - part_cxml, rel_should_be_dropped = request.param - rId = "rId42" - part._element = element(part_cxml) - part._rels = {rId: None} - return part, rId, rel_should_be_dropped - @pytest.fixture - def load_rel_fixture(self, part, rels_, reltype_, part_, rId_): - part._rels = rels_ + def load_rel_fixture(self, part, _rels_prop_, rels_, reltype_, part_, rId_): + _rels_prop_.return_value = rels_ return part, rels_, reltype_, part_, rId_ @pytest.fixture @@ -430,19 +413,19 @@ def partname_set_fixture(self): return part, new_partname @pytest.fixture - def relate_to_part_fixture(self, request, part, reltype_, part_, rels_, rId_): - part._rels = rels_ + def relate_to_part_fixture(self, part, _rels_prop_, reltype_, part_, rels_, rId_): + _rels_prop_.return_value = rels_ target_ = part_ return part, target_, reltype_, rId_ @pytest.fixture - def relate_to_url_fixture(self, request, part, rels_, url_, reltype_, rId_): - part._rels = rels_ + def relate_to_url_fixture(self, part, _rels_prop_, rels_, url_, reltype_, rId_): + _rels_prop_.return_value = rels_ return part, url_, reltype_, rId_ @pytest.fixture - def related_part_fixture(self, request, part, rels_, reltype_, part_): - part._rels = rels_ + def related_part_fixture(self, part, _rels_prop_, rels_, reltype_, part_): + _rels_prop_.return_value = rels_ return part, reltype_, part_ @pytest.fixture @@ -451,8 +434,8 @@ def rels_fixture(self, Relationships_, partname_, rels_): return part, Relationships_, partname_, rels_ @pytest.fixture - def target_ref_fixture(self, request, part, rId_, rel_, url_): - part._rels = {rId_: rel_} + def target_ref_fixture(self, part, _rels_prop_, rId_, rel_, url_): + _rels_prop_.return_value = {rId_: rel_} return part, rId_, url_ # fixture components --------------------------------------------- @@ -504,8 +487,8 @@ def rels_(self, request, part_, rel_, rId_): return rels_ @pytest.fixture - def rels_prop_(self, request): - return property_mock(request, Part, "rels") + def _rels_prop_(self, request): + return property_mock(request, Part, "_rels") @pytest.fixture def reltype_(self, request): diff --git a/tests/test_shared.py b/tests/test_shared.py index 4b57d47c6..e2d6bdc01 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for the docx.shared module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.shared` module.""" import pytest @@ -16,10 +12,7 @@ class DescribeElementProxy(object): - def it_raises_on_assign_to_undefined_attr(self): - element_proxy = ElementProxy(None) - with pytest.raises(AttributeError): - element_proxy.foobar = 42 + """Unit-test suite for `pptx.shared.ElementProxy` objects.""" def it_knows_when_its_equal_to_another_proxy_object(self, eq_fixture): proxy, proxy_2, proxy_3, not_a_proxy = eq_fixture @@ -55,6 +48,8 @@ def eq_fixture(self): class DescribeParentedElementProxy(object): + """Unit-test suite for `pptx.shared.ParentedElementProxy` objects.""" + def it_knows_its_parent(self, parent_fixture): proxy, parent = parent_fixture assert proxy.parent is parent diff --git a/tests/test_table.py b/tests/test_table.py index b0505a539..4f2f68af5 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Unit-test suite for pptx.table module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.table` module.""" import pytest @@ -28,6 +26,8 @@ class DescribeTable(object): + """Unit-test suite for `pptx.table.Table` objects.""" + def it_provides_access_to_its_cells(self, tbl_, tc_, _Cell_, cell_): row_idx, col_idx = 4, 2 tbl_.tc.return_value = tc_ @@ -40,9 +40,18 @@ def it_provides_access_to_its_cells(self, tbl_, tc_, _Cell_, cell_): _Cell_.assert_called_once_with(tc_, table) assert cell is cell_ - def it_provides_access_to_its_columns(self, columns_fixture): - table, expected_columns_ = columns_fixture - assert table.columns is expected_columns_ + def it_provides_access_to_its_columns(self, request): + columns_ = instance_mock(request, _ColumnCollection) + _ColumnCollection_ = class_mock( + request, "pptx.table._ColumnCollection", return_value=columns_ + ) + tbl = element("a:tbl") + table = Table(tbl, None) + + columns = table.columns + + _ColumnCollection_.assert_called_once_with(tbl, table) + assert columns is columns_ def it_can_iterate_its_grid_cells(self, request, _Cell_): tbl = element("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))") @@ -57,9 +66,18 @@ def it_can_iterate_its_grid_cells(self, request, _Cell_): assert cells == expected_cells assert _Cell_.call_args_list == [call(tc, table) for tc in expected_tcs] - def it_provides_access_to_its_rows(self, rows_fixture): - table, expected_rows_ = rows_fixture - assert table.rows is expected_rows_ + def it_provides_access_to_its_rows(self, request): + rows_ = instance_mock(request, _RowCollection) + _RowCollection_ = class_mock( + request, "pptx.table._RowCollection", return_value=rows_ + ) + tbl = element("a:tbl") + table = Table(tbl, None) + + rows = table.rows + + _RowCollection_.assert_called_once_with(tbl, table) + assert rows is rows_ def it_updates_graphic_frame_width_on_width_change(self, dx_fixture): table, expected_width = dx_fixture @@ -73,11 +91,6 @@ def it_updates_graphic_frame_height_on_height_change(self, dy_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def columns_fixture(self, table, columns_): - table._columns = columns_ - return table, columns_ - @pytest.fixture def dx_fixture(self, graphic_frame_): tbl_cxml = "a:tbl/a:tblGrid/(a:gridCol{w=111},a:gridCol{w=222})" @@ -92,11 +105,6 @@ def dy_fixture(self, graphic_frame_): expected_height = 300 return table, expected_height - @pytest.fixture - def rows_fixture(self, table, rows_): - table._rows = rows_ - return table, rows_ - # fixture components --------------------------------------------- @pytest.fixture @@ -107,22 +115,10 @@ def _Cell_(self, request): def cell_(self, request): return instance_mock(request, _Cell) - @pytest.fixture - def columns_(self, request): - return instance_mock(request, _ColumnCollection) - @pytest.fixture def graphic_frame_(self, request): return instance_mock(request, GraphicFrame) - @pytest.fixture - def rows_(self, request): - return instance_mock(request, _RowCollection) - - @pytest.fixture - def table(self): - return Table(element("a:tbl"), None) - @pytest.fixture def tbl_(self, request): return instance_mock(request, CT_Table) From c3966b6da49ecbc5d578c959a8a45ea21acf7132 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Aug 2021 21:07:20 -0700 Subject: [PATCH 09/69] rfctr: make OpcPackage._rels private --- pptx/opc/package.py | 38 ++++++++++++++++----------------- tests/opc/test_package.py | 45 +++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 3a11aa56b..8dae7b917 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -36,8 +36,8 @@ def open(cls, pkg_file): def iter_parts(self): """Generate exactly one reference to each part in the package.""" - def walk_parts(source, visited=list()): - for rel in source.rels.values(): + def walk_parts(rels, visited=list()): + for rel in rels.values(): if rel.is_external: continue part = rel.target_part @@ -45,11 +45,11 @@ def walk_parts(source, visited=list()): continue visited.append(part) yield part - new_source = part - for part in walk_parts(new_source, visited): + new_rels = part.rels + for part in walk_parts(new_rels, visited): yield part - for part in walk_parts(self): + for part in walk_parts(self._rels): yield part def iter_rels(self): @@ -58,9 +58,9 @@ def iter_rels(self): Performs a depth-first traversal of the rels graph. """ - def walk_rels(source, visited=None): + def walk_rels(rels, visited=None): visited = [] if visited is None else visited - for rel in source.rels.values(): + for rel in rels.values(): yield rel # --- external items can have no relationships --- if rel.is_external: @@ -72,12 +72,12 @@ def walk_rels(source, visited=None): if part in visited: continue visited.append(part) - new_source = part + new_rels = part.rels # --- recurse into relationships of each unvisited target-part --- - for rel in walk_rels(new_source, visited): + for rel in walk_rels(new_rels, visited): yield rel - for rel in walk_rels(self): + for rel in walk_rels(self._rels): yield rel def load_rel(self, reltype, target, rId, is_external=False): @@ -89,7 +89,7 @@ def load_rel(self, reltype, target, rId, is_external=False): methods exist for adding a new relationship to the package during processing. """ - return self.rels.add_relationship(reltype, target, rId, is_external) + return self._rels.add_relationship(reltype, target, rId, is_external) @property def main_document_part(self): @@ -119,7 +119,7 @@ def part_related_by(self, reltype): Raises |KeyError| if no such relationship is found and |ValueError| if more than one such relationship is found. """ - return self.rels.part_with_reltype(reltype) + return self._rels.part_with_reltype(reltype) @property def parts(self): @@ -135,20 +135,20 @@ def relate_to(self, part, reltype): If such a relationship already exists, its rId is returned. Otherwise the relationship is added and its new rId returned. """ - rel = self.rels.get_or_add(reltype, part) + rel = self._rels.get_or_add(reltype, part) return rel.rId - @lazyproperty - def rels(self): - """The |_Relationships| object containing the relationships for this package.""" - return _Relationships(PACKAGE_URI.baseURI) - def save(self, pkg_file): """Save this package to `pkg_file`. `pkg_file` can be either a path to a file (a string) or a file-like object. """ - PackageWriter.write(pkg_file, self.rels, self.parts) + PackageWriter.write(pkg_file, self._rels, self.parts) + + @lazyproperty + def _rels(self): + """The |_Relationships| object containing the relationships for this package.""" + return _Relationships(PACKAGE_URI.baseURI) class Part(object): diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index a63392627..69526d3f9 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -54,14 +54,8 @@ def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, PartFactory_) assert isinstance(pkg, OpcPackage) - def it_initializes_its_rels_collection_on_first_reference(self, _Relationships_): - pkg = OpcPackage() - rels = pkg.rels - _Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) - assert rels == _Relationships_.return_value - - def it_can_add_a_relationship_to_a_part(self, request, rels_prop_, relationships_): - rels_prop_.return_value = relationships_ + def it_can_add_a_relationship_to_a_part(self, request, _rels_prop_, relationships_): + _rels_prop_.return_value = relationships_ relationship_ = instance_mock(request, _Relationship) relationships_.add_relationship.return_value = relationship_ target_ = instance_mock(request, Part, name="target_part") @@ -75,9 +69,9 @@ def it_can_add_a_relationship_to_a_part(self, request, rels_prop_, relationships assert relationship is relationship_ def it_can_establish_a_relationship_to_another_part( - self, request, rels_prop_, relationships_ + self, request, _rels_prop_, relationships_ ): - rels_prop_.return_value = relationships_ + _rels_prop_.return_value = relationships_ relationship_ = instance_mock(request, _Relationship) relationships_.get_or_add.return_value = relationship_ relationship_.rId = "rId99" @@ -108,11 +102,11 @@ def it_can_iterate_over_its_relationships(self, iter_rels_fixture): assert rels == expected_rels def it_can_find_a_part_related_by_reltype( - self, request, rels_prop_, relationships_ + self, request, _rels_prop_, relationships_ ): related_part_ = instance_mock(request, Part, name="related_part_") relationships_.part_with_reltype.return_value = related_part_ - rels_prop_.return_value = relationships_ + _rels_prop_.return_value = relationships_ package = OpcPackage() related_part = package.part_related_by(RT.SLIDE) @@ -126,9 +120,9 @@ def it_can_find_the_next_available_vector_partname(self, next_partname_fixture): assert isinstance(partname, PackURI) assert partname == expected_partname - def it_can_save_to_a_pkg_file(self, request, rels_): + def it_can_save_to_a_pkg_file(self, request, _rels_prop_, rels_): PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") - property_mock(request, OpcPackage, "rels", return_value=rels_) + _rels_prop_.return_value = rels_ property_mock(request, OpcPackage, "parts", return_value=["parts"]) package = OpcPackage() @@ -136,7 +130,16 @@ def it_can_save_to_a_pkg_file(self, request, rels_): PackageWriter_.write.assert_called_once_with("prs.pptx", rels_, ["parts"]) - # assert False + def it_constructs_its_relationships_object_to_help(self, request, relationships_): + _Relationships_ = class_mock( + request, "pptx.opc.package._Relationships", return_value=relationships_ + ) + package = OpcPackage() + + rels = package._rels + + _Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) + assert rels is relationships_ # fixtures --------------------------------------------- @@ -168,7 +171,7 @@ def next_partname_fixture(self, request, iter_parts_): return package, partname_template, expected_partname @pytest.fixture - def rels_fixture(self, request, rels_prop_, part_1_, part_2_): + def rels_fixture(self, request, _rels_prop_, part_1_, part_2_): """ +----------+ +--------+ | pkg_rels |-- r1 --> | part_1 | @@ -189,7 +192,7 @@ def rels_fixture(self, request, rels_prop_, part_1_, part_2_): package = OpcPackage() - rels_prop_.return_value = self.rels(request, (r1, r4, r5)) + _rels_prop_.return_value = self.rels(request, (r1, r4, r5)) part_1_.rels = self.rels(request, (r2,)) part_2_.rels = self.rels(request, (r3,)) @@ -217,10 +220,6 @@ def part_1_(self, request): def part_2_(self, request): return instance_mock(request, Part) - @pytest.fixture - def _Relationships_(self, request): - return class_mock(request, "pptx.opc.package._Relationships") - def rel(self, request, is_external, target_part, name): return instance_mock( request, @@ -244,8 +243,8 @@ def rels_(self, request): return instance_mock(request, _Relationships) @pytest.fixture - def rels_prop_(self, request): - return property_mock(request, OpcPackage, "rels") + def _rels_prop_(self, request): + return property_mock(request, OpcPackage, "_rels") @pytest.fixture def Unmarshaller_(self, request): From 27d66a6ca0891c7eccc2ca974f3c68f4a8a97853 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Aug 2021 22:12:40 -0700 Subject: [PATCH 10/69] rfctr: modernize _Relationships interface Make `_Relationships` a Mapping subtype. Also improve `_Relationship` interface, implementation, and tests. --- pptx/compat/__init__.py | 8 +- pptx/opc/package.py | 278 ++++++--- tests/opc/test_package.py | 583 +++++++++++------- tests/test_files/snippets/relationships.txt | 2 + .../snippets/rels-load-from-xml.txt | 6 + tests/unitutil/file.py | 13 +- 6 files changed, 571 insertions(+), 319 deletions(-) create mode 100644 tests/test_files/snippets/relationships.txt create mode 100644 tests/test_files/snippets/rels-load-from-xml.txt diff --git a/pptx/compat/__init__.py b/pptx/compat/__init__.py index 3e7715fe5..415e9787e 100644 --- a/pptx/compat/__init__.py +++ b/pptx/compat/__init__.py @@ -1,18 +1,16 @@ # encoding: utf-8 -""" -Provides Python 2/3 compatibility objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Provides Python 2/3 compatibility objects.""" import sys import collections try: + Mapping = collections.abc.Mapping Sequence = collections.abc.Sequence except AttributeError: + Mapping = collections.Mapping Sequence = collections.Sequence if sys.version_info >= (3, 0): diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 8dae7b917..42fd97d76 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -6,8 +6,10 @@ presentations to and from a .pptx file. """ -from pptx.compat import is_string -from pptx.opc.constants import RELATIONSHIP_TYPE as RT +import collections + +from pptx.compat import is_string, Mapping +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM, RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationships, serialize_part_xml from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.opc.serialized import PackageReader, PackageWriter @@ -37,7 +39,7 @@ def iter_parts(self): """Generate exactly one reference to each part in the package.""" def walk_parts(rels, visited=list()): - for rel in rels.values(): + for rel in rels: if rel.is_external: continue part = rel.target_part @@ -60,7 +62,7 @@ def iter_rels(self): def walk_rels(rels, visited=None): visited = [] if visited is None else visited - for rel in rels.values(): + for rel in rels: yield rel # --- external items can have no relationships --- if rel.is_external: @@ -129,14 +131,16 @@ def parts(self): """ return [part for part in self.iter_parts()] - def relate_to(self, part, reltype): + def relate_to(self, target, reltype, is_external=False): """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. """ - rel = self._rels.get_or_add(reltype, part) - return rel.rId + if is_external: + return self._rels.get_or_add_ext_rel(reltype, target) + else: + return self._rels.get_or_add(reltype, target) def save(self, pkg_file): """Save this package to `pkg_file`. @@ -250,11 +254,11 @@ def relate_to(self, target, reltype, is_external=False): If such a relationship already exists, its rId is returned. Otherwise the relationship is added and its new rId returned. """ - if is_external: - return self.rels.get_or_add_ext_rel(reltype, target) - else: - rel = self.rels.get_or_add(reltype, target) - return rel.rId + return ( + self._rels.get_or_add_ext_rel(reltype, target) + if is_external + else self._rels.get_or_add(reltype, target) + ) def related_part(self, rId): """Return related |Part| subtype identified by `rId`.""" @@ -263,12 +267,12 @@ def related_part(self, rId): @lazyproperty def rels(self): """|Relationships| collection of relationships from this part to other parts.""" + # --- this must be public to allow the part graph to be traversed --- return self._rels def target_ref(self, rId): """Return URL contained in target ref of relationship identified by `rId`.""" - rel = self.rels[rId] - return rel.target_ref + return self._rels[rId].target_ref def _blob_from_file(self, file): """Return bytes of `file`, which is either a str path or a file-like object.""" @@ -352,7 +356,7 @@ def _part_cls_for(cls, content_type): return cls.default_part_type -class _Relationships(dict): +class _Relationships(Mapping): """Collection of |_Relationship| instances, largely having dict semantics. Relationships are keyed by their rId, but may also be found in other ways, such as @@ -363,41 +367,84 @@ class _Relationships(dict): not rIds (keys) as it would for a dict. """ - def __init__(self, baseURI): - self._baseURI = baseURI - self._target_parts_by_rId = {} + def __init__(self, base_uri): + self._base_uri = base_uri + + def __contains__(self, rId): + """Implement 'in' operation, like `"rId7" in relationships`.""" + return rId in self._rels + + def __getitem__(self, rId): + """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): + """Implement iteration of relationships.""" + return iter(list(self._rels.values())) + + def __len__(self): + """Return count of relationships in collection.""" + return len(self._rels) def add_relationship(self, reltype, target, rId, is_external=False): - """ - Return a newly added |_Relationship| instance. - """ - rel = _Relationship(rId, reltype, target, self._baseURI, is_external) - self[rId] = rel - if not is_external: - self._target_parts_by_rId[rId] = target + """Return a newly added |_Relationship| instance.""" + rel = _Relationship( + self._base_uri, + rId, + reltype, + RTM.EXTERNAL if is_external else RTM.INTERNAL, + target, + ) + self._rels[rId] = rel return rel def get_or_add(self, reltype, target_part): + """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. """ - Return relationship of *reltype* to *target_part*, newly added if not - already present in collection. - """ - rel = self._get_matching(reltype, target_part) - if rel is None: - rId = self._next_rId - rel = self.add_relationship(reltype, target_part, rId) - return rel + existing_rId = self._get_matching(reltype, target_part) + return ( + self._add_relationship(reltype, target_part) + if existing_rId is None + else existing_rId + ) def get_or_add_ext_rel(self, reltype, target_ref): - """ - Return rId of external relationship of *reltype* to *target_ref*, - newly added if not already present in collection. - """ - rel = self._get_matching(reltype, target_ref, is_external=True) - if rel is None: - rId = self._next_rId - rel = self.add_relationship(reltype, target_ref, rId, is_external=True) - return rel.rId + """Return str rId of external relationship of `reltype` to `target_ref`. + + The rId of an existing matching relationship is used if present. Otherwise, a + new relationship is added and that rId is returned. + """ + existing_rId = self._get_matching(reltype, target_ref, is_external=True) + return ( + self._add_relationship(reltype, target_ref, is_external=True) + if existing_rId is None + else existing_rId + ) + + def load_from_xml(self, base_uri, xml_rels, parts): + """Replace any relationships in this collection with those from `xml_rels`.""" + + def iter_valid_rels(): + """Filter out broken relationships such as those pointing to NULL.""" + for rel_elm in xml_rels.relationship_lst: + # --- Occasionally a PowerPoint plugin or other client will "remove" + # --- a relationship simply by "voiding" its Target value, like making + # --- it "/ppt/slides/NULL". Skip any relationships linking to a + # --- partname that is not present in the package. + if rel_elm.targetMode == RTM.INTERNAL: + partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref) + if partname not in parts: + continue + yield _Relationship.from_xml(base_uri, rel_elm, parts) + + self._rels.clear() + self._rels.update((rel.rId, rel) for rel in iter_valid_rels()) def part_with_reltype(self, reltype): """Return target part of relationship with matching `reltype`. @@ -405,8 +452,24 @@ def part_with_reltype(self, reltype): Raises |KeyError| if not found and |ValueError| if more than one matching relationship is found. """ - rel = self._get_rel_of_type(reltype) - return rel.target_part + rels_of_reltype = self._rels_by_reltype[reltype] + + if len(rels_of_reltype) == 0: + raise KeyError("no relationship of type '%s' in collection" % reltype) + + if len(rels_of_reltype) > 1: + raise ValueError( + "multiple relationships of type '%s' in collection" % reltype + ) + + return rels_of_reltype[0].target_part + + def pop(self, rId): + """Return |Relationship| identified by `rId` after removing it from collection. + + The caller is responsible for ensuring it is no longer required. + """ + return self._rels.pop(rId) @property def xml(self): @@ -416,46 +479,37 @@ def xml(self): a ` 1: - tmpl = "multiple relationships of type '%s' in collection" - raise ValueError(tmpl % reltype) - return matching[0] - @property def _next_rId(self): """Next str rId available in collection. @@ -463,11 +517,27 @@ def _next_rId(self): 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']. """ - for n in range(1, len(self) + 2): + # --- The common case is where all sequential numbers starting at "rId1" are + # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start + # --- there and count down to produce the best performance. + for n in range(len(self) + 1, 0, -1): rId_candidate = "rId%d" % n # like 'rId19' - if rId_candidate not in self: + if rId_candidate not in self._rels: return rId_candidate + @lazyproperty + def _rels(self): + """dict {rId: _Relationship} containing relationships of this collection.""" + return dict() + + @property + def _rels_by_reltype(self): + """defaultdict {reltype: [rels]} for all relationships in collection.""" + D = collections.defaultdict(list) + for rel in self: + D[rel.reltype].append(rel) + return D + class Unmarshaller(object): """ @@ -515,29 +585,38 @@ def _unmarshal_relationships(pkg_reader, package, parts): class _Relationship(object): """Value object describing link from a part or package to another part.""" - def __init__(self, rId, reltype, target, baseURI, external=False): - super(_Relationship, self).__init__() + def __init__(self, base_uri, rId, reltype, target_mode, target): + self._base_uri = base_uri self._rId = rId self._reltype = reltype + self._target_mode = target_mode self._target = target - self._baseURI = baseURI - self._is_external = bool(external) - @property + @classmethod + def from_xml(cls, base_uri, rel, parts): + """Return |_Relationship| object based on CT_Relationship element `rel`.""" + target = ( + rel.target_ref + if rel.targetMode == RTM.EXTERNAL + else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)] + ) + return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target) + + @lazyproperty def is_external(self): """True if target_mode is `RTM.EXTERNAL`. An external relationship is a link to a resource outside the package, such as a web-resource (URL). """ - return self._is_external + return self._target_mode == RTM.EXTERNAL - @property + @lazyproperty def reltype(self): """Member of RELATIONSHIP_TYPE describing relationship of target to source.""" return self._reltype - @property + @lazyproperty def rId(self): """str relationship-id, like 'rId9'. @@ -547,24 +626,39 @@ def rId(self): """ return self._rId - @property + @lazyproperty def target_part(self): """|Part| or subtype referred to by this relationship.""" - if self._is_external: + if self.is_external: raise ValueError( - "target_part property on _Relationship is undef" - "ined when target mode is External" + "`.target_part` property on _Relationship is undefined when " + "target-mode is external" ) return self._target - @property + @lazyproperty + def target_partname(self): + """|PackURI| instance containing partname targeted by this relationship. + + Raises `ValueError` on reference if target_mode is external. Use + :attr:`target_mode` to check before referencing. + """ + if self.is_external: + raise ValueError( + "`.target_partname` property on _Relationship is undefined when " + "target-mode is external" + ) + return self._target.partname + + @lazyproperty def target_ref(self): """str reference to relationship target. For internal relationships this is the relative partname, suitable for serialization purposes. For an external relationship it is typically a URL. """ - if self._is_external: - return self._target - else: - return self._target.partname.relative_ref(self._baseURI) + return ( + self._target + if self.is_external + else self.target_partname.relative_ref(self._base_uri) + ) diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 69526d3f9..4c058235c 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -2,12 +2,17 @@ """Unit-test suite for `pptx.opc.package` module.""" +import collections import io import pytest -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT -from pptx.opc.oxml import CT_Relationships +from pptx.opc.constants import ( + CONTENT_TYPE as CT, + RELATIONSHIP_TARGET_MODE as RTM, + RELATIONSHIP_TYPE as RT, +) +from pptx.opc.oxml import CT_Relationship from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.opc.package import ( OpcPackage, @@ -19,11 +24,12 @@ XmlPart, ) from pptx.opc.serialized import PackageReader +from pptx.oxml import parse_xml from pptx.oxml.xmlchemy import BaseOxmlElement from pptx.package import Package from ..unitutil.cxml import element -from ..unitutil.file import absjoin, test_file_dir +from ..unitutil.file import absjoin, snippet_bytes, test_file_dir from ..unitutil.mock import ( call, class_mock, @@ -36,7 +42,6 @@ Mock, patch, property_mock, - PropertyMock, ) @@ -71,10 +76,8 @@ def it_can_add_a_relationship_to_a_part(self, request, _rels_prop_, relationship def it_can_establish_a_relationship_to_another_part( self, request, _rels_prop_, relationships_ ): + relationships_.get_or_add.return_value = "rId99" _rels_prop_.return_value = relationships_ - relationship_ = instance_mock(request, _Relationship) - relationships_.get_or_add.return_value = relationship_ - relationship_.rId = "rId99" part_ = instance_mock(request, Part) package = OpcPackage() @@ -235,7 +238,7 @@ def relationships_(self, request): def rels(self, request, values): rels = instance_mock(request, _Relationships) - rels.values.return_value = values + rels.__iter__.return_value = iter(values) return rels @pytest.fixture @@ -319,13 +322,17 @@ def it_can_change_its_partname(self, partname_set_fixture): part.partname = new_partname assert part.partname == new_partname - def it_can_establish_a_relationship_to_another_part(self, relate_to_part_fixture): - part, target_, reltype_, rId_ = relate_to_part_fixture + 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_ + part = Part(None, None, None) - rId = part.relate_to(target_, reltype_) + rId = part.relate_to(part_, RT.SLIDE) - part.rels.get_or_add.assert_called_once_with(reltype_, target_) - assert rId is rId_ + relationships_.get_or_add.assert_called_once_with(RT.SLIDE, part_) + assert rId == "rId42" def it_can_establish_an_external_relationship(self, relate_to_url_fixture): part, url_, reltype_, rId_ = relate_to_url_fixture @@ -411,12 +418,6 @@ def partname_set_fixture(self): part = Part(old_partname, None, None, None) return part, new_partname - @pytest.fixture - def relate_to_part_fixture(self, part, _rels_prop_, reltype_, part_, rels_, rId_): - _rels_prop_.return_value = rels_ - target_ = part_ - return part, target_, reltype_, rId_ - @pytest.fixture def relate_to_url_fixture(self, part, _rels_prop_, rels_, url_, reltype_, rId_): _rels_prop_.return_value = rels_ @@ -619,254 +620,398 @@ def part_args_2_(self, request): class Describe_Relationships(object): """Unit-test suite for `pptx.opc.package._Relationships` objects.""" - def it_has_a_len(self): - rels = _Relationships(None) - assert len(rels) == 0 - - def it_has_dict_style_lookup_of_rel_by_rId(self): - rel = Mock(name="rel", rId="foobar") - rels = _Relationships(None) - rels["foobar"] = rel - assert rels["foobar"] == rel - - def it_should_raise_on_failed_lookup_by_rId(self): - rels = _Relationships(None) - with pytest.raises(KeyError): - rels["barfoo"] - - def it_can_add_a_relationship(self, _Relationship_): - baseURI, rId, reltype, target, external = ( - "baseURI", - "rId9", - "reltype", - "target", - False, - ) - rels = _Relationships(baseURI) - rel = rels.add_relationship(reltype, target, rId, external) - _Relationship_.assert_called_once_with(rId, reltype, target, baseURI, external) - assert rels[rId] == rel - assert rel == _Relationship_.return_value - - def it_can_add_a_relationship_if_not_found( - self, rels_with_matching_rel_, rels_with_missing_rel_ + def it_has_dict_style_lookup_of_rel_by_rId(self, _rels_prop_, relationship_): + _rels_prop_.return_value = {"rId17": relationship_} + assert _Relationships(None)["rId17"] is relationship_ + + def but_it_raises_KeyError_when_no_relationship_has_rId(self, _rels_prop_): + _rels_prop_.return_value = {} + with pytest.raises(KeyError) as e: + _Relationships(None)["rId6"] + assert str(e.value) == "\"no relationship with key 'rId6'\"" + + def it_can_iterate_the_relationships_it_contains(self, request, _rels_prop_): + rels_ = set(instance_mock(request, _Relationship) for n in range(5)) + _rels_prop_.return_value = {"rId%d" % (i + 1): r for i, r in enumerate(rels_)} + relationships = _Relationships(None) + + for r in relationships: + rels_.remove(r) + + assert len(rels_) == 0 + + def it_has_a_len(self, _rels_prop_): + _rels_prop_.return_value = {"a": 0, "b": 1} + assert len(_Relationships(None)) == 2 + + def it_can_add_a_relationship_to_a_target_part( + self, part_, _get_matching_, _add_relationship_ ): + _get_matching_.return_value = None + _add_relationship_.return_value = "rId7" + relationships = _Relationships(None) - rels, reltype, part, matching_rel = rels_with_matching_rel_ - assert rels.get_or_add(reltype, part) == matching_rel + rId = relationships.get_or_add(RT.IMAGE, part_) - rels, reltype, part, new_rel = rels_with_missing_rel_ - assert rels.get_or_add(reltype, part) == new_rel + _get_matching_.assert_called_once_with(relationships, RT.IMAGE, part_) + _add_relationship_.assert_called_once_with(relationships, RT.IMAGE, part_) + assert rId == "rId7" - def it_can_add_an_external_relationship(self, add_ext_rel_fixture_): - rels, reltype, url = add_ext_rel_fixture_ - rId = rels.get_or_add_ext_rel(reltype, url) - rel = rels[rId] - assert rel.is_external - assert rel.target_ref == url - assert rel.reltype == reltype + def but_it_returns_an_existing_relationship_if_it_matches( + self, part_, _get_matching_ + ): + _get_matching_.return_value = "rId3" + relationships = _Relationships(None) - def it_should_return_an_existing_one_if_it_matches( - self, add_matching_ext_rel_fixture_ + rId = relationships.get_or_add(RT.IMAGE, part_) + + _get_matching_.assert_called_once_with(relationships, RT.IMAGE, part_) + assert rId == "rId3" + + def it_can_add_an_external_relationship_to_a_URI( + self, _get_matching_, _add_relationship_ ): - rels, reltype, url, rId = add_matching_ext_rel_fixture_ - _rId = rels.get_or_add_ext_rel(reltype, url) - assert _rId == rId - assert len(rels) == 1 - - def it_can_find_a_related_part_by_reltype(self, rels_with_target_known_by_reltype): - rels, reltype, known_target_part = rels_with_target_known_by_reltype - part = rels.part_with_reltype(reltype) - assert part is known_target_part - - def it_knows_the_next_available_rId_to_help(self, rels_with_rId_gap): - rels, expected_next_rId = rels_with_rId_gap - next_rId = rels._next_rId - assert next_rId == expected_next_rId - - def it_can_compose_rels_xml(self, rels, rels_elm): - rels.xml - - rels_elm.assert_has_calls( - [ - call.add_rel("rId1", "http://rt-hyperlink", "http://some/link", True), - call.add_rel("rId2", "http://rt-image", "../media/image1.png", False), - call.xml(), - ], - any_order=True, + _get_matching_.return_value = None + _add_relationship_.return_value = "rId2" + relationships = _Relationships(None) + + rId = relationships.get_or_add_ext_rel(RT.HYPERLINK, "http://url") + + _get_matching_.assert_called_once_with( + relationships, RT.HYPERLINK, "http://url", is_external=True + ) + _add_relationship_.assert_called_once_with( + relationships, RT.HYPERLINK, "http://url", is_external=True ) + assert rId == "rId2" - # --- fixtures ----------------------------------------- + def but_it_returns_an_existing_external_relationship_if_it_matches( + self, part_, _get_matching_ + ): + _get_matching_.return_value = "rId10" + relationships = _Relationships(None) - @pytest.fixture - def add_ext_rel_fixture_(self, reltype, url): - rels = _Relationships(None) - return rels, reltype, url + rId = relationships.get_or_add_ext_rel(RT.HYPERLINK, "http://url") - @pytest.fixture - def add_matching_ext_rel_fixture_(self, request, reltype, url): - rId = "rId369" - rels = _Relationships(None) - rels.add_relationship(reltype, url, rId, is_external=True) - return rels, reltype, url, rId + _get_matching_.assert_called_once_with( + relationships, RT.HYPERLINK, "http://url", is_external=True + ) + assert rId == "rId10" - @pytest.fixture - def _rel_with_target_known_by_reltype(self, _rId, _reltype, _target_part, _baseURI): - rel = _Relationship(_rId, _reltype, _target_part, _baseURI) - return rel, _reltype, _target_part + def it_can_load_from_the_xml_in_a_rels_part(self, request, _Relationship_, part_): + rels_ = tuple( + instance_mock(request, _Relationship, rId="rId%d" % (i + 1)) + for i in range(5) + ) + _Relationship_.from_xml.side_effect = iter(rels_) + parts = {"/ppt/slideLayouts/slideLayout1.xml": part_} + xml_rels = parse_xml(snippet_bytes("rels-load-from-xml")) + relationships = _Relationships(None) - @pytest.fixture - def rels_elm(self, request): - """Return a rels_elm mock that will be returned from CT_Relationships.new()""" - # --- create rels_elm mock with a .xml property --- - rels_elm = Mock(name="rels_elm") - xml = PropertyMock(name="xml") - type(rels_elm).xml = xml - rels_elm.attach_mock(xml, "xml") - rels_elm.reset_mock() # to clear attach_mock call - # --- patch CT_Relationships to return that rels_elm --- - patch_ = patch.object(CT_Relationships, "new", return_value=rels_elm) - patch_.start() - request.addfinalizer(patch_.stop) - return rels_elm + relationships.load_from_xml("/ppt/slides", xml_rels, parts) - @pytest.fixture - def rels_with_matching_rel_(self, request, rels): - matching_reltype_ = instance_mock(request, str, name="matching_reltype_") - matching_part_ = instance_mock(request, Part, name="matching_part_") - matching_rel_ = instance_mock( - request, - _Relationship, - name="matching_rel_", - reltype=matching_reltype_, - target_part=matching_part_, - is_external=False, + assert _Relationship_.from_xml.call_args_list == [ + call("/ppt/slides", xml_rels[0], parts), + call("/ppt/slides", xml_rels[1], parts), + ] + assert relationships._rels == {"rId1": rels_[0], "rId2": rels_[1]} + + 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_]),) ) - rels[1] = matching_rel_ - return rels, matching_reltype_, matching_part_, matching_rel_ + relationships = _Relationships(None) - @pytest.fixture - def rels_with_missing_rel_(self, request, rels, _Relationship_): - missing_reltype_ = instance_mock(request, str, name="missing_reltype_") - missing_part_ = instance_mock(request, Part, name="missing_part_") - new_rel_ = instance_mock( - request, - _Relationship, - name="new_rel_", - reltype=missing_reltype_, - target_part=missing_part_, - is_external=False, + assert relationships.part_with_reltype(RT.SLIDE_LAYOUT) is part_ + + def but_it_raises_KeyError_when_there_is_no_such_part(self, _rels_by_reltype_prop_): + _rels_by_reltype_prop_.return_value = collections.defaultdict(list) + relationships = _Relationships(None) + + with pytest.raises(KeyError) as e: + relationships.part_with_reltype(RT.SLIDE_LAYOUT) + assert str(e.value) == ( + "\"no relationship of type 'http://schemas.openxmlformats.org/" + "officeDocument/2006/relationships/slideLayout' in collection\"" ) - _Relationship_.return_value = new_rel_ - return rels, missing_reltype_, missing_part_, new_rel_ - @pytest.fixture - def rels_with_rId_gap(self, request): - rels = _Relationships(None) + def and_it_raises_ValueError_when_there_is_more_than_one_part_with_reltype( + self, _rels_by_reltype_prop_, relationship_, part_ + ): + relationship_.target_part = part_ + _rels_by_reltype_prop_.return_value = collections.defaultdict( + list, ((RT.SLIDE_LAYOUT, [relationship_, relationship_]),) + ) + relationships = _Relationships(None) + + with pytest.raises(ValueError) as e: + relationships.part_with_reltype(RT.SLIDE_LAYOUT) + assert str(e.value) == ( + "multiple relationships of type 'http://schemas.openxmlformats.org/" + "officeDocument/2006/relationships/slideLayout' in collection" + ) + + def it_can_pop_a_relationship_to_remove_it_from_the_collection( + self, _rels_prop_, relationship_ + ): + _rels_prop_.return_value = {"rId22": relationship_} + relationships = _Relationships(None) + + relationships.pop("rId22") + + assert relationships._rels == {} + + def it_can_serialize_itself_to_XML(self, request, _rels_prop_): + _rels_prop_.return_value = { + "rId1": instance_mock( + request, + _Relationship, + rId="rId1", + reltype=RT.SLIDE, + target_ref="../slides/slide1.xml", + is_external=False, + ), + "rId2": instance_mock( + request, + _Relationship, + rId="rId2", + reltype=RT.HYPERLINK, + target_ref="http://url", + is_external=True, + ), + } + relationships = _Relationships(None) + + assert relationships.xml == snippet_bytes("relationships") + + def it_can_add_a_relationship_to_a_part_to_help( + self, + request, + _next_rId_prop_, + _Relationship_, + relationship_, + _rels_prop_, + part_, + ): + _next_rId_prop_.return_value = "rId8" + _Relationship_.return_value = relationship_ + _rels_prop_.return_value = {} + relationships = _Relationships("/ppt") - rel_with_rId1 = instance_mock( - request, _Relationship, name="rel_with_rId1", rId="rId1" + rId = relationships._add_relationship(RT.SLIDE, part_) + + _Relationship_.assert_called_once_with( + "/ppt", "rId8", RT.SLIDE, target_mode=RTM.INTERNAL, target=part_ ) - rel_with_rId3 = instance_mock( - request, _Relationship, name="rel_with_rId3", rId="rId3" + assert relationships._rels == {"rId8": relationship_} + assert rId == "rId8" + + def and_it_can_add_an_external_relationship_to_help( + self, request, _next_rId_prop_, _rels_prop_, _Relationship_, relationship_ + ): + _next_rId_prop_.return_value = "rId9" + _Relationship_.return_value = relationship_ + _rels_prop_.return_value = {} + relationships = _Relationships("/ppt") + + rId = relationships._add_relationship( + RT.HYPERLINK, "http://url", is_external=True ) - rels["rId1"] = rel_with_rId1 - rels["rId3"] = rel_with_rId3 - return rels, "rId2" - @pytest.fixture - def rels_with_target_known_by_reltype( - self, rels, _rel_with_target_known_by_reltype + _Relationship_.assert_called_once_with( + "/ppt", "rId9", RT.HYPERLINK, target_mode=RTM.EXTERNAL, target="http://url" + ) + assert relationships._rels == {"rId9": relationship_} + assert rId == "rId9" + + def it_can_get_a_matching_relationship_to_help( + self, _rels_by_reltype_prop_, relationship_, part_ ): - rel, reltype, target_part = _rel_with_target_known_by_reltype - rels[1] = rel - return rels, reltype, target_part + relationship_.is_external = False + relationship_.target_part = part_ + relationship_.rId = "rId10" + _rels_by_reltype_prop_.return_value = {RT.SLIDE: [relationship_]} + relationships = _Relationships(None) - # --- fixture components ------------------------------- + assert relationships._get_matching(RT.SLIDE, part_) == "rId10" + + def but_it_returns_None_when_there_is_no_matching_relationship( + self, _rels_by_reltype_prop_ + ): + _rels_by_reltype_prop_.return_value = collections.defaultdict(list) + relationships = _Relationships(None) + + assert relationships._get_matching(RT.HYPERLINK, "http://url", True) is None + + @pytest.mark.parametrize( + "rIds, expected_value", + ( + ((), "rId1"), + (("rId1",), "rId2"), + (("rId1", "rId2"), "rId3"), + (("rId1", "rId4"), "rId3"), + (("rId1", "rId4", "rId6"), "rId3"), + (("rId1", "rId2", "rId6"), "rId4"), + ), + ) + def it_finds_the_next_rId_to_help(self, _rels_prop_, rIds, expected_value): + _rels_prop_.return_value = {rId: None for rId in rIds} + relationships = _Relationships(None) + + assert relationships._next_rId == expected_value + + def it_collects_relationships_by_reltype_to_help(self, request, _rels_prop_): + rels = { + "rId%d" % (i + 1): instance_mock(request, _Relationship, reltype=reltype) + for i, reltype in enumerate((RT.SLIDE, RT.IMAGE, RT.SLIDE, RT.HYPERLINK)) + } + _rels_prop_.return_value = rels + relationships = _Relationships(None) + + rels_by_reltype = relationships._rels_by_reltype + + assert rels["rId1"] in rels_by_reltype[RT.SLIDE] + assert rels["rId2"] in rels_by_reltype[RT.IMAGE] + assert rels["rId3"] in rels_by_reltype[RT.SLIDE] + assert rels["rId4"] in rels_by_reltype[RT.HYPERLINK] + assert rels_by_reltype[RT.CHART] == [] + + # fixture components ----------------------------------- @pytest.fixture - def _baseURI(self): - return "/baseURI" + def _add_relationship_(self, request): + return method_mock(request, _Relationships, "_add_relationship") @pytest.fixture - def _Relationship_(self, request): - return class_mock(request, "pptx.opc.package._Relationship") + def _get_matching_(self, request): + return method_mock(request, _Relationships, "_get_matching") @pytest.fixture - def rels(self): - """ - Populated _Relationships instance that will exercise the - rels.xml property. - """ - rels = _Relationships("/baseURI") - rels.add_relationship( - reltype="http://rt-hyperlink", - target="http://some/link", - rId="rId1", - is_external=True, - ) - part = Mock(name="part") - part.partname.relative_ref.return_value = "../media/image1.png" - rels.add_relationship(reltype="http://rt-image", target=part, rId="rId2") - return rels + def _next_rId_prop_(self, request): + return property_mock(request, _Relationships, "_next_rId") @pytest.fixture - def _reltype(self): - return RT.SLIDE + def part_(self, request): + return instance_mock(request, Part) @pytest.fixture - def reltype(self): - return "http://rel/type" + def _Relationship_(self, request): + return class_mock(request, "pptx.opc.package._Relationship") @pytest.fixture - def _rId(self): - return "rId6" + def relationship_(self, request): + return instance_mock(request, _Relationship) @pytest.fixture - def _target_part(self, request): - return loose_mock(request) + def _rels_by_reltype_prop_(self, request): + return property_mock(request, _Relationships, "_rels_by_reltype") @pytest.fixture - def url(self): - return "https://github.com/scanny/python-pptx" + def _rels_prop_(self, request): + return property_mock(request, _Relationships, "_rels") class Describe_Relationship(object): """Unit-test suite for `pptx.opc.package._Relationship` objects.""" - def it_remembers_construction_values(self): - # test data -------------------- - rId = "rId9" - reltype = "reltype" - target = Mock(name="target_part") - external = False - # exercise --------------------- - rel = _Relationship(rId, reltype, target, None, external) - # verify ----------------------- - assert rel.rId == rId - assert rel.reltype == reltype - assert rel.target_part == target - assert rel.is_external == external + def it_can_construct_from_xml(self, request, part_): + _init_ = initializer_mock(request, _Relationship) + rel_elm = instance_mock( + request, + CT_Relationship, + rId="rId42", + reltype=RT.SLIDE, + targetMode=RTM.INTERNAL, + target_ref="slides/slide7.xml", + ) + parts = {"/ppt/slides/slide7.xml": part_} + + relationship = _Relationship.from_xml("/ppt", rel_elm, parts) - def it_should_raise_on_target_part_access_on_external_rel(self): - rel = _Relationship(None, None, None, None, external=True) - with pytest.raises(ValueError): - rel.target_part + _init_.assert_called_once_with( + relationship, + "/ppt", + "rId42", + RT.SLIDE, + RTM.INTERNAL, + part_, + ) + assert isinstance(relationship, _Relationship) + + @pytest.mark.parametrize( + "target_mode, expected_value", + ((RTM.INTERNAL, False), (RTM.EXTERNAL, True), (None, False)), + ) + def it_knows_whether_it_is_external(self, target_mode, expected_value): + relationship = _Relationship(None, None, None, target_mode, None) + assert relationship.is_external == expected_value + + def it_knows_its_relationship_type(self): + relationship = _Relationship(None, None, RT.SLIDE, None, None) + assert relationship.reltype == RT.SLIDE + + def it_knows_its_rId(self): + relationship = _Relationship(None, "rId42", None, None, None) + assert relationship.rId == "rId42" + + def it_provides_access_to_its_target_part(self, part_): + relationship = _Relationship(None, None, None, RTM.INTERNAL, part_) + assert relationship.target_part is part_ + + def but_it_raises_ValueError_on_target_part_for_external_rel(self): + relationship = _Relationship(None, None, None, RTM.EXTERNAL, None) + with pytest.raises(ValueError) as e: + relationship.target_part + assert str(e.value) == ( + "`.target_part` property on _Relationship is undefined when " + "target-mode is external" + ) - def it_should_have_target_ref_for_external_rel(self): - rel = _Relationship(None, None, "target", None, external=True) - assert rel.target_ref == "target" + def it_knows_its_target_partname(self, part_): + part_.partname = PackURI("/ppt/slideLayouts/slideLayout4.xml") + relationship = _Relationship(None, None, None, RTM.INTERNAL, part_) - def it_should_have_relative_ref_for_internal_rel(self): - """ - Internal relationships (TargetMode == 'Internal' in the XML) should - have a relative ref, e.g. '../slideLayouts/slideLayout1.xml', for - the target_ref attribute. + assert relationship.target_partname == "/ppt/slideLayouts/slideLayout4.xml" + + def but_it_raises_ValueError_on_target_partname_for_external_rel(self): + relationship = _Relationship(None, None, None, RTM.EXTERNAL, None) + + with pytest.raises(ValueError) as e: + relationship.target_partname + + assert str(e.value) == ( + "`.target_partname` property on _Relationship is undefined when " + "target-mode is external" + ) + + def it_knows_the_target_uri_for_an_external_rel(self): + relationship = _Relationship(None, None, None, RTM.EXTERNAL, "http://url") + assert relationship.target_ref == "http://url" + + def and_it_knows_the_relative_partname_for_an_internal_rel(self, request): + """Internal relationships have a relative reference for `.target_ref`. + + A relative reference looks like "../slideLayouts/slideLayout1.xml". This form + is suitable for writing to a .rels file. """ - part = Mock(name="part", partname=PackURI("/ppt/media/image1.png")) - baseURI = "/ppt/slides" - rel = _Relationship(None, None, part, baseURI) # external=False - assert rel.target_ref == "../media/image1.png" + property_mock( + request, + _Relationship, + "target_partname", + return_value=PackURI("/ppt/media/image1.png"), + ) + relationship = _Relationship("/ppt/slides", None, None, None, None) + + assert relationship.target_ref == "../media/image1.png" + + # --- fixture components ------------------------------- + + @pytest.fixture + def part_(self, request): + return instance_mock(request, Part) class DescribeUnmarshaller(object): diff --git a/tests/test_files/snippets/relationships.txt b/tests/test_files/snippets/relationships.txt new file mode 100644 index 000000000..efdbee450 --- /dev/null +++ b/tests/test_files/snippets/relationships.txt @@ -0,0 +1,2 @@ + + diff --git a/tests/test_files/snippets/rels-load-from-xml.txt b/tests/test_files/snippets/rels-load-from-xml.txt new file mode 100644 index 000000000..acbdfcc96 --- /dev/null +++ b/tests/test_files/snippets/rels-load-from-xml.txt @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 1e257d92e..dcaaeec15 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,8 +1,6 @@ # encoding: utf-8 -""" -Utility functions for loading files for unit testing -""" +"""Utility functions for loading files for unit testing.""" import os import sys @@ -39,6 +37,15 @@ def parse_xml_file(file_): return etree.parse(file_, oxml_parser) +def snippet_bytes(snippet_file_name): + """Return bytes read from snippet file having `snippet_file_name`.""" + snippet_file_path = os.path.join( + test_file_dir, "snippets", "%s.txt" % snippet_file_name + ) + with open(snippet_file_path, "rb") as f: + return f.read().strip() + + def snippet_seq(name, offset=0, count=sys.maxsize): """ Return a tuple containing the unicode text snippets read from the snippet From aafa4814651d3677c63eae2bfab94c13337e750a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Aug 2021 22:24:06 -0700 Subject: [PATCH 11/69] rfctr: improve part graph traversal * Use `set()` for visited parts for O(1) lookup. * Walk graph in `.iter_rels()` and call that in `.iter_parts()` instead of reimplementing traversal. --- pptx/opc/package.py | 32 +++----- tests/opc/test_package.py | 153 ++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 99 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 42fd97d76..ededbf4df 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -37,31 +37,24 @@ def open(cls, pkg_file): def iter_parts(self): """Generate exactly one reference to each part in the package.""" - - def walk_parts(rels, visited=list()): - for rel in rels: - if rel.is_external: - continue - part = rel.target_part - if part in visited: - continue - visited.append(part) - yield part - new_rels = part.rels - for part in walk_parts(new_rels, visited): - yield part - - for part in walk_parts(self._rels): + visited = set() + for rel in self.iter_rels(): + if rel.is_external: + continue + part = rel.target_part + if part in visited: + continue yield part + visited.add(part) def iter_rels(self): """Generate exactly one reference to each relationship in package. Performs a depth-first traversal of the rels graph. """ + visited = set() - def walk_rels(rels, visited=None): - visited = [] if visited is None else visited + def walk_rels(rels): for rel in rels: yield rel # --- external items can have no relationships --- @@ -73,10 +66,9 @@ def walk_rels(rels, visited=None): part = rel.target_part if part in visited: continue - visited.append(part) - new_rels = part.rels + visited.add(part) # --- recurse into relationships of each unvisited target-part --- - for rel in walk_rels(new_rels, visited): + for rel in walk_rels(part.rels): yield rel for rel in walk_rels(self._rels): diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 4c058235c..dc5538333 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -59,6 +59,75 @@ def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, PartFactory_) assert isinstance(pkg, OpcPackage) + def it_can_iterate_over_its_parts(self, request): + part_, part_2_ = [ + instance_mock(request, Part, name="part_%d" % i) for i in range(2) + ] + rels_iter = ( + instance_mock( + request, _Relationship, is_external=is_external, target_part=target + ) + for is_external, target in ( + (True, "http://some/url/"), + (False, part_), + (False, part_), + (False, part_2_), + (False, part_), + (False, part_2_), + ) + ) + method_mock(request, OpcPackage, "iter_rels", return_value=rels_iter) + package = OpcPackage() + + assert list(package.iter_parts()) == [part_, part_2_] + + def it_can_iterate_over_its_relationships(self, request, _rels_prop_): + """ + +----------+ +--------+ + | pkg_rels |-- r0 --> | part_0 | + +----------+ +--------+ + | | | ^ + r2 | | r1 r3 | | r4 + | | | | + v | v | + external | +--------+ + +--------> | part_1 | + +--------+ + """ + part_0_, part_1_ = [ + instance_mock(request, Part, name="part_%d" % i) for i in range(2) + ] + rels = tuple( + instance_mock( + request, + _Relationship, + name="r%d" % i, + is_external=ext, + target_part=part, + ) + for i, (ext, part) in enumerate( + ( + (False, part_0_), + (False, part_1_), + (True, None), + (False, part_1_), + (False, part_0_), + ) + ) + ) + _rels_prop_.return_value = rels[:3] + part_0_.rels = rels[3:4] + part_1_.rels = rels[4:] + package = OpcPackage() + + assert tuple(package.iter_rels()) == ( + rels[0], + rels[3], + rels[4], + rels[1], + rels[2], + ) + def it_can_add_a_relationship_to_a_part(self, request, _rels_prop_, relationships_): _rels_prop_.return_value = relationships_ relationship_ = instance_mock(request, _Relationship) @@ -94,16 +163,6 @@ def it_can_provide_a_list_of_the_parts_it_contains(self): with patch.object(OpcPackage, "iter_parts", return_value=parts): assert pkg.parts == [parts[0], parts[1]] - def it_can_iterate_over_its_parts(self, iter_parts_fixture): - package, expected_parts = iter_parts_fixture - parts = list(package.iter_parts()) - assert parts == expected_parts - - def it_can_iterate_over_its_relationships(self, iter_rels_fixture): - package, expected_rels = iter_rels_fixture - rels = list(package.iter_rels()) - assert rels == expected_rels - def it_can_find_a_part_related_by_reltype( self, request, _rels_prop_, relationships_ ): @@ -123,15 +182,17 @@ def it_can_find_the_next_available_vector_partname(self, next_partname_fixture): assert isinstance(partname, PackURI) assert partname == expected_partname - def it_can_save_to_a_pkg_file(self, request, _rels_prop_, rels_): + def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") - _rels_prop_.return_value = rels_ + _rels_prop_.return_value = relationships_ property_mock(request, OpcPackage, "parts", return_value=["parts"]) package = OpcPackage() package.save("prs.pptx") - PackageWriter_.write.assert_called_once_with("prs.pptx", rels_, ["parts"]) + PackageWriter_.write.assert_called_once_with( + "prs.pptx", relationships_, ["parts"] + ) def it_constructs_its_relationships_object_to_help(self, request, relationships_): _Relationships_ = class_mock( @@ -146,18 +207,6 @@ def it_constructs_its_relationships_object_to_help(self, request, relationships_ # fixtures --------------------------------------------- - @pytest.fixture - def iter_parts_fixture(self, request, rels_fixture): - package, parts, rels = rels_fixture - expected_parts = list(parts) - return package, expected_parts - - @pytest.fixture - def iter_rels_fixture(self, request, rels_fixture): - package, parts, rels = rels_fixture - expected_rels = list(rels) - return package, expected_rels - @pytest.fixture(params=[((), 1), ((1,), 2), ((1, 2), 3), ((2, 3), 1), ((1, 3), 2)]) def next_partname_fixture(self, request, iter_parts_): existing_partname_numbers, next_partname_number = request.param @@ -173,34 +222,6 @@ def next_partname_fixture(self, request, iter_parts_): expected_partname = PackURI("/foo/bar/baz%d.xml" % next_partname_number) return package, partname_template, expected_partname - @pytest.fixture - def rels_fixture(self, request, _rels_prop_, part_1_, part_2_): - """ - +----------+ +--------+ - | pkg_rels |-- r1 --> | part_1 | - +----------+ +--------+ - | | | ^ - r5 | | r4 r2 | | r3 - | | | | - v | v | - external | +--------+ - +--------> | part_2 | - +--------+ - """ - r1 = self.rel(request, False, part_1_, "r1") - r2 = self.rel(request, False, part_2_, "r2") - r3 = self.rel(request, False, part_1_, "r3") - r4 = self.rel(request, False, part_2_, "r4") - r5 = self.rel(request, True, None, "r5") - - package = OpcPackage() - - _rels_prop_.return_value = self.rels(request, (r1, r4, r5)) - part_1_.rels = self.rels(request, (r2,)) - part_2_.rels = self.rels(request, (r3,)) - - return package, (part_1_, part_2_), (r1, r2, r3, r4, r5) - # fixture components ----------------------------------- @pytest.fixture @@ -215,36 +236,10 @@ def PackageReader_(self, request): def PartFactory_(self, request): return class_mock(request, "pptx.opc.package.PartFactory") - @pytest.fixture - def part_1_(self, request): - return instance_mock(request, Part) - - @pytest.fixture - def part_2_(self, request): - return instance_mock(request, Part) - - def rel(self, request, is_external, target_part, name): - return instance_mock( - request, - _Relationship, - is_external=is_external, - target_part=target_part, - name=name, - ) - @pytest.fixture def relationships_(self, request): return instance_mock(request, _Relationships) - def rels(self, request, values): - rels = instance_mock(request, _Relationships) - rels.__iter__.return_value = iter(values) - return rels - - @pytest.fixture - def rels_(self, request): - return instance_mock(request, _Relationships) - @pytest.fixture def _rels_prop_(self, request): return property_mock(request, OpcPackage, "_rels") From b2b2b405fbb413a23e10828efcfbcbffdc04af0c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Aug 2021 22:45:35 -0700 Subject: [PATCH 12/69] rfctr: make OpcPackage a function object Store `pkg_file` as an instance variable and use standard classmethod form for `.open()` interface method. --- features/steps/picture.py | 2 +- pptx/opc/package.py | 16 ++++++---- tests/opc/test_package.py | 67 +++++++++++++++++++++------------------ tests/test_package.py | 10 +++--- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/features/steps/picture.py b/features/steps/picture.py index 282d21743..350e939c5 100644 --- a/features/steps/picture.py +++ b/features/steps/picture.py @@ -60,7 +60,7 @@ def when_I_assign_member_to_picture_auto_shape_type(context, member): @then("a {ext} image part appears in the pptx file") def step_then_a_ext_image_part_appears_in_the_pptx_file(context, ext): - pkg = Package().open(saved_pptx_path) + pkg = Package.open(saved_pptx_path) partnames = [part.partname for part in pkg.parts] image_partname = "/ppt/media/image1.%s" % ext assert image_partname in partnames, "got %s" % [ diff --git a/pptx/opc/package.py b/pptx/opc/package.py index ededbf4df..355b47005 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -24,16 +24,13 @@ class OpcPackage(object): to a package file or file-like object containing a package (.pptx file). """ - def __init__(self): - super(OpcPackage, self).__init__() + def __init__(self, pkg_file): + self._pkg_file = pkg_file @classmethod def open(cls, pkg_file): """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" - pkg_reader = PackageReader.from_file(pkg_file) - package = cls() - Unmarshaller.unmarshal(pkg_reader, package, PartFactory) - return package + return cls(pkg_file)._load() def iter_parts(self): """Generate exactly one reference to each part in the package.""" @@ -141,6 +138,13 @@ def save(self, pkg_file): """ PackageWriter.write(pkg_file, self._rels, self.parts) + def _load(self): + """Return the package after loading all parts and relationships.""" + Unmarshaller.unmarshal( + PackageReader.from_file(self._pkg_file), self, PartFactory + ) + return self + @lazyproperty def _rels(self): """The |_Relationships| object containing the relationships for this package.""" diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index dc5538333..0066ed060 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -31,6 +31,7 @@ from ..unitutil.cxml import element from ..unitutil.file import absjoin, snippet_bytes, test_file_dir from ..unitutil.mock import ( + ANY, call, class_mock, cls_attr_mock, @@ -48,16 +49,16 @@ class DescribeOpcPackage(object): """Unit-test suite for `pptx.opc.package.OpcPackage` objects.""" - def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): - # mockery ---------------------- - pkg_file = Mock(name="pkg_file") - pkg_reader = PackageReader_.from_file.return_value - # exercise --------------------- - pkg = OpcPackage.open(pkg_file) - # verify ----------------------- - PackageReader_.from_file.assert_called_once_with(pkg_file) - Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, PartFactory_) - assert isinstance(pkg, OpcPackage) + def it_can_open_a_pkg_file(self, request): + package_ = instance_mock(request, OpcPackage) + _init_ = initializer_mock(request, OpcPackage) + _load_ = method_mock(request, OpcPackage, "_load", return_value=package_) + + package = OpcPackage.open("package.pptx") + + _init_.assert_called_once_with(ANY, "package.pptx") + _load_.assert_called_once_with(ANY) + assert package is package_ def it_can_iterate_over_its_parts(self, request): part_, part_2_ = [ @@ -77,7 +78,7 @@ def it_can_iterate_over_its_parts(self, request): ) ) method_mock(request, OpcPackage, "iter_rels", return_value=rels_iter) - package = OpcPackage() + package = OpcPackage(None) assert list(package.iter_parts()) == [part_, part_2_] @@ -118,7 +119,7 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): _rels_prop_.return_value = rels[:3] part_0_.rels = rels[3:4] part_1_.rels = rels[4:] - package = OpcPackage() + package = OpcPackage(None) assert tuple(package.iter_rels()) == ( rels[0], @@ -133,7 +134,7 @@ def it_can_add_a_relationship_to_a_part(self, request, _rels_prop_, relationship relationship_ = instance_mock(request, _Relationship) relationships_.add_relationship.return_value = relationship_ target_ = instance_mock(request, Part, name="target_part") - package = OpcPackage() + package = OpcPackage(None) relationship = package.load_rel(RT.SLIDE, target_, "rId99") @@ -148,7 +149,7 @@ def it_can_establish_a_relationship_to_another_part( relationships_.get_or_add.return_value = "rId99" _rels_prop_.return_value = relationships_ part_ = instance_mock(request, Part) - package = OpcPackage() + package = OpcPackage(None) rId = package.relate_to(part_, "http://rel/type") @@ -158,7 +159,7 @@ def it_can_establish_a_relationship_to_another_part( def it_can_provide_a_list_of_the_parts_it_contains(self): # mockery ---------------------- parts = [Mock(name="part1"), Mock(name="part2")] - pkg = OpcPackage() + pkg = OpcPackage(None) # verify ----------------------- with patch.object(OpcPackage, "iter_parts", return_value=parts): assert pkg.parts == [parts[0], parts[1]] @@ -169,7 +170,7 @@ def it_can_find_a_part_related_by_reltype( related_part_ = instance_mock(request, Part, name="related_part_") relationships_.part_with_reltype.return_value = related_part_ _rels_prop_.return_value = relationships_ - package = OpcPackage() + package = OpcPackage(None) related_part = package.part_related_by(RT.SLIDE) @@ -186,7 +187,7 @@ def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") _rels_prop_.return_value = relationships_ property_mock(request, OpcPackage, "parts", return_value=["parts"]) - package = OpcPackage() + package = OpcPackage(None) package.save("prs.pptx") @@ -194,11 +195,27 @@ def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): "prs.pptx", relationships_, ["parts"] ) + def it_loads_the_pkg_file_to_help(self, request): + package_reader_ = instance_mock(request, PackageReader) + PackageReader_ = class_mock(request, "pptx.opc.package.PackageReader") + PackageReader_.from_file.return_value = package_reader_ + PartFactory_ = class_mock(request, "pptx.opc.package.PartFactory") + Unmarshaller_ = class_mock(request, "pptx.opc.package.Unmarshaller") + package = OpcPackage("prs.pptx") + + return_value = package._load() + + PackageReader_.from_file.assert_called_once_with("prs.pptx") + Unmarshaller_.unmarshal.assert_called_once_with( + package_reader_, package, PartFactory_ + ) + assert return_value is package + def it_constructs_its_relationships_object_to_help(self, request, relationships_): _Relationships_ = class_mock( request, "pptx.opc.package._Relationships", return_value=relationships_ ) - package = OpcPackage() + package = OpcPackage(None) rels = package._rels @@ -210,7 +227,7 @@ def it_constructs_its_relationships_object_to_help(self, request, relationships_ @pytest.fixture(params=[((), 1), ((1,), 2), ((1, 2), 3), ((2, 3), 1), ((1, 3), 2)]) def next_partname_fixture(self, request, iter_parts_): existing_partname_numbers, next_partname_number = request.param - package = OpcPackage() + package = OpcPackage(None) parts = [ instance_mock( request, Part, name="part[%d]" % idx, partname="/foo/bar/baz%d.xml" % n @@ -228,14 +245,6 @@ def next_partname_fixture(self, request, iter_parts_): def iter_parts_(self, request): return method_mock(request, OpcPackage, "iter_parts") - @pytest.fixture - def PackageReader_(self, request): - return class_mock(request, "pptx.opc.package.PackageReader") - - @pytest.fixture - def PartFactory_(self, request): - return class_mock(request, "pptx.opc.package.PartFactory") - @pytest.fixture def relationships_(self, request): return instance_mock(request, _Relationships) @@ -244,10 +253,6 @@ def relationships_(self, request): def _rels_prop_(self, request): return property_mock(request, OpcPackage, "_rels") - @pytest.fixture - def Unmarshaller_(self, request): - return class_mock(request, "pptx.opc.package.Unmarshaller") - class DescribePart(object): """Unit-test suite for `pptx.opc.package.Part` objects.""" diff --git a/tests/test_package.py b/tests/test_package.py index 2bdf78ce1..c8d739f63 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -56,7 +56,7 @@ def it_provides_access_to_its_MediaParts_object(self, m_parts_fixture): @pytest.fixture def image_part_fixture(self, image_parts_, image_part_, _image_parts_prop_): - package = Package() + package = Package(None) image_file = "foobar.png" _image_parts_prop_.return_value = image_parts_ image_parts_.get_or_add_image_part.return_value = image_part_ @@ -64,21 +64,21 @@ def image_part_fixture(self, image_parts_, image_part_, _image_parts_prop_): @pytest.fixture def media_part_fixture(self, media_, media_part_, _media_parts_prop_, media_parts_): - package = Package() + package = Package(None) _media_parts_prop_.return_value = media_parts_ media_parts_.get_or_add_media_part.return_value = media_part_ return package, media_, media_part_ @pytest.fixture def m_parts_fixture(self, _MediaParts_, media_parts_): - package = Package() + package = Package(None) _MediaParts_.return_value = media_parts_ return package, _MediaParts_, media_parts_ @pytest.fixture(params=[((3, 4, 2), 1), ((4, 2, 1), 3), ((2, 3, 1), 4)]) def next_fixture(self, request, iter_parts_): idxs, idx = request.param - package = Package() + package = Package(None) package.iter_parts.return_value = self.i_image_parts(request, idxs) ext = "foo" expected_value = "/ppt/media/image%d.%s" % (idx, ext) @@ -87,7 +87,7 @@ def next_fixture(self, request, iter_parts_): @pytest.fixture(params=[((3, 4, 2), 1), ((4, 2, 1), 3), ((2, 3, 1), 4)]) def nmp_fixture(self, request, iter_parts_): idxs, idx = request.param - package = Package() + package = Package(None) package.iter_parts.return_value = self.i_media_parts(request, idxs) ext = "foo" expected_value = "/ppt/media/media%d.%s" % (idx, ext) From bf59ad699eb11b2aadf858d7d49269bea4d8d903 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Aug 2021 23:31:48 -0700 Subject: [PATCH 13/69] rfctr: improve OpcPackage.next_partname() --- pptx/opc/package.py | 10 ++++++-- tests/opc/test_package.py | 48 ++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 355b47005..e8d71f1ab 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -97,8 +97,14 @@ def next_partname(self, tmpl): item, a '%d' to be used to insert the integer portion of the partname. Example: '/ppt/slides/slide%d.xml' """ - partnames = [part.partname for part in self.iter_parts()] - for n in range(1, len(partnames) + 2): + # --- 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) + ) + for n in range(len(partnames) + 1, 0, -1): candidate_partname = tmpl % n if candidate_partname not in partnames: return PackURI(candidate_partname) diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 0066ed060..f3791f20b 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -177,11 +177,28 @@ def it_can_find_a_part_related_by_reltype( relationships_.part_with_reltype.assert_called_once_with(RT.SLIDE) assert related_part is related_part_ - def it_can_find_the_next_available_vector_partname(self, next_partname_fixture): - package, partname_template, expected_partname = next_partname_fixture - partname = package.next_partname(partname_template) - assert isinstance(partname, PackURI) - assert partname == expected_partname + @pytest.mark.parametrize( + "ns, expected_n", + (((), 1), ((1,), 2), ((1, 2), 3), ((2, 4), 3), ((1, 4), 3)), + ) + def it_can_find_the_next_available_partname(self, request, ns, expected_n): + tmpl = "/x%d.xml" + method_mock( + request, + OpcPackage, + "iter_parts", + return_value=(instance_mock(request, Part, partname=tmpl % n) for n in ns), + ) + next_partname = tmpl % expected_n + PackURI_ = class_mock( + request, "pptx.opc.package.PackURI", return_value=PackURI(next_partname) + ) + package = OpcPackage(None) + + partname = package.next_partname(tmpl) + + PackURI_.assert_called_once_with(next_partname) + assert partname == next_partname def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") @@ -222,29 +239,8 @@ def it_constructs_its_relationships_object_to_help(self, request, relationships_ _Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) assert rels is relationships_ - # fixtures --------------------------------------------- - - @pytest.fixture(params=[((), 1), ((1,), 2), ((1, 2), 3), ((2, 3), 1), ((1, 3), 2)]) - def next_partname_fixture(self, request, iter_parts_): - existing_partname_numbers, next_partname_number = request.param - package = OpcPackage(None) - parts = [ - instance_mock( - request, Part, name="part[%d]" % idx, partname="/foo/bar/baz%d.xml" % n - ) - for idx, n in enumerate(existing_partname_numbers) - ] - iter_parts_.return_value = iter(parts) - partname_template = "/foo/bar/baz%d.xml" - expected_partname = PackURI("/foo/bar/baz%d.xml" % next_partname_number) - return package, partname_template, expected_partname - # fixture components ----------------------------------- - @pytest.fixture - def iter_parts_(self, request): - return method_mock(request, OpcPackage, "iter_parts") - @pytest.fixture def relationships_(self, request): return instance_mock(request, _Relationships) From aacda7eacd1c6ab02538aec7cbede5be4c2d4ee4 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 23 Aug 2021 12:38:47 -0700 Subject: [PATCH 14/69] rfctr: remove OpcPackage.parts There's no need for both `.iter_parts()` and `.parts`. `.iter_parts()` is needed for other things and is in general more performant (because it can be short-circuited), so choose that one and drop `.parts`. --- features/steps/picture.py | 8 ++------ pptx/opc/package.py | 10 +--------- tests/opc/test_package.py | 18 ++++-------------- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/features/steps/picture.py b/features/steps/picture.py index 350e939c5..ef6dbbe75 100644 --- a/features/steps/picture.py +++ b/features/steps/picture.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Gherkin step implementations for picture-related features. -""" - -from __future__ import absolute_import +"""Gherkin step implementations for picture-related features.""" from behave import given, when, then @@ -61,7 +57,7 @@ def when_I_assign_member_to_picture_auto_shape_type(context, member): @then("a {ext} image part appears in the pptx file") def step_then_a_ext_image_part_appears_in_the_pptx_file(context, ext): pkg = Package.open(saved_pptx_path) - partnames = [part.partname for part in pkg.parts] + 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 diff --git a/pptx/opc/package.py b/pptx/opc/package.py index e8d71f1ab..dc9af4814 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -118,14 +118,6 @@ def part_related_by(self, reltype): """ return self._rels.part_with_reltype(reltype) - @property - def parts(self): - """ - Return a list containing a reference to each of the parts in this - package. - """ - return [part for part in self.iter_parts()] - def relate_to(self, target, reltype, is_external=False): """Return rId key of relationship of `reltype` to `target`. @@ -142,7 +134,7 @@ def save(self, pkg_file): `pkg_file` can be either a path to a file (a string) or a file-like object. """ - PackageWriter.write(pkg_file, self._rels, self.parts) + PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts())) def _load(self): """Return the package after loading all parts and relationships.""" diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index f3791f20b..0b77dddfe 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -41,7 +41,6 @@ loose_mock, method_mock, Mock, - patch, property_mock, ) @@ -156,14 +155,6 @@ def it_can_establish_a_relationship_to_another_part( relationships_.get_or_add.assert_called_once_with("http://rel/type", part_) assert rId == "rId99" - def it_can_provide_a_list_of_the_parts_it_contains(self): - # mockery ---------------------- - parts = [Mock(name="part1"), Mock(name="part2")] - pkg = OpcPackage(None) - # verify ----------------------- - with patch.object(OpcPackage, "iter_parts", return_value=parts): - assert pkg.parts == [parts[0], parts[1]] - def it_can_find_a_part_related_by_reltype( self, request, _rels_prop_, relationships_ ): @@ -201,16 +192,15 @@ def it_can_find_the_next_available_partname(self, request, ns, expected_n): assert partname == next_partname def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): - PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") _rels_prop_.return_value = relationships_ - property_mock(request, OpcPackage, "parts", return_value=["parts"]) + parts_ = tuple(instance_mock(request, Part) for _ in range(3)) + method_mock(request, OpcPackage, "iter_parts", return_value=iter(parts_)) + PackageWriter_ = class_mock(request, "pptx.opc.package.PackageWriter") package = OpcPackage(None) package.save("prs.pptx") - PackageWriter_.write.assert_called_once_with( - "prs.pptx", relationships_, ["parts"] - ) + PackageWriter_.write.assert_called_once_with("prs.pptx", relationships_, parts_) def it_loads_the_pkg_file_to_help(self, request): package_reader_ = instance_mock(request, PackageReader) From fb9e2b2cf2c455c01f619c4dba325c1328a29980 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 23 Aug 2021 13:25:07 -0700 Subject: [PATCH 15/69] rfctr: extract _PackageLoader --- pptx/opc/package.py | 45 +++++++++++++++++++++++++++++++++--- pptx/opc/serialized.py | 13 +++++++++++ tests/opc/test_package.py | 48 ++++++++++++++++++++++++++++++--------- 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index dc9af4814..60a762c5e 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -138,9 +138,8 @@ def save(self, pkg_file): def _load(self): """Return the package after loading all parts and relationships.""" - Unmarshaller.unmarshal( - PackageReader.from_file(self._pkg_file), self, PartFactory - ) + pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, self) + self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts) return self @lazyproperty @@ -149,6 +148,46 @@ def _rels(self): return _Relationships(PACKAGE_URI.baseURI) +class _PackageLoader(object): + """Function-object that loads a package from disk (or other store).""" + + def __init__(self, pkg_file, package): + self._pkg_file = pkg_file + self._package = package + + @classmethod + def load(cls, pkg_file, package): + """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`. + + The returned `parts` value is a {partname: part} mapping with each part in the + package included and constructed complete with its relationships to other parts + in the package. + + The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the + parsed package relationships. It is the caller's responsibility (the package + object) to load those relationships into its |_Relationships| object. + """ + return cls(pkg_file, package)._load() + + def _load(self): + """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file.""" + # --- ugly temporary hack to make this interim `._load()` method produce the + # --- same result as the one that's coming a few commits later. + package = self._package + Unmarshaller.unmarshal(self._package_reader, package, PartFactory) + + pkg_xml_rels = parse_xml( + self._package_reader.rels_xml_for(self._pkg_file, PACKAGE_URI) + ) + + return pkg_xml_rels, {p.partname: p for p in package.iter_parts()} + + @lazyproperty + def _package_reader(self): + """|PackageReader| object providing access to package-items in pkg_file.""" + return PackageReader.from_file(self._pkg_file) + + class Part(object): """Base class for package parts. diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index 0b9517ec0..14293dcfb 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -58,6 +58,19 @@ def iter_srels(self): for srel in spart.srels: yield (spart.partname, srel) + def rels_xml_for(self, pkg_file, partname): + """Return optional rels item XML for `partname`. + + Returns `None` if no rels item is present for `partname`. `partname` is a + |PackURI| instance. + """ + # --- ugly temporary hack to make this interim `._rels_xml_for()` method + # --- produce the same result as the one that's coming a few commits later. + phys_reader = _PhysPkgReader(pkg_file) + rels_xml = phys_reader.rels_xml_for(partname) + phys_reader.close() + return rels_xml + @staticmethod def _load_serialized_parts(phys_reader, pkg_srels, content_types): """ diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 0b77dddfe..bca8d69e1 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -18,10 +18,11 @@ OpcPackage, Part, PartFactory, - _Relationship, - _Relationships, Unmarshaller, XmlPart, + _PackageLoader, + _Relationship, + _Relationships, ) from pptx.opc.serialized import PackageReader from pptx.oxml import parse_xml @@ -202,19 +203,17 @@ def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): PackageWriter_.write.assert_called_once_with("prs.pptx", relationships_, parts_) - def it_loads_the_pkg_file_to_help(self, request): - package_reader_ = instance_mock(request, PackageReader) - PackageReader_ = class_mock(request, "pptx.opc.package.PackageReader") - PackageReader_.from_file.return_value = package_reader_ - PartFactory_ = class_mock(request, "pptx.opc.package.PartFactory") - Unmarshaller_ = class_mock(request, "pptx.opc.package.Unmarshaller") + def it_loads_the_pkg_file_to_help(self, request, _rels_prop_, relationships_): + _PackageLoader_ = class_mock(request, "pptx.opc.package._PackageLoader") + _PackageLoader_.load.return_value = "pkg-rels-xml", {"partname": "part"} + _rels_prop_.return_value = relationships_ package = OpcPackage("prs.pptx") return_value = package._load() - PackageReader_.from_file.assert_called_once_with("prs.pptx") - Unmarshaller_.unmarshal.assert_called_once_with( - package_reader_, package, PartFactory_ + _PackageLoader_.load.assert_called_once_with("prs.pptx", package) + relationships_.load_from_xml.assert_called_once_with( + PACKAGE_URI, "pkg-rels-xml", {"partname": "part"} ) assert return_value is package @@ -240,6 +239,33 @@ def _rels_prop_(self, request): return property_mock(request, OpcPackage, "_rels") +class Describe_PackageLoader(object): + """Unit-test suite for `pptx.opc.package._PackageLoader` objects.""" + + def it_provides_a_load_interface_classmethod(self, request, package_): + _init_ = initializer_mock(request, _PackageLoader) + pkg_xml_rels_ = element("r:Relationships") + _load_ = method_mock( + request, + _PackageLoader, + "_load", + return_value=(pkg_xml_rels_, {"partname": "part"}), + ) + + pkg_xml_rels, parts = _PackageLoader.load("prs.pptx", package_) + + _init_.assert_called_once_with(ANY, "prs.pptx", package_) + _load_.assert_called_once_with(ANY) + assert pkg_xml_rels is pkg_xml_rels_ + assert parts == {"partname": "part"} + + # fixture components ----------------------------------- + + @pytest.fixture + def package_(self, request): + return instance_mock(request, OpcPackage) + + class DescribePart(object): """Unit-test suite for `pptx.opc.package.Part` objects.""" From c082883d18dbde037115efdea787f5f5ab7d4688 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 23 Aug 2021 15:50:50 -0700 Subject: [PATCH 16/69] rfctr: subsume Unmarshaller into _PackageLoader --- pptx/opc/package.py | 78 +++++------- tests/opc/test_package.py | 255 +++++++++++++------------------------- 2 files changed, 115 insertions(+), 218 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 60a762c5e..55ed85ab2 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -173,20 +173,49 @@ def _load(self): """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file.""" # --- ugly temporary hack to make this interim `._load()` method produce the # --- same result as the one that's coming a few commits later. - package = self._package - Unmarshaller.unmarshal(self._package_reader, package, PartFactory) + self._unmarshal_relationships() pkg_xml_rels = parse_xml( self._package_reader.rels_xml_for(self._pkg_file, PACKAGE_URI) ) - return pkg_xml_rels, {p.partname: p for p in package.iter_parts()} + return pkg_xml_rels, self._parts @lazyproperty def _package_reader(self): """|PackageReader| object providing access to package-items in pkg_file.""" return PackageReader.from_file(self._pkg_file) + @lazyproperty + def _parts(self): + """Return a {partname: |Part|} dict unmarshalled from `pkg_reader`. + + Side-effect is that each part in `pkg_reader` is constructed using + `part_factory`. + """ + package = self._package + return { + partname: PartFactory(partname, content_type, blob, package) + for partname, content_type, blob in self._package_reader.iter_sparts() + } + + def _unmarshal_relationships(self): + """Add relationships to each source object. + + Source objects correspond to each relationship-target in `pkg_reader` with its + target_part set to the actual target part in `parts`. + """ + pkg_reader = self._package_reader + package = self._package + parts = self._parts + + for source_uri, srel in pkg_reader.iter_srels(): + source = package if source_uri == "/" else parts[source_uri] + target = ( + srel.target_ref if srel.is_external else parts[srel.target_partname] + ) + source.load_rel(srel.reltype, target, srel.rId, srel.is_external) + class Part(object): """Base class for package parts. @@ -572,49 +601,6 @@ def _rels_by_reltype(self): return D -class Unmarshaller(object): - """ - Hosts static methods for unmarshalling a package from a |PackageReader| - instance. - """ - - @staticmethod - def unmarshal(pkg_reader, package, part_factory): - """ - Construct graph of parts and realized relationships based on the - contents of *pkg_reader*, delegating construction of each part to - *part_factory*. Package relationships are added to *pkg*. - """ - parts = Unmarshaller._unmarshal_parts(pkg_reader, package, part_factory) - Unmarshaller._unmarshal_relationships(pkg_reader, package, parts) - - @staticmethod - def _unmarshal_parts(pkg_reader, package, part_factory): - """ - Return a dictionary of |Part| instances unmarshalled from - *pkg_reader*, keyed by partname. Side-effect is that each part in - *pkg_reader* is constructed using *part_factory*. - """ - parts = {} - for partname, content_type, blob in pkg_reader.iter_sparts(): - parts[partname] = part_factory(partname, content_type, blob, package) - return parts - - @staticmethod - def _unmarshal_relationships(pkg_reader, package, parts): - """ - Add a relationship to the source object corresponding to each of the - relationships in *pkg_reader* with its target_part set to the actual - target part in *parts*. - """ - for source_uri, srel in pkg_reader.iter_srels(): - source = package if source_uri == "/" else parts[source_uri] - target = ( - srel.target_ref if srel.is_external else parts[srel.target_partname] - ) - source.load_rel(srel.reltype, target, srel.rId, srel.is_external) - - class _Relationship(object): """Value object describing link from a part or package to another part.""" diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index bca8d69e1..af295bf0d 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -18,13 +18,12 @@ OpcPackage, Part, PartFactory, - Unmarshaller, XmlPart, _PackageLoader, _Relationship, _Relationships, ) -from pptx.opc.serialized import PackageReader +from pptx.opc.serialized import PackageReader, _SerializedRelationship from pptx.oxml import parse_xml from pptx.oxml.xmlchemy import BaseOxmlElement from pptx.package import Package @@ -39,7 +38,6 @@ function_mock, initializer_mock, instance_mock, - loose_mock, method_mock, Mock, property_mock, @@ -259,12 +257,94 @@ def it_provides_a_load_interface_classmethod(self, request, package_): assert pkg_xml_rels is pkg_xml_rels_ assert parts == {"partname": "part"} + def it_can_unmarshal_parts( + self, request, _package_reader_prop_, package_reader_, package_ + ): + _package_reader_prop_.return_value = package_reader_ + package_reader_.iter_sparts.return_value = ( + ("partname-%d" % n, CT.PML_SLIDE, b"blob-%d" % n) for n in range(1, 4) + ) + parts_ = tuple(instance_mock(request, Part) for _ in range(5)) + PartFactory_ = class_mock( + request, "pptx.opc.package.PartFactory", side_effect=iter(parts_) + ) + package_loader = _PackageLoader(None, package_) + + parts = package_loader._parts + + assert PartFactory_.call_args_list == [ + call("partname-1", CT.PML_SLIDE, b"blob-1", package_), + call("partname-2", CT.PML_SLIDE, b"blob-2", package_), + call("partname-3", CT.PML_SLIDE, b"blob-3", package_), + ] + assert parts == { + "partname-1": parts_[0], + "partname-2": parts_[1], + "partname-3": parts_[2], + } + + def it_can_unmarshal_relationships( + self, request, _package_reader_prop_, package_reader_, _parts_prop_, package_ + ): + _package_reader_prop_.return_value = package_reader_ + package_reader_.iter_srels.return_value = ( + ( + partname, + instance_mock( + request, + _SerializedRelationship, + name="srel_%d" % n, + rId="rId%d" % n, + reltype=reltype, + target_partname="partname_%d" % (n + 2), + target_ref="target_ref_%d" % n, + is_external=is_external, + ), + ) + for n, partname, reltype, is_external in ( + (1, "/", RT.SLIDE, False), + (2, "/", RT.HYPERLINK, True), + (3, "partname_1", RT.SLIDE, False), + (4, "partname_2", RT.HYPERLINK, True), + ) + ) + parts = _parts_prop_.return_value = { + "partname_%d" % n: instance_mock(request, Part, name="part_%d" % n) + for n in range(1, 7) + } + package_loader = _PackageLoader(None, package_) + + package_loader._unmarshal_relationships() + + assert package_.load_rel.call_args_list == [ + call(RT.SLIDE, parts["partname_3"], "rId1", False), + call(RT.HYPERLINK, "target_ref_2", "rId2", True), + ] + parts["partname_1"].load_rel.assert_called_once_with( + RT.SLIDE, parts["partname_5"], "rId3", False + ) + parts["partname_2"].load_rel.assert_called_once_with( + RT.HYPERLINK, "target_ref_4", "rId4", True + ) + # fixture components ----------------------------------- @pytest.fixture def package_(self, request): return instance_mock(request, OpcPackage) + @pytest.fixture + def package_reader_(self, request): + return instance_mock(request, PackageReader) + + @pytest.fixture + def _package_reader_prop_(self, request): + return property_mock(request, _PackageLoader, "_package_reader") + + @pytest.fixture + def _parts_prop_(self, request): + return property_mock(request, _PackageLoader, "_parts") + class DescribePart(object): """Unit-test suite for `pptx.opc.package.Part` objects.""" @@ -1024,172 +1104,3 @@ def and_it_knows_the_relative_partname_for_an_internal_rel(self, request): @pytest.fixture def part_(self, request): return instance_mock(request, Part) - - -class DescribeUnmarshaller(object): - def it_can_unmarshal_from_a_pkg_reader( - self, - pkg_reader_, - pkg_, - part_factory_, - _unmarshal_parts, - _unmarshal_relationships, - parts_dict_, - ): - Unmarshaller.unmarshal(pkg_reader_, pkg_, part_factory_) - - _unmarshal_parts.assert_called_once_with(pkg_reader_, pkg_, part_factory_) - _unmarshal_relationships.assert_called_once_with(pkg_reader_, pkg_, parts_dict_) - - def it_can_unmarshal_parts( - self, - pkg_reader_, - pkg_, - part_factory_, - parts_dict_, - partnames_, - content_types_, - blobs_, - ): - # fixture ---------------------- - partname_, partname_2_ = partnames_ - content_type_, content_type_2_ = content_types_ - blob_, blob_2_ = blobs_ - # exercise --------------------- - parts = Unmarshaller._unmarshal_parts(pkg_reader_, pkg_, part_factory_) - # verify ----------------------- - assert part_factory_.call_args_list == [ - call(partname_, content_type_, blob_, pkg_), - call(partname_2_, content_type_2_, blob_2_, pkg_), - ] - assert parts == parts_dict_ - - def it_can_unmarshal_relationships(self): - # test data -------------------- - reltype = "http://reltype" - # mockery ---------------------- - pkg_reader = Mock(name="pkg_reader") - pkg_reader.iter_srels.return_value = ( - ( - "/", - Mock( - name="srel1", - rId="rId1", - reltype=reltype, - target_partname="partname1", - is_external=False, - ), - ), - ( - "/", - Mock( - name="srel2", - rId="rId2", - reltype=reltype, - target_ref="target_ref_1", - is_external=True, - ), - ), - ( - "partname1", - Mock( - name="srel3", - rId="rId3", - reltype=reltype, - target_partname="partname2", - is_external=False, - ), - ), - ( - "partname2", - Mock( - name="srel4", - rId="rId4", - reltype=reltype, - target_ref="target_ref_2", - is_external=True, - ), - ), - ) - pkg = Mock(name="pkg") - parts = {} - for num in range(1, 3): - name = "part%d" % num - part = Mock(name=name) - parts["partname%d" % num] = part - pkg.attach_mock(part, name) - # exercise --------------------- - Unmarshaller._unmarshal_relationships(pkg_reader, pkg, parts) - # verify ----------------------- - expected_pkg_calls = [ - call.load_rel(reltype, parts["partname1"], "rId1", False), - call.load_rel(reltype, "target_ref_1", "rId2", True), - call.part1.load_rel(reltype, parts["partname2"], "rId3", False), - call.part2.load_rel(reltype, "target_ref_2", "rId4", True), - ] - assert pkg.mock_calls == expected_pkg_calls - - # fixtures --------------------------------------------- - - @pytest.fixture - def blobs_(self, request): - blob_ = loose_mock(request, spec=str, name="blob_") - blob_2_ = loose_mock(request, spec=str, name="blob_2_") - return blob_, blob_2_ - - @pytest.fixture - def content_types_(self, request): - content_type_ = loose_mock(request, spec=str, name="content_type_") - content_type_2_ = loose_mock(request, spec=str, name="content_type_2_") - return content_type_, content_type_2_ - - @pytest.fixture - def part_factory_(self, request, parts_): - part_factory_ = loose_mock(request, spec=Part) - part_factory_.side_effect = parts_ - return part_factory_ - - @pytest.fixture - def partnames_(self, request): - partname_ = loose_mock(request, spec=str, name="partname_") - partname_2_ = loose_mock(request, spec=str, name="partname_2_") - return partname_, partname_2_ - - @pytest.fixture - def parts_(self, request): - part_ = instance_mock(request, Part, name="part_") - part_2_ = instance_mock(request, Part, name="part_2") - return part_, part_2_ - - @pytest.fixture - def parts_dict_(self, request, partnames_, parts_): - partname_, partname_2_ = partnames_ - part_, part_2_ = parts_ - return {partname_: part_, partname_2_: part_2_} - - @pytest.fixture - def pkg_(self, request): - return instance_mock(request, Package) - - @pytest.fixture - def pkg_reader_(self, request, partnames_, content_types_, blobs_): - partname_, partname_2_ = partnames_ - content_type_, content_type_2_ = content_types_ - blob_, blob_2_ = blobs_ - spart_return_values = ( - (partname_, content_type_, blob_), - (partname_2_, content_type_2_, blob_2_), - ) - pkg_reader_ = instance_mock(request, PackageReader) - pkg_reader_.iter_sparts.return_value = spart_return_values - return pkg_reader_ - - @pytest.fixture - def _unmarshal_parts(self, request, parts_dict_): - return method_mock( - request, Unmarshaller, "_unmarshal_parts", return_value=parts_dict_ - ) - - @pytest.fixture - def _unmarshal_relationships(self, request): - return method_mock(request, Unmarshaller, "_unmarshal_relationships") From 81102ffc8836666f432fcea6923a4894abf4ab0a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 23 Aug 2021 20:22:50 -0700 Subject: [PATCH 17/69] pkg: add _PackageLoader._xml_rels This provides the basis for the rest of loading. --- pptx/opc/package.py | 38 +++++++++++++++++ tests/opc/test_package.py | 42 ++++++++++++++++++- .../test_files/snippets/package-rels-xml.txt | 7 ++++ .../snippets/presentation-rels-xml.txt | 6 +++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/test_files/snippets/package-rels-xml.txt create mode 100644 tests/test_files/snippets/presentation-rels-xml.txt diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 55ed85ab2..bfb9c59ce 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -216,6 +216,44 @@ def _unmarshal_relationships(self): ) source.load_rel(srel.reltype, target, srel.rId, srel.is_external) + @lazyproperty + def _xml_rels(self): + """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() + + def load_rels(source_partname, rels): + """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: + if rel.targetMode == RTM.EXTERNAL: + continue + target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref) + if target_partname in visited_partnames: + continue + load_rels(target_partname, self._xml_rels_for(target_partname)) + + load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI)) + return xml_rels + + def _xml_rels_for(self, partname): + """Return CT_Relationships object formed by parsing rels XML for `partname`. + + A CT_Relationships object is returned in all cases. A part that has no + relationships receives an "empty" CT_Relationships object, i.e. containing no + `CT_Relationship` objects. + """ + rels_xml = self._package_reader.rels_xml_for(self._pkg_file, partname) + return CT_Relationships.new() if rels_xml is None else parse_xml(rels_xml) + class Part(object): """Base class for package parts. diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index af295bf0d..0577944ec 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -12,7 +12,7 @@ RELATIONSHIP_TARGET_MODE as RTM, RELATIONSHIP_TYPE as RT, ) -from pptx.opc.oxml import CT_Relationship +from pptx.opc.oxml import CT_Relationship, CT_Relationships from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.opc.package import ( OpcPackage, @@ -327,6 +327,46 @@ def it_can_unmarshal_relationships( RT.HYPERLINK, "target_ref_4", "rId4", True ) + def it_loads_the_xml_relationships_from_the_package_to_help(self, request): + pkg_xml_rels = parse_xml(snippet_bytes("package-rels-xml")) + prs_xml_rels = parse_xml(snippet_bytes("presentation-rels-xml")) + slide_xml_rels = CT_Relationships.new() + thumbnail_xml_rels = CT_Relationships.new() + core_xml_rels = CT_Relationships.new() + _xml_rels_for_ = method_mock( + request, + _PackageLoader, + "_xml_rels_for", + side_effect=iter( + ( + pkg_xml_rels, + prs_xml_rels, + slide_xml_rels, + thumbnail_xml_rels, + core_xml_rels, + ) + ), + ) + package_loader = _PackageLoader(None, None) + + xml_rels = package_loader._xml_rels + + # print(f"{_xml_rels_for_.call_args_list=}") + assert _xml_rels_for_.call_args_list == [ + call(package_loader, "/"), + call(package_loader, "/ppt/presentation.xml"), + call(package_loader, "/ppt/slides/slide1.xml"), + call(package_loader, "/docProps/thumbnail.jpeg"), + call(package_loader, "/docProps/core.xml"), + ] + assert xml_rels == { + "/": pkg_xml_rels, + "/ppt/presentation.xml": prs_xml_rels, + "/ppt/slides/slide1.xml": slide_xml_rels, + "/docProps/thumbnail.jpeg": thumbnail_xml_rels, + "/docProps/core.xml": core_xml_rels, + } + # fixture components ----------------------------------- @pytest.fixture diff --git a/tests/test_files/snippets/package-rels-xml.txt b/tests/test_files/snippets/package-rels-xml.txt new file mode 100644 index 000000000..fc6731bc5 --- /dev/null +++ b/tests/test_files/snippets/package-rels-xml.txt @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/test_files/snippets/presentation-rels-xml.txt b/tests/test_files/snippets/presentation-rels-xml.txt new file mode 100644 index 000000000..bb27c2bfd --- /dev/null +++ b/tests/test_files/snippets/presentation-rels-xml.txt @@ -0,0 +1,6 @@ + + + + + + From ea07f47e0d93ce6c48c8ee02e97dc30da985705a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 23 Aug 2021 20:47:34 -0700 Subject: [PATCH 18/69] rfctr: make PackageReader a value object Use @lazyproperty in place of pre-computed instance variables. --- pptx/compat/__init__.py | 2 + pptx/opc/package.py | 2 +- pptx/opc/serialized.py | 74 ++++++++++++---------- tests/opc/test_serialized.py | 117 +---------------------------------- 4 files changed, 45 insertions(+), 150 deletions(-) diff --git a/pptx/compat/__init__.py b/pptx/compat/__init__.py index 415e9787e..61cd06c3d 100644 --- a/pptx/compat/__init__.py +++ b/pptx/compat/__init__.py @@ -7,9 +7,11 @@ import collections try: + Container = collections.abc.Container Mapping = collections.abc.Mapping Sequence = collections.abc.Sequence except AttributeError: + Container = collections.Container Mapping = collections.Mapping Sequence = collections.Sequence diff --git a/pptx/opc/package.py b/pptx/opc/package.py index bfb9c59ce..00e6ee459 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -184,7 +184,7 @@ def _load(self): @lazyproperty def _package_reader(self): """|PackageReader| object providing access to package-items in pkg_file.""" - return PackageReader.from_file(self._pkg_file) + return PackageReader(self._pkg_file) @lazyproperty def _parts(self): diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index 14293dcfb..89ce0a673 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -5,39 +5,29 @@ import os import zipfile -from pptx.compat import is_string +from pptx.compat import Container, is_string from pptx.exceptions import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM from pptx.opc.oxml import CT_Types, parse_xml, serialize_part_xml from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI from pptx.opc.shared import CaseInsensitiveDict from pptx.opc.spec import default_content_types +from pptx.util import lazyproperty -class PackageReader(object): - """ - Provides access to the contents of a zip-format OPC package via its - :attr:`serialized_parts` and :attr:`pkg_srels` attributes. +class PackageReader(Container): + """Provides access to package-parts of an OPC package with dict semantics. + + The package may be in zip-format (a .pptx file) or expanded into a directory + structure, perhaps by unzipping a .pptx file. """ - def __init__(self, content_types, pkg_srels, sparts): - super(PackageReader, self).__init__() - self._pkg_srels = pkg_srels - self._sparts = sparts + def __init__(self, pkg_file): + self._pkg_file = pkg_file - @staticmethod - def from_file(pkg_file): - """ - Return a |PackageReader| instance loaded with contents of *pkg_file*. - """ - phys_reader = _PhysPkgReader(pkg_file) - content_types = _ContentTypeMap.from_xml(phys_reader.content_types_xml) - pkg_srels = PackageReader._srels_for(phys_reader, PACKAGE_URI) - sparts = PackageReader._load_serialized_parts( - phys_reader, pkg_srels, content_types - ) - phys_reader.close() - return PackageReader(content_types, pkg_srels, sparts) + def __contains__(self, pack_uri): + """Return True when part identified by `pack_uri` is present in package.""" + return pack_uri in self._blob_reader def iter_sparts(self): """ @@ -71,23 +61,40 @@ def rels_xml_for(self, pkg_file, partname): phys_reader.close() return rels_xml - @staticmethod - def _load_serialized_parts(phys_reader, pkg_srels, content_types): - """ + @lazyproperty + def _content_types(self): + """Filty temporary hack during refactoring.""" + phys_reader = _PhysPkgReader(self._pkg_file) + return _ContentTypeMap.from_xml(phys_reader.content_types_xml) + phys_reader.close() + + @lazyproperty + def _pkg_srels(self): + """Filty temporary hack during refactoring.""" + phys_reader = _PhysPkgReader(self._pkg_file) + pkg_srels = self._srels_for(phys_reader, PACKAGE_URI) + phys_reader.close() + return pkg_srels + + @lazyproperty + def _sparts(self): + """Filty temporary hack during refactoring. + Return a list of |_SerializedPart| instances corresponding to the parts in *phys_reader* accessible by walking the relationship graph - starting with *pkg_srels*. + starting with `pkg_srels`. """ + phys_reader = _PhysPkgReader(self._pkg_file) sparts = [] - part_walker = PackageReader._walk_phys_parts(phys_reader, pkg_srels) + part_walker = self._walk_phys_parts(phys_reader, self._pkg_srels) for partname, blob, srels in part_walker: - content_type = content_types[partname] + content_type = self._content_types[partname] spart = _SerializedPart(partname, content_type, blob, srels) sparts.append(spart) + phys_reader.close() return tuple(sparts) - @staticmethod - def _srels_for(phys_reader, source_uri): + def _srels_for(self, phys_reader, source_uri): """ Return |_SerializedRelationshipCollection| instance populated with relationships for source identified by *source_uri*. @@ -97,8 +104,7 @@ def _srels_for(phys_reader, source_uri): source_uri.baseURI, rels_xml ) - @staticmethod - def _walk_phys_parts(phys_reader, srels, visited_partnames=None): + def _walk_phys_parts(self, phys_reader, srels, visited_partnames=None): """ Generate a 3-tuple `(partname, blob, srels)` for each of the parts in *phys_reader* by walking the relationship graph rooted at srels. @@ -112,10 +118,10 @@ def _walk_phys_parts(phys_reader, srels, visited_partnames=None): if partname in visited_partnames: continue visited_partnames.append(partname) - part_srels = PackageReader._srels_for(phys_reader, partname) + part_srels = self._srels_for(phys_reader, partname) blob = phys_reader.blob_for(partname) yield (partname, blob, part_srels) - for partname, blob, srels in PackageReader._walk_phys_parts( + for partname, blob, srels in self._walk_phys_parts( phys_reader, part_srels, visited_partnames ): yield (partname, blob, srels) diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 9f00f70f5..8de59f515 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -40,7 +40,6 @@ call, class_mock, function_mock, - initializer_mock, instance_mock, loose_mock, method_mock, @@ -56,92 +55,6 @@ class DescribePackageReader(object): """Unit-test suite for `pptx.opc.serialized.PackageReader` objects.""" - def it_can_construct_from_pkg_file( - self, init, _PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts - ): - - phys_reader = _PhysPkgReader_.return_value - content_types = from_xml.return_value - pkg_srels = _srels_for.return_value - sparts = _load_serialized_parts.return_value - pkg_file = Mock(name="pkg_file") - - pkg_reader = PackageReader.from_file(pkg_file) - - _PhysPkgReader_.assert_called_once_with(pkg_file) - from_xml.assert_called_once_with(phys_reader.content_types_xml) - _srels_for.assert_called_once_with(phys_reader, "/") - _load_serialized_parts.assert_called_once_with( - phys_reader, pkg_srels, content_types - ) - phys_reader.close.assert_called_once_with() - init.assert_called_once_with(pkg_reader, content_types, pkg_srels, sparts) - assert isinstance(pkg_reader, PackageReader) - - def it_can_iterate_over_the_serialized_parts(self): - # mockery ---------------------- - partname, content_type, blob = ("part/name.xml", "app/vnd.type", "") - spart = Mock( - name="spart", partname=partname, content_type=content_type, blob=blob - ) - pkg_reader = PackageReader(None, None, [spart]) - iter_count = 0 - # exercise --------------------- - for retval in pkg_reader.iter_sparts(): - iter_count += 1 - # verify ----------------------- - assert retval == (partname, content_type, blob) - assert iter_count == 1 - - def it_can_iterate_over_all_the_srels(self): - # mockery ---------------------- - pkg_srels = ["srel1", "srel2"] - sparts = [ - Mock(name="spart1", partname="pn1", srels=["srel3", "srel4"]), - Mock(name="spart2", partname="pn2", srels=["srel5", "srel6"]), - ] - pkg_reader = PackageReader(None, pkg_srels, sparts) - # exercise --------------------- - generated_tuples = [t for t in pkg_reader.iter_srels()] - # verify ----------------------- - expected_tuples = [ - ("/", "srel1"), - ("/", "srel2"), - ("pn1", "srel3"), - ("pn1", "srel4"), - ("pn2", "srel5"), - ("pn2", "srel6"), - ] - assert generated_tuples == expected_tuples - - def it_can_load_serialized_parts(self, _SerializedPart_, _walk_phys_parts): - # test data -------------------- - test_data = ( - ("/part/name1.xml", "app/vnd.type_1", "", "srels_1"), - ("/part/name2.xml", "app/vnd.type_2", "", "srels_2"), - ) - iter_vals = [(t[0], t[2], t[3]) for t in test_data] - content_types = dict((t[0], t[1]) for t in test_data) - # mockery ---------------------- - phys_reader = Mock(name="phys_reader") - pkg_srels = Mock(name="pkg_srels") - _walk_phys_parts.return_value = iter_vals - _SerializedPart_.side_effect = expected_sparts = ( - Mock(name="spart_1"), - Mock(name="spart_2"), - ) - # exercise --------------------- - retval = PackageReader._load_serialized_parts( - phys_reader, pkg_srels, content_types - ) - # verify ----------------------- - expected_calls = [ - call("/part/name1.xml", "app/vnd.type_1", "", "srels_1"), - call("/part/name2.xml", "app/vnd.type_2", "", "srels_2"), - ] - assert _SerializedPart_.call_args_list == expected_calls - assert retval == expected_sparts - def it_can_walk_phys_pkg_parts(self, _srels_for): # test data -------------------- # +----------+ +--------+ @@ -175,7 +88,7 @@ def it_can_walk_phys_pkg_parts(self, _srels_for): phys_reader.blob_for.side_effect = [part_1_blob, part_2_blob, part_3_blob] # exercise --------------------- generated_tuples = [ - t for t in PackageReader._walk_phys_parts(phys_reader, pkg_srels) + t for t in PackageReader(None)._walk_phys_parts(phys_reader, pkg_srels) ] # verify ----------------------- expected_tuples = [ @@ -195,7 +108,7 @@ def it_can_retrieve_srels_for_a_source_uri( load_from_xml = _SerializedRelationshipCollection_.load_from_xml srels = load_from_xml.return_value # exercise --------------------- - retval = PackageReader._srels_for(phys_reader, source_uri) + retval = PackageReader(None)._srels_for(phys_reader, source_uri) # verify ----------------------- phys_reader.rels_xml_for.assert_called_once_with(source_uri) load_from_xml.assert_called_once_with(source_uri.baseURI, rels_xml) @@ -203,28 +116,6 @@ def it_can_retrieve_srels_for_a_source_uri( # fixture components ----------------------------------- - @pytest.fixture - def from_xml(self, request): - return method_mock(request, _ContentTypeMap, "from_xml") - - @pytest.fixture - def init(self, request): - return initializer_mock(request, PackageReader) - - @pytest.fixture - def _load_serialized_parts(self, request): - return method_mock(request, PackageReader, "_load_serialized_parts") - - @pytest.fixture - def _PhysPkgReader_(self, request): - _patch = patch("pptx.opc.serialized._PhysPkgReader", spec_set=_ZipPkgReader) - request.addfinalizer(_patch.stop) - return _patch.start() - - @pytest.fixture - def _SerializedPart_(self, request): - return class_mock(request, "pptx.opc.serialized._SerializedPart") - @pytest.fixture def _SerializedRelationshipCollection_(self, request): return class_mock( @@ -235,10 +126,6 @@ def _SerializedRelationshipCollection_(self, request): def _srels_for(self, request): return method_mock(request, PackageReader, "_srels_for") - @pytest.fixture - def _walk_phys_parts(self, request): - return method_mock(request, PackageReader, "_walk_phys_parts") - class Describe_ContentTypeMap(object): def it_can_construct_from_ct_item_xml(self, from_xml_fixture): From 10ea993610dc45e99a37f9726a2d7fb83185e115 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 23 Aug 2021 21:56:56 -0700 Subject: [PATCH 19/69] rfctr: reimplement _PhysPkgReader and subtypes --- pptx/opc/package.py | 6 +- pptx/opc/serialized.py | 183 +++++++++++---------------- tests/opc/test_serialized.py | 239 ++++++++++++++--------------------- 3 files changed, 169 insertions(+), 259 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 00e6ee459..5f172ae00 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -175,9 +175,7 @@ def _load(self): # --- same result as the one that's coming a few commits later. self._unmarshal_relationships() - pkg_xml_rels = parse_xml( - self._package_reader.rels_xml_for(self._pkg_file, PACKAGE_URI) - ) + pkg_xml_rels = parse_xml(self._package_reader.rels_xml_for(PACKAGE_URI)) return pkg_xml_rels, self._parts @@ -251,7 +249,7 @@ def _xml_rels_for(self, partname): relationships receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects. """ - rels_xml = self._package_reader.rels_xml_for(self._pkg_file, partname) + rels_xml = self._package_reader.rels_xml_for(partname) return CT_Relationships.new() if rels_xml is None else parse_xml(rels_xml) diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index 89ce0a673..d47a6c57e 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -29,6 +29,10 @@ def __contains__(self, pack_uri): """Return True when part identified by `pack_uri` is present in package.""" return pack_uri in self._blob_reader + def __getitem__(self, pack_uri): + """Return bytes for part corresponding to `pack_uri`.""" + return self._blob_reader[pack_uri] + def iter_sparts(self): """ Generate a 3-tuple `(partname, content_type, blob)` for each of the @@ -48,63 +52,56 @@ def iter_srels(self): for srel in spart.srels: yield (spart.partname, srel) - def rels_xml_for(self, pkg_file, partname): + def rels_xml_for(self, partname): """Return optional rels item XML for `partname`. Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI| instance. """ - # --- ugly temporary hack to make this interim `._rels_xml_for()` method - # --- produce the same result as the one that's coming a few commits later. - phys_reader = _PhysPkgReader(pkg_file) - rels_xml = phys_reader.rels_xml_for(partname) - phys_reader.close() - return rels_xml + blob_reader, uri = self._blob_reader, partname.rels_uri + return blob_reader[uri] if uri in blob_reader else None + + @lazyproperty + def _blob_reader(self): + """|_PhysPkgReader| subtype providing read access to the package file.""" + return _PhysPkgReader.factory(self._pkg_file) @lazyproperty def _content_types(self): - """Filty temporary hack during refactoring.""" - phys_reader = _PhysPkgReader(self._pkg_file) - return _ContentTypeMap.from_xml(phys_reader.content_types_xml) - phys_reader.close() + """temporary hack during refactoring.""" + return _ContentTypeMap.from_xml(self._blob_reader[CONTENT_TYPES_URI]) @lazyproperty def _pkg_srels(self): """Filty temporary hack during refactoring.""" - phys_reader = _PhysPkgReader(self._pkg_file) - pkg_srels = self._srels_for(phys_reader, PACKAGE_URI) - phys_reader.close() - return pkg_srels + return self._srels_for(PACKAGE_URI) @lazyproperty def _sparts(self): - """Filty temporary hack during refactoring. - + """ Return a list of |_SerializedPart| instances corresponding to the parts in *phys_reader* accessible by walking the relationship graph - starting with `pkg_srels`. + starting with *pkg_srels*. """ - phys_reader = _PhysPkgReader(self._pkg_file) sparts = [] - part_walker = self._walk_phys_parts(phys_reader, self._pkg_srels) + part_walker = self._walk_phys_parts(self._pkg_srels) for partname, blob, srels in part_walker: content_type = self._content_types[partname] spart = _SerializedPart(partname, content_type, blob, srels) sparts.append(spart) - phys_reader.close() return tuple(sparts) - def _srels_for(self, phys_reader, source_uri): + def _srels_for(self, source_uri): """ Return |_SerializedRelationshipCollection| instance populated with relationships for source identified by *source_uri*. """ - rels_xml = phys_reader.rels_xml_for(source_uri) + rels_xml = self.rels_xml_for(source_uri) return _SerializedRelationshipCollection.load_from_xml( source_uri.baseURI, rels_xml ) - def _walk_phys_parts(self, phys_reader, srels, visited_partnames=None): + def _walk_phys_parts(self, srels, visited_partnames=None): """ Generate a 3-tuple `(partname, blob, srels)` for each of the parts in *phys_reader* by walking the relationship graph rooted at srels. @@ -118,11 +115,11 @@ def _walk_phys_parts(self, phys_reader, srels, visited_partnames=None): if partname in visited_partnames: continue visited_partnames.append(partname) - part_srels = self._srels_for(phys_reader, partname) - blob = phys_reader.blob_for(partname) + part_srels = self._srels_for(partname) + blob = self._blob_reader[partname] yield (partname, blob, part_srels) for partname, blob, srels in self._walk_phys_parts( - phys_reader, part_srels, visited_partnames + part_srels, visited_partnames ): yield (partname, blob, srels) @@ -232,114 +229,76 @@ def _write_pkg_rels(phys_writer, pkg_rels): phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) -class _PhysPkgReader(object): - """ - Factory for physical package reader objects. - """ +class _PhysPkgReader(Container): + """Base class for physical package reader objects.""" - def __new__(cls, pkg_file): - # if *pkg_file* is a string, treat it as a path - if is_string(pkg_file): - if os.path.isdir(pkg_file): - reader_cls = _DirPkgReader - elif zipfile.is_zipfile(pkg_file): - reader_cls = _ZipPkgReader - else: - raise PackageNotFoundError("Package not found at '%s'" % pkg_file) - else: # assume it's a stream and pass it to Zip reader to sort out - reader_cls = _ZipPkgReader + def __contains__(self, item): + """Must be implemented by each subclass.""" + raise NotImplementedError( + "`%s` must implement `.__contains__()`" % type(self).__name__ + ) + + @classmethod + def factory(cls, pkg_file): + """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): + return _ZipPkgReader(pkg_file) - return super(_PhysPkgReader, cls).__new__(reader_cls) + # --- otherwise we treat `pkg_file` as a path --- + if os.path.isdir(pkg_file): + return _DirPkgReader(pkg_file) + + if zipfile.is_zipfile(pkg_file): + return _ZipPkgReader(pkg_file) + + raise PackageNotFoundError("Package not found at '%s'" % pkg_file) class _DirPkgReader(_PhysPkgReader): - """ - Implements |PhysPkgReader| interface for an OPC package extracted into a - directory. + """Implements |PhysPkgReader| interface for OPC package extracted into directory. + + `path` is the path to a directory containing an expanded package. """ def __init__(self, path): - """ - *path* is the path to a directory containing an expanded package. - """ - super(_DirPkgReader, self).__init__() self._path = os.path.abspath(path) - def blob_for(self, pack_uri): - """ - Return contents of file corresponding to *pack_uri* in package - directory. - """ + def __getitem__(self, pack_uri): + """Return bytes of file corresponding to `pack_uri` in package directory.""" path = os.path.join(self._path, pack_uri.membername) - with open(path, "rb") as f: - blob = f.read() - return blob - - def close(self): - """ - Provides interface consistency with |ZipFileSystem|, but does - nothing, a directory file system doesn't need closing. - """ - pass - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri*, or None if the - item has no rels item. - """ try: - rels_xml = self.blob_for(source_uri.rels_uri) + with open(path, "rb") as f: + return f.read() except IOError: - rels_xml = None - return rels_xml + raise KeyError("no member '%s' in package" % pack_uri) class _ZipPkgReader(_PhysPkgReader): - """ - Implements |PhysPkgReader| interface for a zip file OPC package. - """ + """Implements |PhysPkgReader| interface for a zip-file OPC package.""" def __init__(self, pkg_file): - super(_ZipPkgReader, self).__init__() - self._zipf = zipfile.ZipFile(pkg_file, "r") + self._pkg_file = pkg_file - def blob_for(self, pack_uri): - """ - Return blob corresponding to *pack_uri*. Raises |ValueError| if no - matching member is present in zip archive. - """ - return self._zipf.read(pack_uri.membername) + def __contains__(self, pack_uri): + """Return True when part identified by `pack_uri` is present in zip archive.""" + return pack_uri in self._blobs - def close(self): - """ - Close the zip archive, releasing any resources it is using. - """ - self._zipf.close() + def __getitem__(self, pack_uri): + """Return bytes for part corresponding to `pack_uri`. - @property - def content_types_xml(self): + Raises |KeyError| if no matching member is present in zip archive. """ - Return the `[Content_Types].xml` blob from the zip package. - """ - return self.blob_for(CONTENT_TYPES_URI) + if pack_uri not in self._blobs: + raise KeyError("no member '%s' in package" % pack_uri) + return self._blobs[pack_uri] - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri* or None if no rels - item is present. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except KeyError: - rels_xml = None - return rels_xml + @lazyproperty + def _blobs(self): + """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): diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 8de59f515..ee98096da 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -16,7 +16,7 @@ from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM from pptx.opc.oxml import CT_Relationship from pptx.opc.package import Part -from pptx.opc.packuri import PACKAGE_URI, PackURI +from pptx.opc.packuri import PackURI from pptx.opc.serialized import ( PackageReader, PackageWriter, @@ -41,9 +41,9 @@ class_mock, function_mock, instance_mock, - loose_mock, method_mock, patch, + property_mock, ) @@ -55,76 +55,30 @@ class DescribePackageReader(object): """Unit-test suite for `pptx.opc.serialized.PackageReader` objects.""" - def it_can_walk_phys_pkg_parts(self, _srels_for): - # test data -------------------- - # +----------+ +--------+ - # | pkg_rels |-----> | part_1 | - # +----------+ +--------+ - # | | ^ - # v v | - # external +--------+ +--------+ - # | part_2 |---> | part_3 | - # +--------+ +--------+ - partname_1, partname_2, partname_3 = ( - "/part/name1.xml", - "/part/name2.xml", - "/part/name3.xml", - ) - part_1_blob, part_2_blob, part_3_blob = ("", "", "") - srels = [ - Mock(name="rId1", is_external=True), - Mock(name="rId2", is_external=False, target_partname=partname_1), - Mock(name="rId3", is_external=False, target_partname=partname_2), - Mock(name="rId4", is_external=False, target_partname=partname_1), - Mock(name="rId5", is_external=False, target_partname=partname_3), - ] - pkg_srels = srels[:2] - part_1_srels = srels[2:3] - part_2_srels = srels[3:5] - part_3_srels = [] - # mockery ---------------------- - phys_reader = Mock(name="phys_reader") - _srels_for.side_effect = [part_1_srels, part_2_srels, part_3_srels] - phys_reader.blob_for.side_effect = [part_1_blob, part_2_blob, part_3_blob] - # exercise --------------------- - generated_tuples = [ - t for t in PackageReader(None)._walk_phys_parts(phys_reader, pkg_srels) - ] - # verify ----------------------- - expected_tuples = [ - (partname_1, part_1_blob, part_1_srels), - (partname_2, part_2_blob, part_2_srels), - (partname_3, part_3_blob, part_3_srels), - ] - assert generated_tuples == expected_tuples + def it_knows_whether_it_contains_a_partname(self, _blob_reader_prop_): + _blob_reader_prop_.return_value = set(("/ppt", "/docProps")) + package_reader = PackageReader(None) - def it_can_retrieve_srels_for_a_source_uri( - self, _SerializedRelationshipCollection_ - ): - # mockery ---------------------- - phys_reader = Mock(name="phys_reader") - source_uri = Mock(name="source_uri") - rels_xml = phys_reader.rels_xml_for.return_value - load_from_xml = _SerializedRelationshipCollection_.load_from_xml - srels = load_from_xml.return_value - # exercise --------------------- - retval = PackageReader(None)._srels_for(phys_reader, source_uri) - # verify ----------------------- - phys_reader.rels_xml_for.assert_called_once_with(source_uri) - load_from_xml.assert_called_once_with(source_uri.baseURI, rels_xml) - assert retval == srels + assert "/ppt" in package_reader + assert "/xyz" not in package_reader - # fixture components ----------------------------------- + def it_can_get_a_blob_by_partname(self, _blob_reader_prop_): + _blob_reader_prop_.return_value = {"/ppt/slides/slide1.xml": b"blob"} + package_reader = PackageReader(None) - @pytest.fixture - def _SerializedRelationshipCollection_(self, request): - return class_mock( - request, "pptx.opc.serialized._SerializedRelationshipCollection" - ) + assert package_reader["/ppt/slides/slide1.xml"] == b"blob" + + def it_can_get_the_rels_xml_for_a_partname(self, _blob_reader_prop_): + _blob_reader_prop_.return_value = {"/ppt/_rels/presentation.xml.rels": b"blob"} + package_reader = PackageReader(None) + + assert package_reader.rels_xml_for(PackURI("/ppt/presentation.xml")) == b"blob" + + # fixture components ----------------------------------- @pytest.fixture - def _srels_for(self, request): - return method_mock(request, PackageReader, "_srels_for") + def _blob_reader_prop_(self, request): + return property_mock(request, PackageReader, "_blob_reader") class Describe_ContentTypeMap(object): @@ -325,105 +279,104 @@ def xml_for(self, request): return method_mock(request, _ContentTypesItem, "xml_for") -class Describe_DirPkgReader(object): - def it_is_used_by_PhysPkgReader_when_pkg_is_a_dir(self): - phys_reader = _PhysPkgReader(dir_pkg_path) - assert isinstance(phys_reader, _DirPkgReader) +class Describe_PhysPkgReader(object): + """Unit-test suite for `pptx.opc.serialized._PhysPkgReader` objects.""" - def it_doesnt_mind_being_closed_even_though_it_doesnt_need_it(self, dir_reader): - dir_reader.close() + def it_constructs_ZipPkgReader_when_pkg_is_file_like( + self, _ZipPkgReader_, zip_pkg_reader_ + ): + _ZipPkgReader_.return_value = zip_pkg_reader_ + file_like_pkg = BytesIO(b"pkg-bytes") - def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_reader): - pack_uri = PackURI("/ppt/presentation.xml") - blob = dir_reader.blob_for(pack_uri) - sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" + phys_reader = _PhysPkgReader.factory(file_like_pkg) - def it_can_get_the_content_types_xml(self, dir_reader): - sha1 = hashlib.sha1(dir_reader.content_types_xml).hexdigest() - assert sha1 == "a68cf138be3c4eb81e47e2550166f9949423c7df" + _ZipPkgReader_.assert_called_once_with(file_like_pkg) + assert phys_reader is zip_pkg_reader_ - def it_can_retrieve_the_rels_xml_for_a_source_uri(self, dir_reader): - rels_xml = dir_reader.rels_xml_for(PACKAGE_URI) - sha1 = hashlib.sha1(rels_xml).hexdigest() - assert sha1 == "64ffe86bb2bbaad53c3c1976042b907f8e10c5a3" + def and_it_constructs_DirPkgReader_when_pkg_is_a_dir(self, request): + dir_pkg_reader_ = instance_mock(request, _DirPkgReader) + _DirPkgReader_ = class_mock( + request, "pptx.opc.serialized._DirPkgReader", return_value=dir_pkg_reader_ + ) - def it_returns_none_when_part_has_no_rels_xml(self, dir_reader): - partname = PackURI("/ppt/viewProps.xml") - rels_xml = dir_reader.rels_xml_for(partname) - assert rels_xml is None + phys_reader = _PhysPkgReader.factory(dir_pkg_path) - # fixtures --------------------------------------------- + _DirPkgReader_.assert_called_once_with(dir_pkg_path) + assert phys_reader is dir_pkg_reader_ - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) + def and_it_constructs_ZipPkgReader_when_pkg_is_a_zip_file_path( + self, _ZipPkgReader_, zip_pkg_reader_ + ): + _ZipPkgReader_.return_value = zip_pkg_reader_ + pkg_file_path = test_pptx_path - @pytest.fixture(scope="class") - def dir_reader(self): - return _DirPkgReader(dir_pkg_path) + phys_reader = _PhysPkgReader.factory(pkg_file_path) + _ZipPkgReader_.assert_called_once_with(pkg_file_path) + assert phys_reader is zip_pkg_reader_ -class Describe_PhysPkgReader(object): - def it_raises_when_pkg_path_is_not_a_package(self): - with pytest.raises(PackageNotFoundError): - _PhysPkgReader("foobar") + def but_it_raises_when_pkg_path_is_not_a_package(self): + with pytest.raises(PackageNotFoundError) as e: + _PhysPkgReader.factory("foobar") + assert str(e.value) == "Package not found at 'foobar'" + # --- fixture components ------------------------------- -class Describe_ZipPkgReader(object): - def it_is_used_by_PhysPkgReader_when_pkg_is_a_zip(self): - phys_reader = _PhysPkgReader(zip_pkg_path) - assert isinstance(phys_reader, _ZipPkgReader) + @pytest.fixture + def zip_pkg_reader_(self, request): + return instance_mock(request, _ZipPkgReader) - def it_is_used_by_PhysPkgReader_when_pkg_is_a_stream(self): - with open(zip_pkg_path, "rb") as stream: - phys_reader = _PhysPkgReader(stream) - assert isinstance(phys_reader, _ZipPkgReader) + @pytest.fixture + def _ZipPkgReader_(self, request): + return class_mock(request, "pptx.opc.serialized._ZipPkgReader") - def it_opens_pkg_file_zip_on_construction(self, ZipFile_, pkg_file_): - _ZipPkgReader(pkg_file_) - ZipFile_.assert_called_once_with(pkg_file_, "r") - def it_can_be_closed(self, ZipFile_): - # mockery ---------------------- - zipf = ZipFile_.return_value - zip_pkg_reader = _ZipPkgReader(None) - # exercise --------------------- - zip_pkg_reader.close() - # verify ----------------------- - zipf.close.assert_called_once_with() +class Describe_DirPkgReader(object): + """Unit-test suite for `pptx.opc.serialized._DirPkgReader` objects.""" + + def it_can_retrieve_the_blob_for_a_pack_uri(self): + blob = _DirPkgReader(dir_pkg_path)[PackURI("/ppt/presentation.xml")] - def it_can_retrieve_the_blob_for_a_pack_uri(self, phys_reader): - pack_uri = PackURI("/ppt/presentation.xml") - blob = phys_reader.blob_for(pack_uri) sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "efa7bee0ac72464903a67a6744c1169035d52a54" + assert sha1 == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" - def it_has_the_content_types_xml(self, phys_reader): - sha1 = hashlib.sha1(phys_reader.content_types_xml).hexdigest() - assert sha1 == "ab762ac84414fce18893e18c3f53700c01db56c3" + def but_it_raises_KeyError_when_requested_member_is_not_present(self): + with pytest.raises(KeyError) as e: + _DirPkgReader(dir_pkg_path)[PackURI("/ppt/foobar.xml")] + assert str(e.value) == "\"no member '/ppt/foobar.xml' in package\"" - def it_can_retrieve_rels_xml_for_source_uri(self, phys_reader): - rels_xml = phys_reader.rels_xml_for(PACKAGE_URI) - sha1 = hashlib.sha1(rels_xml).hexdigest() - assert sha1 == "e31451d4bbe7d24adbe21454b8e9fdae92f50de5" - def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): - partname = PackURI("/ppt/viewProps.xml") - rels_xml = phys_reader.rels_xml_for(partname) - assert rels_xml is None +class Describe_ZipPkgReader(object): + """Unit-test suite for `pptx.opc.serialized._ZipPkgReader` objects.""" - # fixtures --------------------------------------------- + def it_knows_whether_it_contains_a_partname(self, zip_pkg_reader): + assert PackURI("/ppt/presentation.xml") in zip_pkg_reader + assert PackURI("/ppt/foobar.xml") not in zip_pkg_reader - @pytest.fixture(scope="class") - def phys_reader(self, request): - phys_reader = _ZipPkgReader(zip_pkg_path) - request.addfinalizer(phys_reader.close) - return phys_reader + def it_can_get_a_blob_by_partname(self, zip_pkg_reader): + blob = zip_pkg_reader[PackURI("/ppt/presentation.xml")] + assert hashlib.sha1(blob).hexdigest() == ( + "efa7bee0ac72464903a67a6744c1169035d52a54" + ) - @pytest.fixture - def pkg_file_(self, request): - return loose_mock(request) + def but_it_raises_KeyError_when_requested_member_is_not_present( + self, zip_pkg_reader + ): + 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): + blobs = zip_pkg_reader._blobs + assert len(blobs) == 38 + assert "/ppt/presentation.xml" in blobs + assert "/ppt/_rels/presentation.xml.rels" in blobs + + # --- fixture components ------------------------------- + + @pytest.fixture(scope="class") + def zip_pkg_reader(self, request): + return _ZipPkgReader(zip_pkg_path) class Describe_ZipPkgWriter(object): From 2fa9663370e451f90b22d6e01072127b99beba70 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 24 Aug 2021 13:08:35 -0700 Subject: [PATCH 20/69] rfctr: move `package` arg position left in Part All parts require a `package` argument. Move it to the left and remove any default to `None`. Rationale: `blob` can be `None` sometimes, like for an `XmlPart` instance. `package` is now required, so it must appear before `blob` (All positional parameters must precede all keyword params). --- pptx/opc/package.py | 21 +- pptx/package.py | 9 +- pptx/parts/chart.py | 13 +- pptx/parts/coreprops.py | 16 +- pptx/parts/embeddedpackage.py | 10 +- pptx/parts/image.py | 17 +- pptx/parts/media.py | 14 +- pptx/parts/slide.py | 46 ++-- tests/opc/test_package.py | 57 ++--- tests/parts/test_chart.py | 142 +++-------- tests/parts/test_coreprops.py | 22 +- tests/parts/test_embeddedpackage.py | 6 +- tests/parts/test_image.py | 58 ++--- tests/parts/test_media.py | 55 ++-- tests/parts/test_presentation.py | 238 ++++++------------ tests/parts/test_slide.py | 374 +++++++++------------------- 16 files changed, 376 insertions(+), 722 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 5f172ae00..13ffa8815 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -193,7 +193,7 @@ def _parts(self): """ package = self._package return { - partname: PartFactory(partname, content_type, blob, package) + partname: PartFactory(partname, content_type, package, blob) for partname, content_type, blob in self._package_reader.iter_sparts() } @@ -261,21 +261,21 @@ class Part(object): that are not yet given specific behaviors. """ - def __init__(self, partname, content_type, blob=None, package=None): - super(Part, self).__init__() + def __init__(self, partname, content_type, package, blob=None): + # --- XmlPart subtypes, don't store a blob (the original XML) --- self._partname = partname self._content_type = content_type - self._blob = blob self._package = package + self._blob = blob @classmethod - def load(cls, partname, content_type, blob, package): + def load(cls, partname, content_type, package, blob): """Return `cls` instance loaded from arguments. This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart for an example. """ - return cls(partname, content_type, blob, package) + return cls(partname, content_type, package, blob) @property def blob(self): @@ -403,15 +403,14 @@ class XmlPart(Part): reserializing the XML payload and managing relationships to other parts. """ - def __init__(self, partname, content_type, element, package=None): - super(XmlPart, self).__init__(partname, content_type, package=package) + def __init__(self, partname, content_type, package, element): + super(XmlPart, self).__init__(partname, content_type, package) self._element = element @classmethod - def load(cls, partname, content_type, blob, package): + def load(cls, partname, content_type, package, blob): """Return instance of `cls` loaded with parsed XML from `blob`.""" - element = parse_xml(blob) - return cls(partname, content_type, element, package) + return cls(partname, content_type, package, element=parse_xml(blob)) @property def blob(self): diff --git a/pptx/package.py b/pptx/package.py index 30bc9baf5..1855ec4a1 100644 --- a/pptx/package.py +++ b/pptx/package.py @@ -16,15 +16,14 @@ class Package(OpcPackage): @lazyproperty def core_properties(self): - """ - Instance of |CoreProperties| holding the read/write Dublin Core - document properties for this presentation. Creates a default core - properties part if one is not present (not common). + """Instance of |CoreProperties| holding read/write Dublin Core doc properties. + + Creates a default core properties part if one is not present (not common). """ try: return self.part_related_by(RT.CORE_PROPERTIES) except KeyError: - core_props = CorePropertiesPart.default() + core_props = CorePropertiesPart.default(self) self.relate_to(core_props, RT.CORE_PROPERTIES) return core_props diff --git a/pptx/parts/chart.py b/pptx/parts/chart.py index 202992233..890a25f28 100644 --- a/pptx/parts/chart.py +++ b/pptx/parts/chart.py @@ -23,12 +23,13 @@ def new(cls, chart_type, chart_data, package): Returned chart-part contains a chart of `chart_type` depicting `chart_data`. """ - chart_blob = chart_data.xml_bytes(chart_type) - partname = package.next_partname(cls.partname_template) - content_type = CT.DML_CHART - chart_part = cls.load(partname, content_type, chart_blob, package) - xlsx_blob = chart_data.xlsx_blob - chart_part.chart_workbook.update_from_xlsx_blob(xlsx_blob) + chart_part = cls.load( + package.next_partname(cls.partname_template), + CT.DML_CHART, + package, + chart_data.xml_bytes(chart_type), + ) + chart_part.chart_workbook.update_from_xlsx_blob(chart_data.xlsx_blob) return chart_part @lazyproperty diff --git a/pptx/parts/coreprops.py b/pptx/parts/coreprops.py index 7d1795bff..14fe583d6 100644 --- a/pptx/parts/coreprops.py +++ b/pptx/parts/coreprops.py @@ -17,13 +17,13 @@ class CorePropertiesPart(XmlPart): """ @classmethod - def default(cls): + def default(cls, package): """Return default new |CorePropertiesPart| instance suitable as starting point. This provides a base for adding core-properties to a package that doesn't yet have any. """ - core_props = cls._new() + core_props = cls._new(package) core_props.title = "PowerPoint Presentation" core_props.last_modified_by = "python-pptx" core_props.revision = 1 @@ -151,9 +151,11 @@ def version(self, value): self._element.version_text = value @classmethod - def _new(cls): + def _new(cls, package): """Return new empty |CorePropertiesPart| instance.""" - partname = PackURI("/docProps/core.xml") - content_type = CT.OPC_CORE_PROPERTIES - core_props_elm = CT_CoreProperties.new_coreProperties() - return CorePropertiesPart(partname, content_type, core_props_elm) + return CorePropertiesPart( + PackURI("/docProps/core.xml"), + CT.OPC_CORE_PROPERTIES, + package, + CT_CoreProperties.new_coreProperties(), + ) diff --git a/pptx/parts/embeddedpackage.py b/pptx/parts/embeddedpackage.py index 8754f61e5..a04cc224a 100644 --- a/pptx/parts/embeddedpackage.py +++ b/pptx/parts/embeddedpackage.py @@ -29,8 +29,8 @@ def factory(cls, prog_id, object_blob, package): return cls( package.next_partname("/ppt/embeddings/oleObject%d.bin"), CT.OFC_OLE_OBJECT, - object_blob, package, + object_blob, ) # --- A Microsoft Office file-type is a distinguished package object --- @@ -48,8 +48,12 @@ def new(cls, blob, package): The returned part object contains `blob` and is added to `package`. """ - partname = package.next_partname(cls.partname_template) - return cls(partname, cls.content_type, blob, package) + return cls( + package.next_partname(cls.partname_template), + cls.content_type, + package, + blob, + ) class EmbeddedDocxPart(EmbeddedPackagePart): diff --git a/pptx/parts/image.py b/pptx/parts/image.py index 1ce2f8337..578640cad 100644 --- a/pptx/parts/image.py +++ b/pptx/parts/image.py @@ -25,13 +25,13 @@ class ImagePart(Part): `ppt/media/image[1-9][0-9]*.*`. """ - def __init__(self, partname, content_type, blob, package, filename=None): - super(ImagePart, self).__init__(partname, content_type, blob, package) + def __init__(self, partname, content_type, package, blob, filename=None): + super(ImagePart, self).__init__(partname, content_type, package, blob) self._filename = filename @classmethod - def load(cls, partname, content_type, blob, package): - return cls(partname, content_type, blob, package) + def load(cls, partname, content_type, package, blob): + return cls(partname, content_type, package, blob) @classmethod def new(cls, package, image): @@ -39,8 +39,13 @@ def new(cls, package, image): `image` is an |Image| object. """ - partname = package.next_image_partname(image.ext) - return cls(partname, image.content_type, image.blob, package, image.filename) + return cls( + package.next_image_partname(image.ext), + image.content_type, + package, + image.blob, + image.filename, + ) @property def desc(self): diff --git a/pptx/parts/media.py b/pptx/parts/media.py index 768d890e0..81efb5a5d 100644 --- a/pptx/parts/media.py +++ b/pptx/parts/media.py @@ -12,17 +12,21 @@ class MediaPart(Part): """A media part, containing an audio or video resource. A media part generally has a partname matching the regex - ``ppt/media/media[1-9][0-9]*.*``. + `ppt/media/media[1-9][0-9]*.*`. """ @classmethod def new(cls, package, media): - """Return new |MediaPart| instance containing *media*. + """Return new |MediaPart| instance containing `media`. - *media* must be a |Media| object. + `media` must be a |Media| object. """ - partname = package.next_media_partname(media.ext) - return cls(partname, media.content_type, media.blob, package) + return cls( + package.next_media_partname(media.ext), + media.content_type, + package, + media.blob, + ) @lazyproperty def sha1(self): diff --git a/pptx/parts/slide.py b/pptx/parts/slide.py index c91b27a35..12d9ea319 100644 --- a/pptx/parts/slide.py +++ b/pptx/parts/slide.py @@ -78,18 +78,22 @@ def _new(cls, package): Create and return a standalone, default notes master part based on the built-in template (without any related parts, such as theme). """ - partname = PackURI("/ppt/notesMasters/notesMaster1.xml") - content_type = CT.PML_NOTES_MASTER - notesMaster = CT_NotesMaster.new_default() - return NotesMasterPart(partname, content_type, notesMaster, package) + return NotesMasterPart( + PackURI("/ppt/notesMasters/notesMaster1.xml"), + CT.PML_NOTES_MASTER, + package, + CT_NotesMaster.new_default(), + ) @classmethod def _new_theme_part(cls, package): """Return new default theme-part suitable for use with a notes master.""" - partname = package.next_partname("/ppt/theme/theme%d.xml") - content_type = CT.OFC_THEME - theme = CT_OfficeStyleSheet.new_default() - return XmlPart(partname, content_type, theme, package) + return XmlPart( + package.next_partname("/ppt/theme/theme%d.xml"), + CT.OFC_THEME, + package, + CT_OfficeStyleSheet.new_default(), + ) class NotesSlidePart(BaseSlidePart): @@ -128,14 +132,17 @@ def notes_slide(self): @classmethod def _add_notes_slide_part(cls, package, slide_part, notes_master_part): + """Create and return a new notes-slide part. + + The return part is fully related, but has no shape content (i.e. placeholders + not cloned). """ - Create and return a new notes slide part that is fully related, but - has no shape content (i.e. placeholders not cloned). - """ - partname = package.next_partname("/ppt/notesSlides/notesSlide%d.xml") - content_type = CT.PML_NOTES_SLIDE - notes = CT_NotesSlide.new() - notes_slide_part = NotesSlidePart(partname, content_type, notes, package) + notes_slide_part = NotesSlidePart( + package.next_partname("/ppt/notesSlides/notesSlide%d.xml"), + CT.PML_NOTES_SLIDE, + package, + CT_NotesSlide.new(), + ) notes_slide_part.relate_to(notes_master_part, RT.NOTES_MASTER) notes_slide_part.relate_to(slide_part, RT.SLIDE) return notes_slide_part @@ -146,12 +153,11 @@ class SlidePart(BaseSlidePart): @classmethod def new(cls, partname, package, slide_layout_part): + """Return newly-created blank slide part. + + The new slide-part has `partname` and a relationship to `slide_layout_part`. """ - Return a newly-created blank slide part having *partname* and related - to *slide_layout_part*. - """ - sld = CT_Slide.new() - slide_part = cls(partname, CT.PML_SLIDE, sld, package) + slide_part = cls(partname, CT.PML_SLIDE, package, CT_Slide.new()) slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT) return slide_part diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 0577944ec..c0572d32b 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -25,7 +25,6 @@ ) from pptx.opc.serialized import PackageReader, _SerializedRelationship from pptx.oxml import parse_xml -from pptx.oxml.xmlchemy import BaseOxmlElement from pptx.package import Package from ..unitutil.cxml import element @@ -273,9 +272,9 @@ def it_can_unmarshal_parts( parts = package_loader._parts assert PartFactory_.call_args_list == [ - call("partname-1", CT.PML_SLIDE, b"blob-1", package_), - call("partname-2", CT.PML_SLIDE, b"blob-2", package_), - call("partname-3", CT.PML_SLIDE, b"blob-3", package_), + call("partname-1", CT.PML_SLIDE, package_, b"blob-1"), + call("partname-2", CT.PML_SLIDE, package_, b"blob-2"), + call("partname-3", CT.PML_SLIDE, package_, b"blob-3"), ] assert parts == { "partname-1": parts_[0], @@ -398,9 +397,8 @@ def it_can_be_constructed_by_PartFactory(self, request, package_): _init_.assert_called_once_with(part, partname_, CT.PML_SLIDE, b"blob", package_) assert isinstance(part, Part) - def it_uses_the_load_blob_as_its_blob(self, blob_fixture): - part, load_blob = blob_fixture - assert part.blob is load_blob + def it_uses_the_load_blob_as_its_blob(self): + assert Part(None, None, None, b"blob").blob == b"blob" def it_can_change_its_blob(self): part, new_blob = Part(None, None, "xyz", None), "foobar" @@ -534,7 +532,7 @@ def load_rel_fixture(self, part, _rels_prop_, rels_, reltype_, part_, rId_): @pytest.fixture def package_get_fixture(self, package_): - part = Part(None, None, None, package_) + part = Part(None, None, package_) return part, package_ @pytest.fixture @@ -562,7 +560,7 @@ def related_part_fixture(self, part, _rels_prop_, rels_, reltype_, part_): @pytest.fixture def rels_fixture(self, Relationships_, partname_, rels_): - part = Part(partname_, None) + part = Part(partname_, None, None) return part, Relationships_, partname_, rels_ @pytest.fixture @@ -582,7 +580,7 @@ def package_(self, request): @pytest.fixture def part(self): - return Part(None, None) + return Part(None, None, None) @pytest.fixture def part_(self, request): @@ -647,43 +645,28 @@ def it_can_be_constructed_by_PartFactory(self, request): ) _init_ = initializer_mock(request, XmlPart) - part = XmlPart.load(partname, CT.PML_SLIDE, b"blob", package_) + part = XmlPart.load(partname, CT.PML_SLIDE, package_, b"blob") parse_xml_.assert_called_once_with(b"blob") - _init_.assert_called_once_with(part, partname, CT.PML_SLIDE, element_, package_) + _init_.assert_called_once_with(part, partname, CT.PML_SLIDE, package_, element_) assert isinstance(part, XmlPart) - def it_can_serialize_to_xml(self, blob_fixture): - xml_part, element_, serialize_part_xml_ = blob_fixture + def it_can_serialize_to_xml(self, request): + element_ = element("p:sld") + serialize_part_xml_ = function_mock( + request, "pptx.opc.package.serialize_part_xml" + ) + xml_part = XmlPart(None, None, None, element_) + blob = xml_part.blob + serialize_part_xml_.assert_called_once_with(element_) assert blob is serialize_part_xml_.return_value - def it_knows_its_the_part_for_its_child_objects(self, part_fixture): - xml_part = part_fixture + 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 blob_fixture(self, request, element_, serialize_part_xml_): - xml_part = XmlPart(None, None, element_, None) - return xml_part, element_, serialize_part_xml_ - - @pytest.fixture - def part_fixture(self): - return XmlPart(None, None, None, None) - - # fixture components --------------------------------------------- - - @pytest.fixture - def element_(self, request): - return instance_mock(request, BaseOxmlElement) - - @pytest.fixture - def serialize_part_xml_(self, request): - return function_mock(request, "pptx.opc.package.serialize_part_xml") - class DescribePartFactory(object): """Unit-test suite for `pptx.opc.package.PartFactory` objects.""" diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index aaa7e39e7..5f9428857 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -6,7 +6,7 @@ from pptx.chart.chart import Chart from pptx.chart.data import ChartData -from pptx.enum.base import EnumValue +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.package import OpcPackage from pptx.opc.packuri import PackURI @@ -21,134 +21,54 @@ class DescribeChartPart(object): """Unit-test suite for `pptx.parts.chart.ChartPart` objects.""" - def it_can_construct_from_chart_type_and_data(self, new_fixture): - chart_type_, chart_data_, package_ = new_fixture[:3] - partname_template, load_, partname_ = new_fixture[3:6] - content_type, chart_blob_, chart_part_, xlsx_blob_ = new_fixture[6:] + def it_can_construct_from_chart_type_and_data(self, request): + chart_data_ = instance_mock(request, ChartData, xlsx_blob=b"xlsx-blob") + chart_data_.xml_bytes.return_value = b"chart-blob" + package_ = instance_mock(request, OpcPackage) + package_.next_partname.return_value = PackURI("/ppt/charts/chart42.xml") + chart_part_ = instance_mock(request, ChartPart) + load_ = method_mock(request, ChartPart, "load", return_value=chart_part_) - chart_part = ChartPart.new(chart_type_, chart_data_, package_) + chart_part = ChartPart.new(XCT.RADAR, chart_data_, package_) - chart_data_.xml_bytes.assert_called_once_with(chart_type_) - package_.next_partname.assert_called_once_with(partname_template) - load_.assert_called_once_with(partname_, content_type, chart_blob_, package_) - chart_workbook_ = chart_part_.chart_workbook - chart_workbook_.update_from_xlsx_blob.assert_called_once_with(xlsx_blob_) + package_.next_partname.assert_called_once_with("/ppt/charts/chart%d.xml") + chart_data_.xml_bytes.assert_called_once_with(XCT.RADAR) + load_.assert_called_once_with( + "/ppt/charts/chart42.xml", CT.DML_CHART, package_, b"chart-blob" + ) + chart_part_.chart_workbook.update_from_xlsx_blob.assert_called_once_with( + b"xlsx-blob" + ) assert chart_part is chart_part_ - def it_provides_access_to_the_chart_object(self, chart_fixture): - chart_part, chart_, Chart_ = chart_fixture + def it_provides_access_to_the_chart_object(self, request, chartSpace_): + chart_ = instance_mock(request, Chart) + Chart_ = class_mock(request, "pptx.parts.chart.Chart", return_value=chart_) + chart_part = ChartPart(None, None, None, chartSpace_) + chart = chart_part.chart + Chart_.assert_called_once_with(chart_part._element, chart_part) assert chart is chart_ - def it_provides_access_to_the_chart_workbook(self, workbook_fixture): - chart_part, ChartWorkbook_, chartSpace_, chart_workbook_ = workbook_fixture + def it_provides_access_to_the_chart_workbook(self, request, chartSpace_): + chart_workbook_ = instance_mock(request, ChartWorkbook) + ChartWorkbook_ = class_mock( + request, "pptx.parts.chart.ChartWorkbook", return_value=chart_workbook_ + ) + chart_part = ChartPart(None, None, None, chartSpace_) + chart_workbook = chart_part.chart_workbook + ChartWorkbook_.assert_called_once_with(chartSpace_, chart_part) assert chart_workbook is chart_workbook_ - # fixtures ------------------------------------------------------- - - @pytest.fixture - def chart_fixture(self, chartSpace_, Chart_, chart_): - chart_part = ChartPart(None, None, chartSpace_) - return chart_part, chart_, Chart_ - - @pytest.fixture - def new_fixture( - self, - chart_type_, - chart_data_, - package_, - load_, - partname_, - chart_blob_, - chart_part_, - xlsx_blob_, - ): - partname_template = "/ppt/charts/chart%d.xml" - content_type = CT.DML_CHART - return ( - chart_type_, - chart_data_, - package_, - partname_template, - load_, - partname_, - content_type, - chart_blob_, - chart_part_, - xlsx_blob_, - ) - - @pytest.fixture - def workbook_fixture(self, chartSpace_, ChartWorkbook_, chart_workbook_): - chart_part = ChartPart(None, None, chartSpace_) - return chart_part, ChartWorkbook_, chartSpace_, chart_workbook_ - # fixture components --------------------------------------------- - @pytest.fixture - def ChartWorkbook_(self, request, chart_workbook_): - return class_mock( - request, "pptx.parts.chart.ChartWorkbook", return_value=chart_workbook_ - ) - - @pytest.fixture - def Chart_(self, request, chart_): - return class_mock(request, "pptx.parts.chart.Chart", return_value=chart_) - @pytest.fixture def chartSpace_(self, request): return instance_mock(request, CT_ChartSpace) - @pytest.fixture - def chart_(self, request): - return instance_mock(request, Chart) - - @pytest.fixture - def chart_blob_(self, request): - return instance_mock(request, bytes) - - @pytest.fixture - def chart_data_(self, request, chart_blob_, xlsx_blob_): - chart_data_ = instance_mock(request, ChartData) - chart_data_.xml_bytes.return_value = chart_blob_ - chart_data_.xlsx_blob = xlsx_blob_ - return chart_data_ - - @pytest.fixture - def chart_part_(self, request, chart_workbook_): - chart_part_ = instance_mock(request, ChartPart) - chart_part_.chart_workbook = chart_workbook_ - return chart_part_ - - @pytest.fixture - def chart_type_(self, request): - return instance_mock(request, EnumValue) - - @pytest.fixture - def chart_workbook_(self, request): - return instance_mock(request, ChartWorkbook) - - @pytest.fixture - def load_(self, request, chart_part_): - return method_mock(request, ChartPart, "load", return_value=chart_part_) - - @pytest.fixture - def package_(self, request, partname_): - package_ = instance_mock(request, OpcPackage) - package_.next_partname.return_value = partname_ - return package_ - - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - - @pytest.fixture - def xlsx_blob_(self, request): - return instance_mock(request, bytes) - class DescribeChartWorkbook(object): """Unit-test suite for `pptx.parts.chart.ChartWorkbook` objects.""" diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 6026ac854..86257745d 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.coreprops module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.parts.coreprops` module.""" import pytest @@ -15,7 +11,9 @@ from pptx.parts.coreprops import CorePropertiesPart -class DescribeCoreProperties(object): +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) @@ -46,7 +44,7 @@ def it_can_change_the_revision_number(self, revision_set_fixture): assert core_properties._element.xml == expected_xml def it_can_construct_a_default_core_props(self): - core_props = CorePropertiesPart.default() + core_props = CorePropertiesPart.default(None) # verify ----------------------- assert isinstance(core_props, CorePropertiesPart) assert core_props.content_type is CT.OPC_CORE_PROPERTIES @@ -102,7 +100,7 @@ def date_prop_get_fixture(self, request, core_properties): def date_prop_set_fixture(self, request): prop_name, tagname, value, str_val, attrs = request.param coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) expected_xml = self.coreProperties(tagname, str_val, attrs) return core_properties, prop_name, value, expected_xml @@ -143,7 +141,7 @@ def str_prop_get_fixture(self, request, core_properties): def str_prop_set_fixture(self, request): prop_name, tagname, value = request.param coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) expected_xml = self.coreProperties(tagname, value) return core_properties, prop_name, value, expected_xml @@ -154,14 +152,14 @@ def revision_get_fixture(self, request): str_val, expected_revision = request.param tagname = "" if str_val is None else "cp:revision" coreProperties = self.coreProperties(tagname, str_val) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) return core_properties, expected_revision @pytest.fixture(params=[(42, "42")]) def revision_set_fixture(self, request): value, str_val = request.param coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, coreProperties, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) expected_xml = self.coreProperties("cp:revision", str_val) return core_properties, value, expected_xml @@ -208,4 +206,4 @@ def core_properties(self): b" 1.2.88\n" b"\n" ) - return CorePropertiesPart.load(None, None, xml, None) + return CorePropertiesPart.load(None, None, None, xml) diff --git a/tests/parts/test_embeddedpackage.py b/tests/parts/test_embeddedpackage.py index 3b4584175..1f368d557 100644 --- a/tests/parts/test_embeddedpackage.py +++ b/tests/parts/test_embeddedpackage.py @@ -1,6 +1,6 @@ # encoding: utf-8 -"""Test suite for `pptx.parts.embeddedpackage` module.""" +"""Unit-test suite for `pptx.parts.embeddedpackage` module.""" import pytest @@ -58,7 +58,7 @@ def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request): "/ppt/embeddings/oleObject%d.bin" ) _init_.assert_called_once_with( - ANY, partname_, CT.OFC_OLE_OBJECT, object_blob_, package_ + ANY, partname_, CT.OFC_OLE_OBJECT, package_, object_blob_ ) assert isinstance(ole_object_part, EmbeddedPackagePart) @@ -75,6 +75,6 @@ def it_provides_a_contructor_classmethod_for_subclasses(self, request): EmbeddedXlsxPart.partname_template ) _init_.assert_called_once_with( - ANY, partname_, EmbeddedXlsxPart.content_type, blob_, package_ + xlsx_part, partname_, EmbeddedXlsxPart.content_type, package_, blob_ ) assert isinstance(xlsx_part, EmbeddedXlsxPart) diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 035c9f2c4..6240bf5e9 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -41,61 +41,49 @@ def it_can_construct_from_an_image_object(self, request, image_): image_part, partname_, image_.content_type, - image_.blob, package_, + image_.blob, image_.filename, ) assert isinstance(image_part, ImagePart) - def it_provides_access_to_its_image(self, image_fixture): - image_part, Image_, blob, desc, image_ = image_fixture - image = image_part.image - Image_.assert_called_once_with(blob, desc) - assert image is image_ - - def it_can_scale_its_dimensions(self, scale_fixture): - image_part, width, height, expected_values = scale_fixture - assert image_part.scale(width, height) == expected_values + def it_provides_access_to_its_image(self, request, image_): + Image_ = class_mock(request, "pptx.parts.image.Image") + Image_.return_value = image_ + property_mock(request, ImagePart, "desc", return_value="foobar.png") + image_part = ImagePart(None, None, None, b"blob", None) - def it_knows_its_pixel_dimensions(self, size_fixture): - image, expected_size = size_fixture - assert image._px_size == expected_size - - # fixtures ------------------------------------------------------- + image = image_part.image - @pytest.fixture - def image_fixture(self, Image_, image_): - blob, filename = "blob", "foobar.png" - image_part = ImagePart(None, None, blob, None, filename) - return image_part, Image_, blob, filename, image_ + Image_.assert_called_once_with(b"blob", "foobar.png") + assert image is image_ - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + "width, height, expected_width, expected_height", + ( (None, None, Emu(2590800), Emu(2590800)), (1000, None, 1000, 1000), (None, 3000, 3000, 3000), (3337, 9999, 3337, 9999), - ] + ), ) - def scale_fixture(self, request): - width, height, expected_width, expected_height = request.param + def it_can_scale_its_dimensions( + self, width, height, expected_width, expected_height + ): with open(test_image_path, "rb") as f: blob = f.read() - image = ImagePart(None, None, blob, None) - return image, width, height, (expected_width, expected_height) + image_part = ImagePart(None, None, None, blob) - @pytest.fixture - def size_fixture(self): + assert image_part.scale(width, height) == (expected_width, expected_height) + + def it_knows_its_pixel_dimensions_to_help(self): with open(test_image_path, "rb") as f: blob = f.read() - image = ImagePart(None, None, blob, None) - return image, (204, 204) + image_part = ImagePart(None, None, None, blob) - # fixture components --------------------------------------------- + assert image_part._px_size == (204, 204) - @pytest.fixture - def Image_(self, request, image_): - return class_mock(request, "pptx.parts.image.Image", return_value=image_) + # fixture components --------------------------------------------- @pytest.fixture def image_(self, request): diff --git a/tests/parts/test_media.py b/tests/parts/test_media.py index 636e50ecb..f183d7c47 100644 --- a/tests/parts/test_media.py +++ b/tests/parts/test_media.py @@ -1,10 +1,6 @@ # encoding: utf-8 -"""Unit test suite for pptx.parts.image module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import pytest +"""Unit test suite for `pptx.parts.media` module.""" from pptx.media import Video from pptx.package import Package @@ -14,47 +10,24 @@ class DescribeMediaPart(object): - def it_can_construct_from_a_media_object(self, new_fixture): - package_, media_, _init_, partname_ = new_fixture + """Unit-test suite for `pptx.parts.media.MediaPart` objects.""" + + def it_can_construct_from_a_media_object(self, request): + media_ = instance_mock(request, Video) + _init_ = initializer_mock(request, MediaPart) + package_ = instance_mock(request, Package) + package_.next_media_partname.return_value = "media42.mp4" + media_.blob, media_.content_type = b"blob-bytes", "video/mp4" media_part = MediaPart.new(package_, media_) package_.next_media_partname.assert_called_once_with(media_.ext) _init_.assert_called_once_with( - media_part, partname_, media_.content_type, media_.blob, package_ + media_part, "media42.mp4", media_.content_type, package_, media_.blob ) assert isinstance(media_part, MediaPart) - def it_knows_the_sha1_hash_of_the_media(self, sha1_fixture): - media_part, expected_value = sha1_fixture - sha1 = media_part.sha1 - assert sha1 == expected_value - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def new_fixture(self, request, package_, media_, _init_): - partname_ = package_.next_media_partname.return_value = "media42.mp4" - media_.blob, media_.content_type = b"blob-bytes", "video/mp4" - return package_, media_, _init_, partname_ - - @pytest.fixture - def sha1_fixture(self): - blob = b"blobish-bytes" - media_part = MediaPart(None, None, blob, None) - expected_value = "61efc464c21e54cfc1382fb5b6ef7512e141ceae" - return media_part, expected_value - - # fixture components --------------------------------------------- - - @pytest.fixture - def media_(self, request): - return instance_mock(request, Video) - - @pytest.fixture - def _init_(self, request): - return initializer_mock(request, MediaPart, autospec=True) - - @pytest.fixture - def package_(self, request): - return instance_mock(request, Package) + def it_knows_the_sha1_hash_of_the_media(self): + assert MediaPart(None, None, None, b"blobish-bytes").sha1 == ( + "61efc464c21e54cfc1382fb5b6ef7512e141ceae" + ) diff --git a/tests/parts/test_presentation.py b/tests/parts/test_presentation.py index 3d42788d2..7089e73de 100644 --- a/tests/parts/test_presentation.py +++ b/tests/parts/test_presentation.py @@ -20,48 +20,75 @@ class DescribePresentationPart(object): """Unit-test suite for `pptx.parts.presentation.PresentationPart` objects.""" - def it_provides_access_to_its_presentation(self, prs_fixture): - prs_part, Presentation_, prs_elm, prs_ = prs_fixture + def it_provides_access_to_its_presentation(self, request): + prs_ = instance_mock(request, Presentation) + Presentation_ = class_mock( + request, "pptx.parts.presentation.Presentation", return_value=prs_ + ) + prs_elm = element("p:presentation") + prs_part = PresentationPart(None, None, None, prs_elm) + prs = prs_part.presentation + Presentation_.assert_called_once_with(prs_elm, prs_part) assert prs is prs_ - def it_provides_access_to_its_core_properties(self, core_props_fixture): - prs_part, core_properties_ = core_props_fixture - core_properties = prs_part.core_properties - assert core_properties is core_properties_ + def it_provides_access_to_its_core_properties(self, request, package_): + core_properties_ = instance_mock(request, CorePropertiesPart) + package_.core_properties = core_properties_ + prs_part = PresentationPart(None, None, package_, None) - def it_provides_access_to_the_notes_master_part(self, nmp_get_fixture): - """ - This is the first of a two-part test to cover the existing notes - master case. The notes master not-present case follows. + assert prs_part.core_properties is core_properties_ + + def it_provides_access_to_an_existing_notes_master_part( + self, notes_master_part_, part_related_by_ + ): + """This is the first of a two-part test to cover the existing notes master case. + + The notes master not-present case follows. """ - prs_part, notes_master_part_ = nmp_get_fixture + prs_part = PresentationPart(None, None, None, None) + part_related_by_.return_value = notes_master_part_ + notes_master_part = prs_part.notes_master_part + prs_part.part_related_by.assert_called_once_with(prs_part, RT.NOTES_MASTER) assert notes_master_part is notes_master_part_ - def it_adds_a_notes_master_part_when_needed(self, nmp_add_fixture): - """ - This is the second of a two-part test to cover the - notes-master-not-present case. The notes master present case is just - above. + def but_it_adds_a_notes_master_part_when_needed( + self, request, package_, notes_master_part_, part_related_by_, relate_to_ + ): + """This is the second of a two-part test to cover notes-master-not-present case. + + The notes master present case is just above. """ - prs_part, NotesMasterPart_ = nmp_add_fixture[:2] - package_, notes_master_part_ = nmp_add_fixture[2:] + NotesMasterPart_ = class_mock( + request, "pptx.parts.presentation.NotesMasterPart" + ) + NotesMasterPart_.create_default.return_value = notes_master_part_ + part_related_by_.side_effect = KeyError + prs_part = PresentationPart(None, None, package_, None) notes_master_part = prs_part.notes_master_part NotesMasterPart_.create_default.assert_called_once_with(package_) - prs_part.relate_to.assert_called_once_with( + relate_to_.assert_called_once_with( prs_part, notes_master_part_, RT.NOTES_MASTER ) assert notes_master_part is notes_master_part_ - def it_provides_access_to_its_notes_master(self, notes_master_fixture): - prs_part, notes_master_ = notes_master_fixture - notes_master = prs_part.notes_master - assert notes_master is notes_master_ + def it_provides_access_to_its_notes_master(self, request, notes_master_part_): + notes_master_ = instance_mock(request, NotesMaster) + property_mock( + request, + PresentationPart, + "notes_master_part", + return_value=notes_master_part_, + ) + notes_master_part_.notes_master = notes_master_ + prs_part = PresentationPart(None, None, None, None) + + assert prs_part.notes_master is notes_master_ def it_provides_access_to_a_related_slide(self, request, slide_, related_part_): slide_part_ = instance_mock(request, SlidePart, slide=slide_) @@ -100,21 +127,30 @@ def it_can_rename_related_slide_parts(self, request, related_part_): PackURI("/ppt/slides/slide%d.xml" % (i + 1)) for i in range(len(rIds)) ] - def it_can_save_the_package_to_a_file(self, save_fixture): - prs_part, file_, package_ = save_fixture - prs_part.save(file_) - package_.save.assert_called_once_with(file_) + 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, add_slide_fixture): - prs_part, slide_layout_, SlidePart_, partname = add_slide_fixture[:4] - package_, slide_layout_part_, slide_part_ = add_slide_fixture[4:7] - rId_, slide_ = add_slide_fixture[7:] + def it_can_add_a_new_slide( + self, request, package_, slide_part_, slide_, relate_to_ + ): + slide_layout_ = instance_mock(request, SlideLayout) + partname = PackURI("/ppt/slides/slide9.xml") + property_mock( + request, PresentationPart, "_next_slide_partname", return_value=partname + ) + SlidePart_ = class_mock(request, "pptx.parts.presentation.SlidePart") + SlidePart_.new.return_value = slide_part_ + relate_to_.return_value = "rId42" + slide_layout_part_ = slide_layout_.part + slide_part_.slide = slide_ + prs_part = PresentationPart(None, None, package_, None) rId, slide = prs_part.add_slide(slide_layout_) SlidePart_.new.assert_called_once_with(partname, package_, slide_layout_part_) prs_part.relate_to.assert_called_once_with(prs_part, slide_part_, RT.SLIDE) - assert rId is rId_ + assert rId == "rId42" assert slide is slide_ def it_finds_the_slide_id_of_a_slide_part(self, slide_part_, related_part_): @@ -123,7 +159,7 @@ def it_finds_the_slide_id_of_a_slide_part(self, slide_part_, related_part_): "b,id=257},p:sldId{r:id=c,id=258})" ) related_part_.side_effect = iter((None, slide_part_, None)) - prs_part = PresentationPart(None, None, prs_elm, None) + prs_part = PresentationPart(None, None, None, prs_elm) _slide_id = prs_part.slide_id(slide_part_) @@ -139,7 +175,7 @@ def it_raises_on_slide_id_not_found(self, slide_part_, related_part_): "b,id=257},p:sldId{r:id=c,id=258})" ) related_part_.return_value = "not the slide you're looking for" - prs_part = PresentationPart(None, None, prs_elm, None) + prs_part = PresentationPart(None, None, None, prs_elm) with pytest.raises(ValueError): prs_part.slide_id(slide_part_) @@ -156,149 +192,35 @@ def it_finds_a_slide_by_slide_id( expected_value = slide_ if is_present else None related_part_.return_value = slide_part_ slide_part_.slide = slide_ - prs_part = PresentationPart(None, None, prs_elm, None) + prs_part = PresentationPart(None, None, None, prs_elm) slide = prs_part.get_slide(slide_id) assert slide == expected_value - def it_knows_the_next_slide_partname_to_help(self, next_fixture): - prs_part, partname = next_fixture - assert prs_part._next_slide_partname == partname - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def add_slide_fixture( - self, - package_, - slide_layout_, - SlidePart_, - slide_part_, - slide_, - _next_slide_partname_prop_, - relate_to_, - ): - prs_part = PresentationPart(None, None, None, package_) - partname = _next_slide_partname_prop_.return_value - rId_ = "rId42" - SlidePart_.new.return_value = slide_part_ - relate_to_.return_value = rId_ - slide_layout_part_ = slide_layout_.part - slide_part_.slide = slide_ - return ( - prs_part, - slide_layout_, - SlidePart_, - partname, - package_, - slide_layout_part_, - slide_part_, - rId_, - slide_, - ) - - @pytest.fixture - def core_props_fixture(self, package_, core_properties_): - prs_part = PresentationPart(None, None, None, package_) - package_.core_properties = core_properties_ - return prs_part, core_properties_ - - @pytest.fixture - def next_fixture(self): + def it_knows_the_next_slide_partname_to_help(self): prs_elm = element("p:presentation/p:sldIdLst/(p:sldId,p:sldId)") - prs_part = PresentationPart(None, None, prs_elm) - partname = PackURI("/ppt/slides/slide3.xml") - return prs_part, partname - - @pytest.fixture - def notes_master_fixture( - self, notes_master_part_prop_, notes_master_part_, notes_master_ - ): - prs_part = PresentationPart(None, None, None, None) - notes_master_part_prop_.return_value = notes_master_part_ - notes_master_part_.notes_master = notes_master_ - return prs_part, notes_master_ - - @pytest.fixture - def nmp_add_fixture( - self, - package_, - NotesMasterPart_, - notes_master_part_, - part_related_by_, - relate_to_, - ): - prs_part = PresentationPart(None, None, None, package_) - part_related_by_.side_effect = KeyError - NotesMasterPart_.create_default.return_value = notes_master_part_ - return prs_part, NotesMasterPart_, package_, notes_master_part_ - - @pytest.fixture - def nmp_get_fixture(self, notes_master_part_, part_related_by_): - prs_part = PresentationPart(None, None, None, None) - part_related_by_.return_value = notes_master_part_ - return prs_part, notes_master_part_ + prs_part = PresentationPart(None, None, None, prs_elm) - @pytest.fixture - def prs_fixture(self, Presentation_, prs_): - prs_elm = element("p:presentation") - prs_part = PresentationPart(None, None, prs_elm) - return prs_part, Presentation_, prs_elm, prs_ - - @pytest.fixture - def save_fixture(self, package_): - prs_part = PresentationPart(None, None, None, package_) - file_ = "foobar.docx" - return prs_part, file_, package_ + assert prs_part._next_slide_partname == PackURI("/ppt/slides/slide3.xml") # fixture components --------------------------------------------- - @pytest.fixture - def core_properties_(self, request): - return instance_mock(request, CorePropertiesPart) - - @pytest.fixture - def _next_slide_partname_prop_(self, request): - return property_mock(request, PresentationPart, "_next_slide_partname") - - @pytest.fixture - def NotesMasterPart_(self, request, prs_): - return class_mock(request, "pptx.parts.presentation.NotesMasterPart") - - @pytest.fixture - def notes_master_(self, request): - return instance_mock(request, NotesMaster) - @pytest.fixture def notes_master_part_(self, request): return instance_mock(request, NotesMasterPart) - @pytest.fixture - def notes_master_part_prop_(self, request, notes_master_part_): - return property_mock(request, PresentationPart, "notes_master_part") - @pytest.fixture def package_(self, request): return instance_mock(request, Package) @pytest.fixture def part_related_by_(self, request): - return method_mock(request, PresentationPart, "part_related_by", autospec=True) - - @pytest.fixture - def Presentation_(self, request, prs_): - return class_mock( - request, "pptx.parts.presentation.Presentation", return_value=prs_ - ) - - @pytest.fixture - def prs_(self, request): - return instance_mock(request, Presentation) + return method_mock(request, PresentationPart, "part_related_by") @pytest.fixture def relate_to_(self, request): - return method_mock(request, PresentationPart, "relate_to", autospec=True) + return method_mock(request, PresentationPart, "relate_to") @pytest.fixture def related_part_(self, request): @@ -308,10 +230,6 @@ def related_part_(self, request): def slide_(self, request): return instance_mock(request, Slide) - @pytest.fixture - def slide_layout_(self, request): - return instance_mock(request, SlideLayout) - @pytest.fixture def slide_master_(self, request): return instance_mock(request, SlideMaster) @@ -319,7 +237,3 @@ def slide_master_(self, request): @pytest.fixture def slide_part_(self, request): return instance_mock(request, SlidePart) - - @pytest.fixture - def SlidePart_(self, request): - return class_mock(request, "pptx.parts.presentation.SlidePart") diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index 099fde94f..6a081e49f 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -43,9 +43,11 @@ class DescribeBaseSlidePart(object): """Unit-test suite for `pptx.parts.slide.BaseSlidePart` objects.""" - def it_knows_its_name(self, name_fixture): - base_slide, expected_value = name_fixture - assert base_slide.name == expected_value + def it_knows_its_name(self): + slide_part = BaseSlidePart( + None, None, None, element("p:sld/p:cSld{name=Foobar}") + ) + assert slide_part.name == "Foobar" def it_can_get_a_related_image_by_rId(self, request, image_part_): image_ = instance_mock(request, Image) @@ -66,7 +68,7 @@ def it_can_add_an_image_part(self, request, image_part_): relate_to_ = method_mock( request, BaseSlidePart, "relate_to", return_value="rId6" ) - slide_part = BaseSlidePart(None, None, None, package_) + slide_part = BaseSlidePart(None, None, package_, None) image_part, rId = slide_part.get_or_add_image_part("foobar.png") @@ -75,15 +77,6 @@ def it_can_add_an_image_part(self, request, image_part_): assert image_part is image_part_ assert rId == "rId6" - # fixtures ------------------------------------------------------- - - @pytest.fixture - def name_fixture(self): - sld_cxml, expected_value = "p:sld/p:cSld{name=Foobar}", "Foobar" - sld = element(sld_cxml) - base_slide = BaseSlidePart(None, None, sld, None) - return base_slide, expected_value - # fixture components --------------------------------------------- @pytest.fixture @@ -94,9 +87,13 @@ 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, create_fixture): - package_, theme_part_, notes_master_part_ = create_fixture - + def it_can_create_a_notes_master_part( + self, request, package_, notes_master_part_, theme_part_ + ): + method_mock(request, NotesMasterPart, "_new", return_value=notes_master_part_) + method_mock( + request, NotesMasterPart, "_new_theme_part", return_value=theme_part_ + ) notes_master_part = NotesMasterPart.create_default(package_) NotesMasterPart._new.assert_called_once_with(package_) @@ -104,108 +101,56 @@ def it_can_create_a_notes_master_part(self, create_fixture): notes_master_part.relate_to.assert_called_once_with(theme_part_, RT.THEME) assert notes_master_part is notes_master_part_ - def it_provides_access_to_its_notes_master(self, notes_master_fixture): - notes_master_part, NotesMaster_ = notes_master_fixture[:2] - notesMaster, notes_master_ = notes_master_fixture[2:] + def it_provides_access_to_its_notes_master(self, request): + notes_master_ = instance_mock(request, NotesMaster) + NotesMaster_ = class_mock( + request, "pptx.parts.slide.NotesMaster", return_value=notes_master_ + ) + notesMaster = element("p:notesMaster") + notes_master_part = NotesMasterPart(None, None, None, notesMaster) notes_master = notes_master_part.notes_master NotesMaster_.assert_called_once_with(notesMaster, notes_master_part) assert notes_master is notes_master_ - def it_creates_a_new_notes_master_part_to_help(self, new_fixture): - package_, NotesMasterPart_, partname = new_fixture[:3] - notesMaster_, notes_master_part_ = new_fixture[3:] + def it_creates_a_new_notes_master_part_to_help( + self, request, package_, notes_master_part_ + ): + NotesMasterPart_ = class_mock( + request, "pptx.parts.slide.NotesMasterPart", return_value=notes_master_part_ + ) + notesMaster = element("p:notesMaster") + method_mock(request, CT_NotesMaster, "new_default", return_value=notesMaster) + partname = PackURI("/ppt/notesMasters/notesMaster1.xml") notes_master_part = NotesMasterPart._new(package_) CT_NotesMaster.new_default.assert_called_once_with() NotesMasterPart_.assert_called_once_with( - partname, CT.PML_NOTES_MASTER, notesMaster_, package_ + partname, CT.PML_NOTES_MASTER, package_, notesMaster ) assert notes_master_part is notes_master_part_ - def it_creates_a_new_theme_part_to_help(self, theme_fixture): - package_, pn_tmpl, XmlPart_, partname = theme_fixture[:4] - theme_elm_, theme_part_ = theme_fixture[4:] + def it_creates_a_new_theme_part_to_help(self, request, package_, theme_part_): + XmlPart_ = class_mock( + request, "pptx.parts.slide.XmlPart", return_value=theme_part_ + ) + theme_elm = element("p:theme") + method_mock(request, CT_OfficeStyleSheet, "new_default", return_value=theme_elm) + pn_tmpl = "/ppt/theme/theme%d.xml" + partname = PackURI("/ppt/theme/theme2.xml") + package_.next_partname.return_value = partname theme_part = NotesMasterPart._new_theme_part(package_) package_.next_partname.assert_called_once_with(pn_tmpl) CT_OfficeStyleSheet.new_default.assert_called_once_with() - XmlPart_.assert_called_once_with(partname, CT.OFC_THEME, theme_elm_, package_) + XmlPart_.assert_called_once_with(partname, CT.OFC_THEME, package_, theme_elm) assert theme_part is theme_part_ - # fixtures ------------------------------------------------------- - - @pytest.fixture - def create_fixture( - self, package_, theme_part_, notes_master_part_, _new_, _new_theme_part_ - ): - return package_, theme_part_, notes_master_part_ - - @pytest.fixture - def new_fixture( - self, package_, NotesMasterPart_, notesMaster_, notes_master_part_, new_default_ - ): - partname = PackURI("/ppt/notesMasters/notesMaster1.xml") - return (package_, NotesMasterPart_, partname, notesMaster_, notes_master_part_) - - @pytest.fixture - def notes_master_fixture(self, NotesMaster_, notes_master_): - notesMaster = element("p:notesMaster") - notes_master_part = NotesMasterPart(None, None, notesMaster, None) - return notes_master_part, NotesMaster_, notesMaster, notes_master_ - - @pytest.fixture - def theme_fixture( - self, package_, XmlPart_, theme_elm_, theme_part_, theme_new_default_ - ): - pn_tmpl = "/ppt/theme/theme%d.xml" - partname = PackURI("/ppt/theme/theme2.xml") - package_.next_partname.return_value = partname - return (package_, pn_tmpl, XmlPart_, partname, theme_elm_, theme_part_) - # fixture components --------------------------------------------- - @pytest.fixture - def _new_(self, request, notes_master_part_): - return method_mock( - request, NotesMasterPart, "_new", return_value=notes_master_part_ - ) - - @pytest.fixture - def new_default_(self, request, notesMaster_): - return method_mock( - request, CT_NotesMaster, "new_default", return_value=notesMaster_ - ) - - @pytest.fixture - def _new_theme_part_(self, request, theme_part_): - return method_mock( - request, NotesMasterPart, "_new_theme_part", return_value=theme_part_ - ) - - @pytest.fixture - def NotesMaster_(self, request, notes_master_): - return class_mock( - request, "pptx.parts.slide.NotesMaster", return_value=notes_master_ - ) - - @pytest.fixture - def NotesMasterPart_(self, request, notes_master_part_): - return class_mock( - request, "pptx.parts.slide.NotesMasterPart", return_value=notes_master_part_ - ) - - @pytest.fixture - def notesMaster_(self, request): - return instance_mock(request, CT_NotesMaster) - - @pytest.fixture - def notes_master_(self, request): - return instance_mock(request, NotesMaster) - @pytest.fixture def notes_master_part_(self, request): return instance_mock(request, NotesMasterPart) @@ -214,58 +159,80 @@ def notes_master_part_(self, request): def package_(self, request): return instance_mock(request, Package) - @pytest.fixture - def theme_elm_(self, request): - return instance_mock(request, CT_OfficeStyleSheet) - - @pytest.fixture - def theme_new_default_(self, request, theme_elm_): - return method_mock( - request, CT_OfficeStyleSheet, "new_default", return_value=theme_elm_ - ) - @pytest.fixture def theme_part_(self, request): return instance_mock(request, Part) - @pytest.fixture - def XmlPart_(self, request, theme_part_): - return class_mock(request, "pptx.parts.slide.XmlPart", return_value=theme_part_) - class DescribeNotesSlidePart(object): """Unit-test suite for `pptx.parts.slide.NotesSlidePart` objects.""" - def it_can_create_a_notes_slide_part(self, new_fixture): - package_, slide_part_, notes_master_part_ = new_fixture[:3] - notes_slide_, notes_master_, notes_slide_part_ = new_fixture[3:] + def it_can_create_a_notes_slide_part( + self, + request, + package_, + slide_part_, + notes_master_part_, + notes_slide_, + notes_master_, + notes_slide_part_, + ): + presentation_part_ = instance_mock(request, PresentationPart) + package_.presentation_part = presentation_part_ + presentation_part_.notes_master_part = notes_master_part_ + _add_notes_slide_part_ = method_mock( + request, + NotesSlidePart, + "_add_notes_slide_part", + return_value=notes_slide_part_, + ) + notes_slide_part_.notes_slide = notes_slide_ + notes_master_part_.notes_master = notes_master_ notes_slide_part = NotesSlidePart.new(package_, slide_part_) - NotesSlidePart._add_notes_slide_part.assert_called_once_with( + _add_notes_slide_part_.assert_called_once_with( package_, slide_part_, notes_master_part_ ) notes_slide_.clone_master_placeholders.assert_called_once_with(notes_master_) assert notes_slide_part is notes_slide_part_ - def it_provides_access_to_the_notes_master(self, notes_master_fixture): - notes_slide_part, notes_master_ = notes_master_fixture - notes_master = notes_slide_part.notes_master - notes_slide_part.part_related_by.assert_called_once_with( - notes_slide_part, RT.NOTES_MASTER + def it_provides_access_to_the_notes_master( + self, request, notes_master_, notes_master_part_ + ): + part_related_by_ = method_mock( + request, NotesSlidePart, "part_related_by", return_value=notes_master_part_ ) + notes_slide_part = NotesSlidePart(None, None, None, None) + notes_master_part_.notes_master = notes_master_ + + notes_master = notes_slide_part.notes_master + + part_related_by_.assert_called_once_with(notes_slide_part, RT.NOTES_MASTER) assert notes_master is notes_master_ - def it_provides_access_to_its_notes_slide(self, notes_slide_fixture): - notes_slide_part, NotesSlide_, notes, notes_slide_ = notes_slide_fixture + def it_provides_access_to_its_notes_slide(self, request, notes_slide_): + NotesSlide_ = class_mock( + request, "pptx.parts.slide.NotesSlide", return_value=notes_slide_ + ) + notes = element("p:notes") + notes_slide_part = NotesSlidePart(None, None, None, notes) + notes_slide = notes_slide_part.notes_slide + NotesSlide_.assert_called_once_with(notes, notes_slide_part) assert notes_slide is notes_slide_ - def it_adds_a_notes_slide_part_to_help(self, add_fixture): - package_, slide_part_, notes_master_part_ = add_fixture[:3] - notes_slide_part_, NotesSlidePart_, partname = add_fixture[3:6] - content_type, notes, calls = add_fixture[6:] + def it_adds_a_notes_slide_part_to_help( + self, request, package_, slide_part_, notes_master_part_, notes_slide_part_ + ): + NotesSlidePart_ = class_mock( + request, "pptx.parts.slide.NotesSlidePart", return_value=notes_slide_part_ + ) + notes = element("p:notes") + new_ = method_mock(request, CT_NotesSlide, "new", return_value=notes) + partname = PackURI("/ppt/notesSlides/notesSlide42.xml") + package_.next_partname.return_value = partname notes_slide_part = NotesSlidePart._add_notes_slide_part( package_, slide_part_, notes_master_part_ @@ -274,94 +241,18 @@ def it_adds_a_notes_slide_part_to_help(self, add_fixture): package_.next_partname.assert_called_once_with( "/ppt/notesSlides/notesSlide%d.xml" ) - CT_NotesSlide.new.assert_called_once_with() - NotesSlidePart_.assert_called_once_with(partname, content_type, notes, package_) - assert notes_slide_part_.relate_to.call_args_list == calls - assert notes_slide_part is notes_slide_part_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def add_fixture( - self, - package_, - slide_part_, - notes_master_part_, - notes_slide_part_, - NotesSlidePart_, - new_, - ): - partname = PackURI("/ppt/notesSlides/notesSlide42.xml") - content_type = CT.PML_NOTES_SLIDE - notes = element("p:notes") - calls = [call(notes_master_part_, RT.NOTES_MASTER), call(slide_part_, RT.SLIDE)] - package_.next_partname.return_value = partname - new_.return_value = notes - return ( - package_, - slide_part_, - notes_master_part_, - notes_slide_part_, - NotesSlidePart_, - partname, - content_type, - notes, - calls, + new_.assert_called_once_with() + NotesSlidePart_.assert_called_once_with( + partname, CT.PML_NOTES_SLIDE, package_, notes ) - - @pytest.fixture - def new_fixture( - self, - package_, - slide_part_, - notes_master_part_, - notes_slide_, - notes_master_, - notes_slide_part_, - _add_notes_slide_part_, - presentation_part_, - ): - package_.presentation_part = presentation_part_ - presentation_part_.notes_master_part = notes_master_part_ - notes_slide_part_.notes_slide = notes_slide_ - notes_master_part_.notes_master = notes_master_ - return ( - package_, - slide_part_, - notes_master_part_, - notes_slide_, - notes_master_, - notes_slide_part_, - ) - - @pytest.fixture - def notes_master_fixture(self, notes_master_, part_related_by_, notes_master_part_): - notes_slide_part = NotesSlidePart(None, None, None, None) - part_related_by_.return_value = notes_master_part_ - notes_master_part_.notes_master = notes_master_ - return notes_slide_part, notes_master_ - - @pytest.fixture - def notes_slide_fixture(self, NotesSlide_, notes_slide_): - notes = element("p:notes") - notes_slide_part = NotesSlidePart(None, None, notes, None) - return notes_slide_part, NotesSlide_, notes, notes_slide_ + assert notes_slide_part_.relate_to.call_args_list == [ + call(notes_master_part_, RT.NOTES_MASTER), + call(slide_part_, RT.SLIDE), + ] + assert notes_slide_part is notes_slide_part_ # fixture components --------------------------------------------- - @pytest.fixture - def _add_notes_slide_part_(self, request, notes_slide_part_): - return method_mock( - request, - NotesSlidePart, - "_add_notes_slide_part", - return_value=notes_slide_part_, - ) - - @pytest.fixture - def new_(self, request): - return method_mock(request, CT_NotesSlide, "new") - @pytest.fixture def notes_master_(self, request): return instance_mock(request, NotesMaster) @@ -370,34 +261,14 @@ def notes_master_(self, request): def notes_master_part_(self, request): return instance_mock(request, NotesMasterPart) - @pytest.fixture - def NotesSlide_(self, request, notes_slide_): - return class_mock( - request, "pptx.parts.slide.NotesSlide", return_value=notes_slide_ - ) - @pytest.fixture def notes_slide_(self, request): return instance_mock(request, NotesSlide) - @pytest.fixture - def NotesSlidePart_(self, request, notes_slide_part_): - return class_mock( - request, "pptx.parts.slide.NotesSlidePart", return_value=notes_slide_part_ - ) - @pytest.fixture def notes_slide_part_(self, request): return instance_mock(request, NotesSlidePart) - @pytest.fixture - def part_related_by_(self, request): - return method_mock(request, NotesSlidePart, "part_related_by", autospec=True) - - @pytest.fixture - def presentation_part_(self, request): - return instance_mock(request, PresentationPart) - @pytest.fixture def package_(self, request): return instance_mock(request, Package) @@ -429,7 +300,7 @@ def it_can_add_a_chart_part(self, request, package_, relate_to_): chart_type_ = instance_mock(request, EnumValue) chart_data_ = instance_mock(request, ChartData) relate_to_.return_value = "rId42" - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) _rId = slide_part.add_chart_part(chart_type_, chart_data_) @@ -458,7 +329,7 @@ def it_can_add_an_embedded_ole_object_part( ) EmbeddedPackagePart_.factory.return_value = embedded_package_part_ relate_to_.return_value = "rId9" - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) _rId = slide_part.add_embedded_ole_object_part(prog_id, "workbook.xlsx") @@ -473,7 +344,7 @@ def it_can_get_or_add_a_video_part(self, package_, video_, relate_to_, media_par media_rId, video_rId = "rId1", "rId2" package_.get_or_add_media_part.return_value = media_part_ relate_to_.side_effect = [media_rId, video_rId] - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) result = slide_part.get_or_add_video_media_part(video_) @@ -494,7 +365,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, sld, package_ + slide_part, partname, CT.PML_SLIDE, package_, sld ) slide_part.relate_to.assert_called_once_with( slide_part, slide_layout_part_, RT.SLIDE_LAYOUT @@ -533,7 +404,7 @@ def it_adds_a_notes_slide_part_to_help( self, package_, NotesSlidePart_, notes_slide_part_, relate_to_ ): NotesSlidePart_.new.return_value = notes_slide_part_ - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) notes_slide_part = slide_part._add_notes_slide_part() @@ -579,12 +450,12 @@ def notes_slide_fixture( @pytest.fixture def slide_fixture(self, Slide_, slide_): sld = element("p:sld") - slide_part = SlidePart(None, None, sld, None) + slide_part = SlidePart(None, None, None, sld) return slide_part, Slide_, sld, slide_ @pytest.fixture def slide_id_fixture(self, package_, presentation_part_): - slide_part = SlidePart(None, None, None, package_) + slide_part = SlidePart(None, None, package_, None) slide_id = 256 package_.presentation_part = presentation_part_ presentation_part_.slide_id.return_value = slide_id @@ -674,7 +545,7 @@ def it_provides_access_to_its_slide_layout(self, layout_fixture): @pytest.fixture def layout_fixture(self, SlideLayout_, slide_layout_): sldLayout = element("p:sldLayout") - slide_layout_part = SlideLayoutPart(None, None, sldLayout) + slide_layout_part = SlideLayoutPart(None, None, None, sldLayout) return slide_layout_part, SlideLayout_, sldLayout, slide_layout_ @pytest.fixture @@ -712,9 +583,16 @@ def slide_master_part_(self, request): class DescribeSlideMasterPart(object): """Unit-test suite for `pptx.parts.slide.SlideMasterPart` objects.""" - def it_provides_access_to_its_slide_master(self, master_fixture): - slide_master_part, SlideMaster_, sldMaster, slide_master_ = master_fixture + def it_provides_access_to_its_slide_master(self, request): + slide_master_ = instance_mock(request, SlideMaster) + SlideMaster_ = class_mock( + request, "pptx.parts.slide.SlideMaster", return_value=slide_master_ + ) + sldMaster = element("p:sldMaster") + slide_master_part = SlideMasterPart(None, None, None, sldMaster) + slide_master = slide_master_part.slide_master + SlideMaster_.assert_called_once_with(sldMaster, slide_master_part) assert slide_master is slide_master_ @@ -732,23 +610,3 @@ def it_provides_access_to_a_related_slide_layout(self, request): related_part_.assert_called_once_with(slide_master_part, "rId42") assert slide_layout is slide_layout_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def master_fixture(self, SlideMaster_, slide_master_): - sldMaster = element("p:sldMaster") - slide_master_part = SlideMasterPart(None, None, sldMaster) - return slide_master_part, SlideMaster_, sldMaster, slide_master_ - - # fixture components --------------------------------------------- - - @pytest.fixture - def SlideMaster_(self, request, slide_master_): - return class_mock( - request, "pptx.parts.slide.SlideMaster", return_value=slide_master_ - ) - - @pytest.fixture - def slide_master_(self, request): - return instance_mock(request, SlideMaster) From a9eb5e5f84f2ebe7687555cc09b55f84ff0a9859 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 24 Aug 2021 10:54:29 -0700 Subject: [PATCH 21/69] rfctr: ditch sparts and srels SerializedParts are an unnecessary intermediate step. Just use the oxml CT_Relationship(s) tree to do the needful. --- pptx/opc/package.py | 142 +++++++++++-------- pptx/opc/serialized.py | 265 +---------------------------------- tests/opc/test_package.py | 192 +++++++++++++------------ tests/opc/test_serialized.py | 224 +---------------------------- tests/unitutil/file.py | 7 + 5 files changed, 191 insertions(+), 639 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 13ffa8815..c59e8c86d 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -11,8 +11,9 @@ from pptx.compat import is_string, Mapping from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM, RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationships, serialize_part_xml -from pptx.opc.packuri import PACKAGE_URI, PackURI +from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI from pptx.opc.serialized import PackageReader, PackageWriter +from pptx.opc.shared import CaseInsensitiveDict from pptx.oxml import parse_xml from pptx.util import lazyproperty @@ -71,17 +72,6 @@ def walk_rels(rels): for rel in walk_rels(self._rels): yield rel - def load_rel(self, reltype, target, rId, is_external=False): - """ - Return newly added |_Relationship| instance of *reltype* between this - part and *target* with key *rId*. Target mode is set to - ``RTM.EXTERNAL`` if *is_external* is |True|. Intended for use during - load from a serialized package, where the rId is well known. Other - methods exist for adding a new relationship to the package during - processing. - """ - return self._rels.add_relationship(reltype, target, rId, is_external) - @property def main_document_part(self): """Return |Part| subtype serving as the main document part for this package. @@ -171,13 +161,20 @@ def load(cls, pkg_file, package): def _load(self): """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file.""" - # --- ugly temporary hack to make this interim `._load()` method produce the - # --- same result as the one that's coming a few commits later. - self._unmarshal_relationships() + parts, xml_rels = self._parts, self._xml_rels - pkg_xml_rels = parse_xml(self._package_reader.rels_xml_for(PACKAGE_URI)) + for partname, part in parts.items(): + part.load_rels_from_xml(xml_rels[partname], parts) - return pkg_xml_rels, self._parts + return xml_rels["/"], parts + + @lazyproperty + def _content_types(self): + """|_ContentTypeMap| object providing content-types for items of this package. + + Provides a content-type (MIME-type) for any given partname. + """ + return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI]) @lazyproperty def _package_reader(self): @@ -186,33 +183,29 @@ def _package_reader(self): @lazyproperty def _parts(self): - """Return a {partname: |Part|} dict unmarshalled from `pkg_reader`. - - Side-effect is that each part in `pkg_reader` is constructed using - `part_factory`. - """ - package = self._package - return { - partname: PartFactory(partname, content_type, package, blob) - for partname, content_type, blob in self._package_reader.iter_sparts() - } + """dict {partname: Part} populated with parts loading from package. - def _unmarshal_relationships(self): - """Add relationships to each source object. - - Source objects correspond to each relationship-target in `pkg_reader` with its - target_part set to the actual target part in `parts`. + 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. """ - pkg_reader = self._package_reader + content_types = self._content_types package = self._package - parts = self._parts + package_reader = self._package_reader - for source_uri, srel in pkg_reader.iter_srels(): - source = package if source_uri == "/" else parts[source_uri] - target = ( - srel.target_ref if srel.is_external else parts[srel.target_partname] + return { + partname: PartFactory( + partname, + content_types[partname], + package, + blob=package_reader[partname], ) - source.load_rel(srel.reltype, target, srel.rId, srel.is_external) + 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. + if partname in package_reader + } @lazyproperty def _xml_rels(self): @@ -310,16 +303,15 @@ def drop_rel(self, rId): if self._rel_ref_count(rId) < 2: self._rels.pop(rId) - def load_rel(self, reltype, target, rId, is_external=False): - """ - Return newly added |_Relationship| instance of *reltype* between this - part and *target* with key *rId*. Target mode is set to - ``RTM.EXTERNAL`` if *is_external* is |True|. Intended for use during - load from a serialized package, where the rId is well known. Other - methods exist for adding a new relationship to a part when - manipulating a part. + def load_rels_from_xml(self, xml_rels, parts): + """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. """ - return self._rels.add_relationship(reltype, target, rId, is_external) + self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts) @property def package(self): @@ -438,9 +430,9 @@ class PartFactory(object): part_type_for = {} default_part_type = Part - def __new__(cls, partname, content_type, blob, package): + def __new__(cls, partname, content_type, package, blob): PartClass = cls._part_cls_for(content_type) - return PartClass.load(partname, content_type, blob, package) + return PartClass.load(partname, content_type, package, blob) @classmethod def _part_cls_for(cls, content_type): @@ -453,6 +445,44 @@ def _part_cls_for(cls, content_type): return cls.default_part_type +class _ContentTypeMap(object): + """Value type providing dict semantics for looking up content type by partname.""" + + def __init__(self, overrides, defaults): + self._overrides = overrides + self._defaults = defaults + + def __getitem__(self, partname): + """Return content-type (MIME-type) for part identified by *partname*.""" + if not isinstance(partname, PackURI): + raise TypeError( + "_ContentTypeMap key must be , got %s" + % type(partname).__name__ + ) + + if partname in self._overrides: + return self._overrides[partname] + + if partname.ext in self._defaults: + return self._defaults[partname.ext] + + raise KeyError( + "no content-type for partname '%s' in [Content_Types].xml" % partname + ) + + @classmethod + def from_xml(cls, content_types_xml): + """Return |_ContentTypeMap| instance populated from `content_types_xml`.""" + types_elm = parse_xml(content_types_xml) + overrides = CaseInsensitiveDict( + (o.partName.lower(), o.contentType) for o in types_elm.override_lst + ) + defaults = CaseInsensitiveDict( + (d.extension.lower(), d.contentType) for d in types_elm.default_lst + ) + return cls(overrides, defaults) + + class _Relationships(Mapping): """Collection of |_Relationship| instances, largely having dict semantics. @@ -486,18 +516,6 @@ def __len__(self): """Return count of relationships in collection.""" return len(self._rels) - def add_relationship(self, reltype, target, rId, is_external=False): - """Return a newly added |_Relationship| instance.""" - rel = _Relationship( - self._base_uri, - rId, - reltype, - RTM.EXTERNAL if is_external else RTM.INTERNAL, - target, - ) - self._rels[rId] = rel - return rel - def get_or_add(self, reltype, target_part): """Return str rId of `reltype` to `target_part`. diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index d47a6c57e..2e5a20127 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -7,8 +7,8 @@ from pptx.compat import Container, is_string from pptx.exceptions import PackageNotFoundError -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM -from pptx.opc.oxml import CT_Types, parse_xml, serialize_part_xml +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.oxml import CT_Types, serialize_part_xml from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI from pptx.opc.shared import CaseInsensitiveDict from pptx.opc.spec import default_content_types @@ -33,25 +33,6 @@ def __getitem__(self, pack_uri): """Return bytes for part corresponding to `pack_uri`.""" return self._blob_reader[pack_uri] - def iter_sparts(self): - """ - Generate a 3-tuple `(partname, content_type, blob)` for each of the - serialized parts in the package. - """ - for spart in self._sparts: - yield (spart.partname, spart.content_type, spart.blob) - - def iter_srels(self): - """ - Generate a 2-tuple `(source_uri, srel)` for each of the relationships - in the package. - """ - for srel in self._pkg_srels: - yield (PACKAGE_URI, srel) - for spart in self._sparts: - for srel in spart.srels: - yield (spart.partname, srel) - def rels_xml_for(self, partname): """Return optional rels item XML for `partname`. @@ -66,118 +47,6 @@ def _blob_reader(self): """|_PhysPkgReader| subtype providing read access to the package file.""" return _PhysPkgReader.factory(self._pkg_file) - @lazyproperty - def _content_types(self): - """temporary hack during refactoring.""" - return _ContentTypeMap.from_xml(self._blob_reader[CONTENT_TYPES_URI]) - - @lazyproperty - def _pkg_srels(self): - """Filty temporary hack during refactoring.""" - return self._srels_for(PACKAGE_URI) - - @lazyproperty - def _sparts(self): - """ - Return a list of |_SerializedPart| instances corresponding to the - parts in *phys_reader* accessible by walking the relationship graph - starting with *pkg_srels*. - """ - sparts = [] - part_walker = self._walk_phys_parts(self._pkg_srels) - for partname, blob, srels in part_walker: - content_type = self._content_types[partname] - spart = _SerializedPart(partname, content_type, blob, srels) - sparts.append(spart) - return tuple(sparts) - - def _srels_for(self, source_uri): - """ - Return |_SerializedRelationshipCollection| instance populated with - relationships for source identified by *source_uri*. - """ - rels_xml = self.rels_xml_for(source_uri) - return _SerializedRelationshipCollection.load_from_xml( - source_uri.baseURI, rels_xml - ) - - def _walk_phys_parts(self, srels, visited_partnames=None): - """ - Generate a 3-tuple `(partname, blob, srels)` for each of the parts in - *phys_reader* by walking the relationship graph rooted at srels. - """ - if visited_partnames is None: - visited_partnames = [] - for srel in srels: - if srel.is_external: - continue - partname = srel.target_partname - if partname in visited_partnames: - continue - visited_partnames.append(partname) - part_srels = self._srels_for(partname) - blob = self._blob_reader[partname] - yield (partname, blob, part_srels) - for partname, blob, srels in self._walk_phys_parts( - part_srels, visited_partnames - ): - yield (partname, blob, srels) - - -class _ContentTypeMap(object): - """ - Value type providing dictionary semantics for looking up content type by - part name, e.g. ``content_type = cti['/ppt/presentation.xml']``. - """ - - def __init__(self): - super(_ContentTypeMap, self).__init__() - self._overrides = CaseInsensitiveDict() - self._defaults = CaseInsensitiveDict() - - def __getitem__(self, partname): - """ - Return content type for part identified by *partname*. - """ - if not isinstance(partname, PackURI): - tmpl = "_ContentTypeMap key must be , got %s" - raise KeyError(tmpl % type(partname)) - if partname in self._overrides: - return self._overrides[partname] - if partname.ext in self._defaults: - return self._defaults[partname.ext] - tmpl = "no content type for partname '%s' in [Content_Types].xml" - raise KeyError(tmpl % partname) - - @staticmethod - def from_xml(content_types_xml): - """ - Return a new |_ContentTypeMap| instance populated with the contents - of *content_types_xml*. - """ - types_elm = parse_xml(content_types_xml) - ct_map = _ContentTypeMap() - for o in types_elm.override_lst: - ct_map._add_override(o.partName, o.contentType) - for d in types_elm.default_lst: - ct_map._add_default(d.extension, d.contentType) - return ct_map - - def _add_default(self, extension, content_type): - """ - Add the default mapping of *extension* to *content_type* to this - content type mapping. *extension* does not include the leading - period. - """ - self._defaults[extension] = content_type - - def _add_override(self, partname, content_type): - """ - Add the default mapping of *partname* to *content_type* to this - content type mapping. - """ - self._overrides[partname] = content_type - class PackageWriter(object): """ @@ -334,136 +203,6 @@ def write(self, pack_uri, blob): self._zipf.writestr(pack_uri.membername, blob) -class _SerializedPart(object): - """ - Value object for an OPC package part. Provides access to the partname, - content type, blob, and serialized relationships for the part. - """ - - def __init__(self, partname, content_type, blob, srels): - super(_SerializedPart, self).__init__() - self._partname = partname - self._content_type = content_type - self._blob = blob - self._srels = srels - - @property - def partname(self): - return self._partname - - @property - def content_type(self): - return self._content_type - - @property - def blob(self): - return self._blob - - @property - def srels(self): - return self._srels - - -class _SerializedRelationship(object): - """ - Value object representing a serialized relationship in an OPC package. - Serialized, in this case, means any target part is referred to via its - partname rather than a direct link to an in-memory |Part| object. - """ - - def __init__(self, baseURI, rel_elm): - super(_SerializedRelationship, self).__init__() - self._baseURI = baseURI - self._rId = rel_elm.rId - self._reltype = rel_elm.reltype - self._target_mode = rel_elm.targetMode - self._target_ref = rel_elm.target_ref - - @property - def is_external(self): - """ - True if target_mode is ``RTM.EXTERNAL`` - """ - return self._target_mode == RTM.EXTERNAL - - @property - def reltype(self): - """Relationship type, like ``RT.OFFICE_DOCUMENT``""" - return self._reltype - - @property - def rId(self): - """ - Relationship id, like 'rId9', corresponds to the ``Id`` attribute on - the ``CT_Relationship`` element. - """ - return self._rId - - @property - def target_mode(self): - """ - String in ``TargetMode`` attribute of ``CT_Relationship`` element, - one of ``RTM.INTERNAL`` or ``RTM.EXTERNAL``. - """ - return self._target_mode - - @property - def target_ref(self): - """ - String in ``Target`` attribute of ``CT_Relationship`` element, a - relative part reference for internal target mode or an arbitrary URI, - e.g. an HTTP URL, for external target mode. - """ - return self._target_ref - - @property - def target_partname(self): - """ - |PackURI| instance containing partname targeted by this relationship. - Raises ``ValueError`` on reference if target_mode is ``'External'``. - Use :attr:`target_mode` to check before referencing. - """ - if self.is_external: - msg = ( - "target_partname attribute on Relationship is undefined w" - 'here TargetMode == "External"' - ) - raise ValueError(msg) - # lazy-load _target_partname attribute - if not hasattr(self, "_target_partname"): - self._target_partname = PackURI.from_rel_ref(self._baseURI, self.target_ref) - return self._target_partname - - -class _SerializedRelationshipCollection(object): - """ - Read-only sequence of |_SerializedRelationship| instances corresponding - to the relationships item XML passed to constructor. - """ - - def __init__(self): - super(_SerializedRelationshipCollection, self).__init__() - self._srels = [] - - def __iter__(self): - """Support iteration, e.g. 'for x in srels:'""" - return self._srels.__iter__() - - @staticmethod - def load_from_xml(baseURI, rels_item_xml): - """ - Return |_SerializedRelationshipCollection| instance loaded with the - relationships contained in *rels_item_xml*. Returns an empty - collection if *rels_item_xml* is |None|. - """ - srels = _SerializedRelationshipCollection() - if rels_item_xml is not None: - rels_elm = parse_xml(rels_item_xml) - for rel_elm in rels_elm.relationship_lst: - srels._srels.append(_SerializedRelationship(baseURI, rel_elm)) - return srels - - class _ContentTypesItem(object): """ Service class that composes a content types item ([Content_Types].xml) diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index c0572d32b..6dd4cfd67 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -4,6 +4,7 @@ import collections import io +import itertools import pytest @@ -19,16 +20,17 @@ Part, PartFactory, XmlPart, + _ContentTypeMap, _PackageLoader, _Relationship, _Relationships, ) -from pptx.opc.serialized import PackageReader, _SerializedRelationship +from pptx.opc.serialized import PackageReader from pptx.oxml import parse_xml from pptx.package import Package from ..unitutil.cxml import element -from ..unitutil.file import absjoin, snippet_bytes, test_file_dir +from ..unitutil.file import absjoin, snippet_bytes, testfile_bytes, test_file_dir from ..unitutil.mock import ( ANY, call, @@ -126,20 +128,6 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): rels[2], ) - def it_can_add_a_relationship_to_a_part(self, request, _rels_prop_, relationships_): - _rels_prop_.return_value = relationships_ - relationship_ = instance_mock(request, _Relationship) - relationships_.add_relationship.return_value = relationship_ - target_ = instance_mock(request, Part, name="target_part") - package = OpcPackage(None) - - relationship = package.load_rel(RT.SLIDE, target_, "rId99") - - relationships_.add_relationship.assert_called_once_with( - RT.SLIDE, target_, "rId99", False - ) - assert relationship is relationship_ - def it_can_establish_a_relationship_to_another_part( self, request, _rels_prop_, relationships_ ): @@ -256,75 +244,32 @@ def it_provides_a_load_interface_classmethod(self, request, package_): assert pkg_xml_rels is pkg_xml_rels_ assert parts == {"partname": "part"} - def it_can_unmarshal_parts( - self, request, _package_reader_prop_, package_reader_, package_ - ): - _package_reader_prop_.return_value = package_reader_ - package_reader_.iter_sparts.return_value = ( - ("partname-%d" % n, CT.PML_SLIDE, b"blob-%d" % n) for n in range(1, 4) - ) - parts_ = tuple(instance_mock(request, Part) for _ in range(5)) - PartFactory_ = class_mock( - request, "pptx.opc.package.PartFactory", side_effect=iter(parts_) - ) - package_loader = _PackageLoader(None, package_) - - parts = package_loader._parts - - assert PartFactory_.call_args_list == [ - call("partname-1", CT.PML_SLIDE, package_, b"blob-1"), - call("partname-2", CT.PML_SLIDE, package_, b"blob-2"), - call("partname-3", CT.PML_SLIDE, package_, b"blob-3"), - ] - assert parts == { - "partname-1": parts_[0], - "partname-2": parts_[1], - "partname-3": parts_[2], + def it_loads_the_package_to_help(self, request, _xml_rels_prop_): + parts_ = { + "partname_%d" % n: instance_mock(request, Part, partname="partname_%d" % n) + for n in range(1, 4) } - - def it_can_unmarshal_relationships( - self, request, _package_reader_prop_, package_reader_, _parts_prop_, package_ - ): - _package_reader_prop_.return_value = package_reader_ - package_reader_.iter_srels.return_value = ( - ( - partname, - instance_mock( - request, - _SerializedRelationship, - name="srel_%d" % n, - rId="rId%d" % n, - reltype=reltype, - target_partname="partname_%d" % (n + 2), - target_ref="target_ref_%d" % n, - is_external=is_external, + property_mock(request, _PackageLoader, "_parts", return_value=parts_) + rels_ = dict( + itertools.chain( + (("/", instance_mock(request, _Relationships)),), + ( + ("partname_%d" % n, instance_mock(request, _Relationships)) + for n in range(1, 4) ), ) - for n, partname, reltype, is_external in ( - (1, "/", RT.SLIDE, False), - (2, "/", RT.HYPERLINK, True), - (3, "partname_1", RT.SLIDE, False), - (4, "partname_2", RT.HYPERLINK, True), - ) ) - parts = _parts_prop_.return_value = { - "partname_%d" % n: instance_mock(request, Part, name="part_%d" % n) - for n in range(1, 7) - } - package_loader = _PackageLoader(None, package_) + _xml_rels_prop_.return_value = rels_ + package_loader = _PackageLoader(None, None) - package_loader._unmarshal_relationships() + pkg_xml_rels, parts = package_loader._load() - assert package_.load_rel.call_args_list == [ - call(RT.SLIDE, parts["partname_3"], "rId1", False), - call(RT.HYPERLINK, "target_ref_2", "rId2", True), - ] - parts["partname_1"].load_rel.assert_called_once_with( - RT.SLIDE, parts["partname_5"], "rId3", False - ) - parts["partname_2"].load_rel.assert_called_once_with( - RT.HYPERLINK, "target_ref_4", "rId4", True - ) + for part_ in parts_.values(): + part_.load_rels_from_xml.assert_called_once_with( + rels_[part_.partname], parts_ + ) + assert pkg_xml_rels is rels_["/"] + assert parts is parts_ def it_loads_the_xml_relationships_from_the_package_to_help(self, request): pkg_xml_rels = parse_xml(snippet_bytes("package-rels-xml")) @@ -384,6 +329,10 @@ def _package_reader_prop_(self, request): def _parts_prop_(self, request): return property_mock(request, _PackageLoader, "_parts") + @pytest.fixture + def _xml_rels_prop_(self, request): + return property_mock(request, _PackageLoader, "_xml_rels") + class DescribePart(object): """Unit-test suite for `pptx.opc.package.Part` objects.""" @@ -424,13 +373,6 @@ def it_can_drop_a_relationship( _rel_ref_count_.assert_called_once_with(part, "rId42") assert relationships_.pop.call_args_list == calls - def it_can_load_a_relationship(self, load_rel_fixture): - part, rels_, reltype_, target_, rId_ = load_rel_fixture - - part.load_rel(reltype_, target_, rId_) - - rels_.add_relationship.assert_called_once_with(reltype_, target_, rId_, False) - def it_knows_the_package_it_belongs_to(self, package_get_fixture): part, expected_package = package_get_fixture assert part.package == expected_package @@ -525,11 +467,6 @@ def content_type_fixture(self): part = Part(None, content_type, None, None) return part, content_type - @pytest.fixture - def load_rel_fixture(self, part, _rels_prop_, rels_, reltype_, part_, rId_): - _rels_prop_.return_value = rels_ - return part, rels_, reltype_, part_, rId_ - @pytest.fixture def package_get_fixture(self, package_): part = Part(None, None, package_) @@ -732,6 +669,79 @@ def part_args_2_(self, request): return partname_2_, content_type_2_, pkg_2_, blob_2_ +class Describe_ContentTypeMap(object): + """Unit-test suite for `pptx.opc.package._ContentTypeMap` objects.""" + + def it_can_construct_from_content_types_xml(self, request): + _init_ = initializer_mock(request, _ContentTypeMap) + content_types_xml = ( + '\n' + ' \n' + ' \n' + ' \n" + "\n" + ) + + ct_map = _ContentTypeMap.from_xml(content_types_xml) + + _init_.assert_called_once_with( + ct_map, + {"/ppt/presentation.xml": CT.PML_PRESENTATION_MAIN}, + {"png": CT.PNG, "xml": CT.XML}, + ) + + @pytest.mark.parametrize( + "partname, expected_value", + ( + ("/docProps/core.xml", CT.OPC_CORE_PROPERTIES), + ("/ppt/presentation.xml", CT.PML_PRESENTATION_MAIN), + ("/PPT/Presentation.XML", CT.PML_PRESENTATION_MAIN), + ("/ppt/viewprops.xml", CT.PML_VIEW_PROPS), + ), + ) + def it_matches_an_override_on_case_insensitive_partname( + self, content_type_map, partname, expected_value + ): + assert content_type_map[PackURI(partname)] == expected_value + + @pytest.mark.parametrize( + "partname, expected_value", + ( + ("/foo/bar.xml", CT.XML), + ("/FOO/BAR.Rels", CT.OPC_RELATIONSHIPS), + ("/foo/bar.jpeg", CT.JPEG), + ), + ) + def it_falls_back_to_case_insensitive_extension_default_match( + self, content_type_map, partname, expected_value + ): + assert content_type_map[PackURI(partname)] == expected_value + + 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"' + ) + + def it_raises_TypeError_on_key_not_instance_of_PackURI(self, content_type_map): + with pytest.raises(TypeError) as e: + content_type_map["/part/name1.xml"] + assert str(e.value) == "_ContentTypeMap key must be , got str" + + # fixtures --------------------------------------------- + + @pytest.fixture(scope="class") + def content_type_map(self): + return _ContentTypeMap.from_xml( + testfile_bytes("expanded_pptx", "[Content_Types].xml") + ) + + class Describe_Relationships(object): """Unit-test suite for `pptx.opc.package._Relationships` objects.""" diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index ee98096da..27ac1d8ce 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -13,21 +13,16 @@ from zipfile import ZIP_DEFLATED, ZipFile from pptx.exceptions import PackageNotFoundError -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM -from pptx.opc.oxml import CT_Relationship +from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import Part from pptx.opc.packuri import PackURI from pptx.opc.serialized import ( PackageReader, PackageWriter, - _ContentTypeMap, _ContentTypesItem, _DirPkgReader, _PhysPkgReader, _PhysPkgWriter, - _SerializedPart, - _SerializedRelationship, - _SerializedRelationshipCollection, _ZipPkgReader, _ZipPkgWriter, ) @@ -81,110 +76,6 @@ def _blob_reader_prop_(self, request): return property_mock(request, PackageReader, "_blob_reader") -class Describe_ContentTypeMap(object): - def it_can_construct_from_ct_item_xml(self, from_xml_fixture): - content_types_xml, expected_defaults, expected_overrides = from_xml_fixture - ct_map = _ContentTypeMap.from_xml(content_types_xml) - assert ct_map._defaults == expected_defaults - assert ct_map._overrides == expected_overrides - - def it_matches_an_override_on_case_insensitive_partname( - self, match_override_fixture - ): - ct_map, partname, content_type = match_override_fixture - assert ct_map[partname] == content_type - - def it_falls_back_to_case_insensitive_extension_default_match( - self, match_default_fixture - ): - ct_map, partname, content_type = match_default_fixture - assert ct_map[partname] == content_type - - def it_should_raise_on_partname_not_found(self): - ct_map = _ContentTypeMap() - with pytest.raises(KeyError): - ct_map[PackURI("/!blat/rhumba.1x&")] - - def it_should_raise_on_key_not_instance_of_PackURI(self): - ct_map = _ContentTypeMap() - ct_map._add_override(PackURI("/part/name1.xml"), "app/vnd.type1") - with pytest.raises(KeyError): - ct_map["/part/name1.xml"] - - # fixtures --------------------------------------------- - - @pytest.fixture - def from_xml_fixture(self): - entries = ( - ("Default", "xml", CT.XML), - ("Default", "PNG", CT.PNG), - ("Override", "/ppt/presentation.xml", CT.PML_PRESENTATION_MAIN), - ) - content_types_xml = self._xml_from(entries) - expected_defaults = {} - expected_overrides = {} - for entry in entries: - if entry[0] == "Default": - ext = entry[1].lower() - content_type = entry[2] - expected_defaults[ext] = content_type - elif entry[0] == "Override": - partname, content_type = entry[1:] - expected_overrides[partname] = content_type - return content_types_xml, expected_defaults, expected_overrides - - @pytest.fixture( - params=[ - ("/foo/bar.xml", "xml", "application/xml"), - ("/foo/bar.PNG", "png", "image/png"), - ("/foo/bar.jpg", "JPG", "image/jpeg"), - ] - ) - def match_default_fixture(self, request): - partname_str, ext, content_type = request.param - partname = PackURI(partname_str) - ct_map = _ContentTypeMap() - ct_map._add_override(PackURI("/bar/foo.xyz"), "application/xyz") - ct_map._add_default(ext, content_type) - return ct_map, partname, content_type - - @pytest.fixture( - params=[ - ("/foo/bar.xml", "/foo/bar.xml"), - ("/foo/bar.xml", "/FOO/Bar.XML"), - ("/FoO/bAr.XmL", "/foo/bar.xml"), - ] - ) - def match_override_fixture(self, request): - partname_str, should_match_partname_str = request.param - partname = PackURI(partname_str) - should_match_partname = PackURI(should_match_partname_str) - content_type = "appl/vnd-foobar" - ct_map = _ContentTypeMap() - ct_map._add_override(partname, content_type) - return ct_map, should_match_partname, content_type - - def _xml_from(self, entries): - """ - Return XML for a [Content_Types].xml based on items in *entries*. - """ - types_bldr = a_Types().with_nsdecls() - for entry in entries: - if entry[0] == "Default": - ext, content_type = entry[1:] - default_bldr = a_Default() - default_bldr.with_Extension(ext) - default_bldr.with_ContentType(content_type) - types_bldr.with_child(default_bldr) - elif entry[0] == "Override": - partname, content_type = entry[1:] - override_bldr = an_Override() - override_bldr.with_PartName(partname) - override_bldr.with_ContentType(content_type) - types_bldr.with_child(override_bldr) - return types_bldr.xml() - - class DescribePackageWriter(object): def it_can_write_a_package(self, _PhysPkgWriter_, _write_methods): # mockery ---------------------- @@ -423,119 +314,6 @@ def pkg_file(self, request): return pkg_file -class Describe_SerializedPart(object): - def it_remembers_construction_values(self): - # test data -------------------- - partname = "/part/name.xml" - content_type = "app/vnd.type" - blob = "" - srels = "srels proxy" - # exercise --------------------- - spart = _SerializedPart(partname, content_type, blob, srels) - # verify ----------------------- - assert spart.partname == partname - assert spart.content_type == content_type - assert spart.blob == blob - assert spart.srels == srels - - -class Describe_SerializedRelationship(object): - def it_remembers_construction_values(self): - # test data -------------------- - rel_elm = CT_Relationship.new( - "rId9", "ReLtYpE", "docProps/core.xml", RTM.INTERNAL - ) - # exercise --------------------- - srel = _SerializedRelationship("/", rel_elm) - # verify ----------------------- - assert srel.rId == "rId9" - assert srel.reltype == "ReLtYpE" - assert srel.target_ref == "docProps/core.xml" - assert srel.target_mode == RTM.INTERNAL - - def it_knows_when_it_is_external(self): - cases = (RTM.INTERNAL, RTM.EXTERNAL) - expected_values = (False, True) - for target_mode, expected_value in zip(cases, expected_values): - rel_elm = CT_Relationship.new( - "rId9", "ReLtYpE", "docProps/core.xml", target_mode - ) - srel = _SerializedRelationship(None, rel_elm) - assert srel.is_external is expected_value - - def it_can_calculate_its_target_partname(self): - # test data -------------------- - cases = ( - ("/", "docProps/core.xml", "/docProps/core.xml"), - ("/ppt", "viewProps.xml", "/ppt/viewProps.xml"), - ( - "/ppt/slides", - "../slideLayouts/slideLayout1.xml", - "/ppt/slideLayouts/slideLayout1.xml", - ), - ) - for baseURI, target_ref, expected_partname in cases: - # setup -------------------- - rel_elm = Mock( - name="rel_elm", - rId=None, - reltype=None, - target_ref=target_ref, - target_mode=RTM.INTERNAL, - ) - # exercise ----------------- - srel = _SerializedRelationship(baseURI, rel_elm) - # verify ------------------- - assert srel.target_partname == expected_partname - - def it_raises_on_target_partname_when_external(self): - rel_elm = CT_Relationship.new( - "rId9", "ReLtYpE", "docProps/core.xml", RTM.EXTERNAL - ) - srel = _SerializedRelationship("/", rel_elm) - with pytest.raises(ValueError): - srel.target_partname - - -class Describe_SerializedRelationshipCollection(object): - def it_can_load_from_xml(self, parse_xml, _SerializedRelationship_): - # mockery ---------------------- - baseURI, rels_item_xml, rel_elm_1, rel_elm_2 = ( - Mock(name="baseURI"), - Mock(name="rels_item_xml"), - Mock(name="rel_elm_1"), - Mock(name="rel_elm_2"), - ) - rels_elm = Mock(name="rels_elm", relationship_lst=[rel_elm_1, rel_elm_2]) - parse_xml.return_value = rels_elm - # exercise --------------------- - srels = _SerializedRelationshipCollection.load_from_xml(baseURI, rels_item_xml) - # verify ----------------------- - expected_calls = [call(baseURI, rel_elm_1), call(baseURI, rel_elm_2)] - parse_xml.assert_called_once_with(rels_item_xml) - assert _SerializedRelationship_.call_args_list == expected_calls - assert isinstance(srels, _SerializedRelationshipCollection) - - def it_should_be_iterable(self): - srels = _SerializedRelationshipCollection() - try: - for x in srels: - pass - except TypeError: - msg = "_SerializedRelationshipCollection object is not iterable" - pytest.fail(msg) - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def parse_xml(self, request): - return function_mock(request, "pptx.opc.serialized.parse_xml") - - @pytest.fixture - def _SerializedRelationship_(self, request): - return class_mock(request, "pptx.opc.serialized._SerializedRelationship") - - class Describe_ContentTypesItem(object): def it_can_compose_content_types_xml(self, xml_for_fixture): parts, expected_xml = xml_for_fixture diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index dcaaeec15..6b8879935 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -78,3 +78,10 @@ def testfile(name): Return the absolute path to test file having *name*. """ return absjoin(test_file_dir, name) + + +def testfile_bytes(*segments): + """Return bytes of file at path formed by adding `segments` to test file dir.""" + path = os.path.join(test_file_dir, *segments) + with open(path, "rb") as f: + return f.read() From 899188390b220505bcefe97e9b8483dccdafe810 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 24 Aug 2021 15:43:08 -0700 Subject: [PATCH 22/69] rfctr: reimplement PackageWriter --- pptx/opc/serialized.py | 213 +++++----- tests/opc/test_serialized.py | 377 +++++++++--------- .../test_files/snippets/content-types-xml.txt | 9 + 3 files changed, 318 insertions(+), 281 deletions(-) create mode 100644 tests/test_files/snippets/content-types-xml.txt diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index 2e5a20127..ada470cc5 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -49,53 +49,62 @@ def _blob_reader(self): class PackageWriter(object): - """ - Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be - either a path to a zip file (a string) or a file-like object. Its single - API method, :meth:`write`, is static, so this class is not intended to - be instantiated. + """Writes a zip-format OPC package to `pkg_file`. + + `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. """ - @staticmethod - def write(pkg_file, pkg_rels, parts): - """ - Write a physical package (.pptx file) to *pkg_file* containing - *pkg_rels* and *parts* and a content types stream based on the - content types of the parts. - """ - phys_writer = _PhysPkgWriter(pkg_file) - PackageWriter._write_content_types_stream(phys_writer, parts) - PackageWriter._write_pkg_rels(phys_writer, pkg_rels) - PackageWriter._write_parts(phys_writer, parts) - phys_writer.close() - - @staticmethod - def _write_content_types_stream(phys_writer, parts): - """ - Write ``[Content_Types].xml`` part to the physical package with an - appropriate content type lookup target for each part in *parts*. + def __init__(self, pkg_file, pkg_rels, parts): + self._pkg_file = pkg_file + self._pkg_rels = pkg_rels + self._parts = parts + + @classmethod + def write(cls, pkg_file, pkg_rels, parts): + """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. """ - content_types_blob = serialize_part_xml(_ContentTypesItem.xml_for(parts)) - phys_writer.write(CONTENT_TYPES_URI, content_types_blob) + cls(pkg_file, pkg_rels, parts)._write() - @staticmethod - def _write_parts(phys_writer, parts): + def _write(self): + """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): + """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. """ - Write the blob of each part in *parts* to the package, along with a - rels item for its relationships if and only if it has any. + phys_writer.write( + CONTENT_TYPES_URI, + serialize_part_xml(_ContentTypesItem.xml_for(self._parts)), + ) + + def _write_parts(self, phys_writer): + """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 parts: + for part in self._parts: phys_writer.write(part.partname, part.blob) - if len(part._rels): - phys_writer.write(part.partname.rels_uri, part._rels.xml) + if part._rels: + phys_writer.write(part.partname.rels_uri, part.rels.xml) - @staticmethod - def _write_pkg_rels(phys_writer, pkg_rels): - """ - Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the - package. - """ - phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) + def _write_pkg_rels(self, phys_writer): + """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): @@ -171,87 +180,95 @@ def _blobs(self): class _PhysPkgWriter(object): - """ - Factory for physical package writer objects. - """ + """Base class for physical package writer objects.""" + + @classmethod + def factory(cls, pkg_file): + """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`. - def __new__(cls, pkg_file): - return super(_PhysPkgWriter, cls).__new__(_ZipPkgWriter) + Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be + implemented or even a `_StreamPkgWriter`. + """ + return _ZipPkgWriter(pkg_file) class _ZipPkgWriter(_PhysPkgWriter): - """ - Implements |PhysPkgWriter| interface for a zip file OPC package. - """ + """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package.""" def __init__(self, pkg_file): - super(_ZipPkgWriter, self).__init__() - self._zipf = zipfile.ZipFile(pkg_file, "w", compression=zipfile.ZIP_DEFLATED) + self._pkg_file = pkg_file - def close(self): - """ - Close the zip archive, flushing any pending physical writes and - releasing any resources it's using. + def __enter__(self): + """Enable use as a context-manager. Opening zip for writing happens here.""" + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + """Close the zip archive on exit from context. + + Closing flushes any pending physical writes and releasing any resources it's + using. """ self._zipf.close() def write(self, pack_uri, blob): - """ - Write *blob* to this zip package with the membername corresponding to - *pack_uri*. - """ + """Write `blob` to zip package with membername corresponding to `pack_uri`.""" self._zipf.writestr(pack_uri.membername, blob) + @lazyproperty + def _zipf(self): + """`ZipFile` instance open for writing.""" + return zipfile.ZipFile(self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED) + class _ContentTypesItem(object): - """ - Service class that composes a content types item ([Content_Types].xml) - based on a list of parts. Not meant to be instantiated directly, its - single interface method is xml_for(), e.g. - ``_ContentTypesItem.xml_for(parts)``. - """ + """Composes content-types "part" ([Content_Types].xml) for a collection of parts.""" - def __init__(self): - self._defaults = CaseInsensitiveDict() - self._overrides = dict() + def __init__(self, parts): + self._parts = parts @classmethod def xml_for(cls, parts): + """Return content-types XML mapping each part in `parts` to a content-type. + + The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC + package. """ - Return content types XML mapping each part in *parts* to the - appropriate content type and suitable for storage as - ``[Content_Types].xml`` in an OPC package. - """ - cti = cls() - cti._defaults["rels"] = CT.OPC_RELATIONSHIPS - cti._defaults["xml"] = CT.XML - for part in parts: - cti._add_content_type(part.partname, part.content_type) - return cti._xml() - - def _add_content_type(self, partname, content_type): - """ - Add a content type for the part with *partname* and *content_type*, - using a default or override as appropriate. - """ - ext = partname.ext - if (ext.lower(), content_type) in default_content_types: - self._defaults[ext] = content_type - else: - self._overrides[partname] = content_type + return cls(parts)._xml + @lazyproperty def _xml(self): + """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. """ - Return etree element containing the XML representation of this content - types item, suitable for serialization to the ``[Content_Types].xml`` - item for an OPC package. Although the sequence of elements is not - strictly significant, as an aid to testing and readability Default - elements are sorted by extension and Override elements are sorted by - partname. - """ + defaults, overrides = self._defaults_and_overrides _types_elm = CT_Types.new() - for ext in sorted(self._defaults.keys()): - _types_elm.add_default(ext, self._defaults[ext]) - for partname in sorted(self._overrides.keys()): - _types_elm.add_override(partname, self._overrides[partname]) + + for ext, content_type in sorted(defaults.items()): + _types_elm.add_default(ext, content_type) + for partname, content_type in sorted(overrides.items()): + _types_elm.add_override(partname, content_type) + return _types_elm + + @lazyproperty + def _defaults_and_overrides(self): + """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() + + for part in self._parts: + partname, content_type = part.partname, part.content_type + ext = partname.ext + if (ext.lower(), content_type) in default_content_types: + defaults[ext] = content_type + else: + overrides[partname] = content_type + + return defaults, overrides diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 27ac1d8ce..95695a075 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -9,13 +9,12 @@ import hashlib import pytest - -from zipfile import ZIP_DEFLATED, ZipFile +import zipfile from pptx.exceptions import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT -from pptx.opc.package import Part -from pptx.opc.packuri import PackURI +from pptx.opc.package import Part, _Relationships +from pptx.opc.packuri import CONTENT_TYPES_URI, PackURI from pptx.opc.serialized import ( PackageReader, PackageWriter, @@ -27,17 +26,15 @@ _ZipPkgWriter, ) -from .unitdata.types import a_Default, a_Types, an_Override -from ..unitutil.file import absjoin, test_file_dir +from ..unitutil.file import absjoin, snippet_text, test_file_dir from ..unitutil.mock import ( - MagicMock, - Mock, + ANY, call, class_mock, function_mock, + initializer_mock, instance_mock, method_mock, - patch, property_mock, ) @@ -69,6 +66,23 @@ def it_can_get_the_rels_xml_for_a_partname(self, _blob_reader_prop_): 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_): + _blob_reader_prop_.return_value = {"/ppt/_rels/presentation.xml.rels": b"blob"} + package_reader = PackageReader(None) + + assert package_reader.rels_xml_for(PackURI("/ppt/slides.slide1.xml")) is None + + def it_constructs_its_blob_reader_to_help(self, request): + phys_pkg_reader_ = instance_mock(request, _PhysPkgReader) + _PhysPkgReader_ = class_mock(request, "pptx.opc.serialized._PhysPkgReader") + _PhysPkgReader_.factory.return_value = phys_pkg_reader_ + package_reader = PackageReader("prs.pptx") + + blob_reader = package_reader._blob_reader + + _PhysPkgReader_.factory.assert_called_once_with("prs.pptx") + assert blob_reader is phys_pkg_reader_ + # fixture components ----------------------------------- @pytest.fixture @@ -77,97 +91,96 @@ def _blob_reader_prop_(self, request): class DescribePackageWriter(object): - def it_can_write_a_package(self, _PhysPkgWriter_, _write_methods): - # mockery ---------------------- - pkg_file = Mock(name="pkg_file") - pkg_rels = Mock(name="pkg_rels") - parts = Mock(name="parts") - phys_writer = _PhysPkgWriter_.return_value - # exercise --------------------- - PackageWriter.write(pkg_file, pkg_rels, parts) - # verify ----------------------- - expected_calls = [ - call._write_content_types_stream(phys_writer, parts), - call._write_pkg_rels(phys_writer, pkg_rels), - call._write_parts(phys_writer, parts), - ] - _PhysPkgWriter_.assert_called_once_with(pkg_file) - assert _write_methods.mock_calls == expected_calls - phys_writer.close.assert_called_once_with() - - def it_can_write_a_content_types_stream(self, xml_for, serialize_part_xml_): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - parts = Mock(name="parts") - # exercise --------------------- - PackageWriter._write_content_types_stream(phys_writer, parts) - # verify ----------------------- - xml_for.assert_called_once_with(parts) - serialize_part_xml_.assert_called_once_with(xml_for.return_value) - phys_writer.write.assert_called_once_with( - "/[Content_Types].xml", serialize_part_xml_.return_value + """Unit-test suite for `pptx.opc.serialized.PackageWriter` objects.""" + + def it_provides_a_write_interface_classmethod(self, request, relationships_): + _init_ = initializer_mock(request, PackageWriter) + _write_ = method_mock(request, PackageWriter, "_write") + + PackageWriter.write("prs.pptx", relationships_, ("part_1", "part_2")) + + _init_.assert_called_once_with( + ANY, "prs.pptx", relationships_, ("part_1", "part_2") ) + _write_.assert_called_once_with(ANY) + + def it_can_write_a_package(self, request, phys_writer_): + _PhysPkgWriter_ = class_mock(request, "pptx.opc.serialized._PhysPkgWriter") + phys_writer_.__enter__.return_value = phys_writer_ + _PhysPkgWriter_.factory.return_value = phys_writer_ + _write_content_types_stream_ = method_mock( + request, PackageWriter, "_write_content_types_stream" + ) + _write_pkg_rels_ = method_mock(request, PackageWriter, "_write_pkg_rels") + _write_parts_ = method_mock(request, PackageWriter, "_write_parts") + package_writer = PackageWriter("prs.pptx", None, None) + + package_writer._write() + + _PhysPkgWriter_.factory.assert_called_once_with("prs.pptx") + _write_content_types_stream_.assert_called_once_with( + package_writer, phys_writer_ + ) + _write_pkg_rels_.assert_called_once_with(package_writer, phys_writer_) + _write_parts_.assert_called_once_with(package_writer, phys_writer_) + + def it_can_write_a_content_types_stream(self, request, phys_writer_): + _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._write_content_types_stream(phys_writer_) + + _ContentTypesItem_.xml_for.assert_called_once_with(("part_1", "part_2")) + 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_ = ( + instance_mock( + request, + Part, + partname=PackURI("/ppt/%s.xml" % x), + blob="blob_%s" % x, + rels=instance_mock(request, _Relationships, xml="rels_xml_%s" % x), + ) + for x in ("a", "b", "c") + ) + package_writer = PackageWriter(None, None, parts_) + + package_writer._write_parts(phys_writer_) - def it_can_write_a_pkg_rels_item(self): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - pkg_rels = Mock(name="pkg_rels") - # exercise --------------------- - PackageWriter._write_pkg_rels(phys_writer, pkg_rels) - # verify ----------------------- - phys_writer.write.assert_called_once_with("/_rels/.rels", pkg_rels.xml) - - def it_can_write_a_list_of_parts(self): - # mockery ---------------------- - phys_writer = Mock(name="phys_writer") - rels = MagicMock(name="rels") - rels.__len__.return_value = 1 - part1 = Mock(name="part1", _rels=rels) - part2 = Mock(name="part2", _rels=[]) - # exercise --------------------- - PackageWriter._write_parts(phys_writer, [part1, part2]) - # verify ----------------------- - expected_calls = [ - call(part1.partname, part1.blob), - call(part1.partname.rels_uri, part1._rels.xml), - call(part2.partname, part2.blob), + assert phys_writer_.write.call_args_list == [ + call("/ppt/a.xml", "blob_a"), + call("/ppt/_rels/a.xml.rels", "rels_xml_a"), + call("/ppt/b.xml", "blob_b"), + call("/ppt/_rels/b.xml.rels", "rels_xml_b"), + call("/ppt/c.xml", "blob_c"), + call("/ppt/_rels/c.xml.rels", "rels_xml_c"), ] - assert phys_writer.write.mock_calls == expected_calls - # fixtures --------------------------------------------- + def it_can_write_a_pkg_rels_item(self, request, phys_writer_, relationships_): + relationships_.xml = b"pkg-rels-xml" + package_writer = PackageWriter(None, relationships_, None) - @pytest.fixture - def _PhysPkgWriter_(self, request): - _patch = patch("pptx.opc.serialized._PhysPkgWriter") - request.addfinalizer(_patch.stop) - return _patch.start() + package_writer._write_pkg_rels(phys_writer_) - @pytest.fixture - def serialize_part_xml_(self, request): - return function_mock(request, "pptx.opc.serialized.serialize_part_xml") + phys_writer_.write.assert_called_once_with("/_rels/.rels", b"pkg-rels-xml") + + # fixture components ----------------------------------- @pytest.fixture - def _write_methods(self, request): - """Mock that patches all the _write_* methods of PackageWriter""" - root_mock = Mock(name="PackageWriter") - patch1 = patch.object(PackageWriter, "_write_content_types_stream") - patch2 = patch.object(PackageWriter, "_write_pkg_rels") - patch3 = patch.object(PackageWriter, "_write_parts") - root_mock.attach_mock(patch1.start(), "_write_content_types_stream") - root_mock.attach_mock(patch2.start(), "_write_pkg_rels") - root_mock.attach_mock(patch3.start(), "_write_parts") - - def fin(): - patch1.stop() - patch2.stop() - patch3.stop() - - request.addfinalizer(fin) - return root_mock + def phys_writer_(self, request): + return instance_mock(request, _ZipPkgWriter) @pytest.fixture - def xml_for(self, request): - return method_mock(request, _ContentTypesItem, "xml_for") + def relationships_(self, request): + return instance_mock(request, _Relationships) class Describe_PhysPkgReader(object): @@ -270,112 +283,110 @@ def zip_pkg_reader(self, request): return _ZipPkgReader(zip_pkg_path) +class Describe_PhysPkgWriter(object): + """Unit-test suite for `pptx.opc.serialized._PhysPkgWriter` objects.""" + + def it_constructs_ZipPkgWriter_unconditionally(self, request): + zip_pkg_writer_ = instance_mock(request, _ZipPkgWriter) + _ZipPkgWriter_ = class_mock( + request, "pptx.opc.serialized._ZipPkgWriter", return_value=zip_pkg_writer_ + ) + + phys_writer = _PhysPkgWriter.factory("prs.pptx") + + _ZipPkgWriter_.assert_called_once_with("prs.pptx") + assert phys_writer is zip_pkg_writer_ + + class Describe_ZipPkgWriter(object): - def it_is_used_by_PhysPkgWriter_unconditionally(self, tmp_pptx_path): - phys_writer = _PhysPkgWriter(tmp_pptx_path) - assert isinstance(phys_writer, _ZipPkgWriter) - - def it_opens_pkg_file_zip_on_construction(self, ZipFile_): - pkg_file = Mock(name="pkg_file") - _ZipPkgWriter(pkg_file) - ZipFile_.assert_called_once_with(pkg_file, "w", compression=ZIP_DEFLATED) - - def it_can_be_closed(self, ZipFile_): - # mockery ---------------------- - zipf = ZipFile_.return_value - zip_pkg_writer = _ZipPkgWriter(None) - # exercise --------------------- - zip_pkg_writer.close() - # verify ----------------------- - zipf.close.assert_called_once_with() - - def it_can_write_a_blob(self, pkg_file): - # setup ------------------------ - pack_uri = PackURI("/part/name.xml") - blob = "".encode("utf-8") - # exercise --------------------- - pkg_writer = _PhysPkgWriter(pkg_file) - pkg_writer.write(pack_uri, blob) - pkg_writer.close() - # verify ----------------------- - written_blob_sha1 = hashlib.sha1(blob).hexdigest() - zipf = ZipFile(pkg_file, "r") - retrieved_blob = zipf.read(pack_uri.membername) - zipf.close() - retrieved_blob_sha1 = hashlib.sha1(retrieved_blob).hexdigest() - assert retrieved_blob_sha1 == written_blob_sha1 + """Unit-test suite for `pptx.opc.serialized._ZipPkgWriter` objects.""" - # fixtures --------------------------------------------- + def it_has_an__enter__method_for_context_management(self): + pkg_writer = _ZipPkgWriter(None) + assert pkg_writer.__enter__() is pkg_writer - @pytest.fixture - def pkg_file(self, request): - pkg_file = BytesIO() - request.addfinalizer(pkg_file.close) - return pkg_file + def and_it_closes_the_zip_archive_on_context__exit__(self, _zipf_prop_): + _ZipPkgWriter(None).__exit__(None, None, None) + _zipf_prop_.return_value.close.assert_called_once_with() + + def it_can_write_a_blob(self, _zipf_prop_): + """Integrates with zipfile.ZipFile.""" + pack_uri = PackURI("/part/name.xml") + _zipf_prop_.return_value = zipf = zipfile.ZipFile(BytesIO(), "w") + pkg_writer = _ZipPkgWriter(None) + pkg_writer.write(pack_uri, b"blob") -class Describe_ContentTypesItem(object): - def it_can_compose_content_types_xml(self, xml_for_fixture): - parts, expected_xml = xml_for_fixture - types_elm = _ContentTypesItem.xml_for(parts) - assert types_elm.xml == expected_xml + members = {PackURI("/%s" % name): zipf.read(name) for name in zipf.namelist()} + assert len(members) == 1 + assert members[pack_uri] == b"blob" - # fixtures --------------------------------------------- + def it_provides_access_to_the_open_zip_file_to_help(self, request): + ZipFile_ = class_mock(request, "pptx.opc.serialized.zipfile.ZipFile") + pkg_writer = _ZipPkgWriter("prs.pptx") - def _mock_part(self, request, name, partname_str, content_type): - partname = PackURI(partname_str) - return instance_mock( - request, Part, name=name, partname=partname, content_type=content_type - ) + zipf = pkg_writer._zipf - @pytest.fixture( - params=[ - ("Default", "/ppt/MEDIA/image.PNG", CT.PNG), - ("Default", "/ppt/media/image.xml", CT.XML), - ("Default", "/ppt/media/image.rels", CT.OPC_RELATIONSHIPS), - ("Default", "/ppt/media/image.jpeg", CT.JPEG), - ("Override", "/docProps/core.xml", "app/vnd.core"), - ("Override", "/ppt/slides/slide1.xml", "app/vnd.ct_sld"), - ("Override", "/zebra/foo.bar", "app/vnd.foobar"), - ] - ) - def xml_for_fixture(self, request): - elm_type, partname_str, content_type = request.param - part_ = self._mock_part(request, "part_", partname_str, content_type) - # expected_xml ----------------- - types_bldr = a_Types().with_nsdecls() - ext = partname_str.split(".")[-1].lower() - if elm_type == "Default" and ext not in ("rels", "xml"): - default_bldr = a_Default() - default_bldr.with_Extension(ext) - default_bldr.with_ContentType(content_type) - types_bldr.with_child(default_bldr) - - types_bldr.with_child( - a_Default().with_Extension("rels").with_ContentType(CT.OPC_RELATIONSHIPS) - ) - types_bldr.with_child( - a_Default().with_Extension("xml").with_ContentType(CT.XML) + ZipFile_.assert_called_once_with( + "prs.pptx", "w", compression=zipfile.ZIP_DEFLATED ) + assert zipf is ZipFile_.return_value - if elm_type == "Override": - override_bldr = an_Override() - override_bldr.with_PartName(partname_str) - override_bldr.with_ContentType(content_type) - types_bldr.with_child(override_bldr) - - expected_xml = types_bldr.xml() - return [part_], expected_xml + # fixtures --------------------------------------------- + @pytest.fixture + def _zipf_prop_(self, request): + return property_mock(request, _ZipPkgWriter, "_zipf") -# fixtures ------------------------------------------------- +class Describe_ContentTypesItem(object): + """Unit-test suite for `pptx.opc.serialized._ContentTypesItem` objects.""" + + def it_provides_an_interface_classmethod(self, request): + _init_ = initializer_mock(request, _ContentTypesItem) + property_mock(request, _ContentTypesItem, "_xml", return_value=b"xml") + + xml = _ContentTypesItem.xml_for(("part", "zuh")) + + _init_.assert_called_once_with(ANY, ("part", "zuh")) + assert xml == b"xml" + + def it_can_compose_content_types_xml(self, request): + defaults = {"png": CT.PNG, "xml": CT.XML, "rels": CT.OPC_RELATIONSHIPS} + overrides = { + "/docProps/core.xml": "app/vnd.core", + "/ppt/slides/slide1.xml": "app/vnd.ct_sld", + "/zebra/foo.bar": "app/vnd.foobar", + } + property_mock( + request, + _ContentTypesItem, + "_defaults_and_overrides", + return_value=(defaults, overrides), + ) -@pytest.fixture -def tmp_pptx_path(tmpdir): - return str(tmpdir.join("test_python-pptx.pptx")) + content_types = _ContentTypesItem(None)._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 + ) + 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 -@pytest.fixture -def ZipFile_(request): - return class_mock(request, "pptx.opc.serialized.zipfile.ZipFile") + assert defaults == {"png": CT.PNG, "rels": CT.OPC_RELATIONSHIPS, "xml": CT.XML} + assert overrides == { + "/ppt/slides/slide1.xml": CT.PML_SLIDE, + "/docProps/core.xml": CT.OPC_CORE_PROPERTIES, + } diff --git a/tests/test_files/snippets/content-types-xml.txt b/tests/test_files/snippets/content-types-xml.txt new file mode 100644 index 000000000..9e8ee1feb --- /dev/null +++ b/tests/test_files/snippets/content-types-xml.txt @@ -0,0 +1,9 @@ + + + + + + + + + From 8fa420d1c379063993a6973c6c15eb5e40fbac71 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 24 Aug 2021 15:51:36 -0700 Subject: [PATCH 23/69] rfctr: improve expression Also modernize PartFactory tests since I had to touch them. --- pptx/opc/oxml.py | 4 +-- pptx/opc/package.py | 22 ++++++------ pptx/parts/chart.py | 14 ++++---- pptx/parts/image.py | 4 --- pptx/parts/slide.py | 6 ++-- tests/opc/test_package.py | 76 ++++++++++++++------------------------- 6 files changed, 47 insertions(+), 79 deletions(-) diff --git a/pptx/opc/oxml.py b/pptx/opc/oxml.py index 321249687..da774c3c9 100644 --- a/pptx/opc/oxml.py +++ b/pptx/opc/oxml.py @@ -101,9 +101,7 @@ def add_rel(self, rId, reltype, target, is_external=False): @classmethod def new(cls): """Return a new ```` element.""" - xml = '' % nsmap["pr"] - relationships = parse_xml(xml) - return relationships + return parse_xml('' % nsmap["pr"]) @property def xml(self): diff --git a/pptx/opc/package.py b/pptx/opc/package.py index c59e8c86d..15c527db7 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -122,7 +122,7 @@ def relate_to(self, target, reltype, is_external=False): def save(self, pkg_file): """Save this package to `pkg_file`. - `pkg_file` can be either a path to a file (a string) or a file-like object. + `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())) @@ -134,7 +134,7 @@ def _load(self): @lazyproperty def _rels(self): - """The |_Relationships| object containing the relationships for this package.""" + """|Relationships| object containing relationships of this package.""" return _Relationships(PACKAGE_URI.baseURI) @@ -289,7 +289,7 @@ def blob(self, bytes_): """ self._blob = bytes_ - @property + @lazyproperty def content_type(self): """Content-type (MIME-type) of this part.""" return self._content_type @@ -313,7 +313,7 @@ def load_rels_from_xml(self, xml_rels, parts): """ self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts) - @property + @lazyproperty def package(self): """|OpcPackage| instance this part belongs to.""" return self._package @@ -334,8 +334,10 @@ def partname(self): @partname.setter def partname(self, partname): if not isinstance(partname, PackURI): - tmpl = "partname must be instance of PackURI, got '%s'" - raise TypeError(tmpl % type(partname).__name__) + raise TypeError( + "partname must be instance of PackURI, got '%s'" + % type(partname).__name__ + ) self._partname = partname def relate_to(self, target, reltype, is_external=False): @@ -379,12 +381,11 @@ def _blob_from_file(self, file): def _rel_ref_count(self, rId): """Return int count of references in this part's XML to `rId`.""" - rIds = self._element.xpath("//@r:id") - return len([_rId for _rId in rIds if _rId == rId]) + return len([r for r in self._element.xpath("//@r:id") if r == rId]) @lazyproperty def _rels(self): - """|Relationships| collection of relationships from this part to other parts.""" + """|Relationships| object containing relationships from this part to others.""" return _Relationships(self._partname.baseURI) @@ -428,7 +429,6 @@ class PartFactory(object): """ part_type_for = {} - default_part_type = Part def __new__(cls, partname, content_type, package, blob): PartClass = cls._part_cls_for(content_type) @@ -442,7 +442,7 @@ def _part_cls_for(cls, content_type): """ if content_type in cls.part_type_for: return cls.part_type_for[content_type] - return cls.default_part_type + return Part class _ContentTypeMap(object): diff --git a/pptx/parts/chart.py b/pptx/parts/chart.py index 890a25f28..2a8a04283 100644 --- a/pptx/parts/chart.py +++ b/pptx/parts/chart.py @@ -62,7 +62,7 @@ def update_from_xlsx_blob(self, xlsx_blob): """ xlsx_part = self.xlsx_part if xlsx_part is None: - self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._package) + self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._chart_part.package) return xlsx_part.blob = xlsx_blob @@ -74,9 +74,11 @@ def xlsx_part(self): is |None| if there is no `` element. """ xlsx_part_rId = self._chartSpace.xlsx_part_rId - if xlsx_part_rId is None: - return None - return self._chart_part.related_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): @@ -87,7 +89,3 @@ def xlsx_part(self, xlsx_part): rId = self._chart_part.relate_to(xlsx_part, RT.PACKAGE) externalData = self._chartSpace.get_or_add_externalData() externalData.rId = rId - - @property - def _package(self): - return self._chart_part.package diff --git a/pptx/parts/image.py b/pptx/parts/image.py index 578640cad..db59c5fcc 100644 --- a/pptx/parts/image.py +++ b/pptx/parts/image.py @@ -29,10 +29,6 @@ def __init__(self, partname, content_type, package, blob, filename=None): super(ImagePart, self).__init__(partname, content_type, package, blob) self._filename = filename - @classmethod - def load(cls, partname, content_type, package, blob): - return cls(partname, content_type, package, blob) - @classmethod def new(cls, package, image): """Return new |ImagePart| instance containing `image`. diff --git a/pptx/parts/slide.py b/pptx/parts/slide.py index 12d9ea319..dfd81b4e4 100644 --- a/pptx/parts/slide.py +++ b/pptx/parts/slide.py @@ -167,9 +167,9 @@ def add_chart_part(self, chart_type, chart_data): The chart depicts `chart_data` and is related to the slide contained in this part by `rId`. """ - chart_part = ChartPart.new(chart_type, chart_data, self.package) - rId = self.relate_to(chart_part, RT.CHART) - return rId + return self.relate_to( + ChartPart.new(chart_type, chart_data, self._package), RT.CHART + ) def add_embedded_ole_object_part(self, prog_id, ole_object_file): """Return rId of newly-added OLE-object part formed from `ole_object_file`.""" diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 6dd4cfd67..225aff97a 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -27,7 +27,6 @@ ) from pptx.opc.serialized import PackageReader from pptx.oxml import parse_xml -from pptx.package import Package from ..unitutil.cxml import element from ..unitutil.file import absjoin, snippet_bytes, testfile_bytes, test_file_dir @@ -35,12 +34,10 @@ ANY, call, class_mock, - cls_attr_mock, function_mock, initializer_mock, instance_mock, method_mock, - Mock, property_mock, ) @@ -609,64 +606,43 @@ class DescribePartFactory(object): """Unit-test suite for `pptx.opc.package.PartFactory` objects.""" def it_constructs_custom_part_type_for_registered_content_types( - self, part_args_, CustomPartClass_, part_of_custom_type_ + self, request, package_, part_ ): - # fixture ---------------------- - partname, content_type, pkg, blob = part_args_ - # exercise --------------------- - PartFactory.part_type_for[content_type] = CustomPartClass_ - part = PartFactory(partname, content_type, pkg, blob) - # verify ----------------------- - CustomPartClass_.load.assert_called_once_with(partname, content_type, pkg, blob) - assert part is part_of_custom_type_ + SlidePart_ = class_mock(request, "pptx.opc.package.XmlPart") + SlidePart_.load.return_value = part_ + partname = PackURI("/ppt/slides/slide7.xml") + PartFactory.part_type_for[CT.PML_SLIDE] = SlidePart_ - def it_constructs_part_using_default_class_when_no_custom_registered( - self, part_args_2_, DefaultPartClass_, part_of_default_type_ - ): - partname, content_type, pkg, blob = part_args_2_ - part = PartFactory(partname, content_type, pkg, blob) - DefaultPartClass_.load.assert_called_once_with( - partname, content_type, pkg, blob - ) - assert part is part_of_default_type_ + part = PartFactory(partname, CT.PML_SLIDE, package_, b"blob") - # fixtures --------------------------------------------- + SlidePart_.load.assert_called_once_with( + partname, CT.PML_SLIDE, package_, b"blob" + ) + assert part is part_ - @pytest.fixture - def part_of_custom_type_(self, request): - return instance_mock(request, Part) + def it_constructs_part_using_default_class_when_no_custom_registered( + self, request, package_, part_ + ): + Part_ = class_mock(request, "pptx.opc.package.Part") + Part_.load.return_value = part_ + partname = PackURI("/bar/foo.xml") - @pytest.fixture - def CustomPartClass_(self, request, part_of_custom_type_): - CustomPartClass_ = Mock(name="CustomPartClass", spec=Part) - CustomPartClass_.load.return_value = part_of_custom_type_ - return CustomPartClass_ + part = PartFactory(partname, CT.OFC_VML_DRAWING, package_, b"blob") - @pytest.fixture - def part_of_default_type_(self, request): - return instance_mock(request, Part) + Part_.load.assert_called_once_with( + partname, CT.OFC_VML_DRAWING, package_, b"blob" + ) + assert part is part_ - @pytest.fixture - def DefaultPartClass_(self, request, part_of_default_type_): - DefaultPartClass_ = cls_attr_mock(request, PartFactory, "default_part_type") - DefaultPartClass_.load.return_value = part_of_default_type_ - return DefaultPartClass_ + # fixtures components ---------------------------------- @pytest.fixture - def part_args_(self, request): - partname_ = PackURI("/foo/bar.xml") - content_type_ = "content/type" - pkg_ = instance_mock(request, Package, name="pkg_") - blob_ = b"blob_" - return partname_, content_type_, pkg_, blob_ + def package_(self, request): + return instance_mock(request, OpcPackage) @pytest.fixture - def part_args_2_(self, request): - partname_2_ = PackURI("/bar/foo.xml") - content_type_2_ = "foobar/type" - pkg_2_ = instance_mock(request, Package, name="pkg_2_") - blob_2_ = b"blob_2_" - return partname_2_, content_type_2_, pkg_2_, blob_2_ + def part_(self, request): + return instance_mock(request, Part) class Describe_ContentTypeMap(object): From 686b01bb2bb261edc19658f5acbf4452b98bca1a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 19 Aug 2021 09:02:46 -0700 Subject: [PATCH 24/69] rfctr: modernize tests related to pptx.opc.package --- tests/opc/test_package.py | 250 ++++++++++--------------------- tests/opc/unitdata/rels.py | 6 +- tests/oxml/test_slide.py | 36 ++--- tests/parts/test_chart.py | 25 ++-- tests/parts/test_slide.py | 91 +++++------ tests/shapes/test_placeholder.py | 48 +++--- tests/test_action.py | 2 + tests/test_table.py | 1 - 8 files changed, 162 insertions(+), 297 deletions(-) diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 225aff97a..f89c66ab8 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -14,7 +14,6 @@ RELATIONSHIP_TYPE as RT, ) from pptx.opc.oxml import CT_Relationship, CT_Relationships -from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.opc.package import ( OpcPackage, Part, @@ -25,7 +24,7 @@ _Relationship, _Relationships, ) -from pptx.opc.serialized import PackageReader +from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.oxml import parse_xml from ..unitutil.cxml import element @@ -125,32 +124,6 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): rels[2], ) - def it_can_establish_a_relationship_to_another_part( - self, request, _rels_prop_, relationships_ - ): - relationships_.get_or_add.return_value = "rId99" - _rels_prop_.return_value = relationships_ - part_ = instance_mock(request, Part) - package = OpcPackage(None) - - rId = package.relate_to(part_, "http://rel/type") - - relationships_.get_or_add.assert_called_once_with("http://rel/type", part_) - assert rId == "rId99" - - def it_can_find_a_part_related_by_reltype( - self, request, _rels_prop_, relationships_ - ): - related_part_ = instance_mock(request, Part, name="related_part_") - relationships_.part_with_reltype.return_value = related_part_ - _rels_prop_.return_value = relationships_ - package = OpcPackage(None) - - related_part = package.part_related_by(RT.SLIDE) - - relationships_.part_with_reltype.assert_called_once_with(RT.SLIDE) - assert related_part is related_part_ - @pytest.mark.parametrize( "ns, expected_n", (((), 1), ((1,), 2), ((1, 2), 3), ((2, 4), 3), ((1, 4), 3)), @@ -174,6 +147,32 @@ def it_can_find_the_next_available_partname(self, request, ns, expected_n): PackURI_.assert_called_once_with(next_partname) assert partname == next_partname + def it_can_find_a_part_related_by_reltype( + self, request, _rels_prop_, relationships_ + ): + related_part_ = instance_mock(request, Part, name="related_part_") + relationships_.part_with_reltype.return_value = related_part_ + _rels_prop_.return_value = relationships_ + package = OpcPackage(None) + + related_part = package.part_related_by(RT.SLIDE) + + relationships_.part_with_reltype.assert_called_once_with(RT.SLIDE) + assert related_part is related_part_ + + def it_can_establish_a_relationship_to_another_part( + self, request, _rels_prop_, relationships_ + ): + relationships_.get_or_add.return_value = "rId99" + _rels_prop_.return_value = relationships_ + part_ = instance_mock(request, Part) + package = OpcPackage(None) + + rId = package.relate_to(part_, "http://rel/type") + + relationships_.get_or_add.assert_called_once_with("http://rel/type", part_) + assert rId == "rId99" + def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): _rels_prop_.return_value = relationships_ parts_ = tuple(instance_mock(request, Part) for _ in range(3)) @@ -314,18 +313,6 @@ def it_loads_the_xml_relationships_from_the_package_to_help(self, request): def package_(self, request): return instance_mock(request, OpcPackage) - @pytest.fixture - def package_reader_(self, request): - return instance_mock(request, PackageReader) - - @pytest.fixture - def _package_reader_prop_(self, request): - return property_mock(request, _PackageLoader, "_package_reader") - - @pytest.fixture - def _parts_prop_(self, request): - return property_mock(request, _PackageLoader, "_parts") - @pytest.fixture def _xml_rels_prop_(self, request): return property_mock(request, _PackageLoader, "_xml_rels") @@ -335,25 +322,24 @@ class DescribePart(object): """Unit-test suite for `pptx.opc.package.Part` objects.""" def it_can_be_constructed_by_PartFactory(self, request, package_): - partname_ = PackURI("/ppt/slides/slide1.xml") + partname_ = instance_mock(request, PackURI) _init_ = initializer_mock(request, Part) - part = Part.load(partname_, CT.PML_SLIDE, b"blob", package_) + part = Part.load(partname_, CT.PML_SLIDE, package_, b"blob") - _init_.assert_called_once_with(part, partname_, CT.PML_SLIDE, b"blob", package_) + _init_.assert_called_once_with(part, partname_, CT.PML_SLIDE, package_, b"blob") assert isinstance(part, Part) def it_uses_the_load_blob_as_its_blob(self): assert Part(None, None, None, b"blob").blob == b"blob" def it_can_change_its_blob(self): - part, new_blob = Part(None, None, "xyz", None), "foobar" - part.blob = new_blob - assert part.blob == new_blob + part = Part(None, None, None, b"old-blob") + part.blob = b"new-blob" + assert part.blob == b"new-blob" - def it_knows_its_content_type(self, content_type_fixture): - part, expected_content_type = content_type_fixture - assert part.content_type == expected_content_type + def it_knows_its_content_type(self): + 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( @@ -370,26 +356,26 @@ def it_can_drop_a_relationship( _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_get_fixture): - part, expected_package = package_get_fixture - assert part.package == expected_package + def it_knows_the_package_it_belongs_to(self, package_): + assert Part(None, None, package_).package is package_ - def it_can_find_a_related_part_by_reltype(self, related_part_fixture): - part, reltype_, related_part_ = related_part_fixture + def it_can_find_a_part_related_by_reltype(self, _rels_prop_, relationships_, part_): + relationships_.part_with_reltype.return_value = part_ + _rels_prop_.return_value = relationships_ + part = Part(None, None, None) - related_part = part.part_related_by(reltype_) + related_part = part.part_related_by(RT.CHART) - part.rels.part_with_reltype.assert_called_once_with(reltype_) - assert related_part is related_part_ + relationships_.part_with_reltype.assert_called_once_with(RT.CHART) + assert related_part is part_ - def it_knows_its_partname(self, partname_get_fixture): - part, expected_partname = partname_get_fixture - assert part.partname == expected_partname + def it_knows_its_partname(self): + assert Part(PackURI("/part/name"), None, None).partname == PackURI("/part/name") - def it_can_change_its_partname(self, partname_set_fixture): - part, new_partname = partname_set_fixture - part.partname = new_partname - assert part.partname == new_partname + def it_can_change_its_partname(self): + part = Part(PackURI("/old/part/name"), None, None) + part.partname = PackURI("/new/part/name") + assert part.partname == PackURI("/new/part/name") def it_can_establish_a_relationship_to_another_part( self, _rels_prop_, relationships_, part_ @@ -403,13 +389,19 @@ def it_can_establish_a_relationship_to_another_part( relationships_.get_or_add.assert_called_once_with(RT.SLIDE, part_) assert rId == "rId42" - def it_can_establish_an_external_relationship(self, relate_to_url_fixture): - part, url_, reltype_, rId_ = relate_to_url_fixture + def and_it_can_establish_an_external_relationship( + self, _rels_prop_, relationships_ + ): + relationships_.get_or_add_ext_rel.return_value = "rId24" + _rels_prop_.return_value = relationships_ + part = Part(None, None, None) - rId = part.relate_to(url_, reltype_, is_external=True) + rId = part.relate_to("http://url", RT.HYPERLINK, is_external=True) - part.rels.get_or_add_ext_rel.assert_called_once_with(reltype_, url_) - assert rId is rId_ + relationships_.get_or_add_ext_rel.assert_called_once_with( + RT.HYPERLINK, "http://url" + ) + assert rId == "rId24" def it_can_find_a_related_part_by_rId( self, request, _rels_prop_, relationships_, relationship_, part_ @@ -424,20 +416,18 @@ def it_can_find_a_related_part_by_rId( relationships_.__getitem__.assert_called_once_with("rId17") assert related_part is part_ - def it_provides_access_to_its_relationships(self, rels_fixture): - part, Relationships_, partname_, rels_ = rels_fixture - - rels = part.rels - - Relationships_.assert_called_once_with(partname_.baseURI) - assert rels is rels_ - - def it_can_find_the_uri_of_an_external_relationship(self, target_ref_fixture): - part, rId_, url_ = target_ref_fixture + def it_can_find_a_target_ref_URI_by_rId( + self, request, _rels_prop_, relationships_, relationship_ + ): + relationship_.target_ref = "http://url" + relationships_.__getitem__.return_value = relationship_ + _rels_prop_.return_value = relationships_ + part = Part(None, None, None) - url = part.target_ref(rId_) + target_ref = part.target_ref("rId9") - assert url == url_ + relationships_.__getitem__.assert_called_once_with("rId9") + assert target_ref == "http://url" def it_can_load_a_blob_from_a_file_path_to_help(self): path = absjoin(test_file_dir, "minimal.pptx") @@ -451,89 +441,27 @@ def it_can_load_a_blob_from_a_file_like_object_to_help(self): part = Part(None, None, None, None) assert part._blob_from_file(io.BytesIO(b"012345")) == b"012345" - # fixtures --------------------------------------------- - - @pytest.fixture - def blob_fixture(self, blob_): - part = Part(None, None, blob_, None) - return part, blob_ - - @pytest.fixture - def content_type_fixture(self): - content_type = "content/type" - part = Part(None, content_type, None, None) - return part, content_type - - @pytest.fixture - def package_get_fixture(self, package_): - part = Part(None, None, package_) - return part, package_ - - @pytest.fixture - def partname_get_fixture(self): - partname = PackURI("/part/name") - part = Part(partname, None, None, None) - return part, partname - - @pytest.fixture - def partname_set_fixture(self): - old_partname = PackURI("/old/part/name") - new_partname = PackURI("/new/part/name") - part = Part(old_partname, None, None, None) - return part, new_partname - - @pytest.fixture - def relate_to_url_fixture(self, part, _rels_prop_, rels_, url_, reltype_, rId_): - _rels_prop_.return_value = rels_ - return part, url_, reltype_, rId_ - - @pytest.fixture - def related_part_fixture(self, part, _rels_prop_, rels_, reltype_, part_): - _rels_prop_.return_value = rels_ - return part, reltype_, part_ + def it_constructs_its_relationships_object_to_help(self, request, relationships_): + _Relationships_ = class_mock( + request, "pptx.opc.package._Relationships", return_value=relationships_ + ) + part = Part(PackURI("/ppt/slides/slide1.xml"), None, None) - @pytest.fixture - def rels_fixture(self, Relationships_, partname_, rels_): - part = Part(partname_, None, None) - return part, Relationships_, partname_, rels_ + rels = part._rels - @pytest.fixture - def target_ref_fixture(self, part, _rels_prop_, rId_, rel_, url_): - _rels_prop_.return_value = {rId_: rel_} - return part, rId_, url_ + _Relationships_.assert_called_once_with("/ppt/slides") + assert rels is relationships_ # fixture components --------------------------------------------- - @pytest.fixture - def blob_(self, request): - return instance_mock(request, bytes) - @pytest.fixture def package_(self, request): return instance_mock(request, OpcPackage) - @pytest.fixture - def part(self): - return Part(None, None, None) - @pytest.fixture def part_(self, request): return instance_mock(request, Part) - @pytest.fixture - def partname_(self, request): - return instance_mock(request, PackURI) - - @pytest.fixture - def Relationships_(self, request, rels_): - return class_mock( - request, "pptx.opc.package._Relationships", return_value=rels_ - ) - - @pytest.fixture - def rel_(self, request, rId_, url_): - return instance_mock(request, _Relationship, rId=rId_, target_ref=url_) - @pytest.fixture def relationship_(self, request): return instance_mock(request, _Relationship) @@ -542,30 +470,10 @@ def relationship_(self, request): def relationships_(self, request): return instance_mock(request, _Relationships) - @pytest.fixture - def rels_(self, request, part_, rel_, rId_): - rels_ = instance_mock(request, _Relationships) - rels_.part_with_reltype.return_value = part_ - rels_.get_or_add.return_value = rel_ - rels_.get_or_add_ext_rel.return_value = rId_ - return rels_ - @pytest.fixture def _rels_prop_(self, request): return property_mock(request, Part, "_rels") - @pytest.fixture - def reltype_(self, request): - return instance_mock(request, str) - - @pytest.fixture - def rId_(self, request): - return instance_mock(request, str) - - @pytest.fixture - def url_(self, request): - return instance_mock(request, str) - class DescribeXmlPart(object): """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index 6412cba22..da81e2d02 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -25,8 +25,8 @@ def with_indent(self, indent): return self -class RelationshipsBuilder(object): - """Builder class for test _Relationships""" +class _RelationshipsBuilder(object): + """Builder class for test _Relationshipss""" partname_tmpls = { RT.SLIDE_MASTER: "/ppt/slideMasters/slideMaster%d.xml", @@ -295,7 +295,7 @@ def a_Relationships(): def a_rels(): - return RelationshipsBuilder() + return _RelationshipsBuilder() def a_Types(): diff --git a/tests/oxml/test_slide.py b/tests/oxml/test_slide.py index c8c336a5b..d1b48ebc4 100644 --- a/tests/oxml/test_slide.py +++ b/tests/oxml/test_slide.py @@ -1,12 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.oxml.slide module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import pytest +"""Unit-test suite for `pptx.oxml.slide` module.""" from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide @@ -14,28 +8,16 @@ class DescribeCT_NotesMaster(object): - def it_can_create_a_default_notesMaster_element(self, new_fixture): - expected_xml = new_fixture - notesMaster = CT_NotesMaster.new_default() - assert notesMaster.xml == expected_xml - - # fixtures ------------------------------------------------------- + """Unit-test suite for `pptx.oxml.slide.CT_NotesMaster` objects.""" - @pytest.fixture - def new_fixture(self): - expected_xml = snippet_text("default-notesMaster") - return expected_xml + def it_can_create_a_default_notesMaster_element(self): + notesMaster = CT_NotesMaster.new_default() + assert notesMaster.xml == snippet_text("default-notesMaster") class DescribeCT_NotesSlide(object): - def it_can_create_a_new_notes_element(self, new_fixture): - expected_xml = new_fixture - notes = CT_NotesSlide.new() - assert notes.xml == expected_xml + """Unit-test suite for `pptx.oxml.slide.CT_NotesSlide` objects.""" - # fixtures ------------------------------------------------------- - - @pytest.fixture - def new_fixture(self): - expected_xml = snippet_text("default-notes") - return expected_xml + def it_can_create_a_new_notes_element(self): + notes = CT_NotesSlide.new() + assert notes.xml == snippet_text("default-notes") diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index 5f9428857..cb12254d5 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -126,36 +126,29 @@ 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_replaces_xlsx_blob_when_part_exists(self, update_blob_fixture): - chart_data, xlsx_blob_ = update_blob_fixture - chart_data.update_from_xlsx_blob(xlsx_blob_) - assert chart_data.xlsx_part.blob is xlsx_blob_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def update_blob_fixture(self, request, xlsx_blob_, xlsx_part_prop_): + 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) - return chart_data, xlsx_blob_ + chart_data.update_from_xlsx_blob(b"xlsx-blob") + + assert chart_data.xlsx_part.blob == b"xlsx-blob" # fixture components --------------------------------------------- @pytest.fixture - def chart_part_(self, request): + def chart_part_(self, request, package_, xlsx_part_): return instance_mock(request, ChartPart) @pytest.fixture def package_(self, request): return instance_mock(request, OpcPackage) - @pytest.fixture - def xlsx_blob_(self, request): - return instance_mock(request, bytes) - @pytest.fixture def xlsx_part_(self, request): return instance_mock(request, EmbeddedXlsxPart) @pytest.fixture - def xlsx_part_prop_(self, request, xlsx_part_): + def xlsx_part_prop_(self, request): return property_mock(request, ChartWorkbook, "xlsx_part") diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index 6a081e49f..3f18a8176 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -5,7 +5,7 @@ import pytest from pptx.chart.data import ChartData -from pptx.enum.base import EnumValue +from pptx.enum.chart import XL_CHART_TYPE as XCT from pptx.enum.shapes import PROG_ID from pptx.media import Video from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT @@ -122,13 +122,15 @@ def it_creates_a_new_notes_master_part_to_help( ) notesMaster = element("p:notesMaster") method_mock(request, CT_NotesMaster, "new_default", return_value=notesMaster) - partname = PackURI("/ppt/notesMasters/notesMaster1.xml") notes_master_part = NotesMasterPart._new(package_) CT_NotesMaster.new_default.assert_called_once_with() NotesMasterPart_.assert_called_once_with( - partname, CT.PML_NOTES_MASTER, package_, notesMaster + PackURI("/ppt/notesMasters/notesMaster1.xml"), + CT.PML_NOTES_MASTER, + package_, + notesMaster, ) assert notes_master_part is notes_master_part_ @@ -231,8 +233,9 @@ def it_adds_a_notes_slide_part_to_help( ) notes = element("p:notes") new_ = method_mock(request, CT_NotesSlide, "new", return_value=notes) - partname = PackURI("/ppt/notesSlides/notesSlide42.xml") - package_.next_partname.return_value = partname + package_.next_partname.return_value = PackURI( + "/ppt/notesSlides/notesSlide42.xml" + ) notes_slide_part = NotesSlidePart._add_notes_slide_part( package_, slide_part_, notes_master_part_ @@ -243,7 +246,10 @@ def it_adds_a_notes_slide_part_to_help( ) new_.assert_called_once_with() NotesSlidePart_.assert_called_once_with( - partname, CT.PML_NOTES_SLIDE, package_, notes + PackURI("/ppt/notesSlides/notesSlide42.xml"), + CT.PML_NOTES_SLIDE, + package_, + notes, ) assert notes_slide_part_.relate_to.call_args_list == [ call(notes_master_part_, RT.NOTES_MASTER), @@ -294,19 +300,18 @@ def it_knows_whether_it_has_a_notes_slide(self, has_notes_slide_fixture): assert value is expected_value def it_can_add_a_chart_part(self, request, package_, relate_to_): + chart_data_ = instance_mock(request, ChartData) chart_part_ = instance_mock(request, ChartPart) ChartPart_ = class_mock(request, "pptx.parts.slide.ChartPart") ChartPart_.new.return_value = chart_part_ - chart_type_ = instance_mock(request, EnumValue) - chart_data_ = instance_mock(request, ChartData) relate_to_.return_value = "rId42" slide_part = SlidePart(None, None, package_, None) - _rId = slide_part.add_chart_part(chart_type_, chart_data_) + rId = slide_part.add_chart_part(XCT.RADAR, chart_data_) - ChartPart_.new.assert_called_once_with(chart_type_, chart_data_, package_) + ChartPart_.new.assert_called_once_with(XCT.RADAR, chart_data_, package_) relate_to_.assert_called_once_with(slide_part, chart_part_, RT.CHART) - assert _rId == "rId42" + assert rId == "rId42" @pytest.mark.parametrize( "prog_id, rel_type", @@ -321,7 +326,7 @@ def it_can_add_an_embedded_ole_object_part( self, request, package_, relate_to_, prog_id, rel_type ): _blob_from_file_ = method_mock( - request, SlidePart, "_blob_from_file", autospec=True, return_value=b"012345" + request, SlidePart, "_blob_from_file", return_value=b"012345" ) embedded_package_part_ = instance_mock(request, EmbeddedPackagePart) EmbeddedPackagePart_ = class_mock( @@ -527,57 +532,33 @@ def video_(self, request): class DescribeSlideLayoutPart(object): """Unit-test suite for `pptx.parts.slide.SlideLayoutPart` objects.""" - def it_provides_access_to_its_slide_master(self, master_fixture): - slide_layout_part, part_related_by_, slide_master_ = master_fixture - slide_master = slide_layout_part.slide_master - part_related_by_.assert_called_once_with(slide_layout_part, RT.SLIDE_MASTER) - assert slide_master is slide_master_ - - def it_provides_access_to_its_slide_layout(self, layout_fixture): - slide_layout_part, SlideLayout_ = layout_fixture[:2] - sldLayout, slide_layout_ = layout_fixture[2:] - slide_layout = slide_layout_part.slide_layout - SlideLayout_.assert_called_once_with(sldLayout, slide_layout_part) - assert slide_layout is slide_layout_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def layout_fixture(self, SlideLayout_, slide_layout_): - sldLayout = element("p:sldLayout") - slide_layout_part = SlideLayoutPart(None, None, None, sldLayout) - return slide_layout_part, SlideLayout_, sldLayout, slide_layout_ - - @pytest.fixture - def master_fixture(self, part_related_by_, slide_master_, slide_master_part_): + def it_provides_access_to_its_slide_master(self, request): + slide_master_ = instance_mock(request, SlideMaster) + slide_master_part_ = instance_mock( + request, SlideMasterPart, slide_master=slide_master_ + ) + part_related_by_ = method_mock( + request, SlideLayoutPart, "part_related_by", return_value=slide_master_part_ + ) slide_layout_part = SlideLayoutPart(None, None, None, None) - part_related_by_.return_value = slide_master_part_ - slide_master_part_.slide_master = slide_master_ - return slide_layout_part, part_related_by_, slide_master_ - # fixture components ----------------------------------- + slide_master = slide_layout_part.slide_master - @pytest.fixture - def part_related_by_(self, request): - return method_mock(request, SlideLayoutPart, "part_related_by", autospec=True) + part_related_by_.assert_called_once_with(slide_layout_part, RT.SLIDE_MASTER) + assert slide_master is slide_master_ - @pytest.fixture - def SlideLayout_(self, request, slide_layout_): - return class_mock( + def it_provides_access_to_its_slide_layout(self, request): + slide_layout_ = instance_mock(request, SlideLayout) + SlideLayout_ = class_mock( request, "pptx.parts.slide.SlideLayout", return_value=slide_layout_ ) + sldLayout = element("p:sldLayout") + slide_layout_part = SlideLayoutPart(None, None, None, sldLayout) - @pytest.fixture - def slide_layout_(self, request): - return instance_mock(request, SlideLayout) - - @pytest.fixture - def slide_master_(self, request): - return instance_mock(request, SlideMaster) + slide_layout = slide_layout_part.slide_layout - @pytest.fixture - def slide_master_part_(self, request): - return instance_mock(request, SlideMasterPart) + SlideLayout_.assert_called_once_with(sldLayout, slide_layout_part) + assert slide_layout is slide_layout_ class DescribeSlideMasterPart(object): diff --git a/tests/shapes/test_placeholder.py b/tests/shapes/test_placeholder.py index 04a7d9e70..00bb6956f 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -338,8 +338,12 @@ def new_fixture(self): # fixture components --------------------------------------------- @pytest.fixture - def part_prop_(self, request): - return property_mock(request, ChartPlaceholder, "part") + def part_prop_(self, request, slide_): + return property_mock(request, ChartPlaceholder, "part", return_value=slide_) + + @pytest.fixture + def slide_(self, request): + return instance_mock(request, SlidePart) class DescribeLayoutPlaceholder(object): @@ -379,10 +383,8 @@ def master_placeholder_(self, request): return instance_mock(request, MasterPlaceholder) @pytest.fixture - def part_prop_(self, request, slide_layout_part_): - return property_mock( - request, LayoutPlaceholder, "part", return_value=slide_layout_part_ - ) + def part_prop_(self, request): + return property_mock(request, LayoutPlaceholder, "part") @pytest.fixture def slide_layout_part_(self, request): @@ -527,8 +529,12 @@ def image_part_(self, request): return instance_mock(request, ImagePart) @pytest.fixture - def part_prop_(self, request): - return property_mock(request, PicturePlaceholder, "part") + def part_prop_(self, request, slide_): + return property_mock(request, PicturePlaceholder, "part", return_value=slide_) + + @pytest.fixture + def slide_(self, request): + return instance_mock(request, SlidePart) class DescribeTablePlaceholder(object): @@ -560,20 +566,14 @@ def it_can_insert_a_table_into_itself(self, request): PlaceholderGraphicFrame_.assert_called_once_with(graphicFrame, table_ph._parent) assert ph_graphic_frame is placeholder_graphic_frame_ - def it_creates_a_graphicFrame_element_to_help(self, new_fixture): - table_ph, rows, cols, expected_xml = new_fixture - graphicFrame = table_ph._new_placeholder_table(rows, cols) - assert graphicFrame.xml == expected_xml - - # fixtures ------------------------------------------------------- - - @pytest.fixture - def new_fixture(self): - sp_cxml = ( - "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/(a:off{x=1," - "y=2},a:ext{cx=3,cy=4}))" + def it_creates_a_graphicFrame_element_to_help(self): + table_ph = TablePlaceholder( + element( + "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/(a:off{x=1,y=2}," + "a:ext{cx=3,cy=4}))" + ), + None, ) - table_ph = TablePlaceholder(element(sp_cxml), None) - rows, cols = 1, 1 - expected_xml = snippet_seq("placeholders")[0] - return table_ph, rows, cols, expected_xml + graphicFrame = table_ph._new_placeholder_table(1, 1) + + assert graphicFrame.xml == snippet_seq("placeholders")[0] diff --git a/tests/test_action.py b/tests/test_action.py index f11eebc73..8a3383c4e 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -2,6 +2,8 @@ """Unit-test suite for `pptx.action` module.""" +from __future__ import unicode_literals + import pytest from pptx.action import ActionSetting, Hyperlink diff --git a/tests/test_table.py b/tests/test_table.py index 4f2f68af5..1207ff275 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -684,7 +684,6 @@ def iter_fixture(self, request): tbl = element(tbl_cxml) columns = _ColumnCollection(tbl, None) expected_column_lst = tbl.xpath("//a:gridCol") - print(expected_column_lst) return columns, expected_column_lst @pytest.fixture( From 2b0343e8bcb3dd9be78e4912d6332e7561e46351 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 24 Aug 2021 17:33:12 -0700 Subject: [PATCH 25/69] rfctr: extract _RelatableMixin Both OpcPackage and Part can have relationships and there are several duplicated methods on both. Extract these to `_RelatableMixin` and subclass that mixin on both those classes. --- pptx/opc/package.py | 92 +++++++++--------- tests/opc/test_package.py | 195 ++++++++++++++++++-------------------- 2 files changed, 134 insertions(+), 153 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 15c527db7..eed381077 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -18,7 +18,44 @@ from pptx.util import lazyproperty -class OpcPackage(object): +class _RelatableMixin(object): + """Provide relationship methods required by both the package and each part.""" + + def part_related_by(self, reltype): + """Return (single) part having relationship to this package of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than + one such relationship is found. + """ + return self._rels.part_with_reltype(reltype) + + def relate_to(self, target, reltype, is_external=False): + """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. + """ + return ( + self._rels.get_or_add_ext_rel(reltype, target) + if is_external + else self._rels.get_or_add(reltype, target) + ) + + def related_part(self, rId): + """Return related |Part| subtype identified by `rId`.""" + return self._rels[rId].target_part + + def target_ref(self, rId): + """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.""" + raise NotImplementedError("`%s` must implement `.rels`" % type(self).__name__) + + +class OpcPackage(_RelatableMixin): """Main API class for |python-opc|. A new instance is constructed by calling the :meth:`open` classmethod with a path @@ -33,6 +70,10 @@ def open(cls, pkg_file): """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" return cls(pkg_file)._load() + def drop_rel(self, rId): + """Remove relationship identified by `rId`.""" + self._rels.pop(rId) + def iter_parts(self): """Generate exactly one reference to each part in the package.""" visited = set() @@ -100,25 +141,6 @@ def next_partname(self, tmpl): return PackURI(candidate_partname) raise Exception("ProgrammingError: ran out of candidate_partnames") - def part_related_by(self, reltype): - """Return (single) part having relationship to this package of `reltype`. - - Raises |KeyError| if no such relationship is found and |ValueError| if more than - one such relationship is found. - """ - return self._rels.part_with_reltype(reltype) - - def relate_to(self, target, reltype, is_external=False): - """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 is_external: - return self._rels.get_or_add_ext_rel(reltype, target) - else: - return self._rels.get_or_add(reltype, target) - def save(self, pkg_file): """Save this package to `pkg_file`. @@ -246,7 +268,7 @@ def _xml_rels_for(self, partname): return CT_Relationships.new() if rels_xml is None else parse_xml(rels_xml) -class Part(object): +class Part(_RelatableMixin): """Base class for package parts. Provides common properties and methods, but intended to be subclassed in client code @@ -318,14 +340,6 @@ def package(self): """|OpcPackage| instance this part belongs to.""" return self._package - def part_related_by(self, reltype): - """Return (single) part having relationship to this part of `reltype`. - - Raises |KeyError| if no such relationship is found and |ValueError| if more than - one such relationship is found. - """ - return self._rels.part_with_reltype(reltype) - @property def partname(self): """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" @@ -340,32 +354,12 @@ def partname(self, partname): ) self._partname = partname - def relate_to(self, target, reltype, is_external=False): - """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. - """ - return ( - self._rels.get_or_add_ext_rel(reltype, target) - if is_external - else self._rels.get_or_add(reltype, target) - ) - - def related_part(self, rId): - """Return related |Part| subtype identified by `rId`.""" - return self._rels[rId].target_part - @lazyproperty def rels(self): """|Relationships| collection of relationships from this part to other parts.""" # --- this must be public to allow the part graph to be traversed --- return self._rels - def target_ref(self, rId): - """Return URL contained in target ref of relationship identified by `rId`.""" - return self._rels[rId].target_ref - def _blob_from_file(self, file): """Return bytes of `file`, which is either a str path or a file-like object.""" # --- a str `file` is assumed to be a path --- diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index f89c66ab8..1b556d86a 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -21,6 +21,7 @@ XmlPart, _ContentTypeMap, _PackageLoader, + _RelatableMixin, _Relationship, _Relationships, ) @@ -41,6 +42,94 @@ ) +class Describe_RelatableMixin(object): + """Unit-test suite for `pptx.opc.package._RelatableMixin`. + + This mixin is used for both OpcPackage and Part because both a package and a part + can have relationships to target parts. + """ + + def it_can_find_a_part_related_by_reltype(self, _rels_prop_, relationships_, part_): + relationships_.part_with_reltype.return_value = part_ + _rels_prop_.return_value = relationships_ + mixin = _RelatableMixin() + + related_part = mixin.part_related_by(RT.CHART) + + relationships_.part_with_reltype.assert_called_once_with(RT.CHART) + assert related_part is part_ + + def it_can_establish_a_relationship_to_another_part( + self, _rels_prop_, relationships_, part_ + ): + relationships_.get_or_add.return_value = "rId42" + _rels_prop_.return_value = relationships_ + mixin = _RelatableMixin() + + rId = mixin.relate_to(part_, RT.SLIDE) + + relationships_.get_or_add.assert_called_once_with(RT.SLIDE, part_) + assert rId == "rId42" + + def and_it_can_establish_a_relationship_to_an_external_link( + self, request, _rels_prop_, relationships_ + ): + relationships_.get_or_add_ext_rel.return_value = "rId24" + _rels_prop_.return_value = relationships_ + mixin = _RelatableMixin() + + 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" + ) + assert rId == "rId24" + + def it_can_find_a_related_part_by_rId( + self, request, _rels_prop_, relationships_, relationship_, part_ + ): + _rels_prop_.return_value = relationships_ + relationships_.__getitem__.return_value = relationship_ + relationship_.target_part = part_ + mixin = _RelatableMixin() + + related_part = mixin.related_part("rId17") + + relationships_.__getitem__.assert_called_once_with("rId17") + assert related_part is part_ + + def it_can_find_a_target_ref_URI_by_rId( + self, request, _rels_prop_, relationships_, relationship_ + ): + _rels_prop_.return_value = relationships_ + relationships_.__getitem__.return_value = relationship_ + relationship_.target_ref = "http://url" + mixin = _RelatableMixin() + + target_ref = mixin.target_ref("rId9") + + relationships_.__getitem__.assert_called_once_with("rId9") + assert target_ref == "http://url" + + # fixture components ----------------------------------- + + @pytest.fixture + def part_(self, request): + return instance_mock(request, Part) + + @pytest.fixture + def relationship_(self, request): + return instance_mock(request, _Relationship) + + @pytest.fixture + def relationships_(self, request): + return instance_mock(request, _Relationships) + + @pytest.fixture + def _rels_prop_(self, request): + return property_mock(request, _RelatableMixin, "_rels") + + class DescribeOpcPackage(object): """Unit-test suite for `pptx.opc.package.OpcPackage` objects.""" @@ -147,32 +236,6 @@ def it_can_find_the_next_available_partname(self, request, ns, expected_n): PackURI_.assert_called_once_with(next_partname) assert partname == next_partname - def it_can_find_a_part_related_by_reltype( - self, request, _rels_prop_, relationships_ - ): - related_part_ = instance_mock(request, Part, name="related_part_") - relationships_.part_with_reltype.return_value = related_part_ - _rels_prop_.return_value = relationships_ - package = OpcPackage(None) - - related_part = package.part_related_by(RT.SLIDE) - - relationships_.part_with_reltype.assert_called_once_with(RT.SLIDE) - assert related_part is related_part_ - - def it_can_establish_a_relationship_to_another_part( - self, request, _rels_prop_, relationships_ - ): - relationships_.get_or_add.return_value = "rId99" - _rels_prop_.return_value = relationships_ - part_ = instance_mock(request, Part) - package = OpcPackage(None) - - rId = package.relate_to(part_, "http://rel/type") - - relationships_.get_or_add.assert_called_once_with("http://rel/type", part_) - assert rId == "rId99" - def it_can_save_to_a_pkg_file(self, request, _rels_prop_, relationships_): _rels_prop_.return_value = relationships_ parts_ = tuple(instance_mock(request, Part) for _ in range(3)) @@ -342,13 +405,11 @@ 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, _rels_prop_, relationships_, ref_count, calls - ): + 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 ) - _rels_prop_.return_value = relationships_ + property_mock(request, Part, "_rels", return_value=relationships_) part = Part(None, None, None) part.drop_rel("rId42") @@ -359,16 +420,6 @@ def it_can_drop_a_relationship( def it_knows_the_package_it_belongs_to(self, package_): assert Part(None, None, package_).package is package_ - def it_can_find_a_part_related_by_reltype(self, _rels_prop_, relationships_, part_): - relationships_.part_with_reltype.return_value = part_ - _rels_prop_.return_value = relationships_ - part = Part(None, None, None) - - related_part = part.part_related_by(RT.CHART) - - relationships_.part_with_reltype.assert_called_once_with(RT.CHART) - assert related_part is part_ - def it_knows_its_partname(self): assert Part(PackURI("/part/name"), None, None).partname == PackURI("/part/name") @@ -377,58 +428,6 @@ def it_can_change_its_partname(self): part.partname = PackURI("/new/part/name") assert part.partname == PackURI("/new/part/name") - 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_ - part = Part(None, None, None) - - rId = part.relate_to(part_, RT.SLIDE) - - relationships_.get_or_add.assert_called_once_with(RT.SLIDE, part_) - assert rId == "rId42" - - def and_it_can_establish_an_external_relationship( - self, _rels_prop_, relationships_ - ): - relationships_.get_or_add_ext_rel.return_value = "rId24" - _rels_prop_.return_value = relationships_ - part = Part(None, None, None) - - rId = part.relate_to("http://url", RT.HYPERLINK, is_external=True) - - relationships_.get_or_add_ext_rel.assert_called_once_with( - RT.HYPERLINK, "http://url" - ) - assert rId == "rId24" - - def it_can_find_a_related_part_by_rId( - self, request, _rels_prop_, relationships_, relationship_, part_ - ): - relationship_.target_part = part_ - relationships_.__getitem__.return_value = relationship_ - _rels_prop_.return_value = relationships_ - part = Part(None, None, None) - - related_part = part.related_part("rId17") - - relationships_.__getitem__.assert_called_once_with("rId17") - assert related_part is part_ - - def it_can_find_a_target_ref_URI_by_rId( - self, request, _rels_prop_, relationships_, relationship_ - ): - relationship_.target_ref = "http://url" - relationships_.__getitem__.return_value = relationship_ - _rels_prop_.return_value = relationships_ - part = Part(None, None, None) - - target_ref = part.target_ref("rId9") - - relationships_.__getitem__.assert_called_once_with("rId9") - assert target_ref == "http://url" - def it_can_load_a_blob_from_a_file_path_to_help(self): path = absjoin(test_file_dir, "minimal.pptx") with open(path, "rb") as f: @@ -458,22 +457,10 @@ def it_constructs_its_relationships_object_to_help(self, request, relationships_ def package_(self, request): return instance_mock(request, OpcPackage) - @pytest.fixture - def part_(self, request): - return instance_mock(request, Part) - - @pytest.fixture - def relationship_(self, request): - return instance_mock(request, _Relationship) - @pytest.fixture def relationships_(self, request): return instance_mock(request, _Relationships) - @pytest.fixture - def _rels_prop_(self, request): - return property_mock(request, Part, "_rels") - class DescribeXmlPart(object): """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" From d4eb0386ddb73e4a1996b351b5c8c614b792a227 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 24 Aug 2021 16:17:33 -0700 Subject: [PATCH 26/69] test: improve test coverage --- pptx/opc/package.py | 10 ++- pptx/opc/packuri.py | 12 +-- pptx/opc/serialized.py | 2 +- pptx/text/text.py | 8 +- pptx/util.py | 2 +- tests/opc/test_package.py | 80 ++++++++++++++++++-- tests/opc/test_packuri.py | 121 +++++++++++++++-------------- tests/opc/test_serialized.py | 6 +- tests/opc/unitdata/rels.py | 45 ----------- tests/opc/unitdata/types.py | 45 ----------- tests/oxml/test_simpletypes.py | 8 +- tests/oxml/unitdata/shape.py | 26 +------ tests/oxml/unitdata/text.py | 126 ------------------------------- tests/parts/test_coreprops.py | 2 +- tests/shapes/test_base.py | 14 +--- tests/shapes/test_placeholder.py | 6 +- tests/shapes/test_shapetree.py | 5 -- tests/text/test_text.py | 14 ++++ tests/unitdata.py | 30 ++------ tests/unitutil/cxml.py | 4 +- tests/unitutil/file.py | 23 ------ tests/unitutil/mock.py | 8 +- 22 files changed, 195 insertions(+), 402 deletions(-) delete mode 100644 tests/opc/unitdata/types.py diff --git a/pptx/opc/package.py b/pptx/opc/package.py index eed381077..5714efdf7 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -52,7 +52,9 @@ def target_ref(self, rId): @lazyproperty def _rels(self): """|Relationships| object containing relationships from this part to others.""" - raise NotImplementedError("`%s` must implement `.rels`" % type(self).__name__) + raise NotImplementedError( # pragma: no cover + "`%s` must implement `.rels`" % type(self).__name__ + ) class OpcPackage(_RelatableMixin): @@ -139,7 +141,9 @@ def next_partname(self, tmpl): candidate_partname = tmpl % n if candidate_partname not in partnames: return PackURI(candidate_partname) - raise Exception("ProgrammingError: ran out of candidate_partnames") + raise Exception( # pragma: no cover + "ProgrammingError: ran out of candidate_partnames" + ) def save(self, pkg_file): """Save this package to `pkg_file`. @@ -348,7 +352,7 @@ def partname(self): @partname.setter def partname(self, partname): if not isinstance(partname, PackURI): - raise TypeError( + raise TypeError( # pragma: no cover "partname must be instance of PackURI, got '%s'" % type(partname).__name__ ) diff --git a/pptx/opc/packuri.py b/pptx/opc/packuri.py index ce268573a..65a0b44ab 100644 --- a/pptx/opc/packuri.py +++ b/pptx/opc/packuri.py @@ -17,8 +17,7 @@ class PackURI(str): def __new__(cls, pack_uri_str): if not pack_uri_str[0] == "/": - tmpl = "PackURI must begin with slash, got '%s'" - raise ValueError(tmpl % pack_uri_str) + raise ValueError("PackURI must begin with slash, got '%s'" % pack_uri_str) return str.__new__(cls, pack_uri_str) @staticmethod @@ -61,10 +60,11 @@ def filename(self): @property def idx(self): - """ - Return partname index as integer for tuple partname or None for - singleton partname, e.g. ``21`` for ``'/ppt/slides/slide21.xml'`` and - |None| for ``'/ppt/presentation.xml'``. + """Optional int partname index. + + Value is an integer for an "array" partname or None for singleton partname, e.g. + ``21`` for ``'/ppt/slides/slide21.xml'`` and |None| for + ``'/ppt/presentation.xml'``. """ filename = self.filename if not filename: diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index ada470cc5..efe14a044 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -112,7 +112,7 @@ class _PhysPkgReader(Container): def __contains__(self, item): """Must be implemented by each subclass.""" - raise NotImplementedError( + raise NotImplementedError( # pragma: no cover "`%s` must implement `.__contains__()`" % type(self).__name__ ) diff --git a/pptx/text/text.py b/pptx/text/text.py index 7cf3a59f1..ba941230a 100644 --- a/pptx/text/text.py +++ b/pptx/text/text.py @@ -50,9 +50,7 @@ def auto_size(self, value): self._bodyPr.autofit = value def clear(self): - """ - Remove all paragraphs except one empty one. - """ + """Remove all paragraphs except one empty one.""" for p in self._txBody.p_lst[1:]: self._txBody.remove(p) p = self.paragraphs[0] @@ -81,7 +79,7 @@ def fit_text( """ # ---no-op when empty as fit behavior not defined for that case--- if self.text == "": - return + return # pragma: no cover font_size = self._best_fit_font_size( font_family, max_size, bold, italic, font_file @@ -209,7 +207,7 @@ def word_wrap(self): @word_wrap.setter def word_wrap(self, value): if value not in (True, False, None): - raise ValueError( + raise ValueError( # pragma: no cover "assigned value must be True, False, or None, got %s" % value ) self._txBody.bodyPr.wrap = { diff --git a/pptx/util.py b/pptx/util.py index 77deabd3a..5e5d92ecd 100644 --- a/pptx/util.py +++ b/pptx/util.py @@ -235,4 +235,4 @@ def __set__(self, obj, value): 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") + raise AttributeError("can't set attribute") # pragma: no cover diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 1b556d86a..ccff47a53 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -27,6 +27,7 @@ ) from pptx.opc.packuri import PACKAGE_URI, PackURI from pptx.oxml import parse_xml +from pptx.parts.presentation import PresentationPart from ..unitutil.cxml import element from ..unitutil.file import absjoin, snippet_bytes, testfile_bytes, test_file_dir @@ -144,6 +145,13 @@ def it_can_open_a_pkg_file(self, request): _load_.assert_called_once_with(ANY) assert package is package_ + def it_can_drop_a_relationship(self, _rels_prop_, relationships_): + _rels_prop_.return_value = relationships_ + + OpcPackage(None).drop_rel("rId42") + + relationships_.pop.assert_called_once_with("rId42") + def it_can_iterate_over_its_parts(self, request): part_, part_2_ = [ instance_mock(request, Part, name="part_%d" % i) for i in range(2) @@ -213,6 +221,18 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): rels[2], ) + def it_provides_access_to_the_main_document_part(self, request): + presentation_part_ = instance_mock(request, PresentationPart) + part_related_by_ = method_mock( + request, OpcPackage, "part_related_by", return_value=presentation_part_ + ) + package = OpcPackage(None) + + presentation_part = package.main_document_part + + part_related_by_.assert_called_once_with(package, RT.OFFICE_DOCUMENT) + assert presentation_part is presentation_part_ + @pytest.mark.parametrize( "ns, expected_n", (((), 1), ((1,), 2), ((1, 2), 3), ((2, 4), 3), ((1, 4), 3)), @@ -428,6 +448,12 @@ 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_ + ): + property_mock(request, Part, "_rels", return_value=relationships_) + assert Part(None, None, None).rels is relationships_ + def it_can_load_a_blob_from_a_file_path_to_help(self): path = absjoin(test_file_dir, "minimal.pptx") with open(path, "rb") as f: @@ -616,6 +642,13 @@ def content_type_map(self): class Describe_Relationships(object): """Unit-test suite for `pptx.opc.package._Relationships` objects.""" + @pytest.mark.parametrize("rId, expected_value", (("rId1", True), ("rId2", False))) + def it_knows_whether_it_contains_a_relationship_with_rId( + self, _rels_prop_, rId, expected_value + ): + _rels_prop_.return_value = {"rId1": None} + assert (rId in _Relationships(None)) is expected_value + def it_has_dict_style_lookup_of_rel_by_rId(self, _rels_prop_, relationship_): _rels_prop_.return_value = {"rId17": relationship_} assert _Relationships(None)["rId17"] is relationship_ @@ -823,16 +856,51 @@ def and_it_can_add_an_external_relationship_to_help( assert relationships._rels == {"rId9": relationship_} assert rId == "rId9" + @pytest.mark.parametrize( + "target_ref, is_external, expected_value", + ( + ("http://url", True, "rId1"), + ("part_1", False, "rId2"), + ("http://foo", True, "rId3"), + ("part_2", False, "rId4"), + ("http://bar", True, None), + ), + ) def it_can_get_a_matching_relationship_to_help( - self, _rels_by_reltype_prop_, relationship_, part_ + self, + request, + _rels_by_reltype_prop_, + target_ref, + is_external, + expected_value, ): - relationship_.is_external = False - relationship_.target_part = part_ - relationship_.rId = "rId10" - _rels_by_reltype_prop_.return_value = {RT.SLIDE: [relationship_]} + part_1, part_2 = (instance_mock(request, Part) for _ in range(2)) + _rels_by_reltype_prop_.return_value = { + RT.SLIDE: [ + instance_mock( + request, + _Relationship, + rId=rId, + target_part=target_part, + target_ref=target_ref, + is_external=is_external, + ) + for rId, target_part, target_ref, is_external in ( + ("rId1", None, "http://url", True), + ("rId2", part_1, "/ppt/foo.bar", False), + ("rId3", None, "http://foo", True), + ("rId4", part_2, "/ppt/bar.foo", False), + ) + ] + } + target = ( + target_ref if is_external else part_1 if target_ref == "part_1" else part_2 + ) relationships = _Relationships(None) - assert relationships._get_matching(RT.SLIDE, part_) == "rId10" + 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_ diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index 5c80bafe7..f77ea68f5 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -1,8 +1,6 @@ # encoding: utf-8 -""" -Test suite for the pptx.opc.packuri module -""" +"""Unit-test suite for the `pptx.opc.packuri` module.""" import pytest @@ -10,76 +8,89 @@ class DescribePackURI(object): - def cases(self, expected_values): - """ - Return list of tuples zipped from uri_str cases and - *expected_values*. Raise if lengths don't match. - """ - uri_str_cases = ["/", "/ppt/presentation.xml", "/ppt/slides/slide1.xml"] - if len(expected_values) != len(uri_str_cases): - msg = "len(expected_values) differs from len(uri_str_cases)" - raise AssertionError(msg) - pack_uris = [PackURI(uri_str) for uri_str in uri_str_cases] - return zip(pack_uris, expected_values) + """Unit-test suite for the `pptx.opc.packuri.PackURI` objects.""" def it_can_construct_from_relative_ref(self): - baseURI = "/ppt/slides" - relative_ref = "../slideLayouts/slideLayout1.xml" - pack_uri = PackURI.from_rel_ref(baseURI, relative_ref) + pack_uri = PackURI.from_rel_ref( + "/ppt/slides", "../slideLayouts/slideLayout1.xml" + ) assert pack_uri == "/ppt/slideLayouts/slideLayout1.xml" def it_should_raise_on_construct_with_bad_pack_uri_str(self): with pytest.raises(ValueError): PackURI("foobar") - def it_can_calculate_baseURI(self): - expected_values = ("/", "/ppt", "/ppt/slides") - for pack_uri, expected_baseURI in self.cases(expected_values): - assert pack_uri.baseURI == expected_baseURI - - def it_can_calculate_extension(self): - expected_values = ("", "xml", "xml") - for pack_uri, expected_ext in self.cases(expected_values): - assert pack_uri.ext == expected_ext + @pytest.mark.parametrize( + "uri, expected_value", + ( + ("/", "/"), + ("/ppt/presentation.xml", "/ppt"), + ("/ppt/slides/slide1.xml", "/ppt/slides"), + ), + ) + def it_knows_its_base_URI(self, uri, expected_value): + assert PackURI(uri).baseURI == expected_value - def it_can_calculate_filename(self): - expected_values = ("", "presentation.xml", "slide1.xml") - for pack_uri, expected_filename in self.cases(expected_values): - assert pack_uri.filename == expected_filename + @pytest.mark.parametrize( + "uri, expected_value", + ( + ("/", ""), + ("/ppt/presentation.xml", "xml"), + ("/ppt/media/image.PnG", "PnG"), + ), + ) + def it_knows_its_extension(self, uri, expected_value): + assert PackURI(uri).ext == expected_value - def it_knows_the_filename_index(self): - expected_values = (None, None, 1) - for pack_uri, expected_idx in self.cases(expected_values): - assert pack_uri.idx == expected_idx + @pytest.mark.parametrize( + "uri, expected_value", + ( + ("/", ""), + ("/ppt/presentation.xml", "presentation.xml"), + ("/ppt/media/image.png", "image.png"), + ), + ) + def it_knows_its_filename(self, uri, expected_value): + assert PackURI(uri).filename == expected_value - def it_can_calculate_membername(self): - expected_values = ("", "ppt/presentation.xml", "ppt/slides/slide1.xml") - for pack_uri, expected_membername in self.cases(expected_values): - assert pack_uri.membername == expected_membername + @pytest.mark.parametrize( + "uri, expected_value", + ( + ("/", None), + ("/ppt/presentation.xml", None), + ("/ppt/,foo,grob!.xml", None), + ("/ppt/media/image42.png", 42), + ), + ) + def it_knows_the_filename_index(self, uri, expected_value): + assert PackURI(uri).idx == expected_value - def it_can_calculate_relative_ref_value(self): - cases = ( - ("/", "/ppt/presentation.xml", "ppt/presentation.xml"), + @pytest.mark.parametrize( + "uri, base_uri, expected_value", + ( + ("/ppt/presentation.xml", "/", "ppt/presentation.xml"), ( - "/ppt", "/ppt/slideMasters/slideMaster1.xml", + "/ppt", "slideMasters/slideMaster1.xml", ), ( - "/ppt/slides", "/ppt/slideLayouts/slideLayout1.xml", + "/ppt/slides", "../slideLayouts/slideLayout1.xml", ), - ) - for baseURI, uri_str, expected_relative_ref in cases: - pack_uri = PackURI(uri_str) - assert pack_uri.relative_ref(baseURI) == expected_relative_ref + ), + ) + def it_can_compute_its_relative_reference(self, uri, base_uri, expected_value): + assert PackURI(uri).relative_ref(base_uri) == expected_value - def it_can_calculate_rels_uri(self): - expected_values = ( - "/_rels/.rels", - "/ppt/_rels/presentation.xml.rels", - "/ppt/slides/_rels/slide1.xml.rels", - ) - for pack_uri, expected_rels_uri in self.cases(expected_values): - assert pack_uri.rels_uri == expected_rels_uri + @pytest.mark.parametrize( + "uri, expected_value", + ( + ("/", "/_rels/.rels"), + ("/ppt/presentation.xml", "/ppt/_rels/presentation.xml.rels"), + ("/ppt/slides/slide42.xml", "/ppt/slides/_rels/slide42.xml.rels"), + ), + ) + def it_knows_the_uri_of_its_rels_part(self, uri, expected_value): + assert PackURI(uri).rels_uri == expected_value diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 95695a075..867d987b8 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -2,15 +2,11 @@ """Unit-test suite for `pptx.opc.serialized` module.""" -try: - from io import BytesIO # Python 3 -except ImportError: - from StringIO import StringIO as BytesIO - import hashlib import pytest import zipfile +from pptx.compat import BytesIO from pptx.exceptions import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import Part, _Relationships diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index da81e2d02..2fd0aa947 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -2,9 +2,6 @@ """Test data for relationship-related unit tests.""" -from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.package import _Relationships - from pptx.opc.constants import NAMESPACE as NS from pptx.oxml import parse_xml @@ -25,44 +22,6 @@ def with_indent(self, indent): return self -class _RelationshipsBuilder(object): - """Builder class for test _Relationshipss""" - - partname_tmpls = { - RT.SLIDE_MASTER: "/ppt/slideMasters/slideMaster%d.xml", - RT.SLIDE: "/ppt/slides/slide%d.xml", - } - - def __init__(self): - self.relationships = [] - self.next_rel_num = 1 - self.next_partnums = {} - - def _next_partnum(self, reltype): - if reltype not in self.next_partnums: - self.next_partnums[reltype] = 1 - partnum = self.next_partnums[reltype] - self.next_partnums[reltype] = partnum + 1 - return partnum - - @property - def next_rId(self): - rId = "rId%d" % self.next_rel_num - self.next_rel_num += 1 - return rId - - def _next_tuple_partname(self, reltype): - partname_tmpl = self.partname_tmpls[reltype] - partnum = self._next_partnum(reltype) - return partname_tmpl % partnum - - def build(self): - rels = _Relationships() - for rel in self.relationships: - rels.add_rel(rel) - return rels - - class CT_DefaultBuilder(BaseBuilder): """ Test data builder for CT_Default (Default) XML element that appears in @@ -294,10 +253,6 @@ def a_Relationships(): return CT_RelationshipsBuilder() -def a_rels(): - return _RelationshipsBuilder() - - def a_Types(): return CT_TypesBuilder() diff --git a/tests/opc/unitdata/types.py b/tests/opc/unitdata/types.py deleted file mode 100644 index c78621f55..000000000 --- a/tests/opc/unitdata/types.py +++ /dev/null @@ -1,45 +0,0 @@ -# encoding: utf-8 - -""" -XML test data builders for [Content_Types].xml elements -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from pptx.opc.oxml import nsmap - -from ...unitdata import BaseBuilder - - -class CT_DefaultBuilder(BaseBuilder): - __tag__ = "Default" - __nspfxs__ = ("ct",) - __attrs__ = ("Extension", "ContentType") - - -class CT_OverrideBuilder(BaseBuilder): - __tag__ = "Override" - __nspfxs__ = ("ct",) - __attrs__ = ("PartName", "ContentType") - - -class CT_TypesBuilder(BaseBuilder): - __tag__ = "Types" - __nspfxs__ = ("ct",) - __attrs__ = () - - def with_nsdecls(self, *nspfxs): - self._nsdecls = ' xmlns="%s"' % nsmap["ct"] - return self - - -def a_Default(): - return CT_DefaultBuilder() - - -def a_Types(): - return CT_TypesBuilder() - - -def an_Override(): - return CT_OverrideBuilder() diff --git a/tests/oxml/test_simpletypes.py b/tests/oxml/test_simpletypes.py index 3ef34a936..fabc86c93 100644 --- a/tests/oxml/test_simpletypes.py +++ b/tests/oxml/test_simpletypes.py @@ -190,7 +190,7 @@ def it_can_validate_a_hex_RGB_string(self, valid_fixture): if exception is None: try: ST_HexColorRGB.validate(str_value) - except ValueError: + except ValueError: # pragma: no cover raise AssertionError("string '%s' did not validate" % str_value) else: with pytest.raises(exception): @@ -258,12 +258,12 @@ def percent_fixture(self, request): class ST_SimpleType(BaseSimpleType): @classmethod def convert_from_xml(cls, str_value): - return 666 + return 666 # pragma: no cover @classmethod def convert_to_xml(cls, value): - return "666" + return "666" # pragma: no cover @classmethod def validate(cls, value): - pass + pass # pragma: no cover diff --git a/tests/oxml/unitdata/shape.py b/tests/oxml/unitdata/shape.py index 23310b4dd..560657e8a 100644 --- a/tests/oxml/unitdata/shape.py +++ b/tests/oxml/unitdata/shape.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test data for autoshape-related unit tests. -""" - -from __future__ import absolute_import +"""Test data for autoshape-related unit tests.""" from ...unitdata import BaseBuilder @@ -33,18 +29,6 @@ class CT_GeomGuideListBuilder(BaseBuilder): __attrs__ = () -class CT_GraphicalObjectBuilder(BaseBuilder): - __tag__ = "a:graphic" - __nspfxs__ = ("a",) - __attrs__ = () - - -class CT_GraphicalObjectDataBuilder(BaseBuilder): - __tag__ = "a:graphicData" - __nspfxs__ = ("a",) - __attrs__ = ("uri",) - - class CT_GraphicalObjectFrameBuilder(BaseBuilder): __tag__ = "p:graphicFrame" __nspfxs__ = ("p", "a") @@ -163,14 +147,6 @@ def a_gd(): return CT_GeomGuideBuilder() -def a_graphic(): - return CT_GraphicalObjectBuilder() - - -def a_graphicData(): - return CT_GraphicalObjectDataBuilder() - - def a_graphicFrame(): return CT_GraphicalObjectFrameBuilder() diff --git a/tests/oxml/unitdata/text.py b/tests/oxml/unitdata/text.py index 5236d05d8..23753fdc8 100644 --- a/tests/oxml/unitdata/text.py +++ b/tests/oxml/unitdata/text.py @@ -28,50 +28,12 @@ def with_rId(self, rId): return self -class CT_OfficeArtExtensionList(BaseBuilder): - __tag__ = "a:extLst" - __nspfxs__ = ("a",) - __attrs__ = () - - class CT_RegularTextRunBuilder(BaseBuilder): __tag__ = "a:r" __nspfxs__ = ("a",) __attrs__ = () -class CT_TextBodyBuilder(BaseBuilder): - __tag__ = "p:txBody" - __nspfxs__ = ("p", "a") - __attrs__ = () - - -class CT_TextBodyPropertiesBuilder(BaseBuilder): - __tag__ = "a:bodyPr" - __nspfxs__ = ("a",) - __attrs__ = ( - "rot", - "spcFirstLastPara", - "vertOverflow", - "horzOverflow", - "vert", - "wrap", - "lIns", - "tIns", - "rIns", - "bIns", - "numCol", - "spcCol", - "rtlCol", - "fromWordArt", - "anchor", - "anchorCtr", - "forceAA", - "upright", - "compatLnSpc", - ) - - class CT_TextCharacterPropertiesBuilder(BaseBuilder): """ Test data builder for CT_TextCharacterProperties XML element that appears @@ -86,24 +48,6 @@ def __init__(self, tag): super(CT_TextCharacterPropertiesBuilder, self).__init__() -class CT_TextFontBuilder(BaseBuilder): - __tag__ = "a:latin" - __nspfxs__ = ("a",) - __attrs__ = ("typeface", "panose", "pitchFamily", "charset") - - -class CT_TextNoAutofitBuilder(BaseBuilder): - __tag__ = "a:noAutofit" - __nspfxs__ = ("a",) - __attrs__ = () - - -class CT_TextNormalAutofitBuilder(BaseBuilder): - __tag__ = "a:normAutofit" - __nspfxs__ = ("a",) - __attrs__ = ("fontScale", "lnSpcReduction") - - class CT_TextParagraphBuilder(BaseBuilder): """ Test data builder for CT_TextParagraph () XML element that appears @@ -115,35 +59,6 @@ class CT_TextParagraphBuilder(BaseBuilder): __attrs__ = () -class CT_TextParagraphPropertiesBuilder(BaseBuilder): - """ - Test data builder for CT_TextParagraphProperties () XML element - that appears as a child of . - """ - - __tag__ = "a:pPr" - __nspfxs__ = ("a",) - __attrs__ = ( - "marL", - "marR", - "lvl", - "indent", - "algn", - "defTabSz", - "rtl", - "eaLnBrk", - "fontAlgn", - "latinLnBrk", - "hangingPunct", - ) - - -class CT_TextShapeAutofitBuilder(BaseBuilder): - __tag__ = "a:spAutoFit" - __nspfxs__ = ("a",) - __attrs__ = () - - class XsdString(BaseBuilder): __attrs__ = () @@ -153,52 +68,15 @@ def __init__(self, tag, nspfxs): super(XsdString, self).__init__() -def a_bodyPr(): - return CT_TextBodyPropertiesBuilder() - - -def a_defRPr(): - return CT_TextCharacterPropertiesBuilder("a:defRPr") - - -def a_latin(): - return CT_TextFontBuilder() - - -def a_noAutofit(): - return CT_TextNoAutofitBuilder() - - -def a_normAutofit(): - return CT_TextNormalAutofitBuilder() - - def a_p(): """Return a CT_TextParagraphBuilder instance""" return CT_TextParagraphBuilder() -def a_pPr(): - """Return a CT_TextParagraphPropertiesBuilder instance""" - return CT_TextParagraphPropertiesBuilder() - - def a_t(): return XsdString("a:t", ("a",)) -def a_txBody(): - return CT_TextBodyBuilder() - - -def an_endParaRPr(): - return CT_TextCharacterPropertiesBuilder("a:endParaRPr") - - -def an_extLst(): - return CT_OfficeArtExtensionList() - - def an_hlinkClick(): return CT_Hyperlink() @@ -209,7 +87,3 @@ def an_r(): def an_rPr(): return CT_TextCharacterPropertiesBuilder("a:rPr") - - -def an_spAutoFit(): - return CT_TextShapeAutofitBuilder() diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 86257745d..0f1b37917 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -176,7 +176,7 @@ def coreProperties(self, tagname, str_val, attrs=""): if not tagname: child_element = "" elif not str_val: - child_element = "\n <%s%s/>\n" % (tagname, attrs) + child_element = "\n <%s%s/>\n" % (tagname, attrs) # pragma: no cover else: child_element = "\n <%s%s>%s\n" % (tagname, attrs, str_val, tagname) return tmpl % child_element diff --git a/tests/shapes/test_base.py b/tests/shapes/test_base.py index 26b441d0c..8182323de 100644 --- a/tests/shapes/test_base.py +++ b/tests/shapes/test_base.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.shapes.shape module -""" - -from __future__ import absolute_import +"""Unit-test suite for `pptx.shapes.base` module.""" import pytest @@ -36,10 +32,12 @@ an_xfrm, ) from ..unitutil.cxml import element, xml -from ..unitutil.mock import class_mock, instance_mock, loose_mock, property_mock +from ..unitutil.mock import class_mock, instance_mock, loose_mock class DescribeBaseShape(object): + """Unit-test suite for `pptx.shapes.base.BaseShape` objects.""" + def it_provides_access_to_its_click_action(self, click_action_fixture): shape, ActionSetting_, cNvPr, click_action_ = click_action_fixture click_action = shape.click_action @@ -524,10 +522,6 @@ def shape_id(self): def shape_name(self): return "Foobar 41" - @pytest.fixture - def shape_text_frame_(self, request): - return property_mock(request, BaseShape, "text_frame") - @pytest.fixture def shapes_(self, request): return instance_mock(request, SlideShapes) diff --git a/tests/shapes/test_placeholder.py b/tests/shapes/test_placeholder.py index 00bb6956f..75b0814ca 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -9,7 +9,7 @@ from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.oxml.shapes.shared import ST_Direction, ST_PlaceholderSize from pptx.parts.image import ImagePart -from pptx.parts.slide import NotesSlidePart, SlideLayoutPart, SlidePart +from pptx.parts.slide import NotesSlidePart, SlidePart from pptx.shapes.placeholder import ( BasePlaceholder, _BaseSlidePlaceholder, @@ -386,10 +386,6 @@ def master_placeholder_(self, request): def part_prop_(self, request): return property_mock(request, LayoutPlaceholder, "part") - @pytest.fixture - def slide_layout_part_(self, request): - return instance_mock(request, SlideLayoutPart) - @pytest.fixture def slide_master_(self, request): return instance_mock(request, SlideMaster) diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 34f6e41ba..b2851ae87 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -9,7 +9,6 @@ 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.oxml import parse_xml -from pptx.oxml.shapes.autoshape import CT_Shape from pptx.oxml.shapes.groupshape import CT_GroupShape from pptx.oxml.shapes.picture import CT_Picture from pptx.oxml.shapes.shared import BaseShapeElement, ST_Direction @@ -1597,10 +1596,6 @@ def _LayoutShapeFactory_(self, request, placeholder_): autospec=True, ) - @pytest.fixture - def ph_elm_(self, request): - return instance_mock(request, CT_Shape) - @pytest.fixture def placeholder_(self, request): return instance_mock(request, LayoutPlaceholder) diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 26695df4b..1857814ea 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -43,6 +43,20 @@ def it_can_change_its_autosize_setting(self, autosize_set_fixture): text_frame.auto_size = value assert text_frame._txBody.xml == expected_xml + @pytest.mark.parametrize( + "txBody_cxml", + ( + "p:txBody/(a:p,a:p,a:p)", + 'p:txBody/a:p/a:r/a:t"foo"', + 'p:txBody/a:p/(a:br,a:r/a:t"foo")', + 'p:txBody/a:p/(a:fld,a:br,a:r/a:t"foo")', + ), + ) + def it_can_clear_itself_of_content(self, txBody_cxml): + text_frame = TextFrame(element(txBody_cxml), None) + text_frame.clear() + assert text_frame._element.xml == xml("p:txBody/a:p") + def it_knows_its_margin_settings(self, margin_get_fixture): text_frame, prop_name, unit, expected_value = margin_get_fixture margin_value = getattr(text_frame, prop_name) diff --git a/tests/unitdata.py b/tests/unitdata.py index 2507230af..f40fcaf8c 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -40,17 +40,6 @@ def with_xmlattr(value): return self return with_xmlattr - else: - tmpl = "'%s' object has no attribute '%s'" - raise AttributeError(tmpl % (self.__class__.__name__, name)) - - def clear(self): - """ - Reset this builder back to initial state so it can be reused within - a single test. - """ - BaseBuilder.__init__(self) - return self @property def element(self): @@ -68,15 +57,6 @@ def with_child(self, child_bldr): self._child_bldrs.append(child_bldr) return self - def with_text(self, text): - """ - Cause *text* to be placed between the start and end tags of this - element. Not robust enough for mixed elements, intended only for - elements having no child elements. - """ - self._text = text - return self - def with_nsdecls(self, *nspfxs): """ Cause the element to contain namespace declarations. By default, the @@ -100,9 +80,6 @@ def xml(self, indent=0): xml = "%s\n" % self._non_empty_element_xml(indent) return xml - def xml_bytes(self, indent=0): - return self.xml(indent=indent).encode("utf-8") - @property def _empty_element_tag(self): return "<%s%s%s/>" % (self.__tag__, self._nsdecls, self._xmlattrs_str) @@ -118,7 +95,12 @@ def _is_empty(self): def _non_empty_element_xml(self, indent): indent_str = " " * indent if self._text: - xml = "%s%s%s%s" % (indent_str, self._start_tag, self._text, self._end_tag) + xml = "%s%s%s%s" % ( # pragma: no cover + indent_str, + self._start_tag, + self._text, + self._end_tag, + ) else: xml = "%s%s\n" % (indent_str, self._start_tag) for child_bldr in self._child_bldrs: diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index c83fea704..d44cb51d1 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -83,7 +83,7 @@ def __repr__(self): Provide a more meaningful repr value for an Element object, one that displays the tagname as a simple empty element, e.g. ````. """ - return "<%s/>" % self._tagname + return "<%s/>" % self._tagname # pragma: no cover def connect_children(self, child_node_list): """ @@ -134,7 +134,7 @@ def nspfx(name, is_element=False): for name, val in self._attrs: pfx = nspfx(name) if pfx is None or pfx in nspfxs: - continue + continue # pragma: no cover nspfxs.append(pfx) return nspfxs diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 6b8879935..0d25aac5a 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -5,38 +5,15 @@ import os import sys -from lxml import etree - -from pptx.oxml import oxml_parser - _thisdir = os.path.split(__file__)[0] test_file_dir = os.path.abspath(os.path.join(_thisdir, "..", "test_files")) -def abspath(relpath): - thisdir = os.path.split(__file__)[0] - return os.path.abspath(os.path.join(thisdir, relpath)) - - def absjoin(*paths): return os.path.abspath(os.path.join(*paths)) -def docx_path(name): - """ - Return the absolute path to test .docx file with root name *name*. - """ - return absjoin(test_file_dir, "%s.docx" % name) - - -def parse_xml_file(file_): - """ - Return ElementTree for XML contained in *file_* - """ - return etree.parse(file_, oxml_parser) - - def snippet_bytes(snippet_file_name): """Return bytes read from snippet file having `snippet_file_name`.""" snippet_file_path = os.path.join( diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index efbe6b17f..e10a8d54c 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -8,7 +8,7 @@ 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: +else: # pragma: no cover import mock # noqa from mock import ANY, call, MagicMock # noqa from mock import create_autospec, Mock, mock_open, patch, PropertyMock @@ -26,7 +26,7 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): return _patch.start() -def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): +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`. Patch is reversed after pytest uses it. @@ -77,9 +77,7 @@ def loose_mock(request, name=None, **kwargs): Additional keyword arguments are passed through to Mock(). If called without a name, it is assigned the name of the fixture. """ - if name is None: - name = request.fixturename - return Mock(name=name, **kwargs) + return Mock(name=request.fixturename if name is None else name, **kwargs) def method_mock(request, cls, method_name, autospec=True, **kwargs): From ce78e00e3758641a3aaa505f3c36b7ec737b9b5d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 13 Sep 2021 12:55:32 -0700 Subject: [PATCH 27/69] test: Add feature for .rels file count Make sure that a .rels parts is not written for any part that has no outgoing relationships. --- features/prs-open-save.feature | 1 + features/steps/presentation.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/features/prs-open-save.feature b/features/prs-open-save.feature index 344412581..ca0110526 100644 --- a/features/prs-open-save.feature +++ b/features/prs-open-save.feature @@ -26,3 +26,4 @@ Feature: Round-trip a presentation Given a presentation with external relationships When I save and reload the presentation Then the external relationships are still there + And the package has the expected number of .rels parts diff --git a/features/steps/presentation.py b/features/steps/presentation.py index 9b02c5f29..91dd57c0b 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -1,12 +1,9 @@ # encoding: utf-8 -""" -Gherkin step implementations for presentation-level features. -""" - -from __future__ import absolute_import +"""Gherkin step implementations for presentation-level features.""" import os +import zipfile from behave import given, when, then @@ -181,6 +178,13 @@ def then_ext_rels_are_preserved(context): assert rel.target_ref == "https://github.com/scanny/python-pptx" +@then("the package has the expected number of .rels parts") +def then_the_package_has_the_expected_number_of_rels_parts(context): + 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): presentation = context.presentation From 6f5133850b220674a152c5d464a68fe91889ab8f Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 25 Aug 2021 17:12:49 -0700 Subject: [PATCH 28/69] docs: document Axis.reverse_order analysis --- docs/dev/analysis/cht-axes.rst | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/dev/analysis/cht-axes.rst b/docs/dev/analysis/cht-axes.rst index 3952161f1..e9ab79eb8 100644 --- a/docs/dev/analysis/cht-axes.rst +++ b/docs/dev/analysis/cht-axes.rst @@ -17,6 +17,40 @@ depending upon the chart type. Likewise for a value axis. PowerPoint behavior ------------------- +Reverse-order +~~~~~~~~~~~~~ + +Normally, categories appear left-to-right in the order specified and values appear +vertically in increasing order. This default ordering can be reversed when desired. + +One common case is for the categories in a "horizontal" bar-chart (as opposed to the +"vertical" column-chart). Because the value axis appears at the bottom, categories +appear from bottom-to-top on the categories axis. For many readers this is odd, perhaps +because we read top-to-bottom. + +The axis "direction" can be switched using the `Axis.reverse_order` property. This +controls the value of the `c:xAx/c:scaling/c:orientation{val=minMax|maxMin}` XML +element/attribute. The default is False. + +MS API protocol:: + + >>> axis = Chart.Axes(xlCategory) + >>> axis.ReversePlotOrder + False + >>> axis.ReversePlotOrder = True + >>> axis.ReversePlotOrder + True + +Proposed python-pptx protocol:: + + >>> axis = chart.category_axis + >>> axis.reverse_order + False + >>> axis.reverse_order = True + >>> axis.reverse_order + True + + Tick label position ~~~~~~~~~~~~~~~~~~~ @@ -288,6 +322,10 @@ Related Schema Definitions + + + + @@ -312,6 +350,13 @@ Related Schema Definitions + + + + + + + From aa8c50cced4c89e8ff98a57bea94e547692e0014 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 11 Aug 2021 17:06:44 -0700 Subject: [PATCH 29/69] rfctr: modernize tests and formatting * Order tests alphabetically by unit-under-test (UUT). * PEP 257 for docstrings. * Get rid of most __future__ imports. --- features/cht-axis-props.feature | 24 ++++----- pptx/chart/axis.py | 52 +++++++----------- pptx/oxml/chart/axis.py | 73 ++++++++++--------------- tests/chart/test_axis.py | 94 ++++++++++++++++----------------- 4 files changed, 105 insertions(+), 138 deletions(-) diff --git a/features/cht-axis-props.feature b/features/cht-axis-props.feature index fecdca00b..c4663ae4f 100644 --- a/features/cht-axis-props.feature +++ b/features/cht-axis-props.feature @@ -70,6 +70,18 @@ Feature: Axis properties | automatic | None | + Scenario Outline: Get Axis.format + Given a axis + Then axis.format is a ChartFormat object + And axis.format.fill is a FillFormat object + And axis.format.line is a LineFormat object + + Examples: axis types + | axis-type | + | category | + | value | + + Scenario Outline: Get Axis.has_[major/minor]_gridlines Given an axis gridlines Then axis.has__gridlines is @@ -150,15 +162,3 @@ Feature: Axis properties Scenario: Get Axis.major_gridlines Given an axis Then axis.major_gridlines is a MajorGridlines object - - - Scenario Outline: Get Axis.format - Given a axis - Then axis.format is a ChartFormat object - And axis.format.fill is a FillFormat object - And axis.format.line is a LineFormat object - - Examples: axis types - | axis-type | - | category | - | value | diff --git a/pptx/chart/axis.py b/pptx/chart/axis.py index 4285c36f4..534e2e305 100644 --- a/pptx/chart/axis.py +++ b/pptx/chart/axis.py @@ -1,29 +1,22 @@ # encoding: utf-8 -""" -Axis-related chart objects. -""" +"""Axis-related chart objects.""" -from __future__ import absolute_import, print_function, unicode_literals - -from ..dml.chtfmt import ChartFormat -from ..enum.chart import ( +from pptx.dml.chtfmt import ChartFormat +from pptx.enum.chart import ( XL_AXIS_CROSSES, XL_CATEGORY_TYPE, XL_TICK_LABEL_POSITION, XL_TICK_MARK, ) -from ..oxml.ns import qn -from ..shared import ElementProxy -from ..text.text import Font, TextFrame -from ..util import lazyproperty +from pptx.oxml.ns import qn +from pptx.shared import ElementProxy +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty class _BaseAxis(object): - """ - Base class for chart axis objects. All axis objects share these - properties. - """ + """Base class for chart axis objects. All axis objects share these properties.""" def __init__(self, xAx): super(_BaseAxis, self).__init__() @@ -277,9 +270,7 @@ def text_frame(self): class CategoryAxis(_BaseAxis): - """ - A category axis of a chart. - """ + """A category axis of a chart.""" @property def category_type(self): @@ -291,10 +282,10 @@ def category_type(self): class DateAxis(_BaseAxis): - """ - A category axis with dates as its category labels and having some special - display behaviors such as making length of equal periods equal and - normalizing month start dates despite unequal month lengths. + """A category axis with dates as its category labels. + + This axis-type has some special display behaviors such as making length of equal + periods equal and normalizing month start dates despite unequal month lengths. """ @property @@ -307,10 +298,7 @@ def category_type(self): class MajorGridlines(ElementProxy): - """ - Provides access to the properties of the major gridlines appearing on an - axis. - """ + """Provides access to the properties of the major gridlines appearing on an axis.""" def __init__(self, xAx): super(MajorGridlines, self).__init__(xAx) @@ -327,9 +315,7 @@ def format(self): class TickLabels(object): - """ - A service class providing access to formatting of axis tick mark labels. - """ + """A service class providing access to formatting of axis tick mark labels.""" def __init__(self, xAx_elm): super(TickLabels, self).__init__() @@ -411,10 +397,10 @@ def offset(self, value): class ValueAxis(_BaseAxis): - """ - An axis having continuous (as opposed to discrete) values. The vertical - axis is generally a value axis, however both axes of an XY-type chart are - value axes. + """An axis having continuous (as opposed to discrete) values. + + The vertical axis is generally a value axis, however both axes of an XY-type chart + are value axes. """ @property diff --git a/pptx/oxml/chart/axis.py b/pptx/oxml/chart/axis.py index d60f6fa8f..5d9586f7c 100644 --- a/pptx/oxml/chart/axis.py +++ b/pptx/oxml/chart/axis.py @@ -1,16 +1,14 @@ # encoding: utf-8 -""" -Axis-related oxml objects. -""" +"""Axis-related oxml objects.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import unicode_literals -from ...enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK -from .shared import CT_Title -from ..simpletypes import ST_AxisUnit, ST_LblOffset -from ..text import CT_TextBody -from ..xmlchemy import ( +from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK +from pptx.oxml.chart.shared import CT_Title +from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset +from pptx.oxml.text import CT_TextBody +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, @@ -20,9 +18,7 @@ class BaseAxisElement(BaseOxmlElement): - """ - Base class for catAx, valAx, and perhaps other axis elements. - """ + """Base class for catAx, dateAx, valAx, and perhaps other axis elements.""" @property def defRPr(self): @@ -42,17 +38,13 @@ def _new_txPr(self): class CT_AxisUnit(BaseOxmlElement): - """ - Used for ```` and ```` elements, and others. - """ + """Used for `c:majorUnit` and `c:minorUnit` elements, and others.""" val = RequiredAttribute("val", ST_AxisUnit) class CT_CatAx(BaseAxisElement): - """ - ```` element, defining a category axis. - """ + """`c:catAx` element, defining a category axis.""" _tag_seq = ( "c:axId", @@ -97,27 +89,22 @@ class CT_CatAx(BaseAxisElement): class CT_ChartLines(BaseOxmlElement): - """ - Used for c:majorGridlines and c:minorGridlines, specifies gridlines - visual properties such as color and width. + """Used for `c:majorGridlines` and `c:minorGridlines`. + + Specifies gridlines visual properties such as color and width. """ spPr = ZeroOrOne("c:spPr", successors=()) -class CT_Crosses(BaseAxisElement): - """ - ```` element, specifying where the other axis crosses this - one. - """ +class CT_Crosses(BaseOxmlElement): + """`c:crosses` element, specifying where the other axis crosses this one.""" val = RequiredAttribute("val", XL_AXIS_CROSSES) class CT_DateAx(BaseAxisElement): - """ - ```` element, defining a date (category) axis. - """ + """`c:dateAx` element, defining a date (category) axis.""" _tag_seq = ( "c:axId", @@ -163,21 +150,21 @@ class CT_DateAx(BaseAxisElement): class CT_LblOffset(BaseOxmlElement): - """ - ```` custom element class - """ + """`c:lblOffset` custom element class.""" val = OptionalAttribute("val", ST_LblOffset, default=100) class CT_Scaling(BaseOxmlElement): - """ - ```` element, defining axis scale characteristics such as - maximum value, log vs. linear, etc. + """`c:scaling` element. + + Defines axis scale characteristics such as maximum value, log vs. linear, etc. """ - max = ZeroOrOne("c:max", successors=("c:min", "c:extLst")) - min = ZeroOrOne("c:min", successors=("c:extLst",)) + _tag_seq = ("c:logBase", "c:orientation", "c:max", "c:min", "c:extLst") + max = ZeroOrOne("c:max", successors=_tag_seq[3:]) + min = ZeroOrOne("c:min", successors=_tag_seq[4:]) + del _tag_seq @property def maximum(self): @@ -225,25 +212,19 @@ def minimum(self, value): class CT_TickLblPos(BaseOxmlElement): - """ - ```` element. - """ + """`c:tickLblPos` element.""" val = OptionalAttribute("val", XL_TICK_LABEL_POSITION) class CT_TickMark(BaseOxmlElement): - """ - Used for ```` and ````. - """ + """Used for `c:minorTickMark` and `c:majorTickMark`.""" val = OptionalAttribute("val", XL_TICK_MARK, default=XL_TICK_MARK.CROSS) class CT_ValAx(BaseAxisElement): - """ - ```` element, defining a value axis. - """ + """`c:valAx` element, defining a value axis.""" _tag_seq = ( "c:axId", diff --git a/tests/chart/test_axis.py b/tests/chart/test_axis.py index 47e60f9c1..83df8c592 100644 --- a/tests/chart/test_axis.py +++ b/tests/chart/test_axis.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.chart module -""" - -from __future__ import absolute_import, print_function +"""Unit-test suite for pptx.chart.axis module.""" import pytest @@ -31,6 +27,20 @@ class Describe_BaseAxis(object): + """Unit-test suite for `pptx.chart.axis._BaseAxis` objects.""" + + def it_provides_access_to_its_title(self, title_fixture): + axis, AxisTitle_, axis_title_ = title_fixture + axis_title = axis.axis_title + AxisTitle_.assert_called_once_with(axis._element.title) + assert axis_title is axis_title_ + + def it_provides_access_to_its_format(self, format_fixture): + axis, ChartFormat_, format_ = format_fixture + format = axis.format + ChartFormat_.assert_called_once_with(axis._xAx) + assert format is format_ + def it_knows_whether_it_has_major_gridlines(self, major_gridlines_get_fixture): base_axis, expected_value = major_gridlines_get_fixture assert base_axis.has_major_gridlines is expected_value @@ -58,47 +68,41 @@ def it_can_change_whether_it_has_a_title(self, has_title_set_fixture): axis.has_title = new_value assert axis._element.xml == expected_xml - def it_knows_whether_it_is_visible(self, visible_get_fixture): - axis, expected_bool_value = visible_get_fixture - assert axis.visible is expected_bool_value + def it_provides_access_to_its_major_gridlines(self, maj_grdlns_fixture): + axis, MajorGridlines_, xAx, major_gridlines_ = maj_grdlns_fixture - def it_can_change_whether_it_is_visible(self, visible_set_fixture): - axis, new_value, expected_xml = visible_set_fixture - axis.visible = new_value - assert axis._element.xml == expected_xml + major_gridlines = axis.major_gridlines - def it_raises_on_assign_non_bool_to_visible(self): - axis = _BaseAxis(None) - with pytest.raises(ValueError): - axis.visible = "foobar" + MajorGridlines_.assert_called_once_with(xAx) + assert major_gridlines is major_gridlines_ - def it_knows_the_scale_maximum(self, maximum_scale_get_fixture): + def it_knows_its_major_tick_setting(self, major_tick_get_fixture): + axis, expected_value = major_tick_get_fixture + assert axis.major_tick_mark == expected_value + + def it_can_change_its_major_tick_mark(self, major_tick_set_fixture): + axis, new_value, expected_xml = major_tick_set_fixture + axis.major_tick_mark = new_value + assert axis._element.xml == expected_xml + + def it_knows_its_maximum_scale(self, maximum_scale_get_fixture): axis, expected_value = maximum_scale_get_fixture assert axis.maximum_scale == expected_value - def it_can_change_the_scale_maximum(self, maximum_scale_set_fixture): + def it_can_change_its_maximum_scale(self, maximum_scale_set_fixture): axis, new_value, expected_xml = maximum_scale_set_fixture axis.maximum_scale = new_value assert axis._element.xml == expected_xml - def it_knows_the_scale_minimum(self, minimum_scale_get_fixture): + def it_knows_its_minimum_scale(self, minimum_scale_get_fixture): axis, expected_value = minimum_scale_get_fixture assert axis.minimum_scale == expected_value - def it_can_change_the_scale_minimum(self, minimum_scale_set_fixture): + def it_can_change_its_minimum_scale(self, minimum_scale_set_fixture): axis, new_value, expected_xml = minimum_scale_set_fixture axis.minimum_scale = new_value assert axis._element.xml == expected_xml - def it_knows_its_major_tick_setting(self, major_tick_get_fixture): - axis, expected_value = major_tick_get_fixture - assert axis.major_tick_mark == expected_value - - def it_can_change_its_major_tick_mark(self, major_tick_set_fixture): - axis, new_value, expected_xml = major_tick_set_fixture - axis.major_tick_mark = new_value - assert axis._element.xml == expected_xml - def it_knows_its_minor_tick_setting(self, minor_tick_get_fixture): axis, expected_value = minor_tick_get_fixture assert axis.minor_tick_mark == expected_value @@ -117,30 +121,26 @@ def it_can_change_its_tick_label_position(self, tick_lbl_pos_set_fixture): axis.tick_label_position = new_value assert axis._element.xml == expected_xml - def it_provides_access_to_its_title(self, title_fixture): - axis, AxisTitle_, axis_title_ = title_fixture - axis_title = axis.axis_title - AxisTitle_.assert_called_once_with(axis._element.title) - assert axis_title is axis_title_ - - def it_provides_access_to_its_format(self, format_fixture): - axis, ChartFormat_, format_ = format_fixture - format = axis.format - ChartFormat_.assert_called_once_with(axis._xAx) - assert format is format_ - - def it_provides_access_to_its_major_gridlines(self, maj_grdlns_fixture): - axis, MajorGridlines_, xAx, major_gridlines_ = maj_grdlns_fixture - major_gridlines = axis.major_gridlines - MajorGridlines_.assert_called_once_with(xAx) - assert major_gridlines is major_gridlines_ - def it_provides_access_to_the_tick_labels(self, tick_labels_fixture): axis, tick_labels_, TickLabels_, xAx = tick_labels_fixture tick_labels = axis.tick_labels TickLabels_.assert_called_once_with(xAx) assert tick_labels is tick_labels_ + def it_knows_whether_it_is_visible(self, visible_get_fixture): + axis, expected_bool_value = visible_get_fixture + assert axis.visible is expected_bool_value + + def it_can_change_whether_it_is_visible(self, visible_set_fixture): + axis, new_value, expected_xml = visible_set_fixture + axis.visible = new_value + assert axis._element.xml == expected_xml + + def but_it_raises_on_assign_non_bool_to_visible(self): + axis = _BaseAxis(None) + with pytest.raises(ValueError): + axis.visible = "foobar" + # fixtures ------------------------------------------------------- @pytest.fixture(params=["c:catAx", "c:dateAx", "c:valAx"]) From 7e48ef233bac77fb71f1fdd188f86e1c31d04862 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 25 Aug 2021 17:24:25 -0700 Subject: [PATCH 30/69] xfail: add Axis.reverse_order acceptance tests --- features/cht-axis-props.feature | 25 ++++++++++++++++++ features/steps/axis.py | 20 ++++++++++++++ features/steps/test_files/cht-axis-props.pptx | Bin 380020 -> 366536 bytes 3 files changed, 45 insertions(+) diff --git a/features/cht-axis-props.feature b/features/cht-axis-props.feature index c4663ae4f..c7065b3a9 100644 --- a/features/cht-axis-props.feature +++ b/features/cht-axis-props.feature @@ -162,3 +162,28 @@ Feature: Axis properties Scenario: Get Axis.major_gridlines Given an axis Then axis.major_gridlines is a MajorGridlines object + + + @wip + Scenario Outline: Get Axis.reverse_order + Given an axis having reverse-order turned + Then axis.reverse_order is + + Examples: axis unit cases + | status | expected-value | + | on | True | + | off | False | + + + @wip + Scenario Outline: Set Axis.reverse_order + Given an axis having reverse-order turned + When I assign to axis.reverse_order + Then axis.reverse_order is + + Examples: major/minor_unit assignment cases + | status | value | expected-value | + | off | False | False | + | off | True | True | + | on | False | False | + | on | True | True | diff --git a/features/steps/axis.py b/features/steps/axis.py index 22c815862..9cffbb93a 100644 --- a/features/steps/axis.py +++ b/features/steps/axis.py @@ -69,6 +69,13 @@ def given_an_axis_having_major_or_minor_unit_of_value(context, major_or_minor, v context.axis = chart.value_axis +@given("an axis having reverse-order turned {status}") +def given_an_axis_having_reverse_order_turned_on_or_off(context, status): + prs = Presentation(test_pptx("cht-axis-props")) + chart = prs.slides[0].shapes[0].chart + context.axis = {"on": chart.value_axis, "off": chart.category_axis}[status] + + @given("an axis of type {cls_name}") def given_an_axis_of_type_cls_name(context, cls_name): slide_idx = {"CategoryAxis": 0, "DateAxis": 6}[cls_name] @@ -132,6 +139,11 @@ def when_I_assign_value_to_axis_major_or_minor_unit(context, value, major_or_min setattr(axis, propname, new_value) +@when("I assign {value} to axis.reverse_order") +def when_I_assign_value_to_axis_reverse_order(context, value): + context.axis.reverse_order = {"True": True, "False": False}[value] + + @when("I assign {value} to axis_title.has_text_frame") def when_I_assign_value_to_axis_title_has_text_frame(context, value): context.axis_title.has_text_frame = {"True": True, "False": False}[value] @@ -227,6 +239,14 @@ def then_axis_major_or_minor_unit_is_value(context, major_or_minor, value): assert actual_value == expected_value, "got %s" % actual_value +@then("axis.reverse_order is {value}") +def then_axis_reverse_order_is_value(context, value): + axis = context.axis + actual_value = axis.reverse_order + expected_value = {"True": True, "False": False}[value] + assert actual_value is expected_value, "got %s" % actual_value + + @then("axis_title.format is a ChartFormat object") def then_axis_title_format_is_a_ChartFormat_object(context): class_name = type(context.axis_title.format).__name__ diff --git a/features/steps/test_files/cht-axis-props.pptx b/features/steps/test_files/cht-axis-props.pptx index 7dd230eb2b05a1fbfb4f3dcab76d2fc4ff184524..4b80fbd6b17dd7f4fbd988a764bcf427e8248fa1 100644 GIT binary patch delta 35193 zcmdSBRZyJawzUhv-QC^YT@&2h-QAtWo#5{7?ryh9uZ^ceG*bAIpFeVvLiQiF~!F9iaM3Iq-W2?PX01ms!54|xa-1f&BG1cU+v`6(qY z=@UvF6Z9v*hU^L>>d;x-Eqt6^$xyHjY+kW;?WrY>crQrmeIpZR^X_)??Wx-q>O9dh zVj49^_|yKMZaw5Nifrvl%PhLSg8E|p;fGWceMy?6G>ntqw?7V|OfZAugtb7xv_R@3 z4@(M0PnUsB3)$6csX^T)xsuW5KRF%uPLGT~YJyq<9=Y7|y-}SuDYh@D7^V{@B- z+QzL+9GGG?5EEvp=h2Um-7Y6c%bHMznoW*OC*|Ts5H^ABSBxhlRe1HL$&Rt<=pB+s-)>X&UE5`r=e>45r2a5Vn7 zrej)mCXeuf<1ncDHIU;u_NY^F)}p@hd8x9`sM#U4ecIEFDG9BquB*sNoFV%l^ebY% zLC*R0Y$9T|ZW40;9E_r{yC>)~|!>dS>< zfApzu(74+Yptle*7jSTb%R$2@8q8_yR_2(C(u;W1GDQksX!P~so)wOJ5{9-x$9W$* z6LIZ0N_Rz-Zq!l$+RBe{(tMe7(-;RADaEy%!1DP-9|^R$4OvGhUBny1%A_fpO)zE* zkHXPXa|xul2vAgr0y8l*v(&;uv%D_Mhq01sbZ@ZBUS-T5-14SfROfuAdBn2!JB!2Z zN-#BsMB7kf4wX2ZePoH2Sv*Act_uvJa=tiTvSc8<6~k*vvLz@In6`zm++0E*6j4GK z1S+6AaHh+3wjZ-VmflEysmvLH(3*@%#@eW@$m0*zvJeR0No|3zL=JzWRm~hFec}>W zO_1G|Xkwn|hw@QBJP_A+@DM@z#mH`{Ce^{}s?;X2rflfef6a!~%+suKMd)Y{cVucO-LSi-aw_QF#lgRc3&(_?aI6!U@oS@XIQtNnH^uck(Qs}q zj8=qHkK0C^WKXeJ_A{%%QkqFvVopuCL-QYmlS+kZOL;a~c`^F*Bo9wJ zLCk!`+J33r-?0lj$zG#1(FsfU@o#we-0hRQ?V(fJCPesg+B3?2u4K@BXk$aq0lYiL z_SY@N@Bi!Fnk`_D{Kz0MbHWTH2Ea9bfU-cAp$Kuw;-3lRGI>j2v+%zXDBwMj(5h&N z(HWwtN;s0CpGCmxoh`?}gP1v^l^H+*N5h1>ykGJRA(J|W97A7Ei8_Tnl)0UuX#FKU z&LnI{sszDeNc%f$Y1m!>NLg{Yu1dR^V244(q@}n(mW^QMkM{(2yM=u1qh)z`+6-bQ zc{d6C&7>y@Uz})KLHQWIqu1-rQ4P;wHd?KW4sRCajq&-t(^HNQmpWFDJ3cg}Aa;X6 zCoDrk|HH0>|Mhm62Sd<)6jzGwn>0XWY~SAk>+cpBU2`1@q%|ClgbXMd{3x&p78{H5 zlg_T-HL!mcn6;b0Hlgjm(}M?YV}IZh1=Q>sJ#AJ|D_YyC5N@Ro*1 zj?*gTU~vi32R)W!zr4{SE5e+9DNaUHWkK`b3QX-^1?I!}BhK5`Q8*1>iC$0|&je2K zg&Yvw^yb|Z-;tZ=`6g|aJ@k){0xN%=m_Iq4z)ScAiwd?U=XCIwiS+RQ!$c8|FIn0{ zxwT4%_<~E4hUrT~j#5{*ERBKSo=XN|&++^|=09{x6tpS-4I6jZ-7z`G9Pf$54Wxi$ zVQQQ>GI7gAdQ;W+p=u!3$y+UALJ*}M_Ms+9i*&)onz3wvaPGsKW4{5sf^{0g1E)vHMJru+#(x&nPQg@qfjQE(rrLYE{Sa79x}H7VcV$#^!{Jl z&P*+DZ}$ii;+W8&oEd)dyv=rQ5DlD&^uT~59jO6E)VKcrDl*z7vtqNBSk!@JfDv%; z1YF1W+{+Fnk$Jm;!%nw`8LAVco6Rwi5lWF*$swSFb0U=S1^UYtBQbE8HhOd^rpE@~ zT9vpyEe4of5zrS;M&pFF^-8_G-9;N_mjdZc%^-uqW>r3U%S&u-Tj4Un)+EKFuUY_( zqX_D3z$LUf@@J}rZTR>sXYiE(q%OMEhYsC7wzYcN3|xGZmj(Y?jIX)HHqiFL+G_e( zTv32SW@noq-H&BFayd z2<}vD7{(a!AWChD`R!R0u>t_;d8uuh;{O>K9dI`d6?Xp#2x#wZN=AJf73}Su8SEWR zoJ?$;4V*3PZ0X%?tWQ#O?3Uj@UW!uz(e5nsZUMm{=tYzCd5O|SfnFI{QJ0eu_9)k7 zz($>fuq12QtgTb<>D_(Pvwi&ixSbqY(^t`v_y*KA4)}V=F%N}$orrAw28jULGDaEz z<8D;pL6W$0$NiIq?v$kvsR1-is--_%0!wCom*UNMvq?7vS~5o`Icy^x2Ab+L8czq} zJ;svS{4Fj;J&iW={5S?d+EmL5Gld(kfO75APff+85}eMMdnA~ z{@c9@mp@QHOQfcL^{gU7F`M~CAN;ECc1`1+2O%Bxn5cA zS4>r<5~pmP&uJ=fEp(IqYbg;jfYRAG_b=y3g6A3|yGgNQFkc~}2Y$P=ns>sa6Gq*H zi1C9<4h;94TM$KgvnGbf+5g&ptiy@+)1-#Wt%abxKyLVh6k9wC6hZID%Md=UrVmixK!x@(&^T`fP7Ww`_rr0elC-}GJA@`U-`#&b57e%Msiy&foLSMT^6c? zN3MjI;))@Zy1nK2H_mXrL(ArHFCrKrjcPI4+tX#J0Yw+uV_ANYDt%GA8`hjsU{p3 z7vS%lb`4Rh;c~3Ez^E~qV6AxG&}$ya{&~;z^JWKUq$3A(bGKGVhmJ0B6v)mi^W2@o z?8TAmV)FXypxF=kQM3&Nn(rMFnDXal;P{&Aw^6Auz#Uqrj8=aBm8ND&4|$;5TiJgVCaazb*9;MD2NFFx_)RXu(Awl>4>~Bdb{74eZ-z2TrW-=oB)XW0}eJS>uX>()E{fi>zS>xh;{Y*Mx zke8BAt=Y>T&zj&^jXRQNsTtgPv$3YqoW^%=91gNTkm`+CAzp$ZaD|zyxZ9`t?uM6T z{7BI9;f}D#xZH`5we@^noJU!Ha(taEW)|TxPuju?LT)h@ybJ(12;cnt8V*d=h32&c zd0kln&j-uP5CU>UnR+?sb@|iwhk+u~6Dwl*5OKh@_~E31Gx^)uR|tukp*Mi9WC|gp%FCV@h+{h1C&o4t$PH}zX$2I+#vqk-yPUOVV-cGVSLPYUDA6|tO-%1H~`kaF|v6F(hI-EEARsk69YU{X(KdZvE;F4*&0G^ z6iO_RR&z#i`t61pj=Ds$#M(T=y>~ay2H%uXwe44(W8BhMxMQVF2Hwr-?Ar3fJF5E^ z<#wx0@!&tuEtMry?nTI}+JG15L6o1WUnhT?1SrYV45C1im05)D0ss@~mF#k7JyTnsw-inD>o1iFvW1A~g<5y3rqpjFn*nz1yJ!Iyj>u+8`~$b-dWh)gD7=$`UWH&AhQ%~l&f~4qUkKThAxju zG1y|HD{PqE`v!@9i5>{{3ft!j<$_(_NOCX%=(ixL#I@@NCujIlXUYjr zOWRy!(G;y2A$>-owr`(#vBLne;{gKVi&+!p)332J7zf9wO~_C`Gq9jk78jhRp&tzK zo+G4zXz&=)MeID1SDnMajUw@=_-{6Bn*inuZvko-dO1!&aEf!1i3946RTT|E5NYJHq*XY1$Aq{b<&9TT}Q^XTv7-Garb=4t2Skoc&;kmEd$0sG9xP`tN z3$xyg$n*_JDM<7nXkb%0XpL-dDIx~^JB3y||9?@aFBswfJ%!TtC065sM`4Db7R-3a z!2yNWswZ7va?NyH4!WlAjO+yQz?8b(<@i@lxuvN?a=?Wb)J0?Qsn*)8dFb~opZFG9 z2^CZ7e%fia&yJjnY^17OiV20)9sG*iM0nC&+O`Yz`sEy*jbTrTeXZ`tfhTR?Duu9k zE<1bI5MIA63v0TtlxYNieXckDgXNtD+7u)c&;9wf zaL6g^4y{Ub{T$~{DEVW^cR=H$r0y*~f&$Gm+Y3%*;laTgM8QJ_&sRqMAvk=znHpBu zcrRy?1W)vzO%q9N)9vGTKV3KqP`Oaa**!o{9hOtha?+wyRRAs0SN;P-HN=o(#6Ou5 zXIvN2(7!d`#jinIbBI{6&tUw!5KF&csO z#n0Ki=LZMC_TSBH6_%EgR8zlJQ@xbJ3RUoM3=Nynzl znp!E*-xSmRh!dHkQ0$|bE&R+5$A`0|ftRJ5ZYW=N`+C+MH&>7xz@^tok#l-SM@}{% z$Co#tEE)?BSMKz)QJa3@-mBa}sF2#IexuR3DE3!q^y53*4D;FW3%k7``w$v33SuuqhE zJvZCTEmzFv)~R;TUMSXl9TBb_`Ikhn-e)D*-4Jux3mlQn^DhYef6!sbfBLreR^|l% zvcHuEHLIEdgzQg#!ciz;49bQH1bXzxinOrOkSPcuUi<5u;0Cg7)9v{4hxCn%}xv zn}qQvR*dOr>1*d3MN3lylVo$jxsPr(5si&68_gE;KizEMt(!Foj5!9ocjY9tCzmw% z2Jbs3?Mv)n;1tmysIt>0EUl|5n6s-H02##DHGi(vat;P#vhgWx)yJgJ#SIcN>5+RIi)N$!3&3a9 z3U~RW`e2kcACIm|q~c1O9i3fkkmCb9F-1S+<+B}!wQmO8c-jGmO~k-Yzw=d+dMv7R zpc79;>Wii0OFvxlJ$ScRQPObw9H|RLeI~UU&5f&QN3yIYUssY5FLuVtcm|(M`W?mJ zn52>9o*0EyA>bXiCA2El@qB8}kX1bE{XSq9gY{Y%ahtBa=)<|kcjw5&gxI;t0n3b=^?YT zc^@hJHW=ZsSA@K%z$NVRj=uM^D0XudV_Q|w?#KpCw{89H4wJ^UJ+gjI!56uK?wEwH zY8(yCjjzMJ9U+p{_qMIMof$#EU;tCa^C8QD_pt~do)N%#9*bcZiKi)lWofVsjI}dY zC_BoTh7A3T7=2wZ5^4YBs_TF`c#IhB6eb^3;KHiIi;+%V%Lv098&?YO6`vpfj3S0& zCDSrX)G@pN!< zBS+`xV41OR1FEK9Am5USq{-vftL^fXCGv#%mM>&jyNWjJGHciCsZs?<8nD)o%b)0t zVLorxRo-*eeXPdYaWEKuS!}AXZ=s=Pfb^bvE-N_$x0pK0_^f653X$Uz36@|jpsl|+ z)$fA)e+|wLoLTP zI1NzAd@)~jw8vgS-L}v#I%Yb3Tz(3Q8fa@PFq+f|v$`hzor|D;5y#xUul#=(HTJ(z z`q#?uPBAn@1Vvr-DX46=UDfgqgmxR0v^QDCqTJ%ur`bQrIBj0Wr*Hpi?VyzTtAdWG(&2($QrV}e2@G*8{?x%e+JgJ9cz zn5Hc@c5wE|zNym4cz5+o6+?TtMrsIv*9C9?i#ntI={|{#&c@ES5qs=y=V#ObGJW6q z>+{>@v*ec6dG`?mp$s*Ve{z|JIA>queC+%x9Ga62m4Av5Sz_1~oGfjl`iO4hlT zgc%UrI#|UpAs}B^=R!Fx-5q!=q|RP98_G&|BF;L%b|)xd3GZPN0@PFiIMW1RiwT-W zy<>=fAcf3_4Oh}tS4xXUuK68Z_`kAbZz3CP5McWCnVOuSObP=~mVpMqZbfkTwE2>- zHB!^C4o4U(0lw1n_64D0Bv#P{1%D^(sHpfI8-oQ9S;VQk0>%AH&?_}}2{nm&D24XL z_5jX1`(_P~6<%#mjtAymPMO6O)+L$t{P>^E{HofJ)t^v_!(QpD4?~5mJjA~F*b($} zR~j)l(dSXUPuSoclYhWIfb?}skasR2f=Un&+!Z+E<2pNlStQHwuj5uJYojtFk3ZPZ z1^)Q1OP_P~9tEDw0rzPXH!{H@5$Kw0e)$LS2oAO99=-ryu)%GI{_!?7AFlEyFv;_FxgwuM+4CDL`WkIJTr%gKGl&U&1&KiMZDsqSD8L{+6-uF=2G z*a>B&NCON?x)Hvv$9IgQ?)E{EEJ&e+nGn|H-pI5qsH)+o4l^ykM)fJ3U;9h7k5G?i zb!$G#n5ycXRll#E@C!5X7oBexiaFjcztaMxO!b4&1EQw7p!*pSAGM8Sjm_P2;BC0e$j3K$P~E$`(fT0+iLaj0{Z)b(D2)VPzL7h zspE{c`8Vya?)Gt?6?m;M9l!DvEy1!9jUkSvNMOtcwI!CRAx+?UDIS+EmtNbj3-YiG zj#`RSXa%-*lRO)}pC-fi@seLYhy`uUXmFF5Nd^zjD=iZ4x&fM%$B==`ZRStbZd2j} zfziO7;}pm@KoZKR=>S5u+w7oxe>mkq@i@YLz6-bwq z6fU}#Dk1!9Yube)3D8%#4e4{Rr=_AbH9gOJ=38p5J0r~)dUM^n^islr($V&O?Is~m6ZO7( z69x9!&sN>!nk1z`jKwzvL^=akaa?0JE_Idwf!QLpJuP_)R_I*t?DUaqI+S(r23R^( z;LDmCZ>^u2?SMM#-=IayTNyc`EOw^-*XIT-D2OP+#4+2@*9*YX{#B;74?~zQx>rdo z7tS51N!hG>4C7SgKlD&@0S15+aKijB0qpViNZZ4k_D){iprvJ%pUPD+z-z(+u}vp zL95)ki$!}3=roz|wZ)sXEDEjc9_;X}THkF^JoSLBU<`|=#FH~T_$=Kd);WgvP%^RL zTJ0JoX_OM7;8Zbaii7T#h;K@h>Lm)bv+L{;ig7iC)ef6)4vH9L2o65j@)X_5QIIUU&783)V%^ zS8XJ+Qr+VqGlEbgpzN4_)-=t{US@L+hcdo%Td2X1{@M7=3k-DyH_;1Bbb09%GTNev zCH+n%7)x!xg92Giebv+)H#p_7PcmCX()ggCOB3+vb&+1{-c+IA*Qqv zwc_e#xiJr)YicYQdVG24&vx}5uowrkgPe_5>n^^pMiaj^I4_NGADF&Ajp0UPxmF?z zqTFc&TV$FKd(=K|Lsc;H0jpxiny~#=2NPmlG`!IdKHSNqP`$T?*#SUGq14yrU>~FN5422?hWKl{hBL4F__*T@wdr5 zU|AuYt`kv=4JpHVCY+8-2!8;<2j%v~7SF#`IedEqj}z8Sy|Ab8IP2SJt+>Ft#Vtq5 zBg@1LT zB7PtwrJ!#u1N3^E<5jtKK8-Q5oB)lvl#p5g>liu9Vl;4(>+D`Ty*fI{gF5l*e2W_MXTl?lYqnGV=^kw7TFmX>muwdVWs517sYpX|uHMh%86l{tl< zt=cZntF&q$+@})a=(>*B3y0&+$!Yu@vZ20nAGprFH2}c`gCM+ws9@+f{4+^`G9iL( z>YfV(9m=@YAjVDTIzg*xlTaj!Nt?P2kfp_j*WaB*2XFcZ@1CcgU4)!s2i||{&yEk# zbtPofaO9uB){U8RLm+HT-;vG@!loao#j~>u!GJAprqJ~^ok{$Love9PplFQ|6+&hr zO_x?GsQg8Q=fo*n7zasA0*5Bk3XPkZx#`lbi7)DO`xi(FT!ff&L#cNxrcC&B$ zUF72gav-OXftc1lGO(Lr1rha@t@WV)YqGzsqv-WoD@lr9ufTzY2Ft&{EKO0`{oZB_(gCq9~m6ZPyb%y(Faxw zcOL#40zrGW(KJCQHtkpgfy&>HYVS^F`+PXLr+Fp&-y*Z$MRp%ImuurNuBL8nvL*(u z_;KnozYG^UT%!|=`hIzLa^u}egAXVDf15E>V0?(YZd8XYaicbS6RAq?k0CEoW9y2r z@gb7QUM2cpB4hqmH3fu$t%g?eV*HCe*MD8^LSMdpVKLirQBek=3fq6MW^3wT}6dtA2<==c-(KL0bW ze?^AB$2IFMu0x*g8nht;5J@CcVK=&eMOM2ad|(YqU~`@FpCS{|L;gFhXh{FtHKyQx z-TSA;luZ}V{}!q8#-0D&My5r9iTqP!!Wh%PYup4tI&u8GB~)ZziG=2Jk3)|kBX8)) zlOQ+Fw|@267W!vJuDn%b^m|3p{7*%4p6C%||4&6`#u@OeZrq$G^l;OzEoH zt)m-aA77Z%VSXi0mgOZ`Z2F6xwG({$kBT(%XsVli>mLGd{e$A)u=-b@Cn8nGY0J++pawcO;z%>eughr?Yy$>l6AHaajIaU(^3ZU;3LqqaS=_jyApaMiE(a z@~*aE|HSHQ{NG~yH{t}p6NlqpSe1LnYQ;a~jpKKJTong@n1bTk220_{{zM3ni9zAO z>{dGh)b7DQERmj6kL1g+`+tI|SQ8waNpmoj=G~4vJ&D%>8uPamVZVeQmYn!X2J#_5XZhj2AktmyGbuUb0ZLo+if*BDyH1%n!Ph$8DW&+{ z1|$q7)mk(IGQ@k(`=Lt38da?%%(-tOP+$E|CuYJPLE%`X?97Hbhd1@|3;EB}IA<%v zKO5BcwH6EPD7&T!4yQKp#iE=i0JoL!eydeJ&plO{$Wi;*rcM=>swt!e$Un~L$)u-% z9C8t%=r<5jKjvtrKS6tf^D8sauK96%YY8R*(%X%YY}%e!7yV*d#j>sO$z;rAea*6A zx%PtW1xM<>O15Nfwf-DDk?!rqgk?pYR?GW~jIl>I*N(O1V2)htpPK!54AxE9?CXBS zUlJ~mHHRC*q=B&8`Yxo0qZ5AwC-uUoP6P{YoV#;J#eO^H7sF zlc2r*rP4b1du5k6_ssWdZ6IOCPp}(s&_&>R_GigUZQ-gN&u!g!;(!+?7OuyAw~;qL z3a^Zc3K$p+30yO~Y>+WI3K_KzN;wxR&;uH5aa4m;0 z8;`TitSc!8Fj{zZf0yyFU}C^NksZ@?De6lOQxuMYvZL|8YY4vNpf@t1`GcM{U8{SOWP7h1GLt>VAmnN)}W||}7D7_@8W+%0BmS?^w<0yu?YB+u3^E=9* z6y`=J%e%hOW8p%dp0YqsO}*?er5n0=vbdkdcfl0TN}F5{Y!H&H7xh9kpQr(RmV{#Ug!S1+QR_h;pZU@K?jmiJ`Wp{VgEU%g1b= zX6(l`AQZ@P~0xa}bZC4VPO{oQ6htAV7cUE=RPl?I+ar$G0{$Z&^~AE#K* zYD~VFXkb7@Bt_3$s86ZtOFa$2n5`gi;`*nXX-gC2fBTs&Ir+ zsR+rRO3}|xJ3(7@F+P`}9|D~v9+Lh+(FHkM!zf8kpO*8|O+f~z5S0oS*NtQ!f2MB5 z#fPpitV$Btbp6zY3bj+83LtAlk6BnyoI_SXrJ~Nrh~`w>YjtdJOZ$z5^moK6h?^6n zqY$Nn7D}^~^{^E@5vSU{j}U0G5eHO0ALz`FPt$n!A_A)E#_u3`%LvobRAL=f8+BuuRQyl*&JaP)6*h8V1jSpL6Y6QUe&N6KZ#ZnYX}@ z9h&fe?~5i@Aj^)q(1^>*mKTyW#%==3j*WVeHZzrU#<4e<2N^w(59+Pl_ga9n^W@8-gV1tf!ECzfsvHk~4Y9$Q)NKbftr*_w^Jxn`JgM-bd z7rHFwRw1FR1I?yqv8FXuSbR7PFyP+`87fEcB?$DHD-1qK^1PiF4e<^p{J_MFDOFgJ z$_dZFaDo4#MuhGfg`gKu?xtSU51BnbdvQ#Z6bOw61G8T0WC5t`%Tj`5b`==xTCc?! zj&ox@%W+q@@sxFHa~X&W+r=_-5Kk$R#^?4+1^c#_0;EDS@%99>Bbl|D!~B*qOC`U)HIY^#NMzce9tU9LQNjNZ@lQvV6#F*rdK2oxpHMx$ zLYXd-OS%HpLE;4Dv?)qMH%H4;;Qc#1!)l&BEk-zRt0bvGlcu&H-;ggb*|rS9HFjmn zKOP|mVI8m3jtbp9T&Qp#6bLF4*$&UhUF5|Ht8x)aEFqLMhkez|Rpw-F4q>uLf^mU2 zJ_Df-XlQm9u`ac=@n2bTYwI<{^k(A8zNGX~0i!{i$n$4=g8~8B!F>P|@NvvGu4eO& z%ol$BcVwz8MxyoPxf+Z~Ako?ca#Gd$6cja71rzH;NS>fSpYbR*>f$m;grceQtM9B` zxK9miUtHd<(HAU&gz@fLTmQkEYW5rCmH6pty|Jdt39|B?%m6Acvu|l##I`5n>2Yku z9n~%@Hj*y9PnZHgOE75UMY8L<)9%sfAGL$m4HnXlhJh8wiDt!tw1bpoHV?;Jvu{b! z>>&6O92ix2Qmfl(%BNQ~i@C>3&#YhXOzejo4!e`#oI%hz=qm>*$7^aq>prPgk8(B6 z$@48}@`wC9|tL(A;!pK7bV90-_h8EXdz%Bv45O2}lI0 zIfGx#HSI+ZYg<*K1b%oC6mqQaae>4Lnk5TALxYpq{@%ut_>F%nu>~Fm*iSx( z^=H#JZBjx_hA)1yux0m1@QkFoeoUN<5`nEM|M3+%{HqgU`&DP5$`$Dmt#Vt0?*GG_dDwn^;>`C zx~>@ptPg2VwqGqK1!i@AzhURBhmQMtYNyb`bDmG>Jh|89L~$Jw$HH?4KItKl3Nhe& zSBdj^<89bS=%FzvD7^gwCe?##Z}EMRW9o_P`AXj)x#S*i4A^4342Y z138R|pc^}w8r)=5vO1ad2b4Z&4{TxT zT}H!~8iT$awrY84+u;&7;_lJA>Yb7Zr}5IR5XG6j$8aIf*#w!2!M|z=ZY4uis9BHr zvW^rVqB?Lv20&&It3f4IkZSHG~*~Un1ph5%(oBX)AxrHf`-11xzv8Al~X0LRAMm0i->7wqXy8 zR9)Br0p|4Hajq)J8P8w4?`}$=B~tnK0Loe_42sqKB@Yx|qQecyC9Y0k|E&M3P&Bxt(E#J#I3S?+2Yo_JC^58}ouieLxrvE0Yl{INFcWwSy$>iCXp1}^m_;ab zdfic)>Kn(PVE=OL<5swFRjoIUp}voPxlkTSE_ElIUrzdyv+~v3u^klky$Z;<_k`o9 zKa;sezTDyIRz@bUoz?Yrk-l_;mHb zyXi2v;?JpmKTGFQ08Nzqx2~Ld3y@*J<)O@w>zlFmHj3Q8eiKn~kF^CRZnv=F+P-c;JD@kYXZEY8 z26iZiFjl989%YHJUP_FcG3nC+mki*JpIFmCjD%Nq0YG$9AjC}Iso67Gpf0p-E}eG8 z>HyfjGGMh|;Mi6RQfvbMZZ>o@?c5ofHlLvKzyjUSU=9m#m;ec|#5Xk~=N*`9!F1)$ z=dP^Fc0BDFXFJGp(_kmcgM;!a=z$5h7N5uApDQ0-^u}%CR^;~%eAHIRc^#fF_)&J} zHDfg&sN~Wu^eo;s&Xv+JDLdrlp>qVaIRPjGO>5PQ_XVJmN}Rx<4-{cQV1#q%-ICaO z;M3(tUcKbzkm_lf+=fP4ua+|mI1G-IK=(9=Jxl2W*7Xs`J3k}FbVkmo9MFXp7`Nfl zhdxt2meWK|KHSgK&ckz|)TlvsJkXx!59JInF73#OVigN|;<+tP@QbpwXB zKL3G^R7Sf|`qQ~=O{tqObxPz*!`3Jef19uGjuc|RikW_ar^X8jK10d>Re;V7k$ui( z6UUtq+;X$4%oMbK;jRd363Q#)7Ve&jaSp9j1m;w2HF-R`u9v>!d(m8GK)4kG1S@{$ zCzEO@uU-wEYsYH}j22QK`I{v{Av{0}f4{5`>@vERQy={I1t>=KYb{%E%Z?-#ttL6t z23_sR6Xd=JoJL$H+8oB}rV;&zD)gpW2Ab8Zoej0rW>7C0@nZVAO8~8>+pxw=wA$Jc zKz|1RzWk)Tvgxh0+R6HZ9n=S1$38q}>%57(L zMfB1T&?6KG6mg|C!+L#ojVfus68> zQ-EsrZ`W?W0t;XoJ4!hKkKFx%j@NovAj%|`OrdO7W1UZ;S<4E*K5fWLwg4Q7AWPJ} zESalHG~(kG^u-UawAn9EOaOL-xn=^D1@?ry3_IJ?VQBt9S9joN1{Ey>NA}7?C{m## z$gALJ*lt}NSEfR?Ef~=T)dhh&f9acw96CJtG zG0{A_ciZN-OuDrdag#f){l2m9Byu3*u@k`D5MR8n$V2%=RQ=x`R?i5_I z#de!KWq5heHcIcSUSgD&zZ9(<@f(<6*f3FQP!ZYb`SB}EQ}keZ{&6|TQvJhrAZwPB zjZvGD8X2S)Tf@Dyj|qte@{7T=_fD5Y@DBj$Wu=aT38P@L5#WG*{w3RF8O~dy&F}Gs z?!@^~7ZZH)SEJV!=(@szv@EyHe&LVo<`~bc0a;Annzip|-3fuqF7tRhQQo5$xb^kr#Hh1hdUiq{)kQwizTvjjcA!m${ zuitJyX^smz@&e+5#dUKFHCPk9{RbN^xzOW|fNysbBA!jxs)borQ1XrfZY;<0+|kg} z$*^oBi!+-saKW<(IZyaG(5}Be>dJHbD4rdvr+mj4H$lX-R#!NOBAlnw$jd}@{mMS% zD#NtMF|FI{WJ^d317&<*L0q-=3u}2#y3gmklm1mytrq}S?M^u0<=0la=-vJ_H(K!~ zK8sV(=@ipJ)I<}gqkadppHRN`m7(m$rIAzJDUzwF)KQ<940qop(so1`=H(!wLfs>X zCl3CLm#0N^RZPIQT4=LZvmu{-O&w7_^jr_mc)TUk&wILaQ)=3ra9O+y7=MR1=}W(q$M0e?(zU>_4)i#8uH zJ9x_*3pqhsf@(e~hNg78f};P&zx+Z5vx9Ci^9L1-`!`%x;q>A)zTvXvAG<2Y`z@j0 z)Ha-!IT3w((*6)|%IWs8V}ANSt-S?YR8818y2Jv~Al=f^-K8|BNFyC04Fb|}B&AbW zLO@Cyq`MIiBqT&Yy1ScY?~326Z`|L#|KE4-z`*RxGtZeg=j=TFC=-eCl?cR;=iWwV z^Bpe)%8F$rC&Zm&KoZI$K+k8j_JRZFdz?gxTdT^{Op{k@zrqq;U0Xy{(?>#gtoL`6 zxecP5-duT9?(ql8@W}?Ml6ZLG&R!ktwKgjUe@1bv?^Q*_G7k&`On43j)K;bEh(Dw7 z(Cf0H7Jk!;uuv$xXLNO6UZu}n?%Eai*xRhj8D*Fg&f5F1;(L<_wXN>B^F>GPxl}HH zhaXJ=2`>4&@DW}7Q}?UnPnb;a?$pGU1ey=(Y{c^rxk9Bi%w*NV=mP_2ti@<~4iSeY z;*q~rr-7!tb-pVO0_Z6Z$#|b6Q1E*vVlv4f=JjcO$=5Ix#iK$ zy_C`_5@*dxz)}zC-z!cq6y|WL(hkgXh0!fuCE>Z!XQN})`Od#`;~Q{CzqXXyWxIQg zt#xU~Z)gj8e%|xxn0Cx0=~*=u9YffGPWR@yLQiV=LxHippPW5+1d0Gl5~S$hNc|Lf z^Iq&P+WgPDu>c?M;w94zcGB{>_blW>Pgte=G5M={(}gAKEpf(GVDrt0lufmH{4efD z@JgPXc?A?QsvtFa3%9w}Qp)sv)qMXEWiuVAn)RtW(O7}h&YZqIPudhWSU7uR7%LZbwi`Dr70}3b}yIYtB1mU z*}QSD$00{q5SBoQLt>jFCr|hC>bh^|6GIF$!)AW;DEZmiy3q|(M+*MN%2`cbbQDKt z&v7fkGLGrW)A@?govwp3U8~ED4uWwfCkLIU*SW8wIre-oei&6whcgab(3A1y1$eYh zW^_R>s_uRQW)xwKjeC7-hgMm`ik-8tV^77vP>;^^QWGpb=-o}y@1@>y9HC>Yru49f z$;#K&9tl;3?^?m>>kr*UB-s;{5izc-TRxRFp6X*)v3gk61kBQ)Up9X{>rflR%KA9L zuY98@JnZ6Ydbom99Ex1<*e3P3+-Z)FlIWeu<(-{(zy=u?|H($zoZc!+|Hxpzy5P|B z?1qfKbj6%P53?ZCO|nt;oHrvenE**^&*#(lzL{@vEKc@VqodV4lUtX`wuJF-_S9X_ zc9RW8dsH6j+lglOm-tVADB7EAn~@V2Db#)$lDk zHNjI>PC&tNdl29R7pPwOEoo5WI6k3-XuwoFqM}!EmbsPHV^lylb30^Qh`~c7y!bm&n6<|C!2jcFF3&P4z9T!%QecP@%Su(8 zp43ZpHB|Q8Tx5)wTI{p;%AIEup9R@I(UIGkkReoZF*iO%|a%^1_7KnUFBO%WbO zV|Q@bgYoGhH8XaMhn zBE#<)Y!htYsBc{**#XL7$ERp#5jhy&n zf1%`VM<;@Fqes)+0uw4rK~aF_*v?c9|Mz@5LIgW3sKRv0jTs#3h8V}=i#pTcfr z2jcgTh+MKoa*O5+L&@sidmWRQnxw8s>Tk54l^pLI=IZs`D5B2mZ~{7xuzd zB5yYWjxpHOeDrNb^Hyx-4hS#C2oA_T0PIf>m_Cn<2)sdJ4cFo)N*j*8%#;kd6v)<9 z_5+00C#+M~$Q8~*H}H(UGN{YkqteW#n}01#q5VbJ#KO{?P>_Vz`Nz}&K?z5l_}u{( zboxof(sy<~5&C)paB0XL<1~EN@XE+A3h2Nmenp3&d0f2$f9X^My5i<;xHNyh*l-v`8y)mpLN#gNMP!Qo*Wt!__i&qbciIO!!u}-`tAa(XTE#qI# zpD7XwH)k79+RqEBB5VdMSm~ZaJI$i&QFbQ$pVI`b@Y21w=kcOTz-9BofmEk-C+W~P;*L>i zmhprZWA6BlYfUlIbR$DFdEb!<4$ThFRM{N@dAN{G zbn}gOjNRik86S&I)4&{eQ?v zu;Kql9KinxawOQ-zav?^xo6|cxDNvb@;}yYzZA7eX(AeJuC7P`^~Lha*AZ!%b08)> zsvkmpEvm#CaPCv$9-zC=kyf;I8rNQ^P;tjB4Xcq`krZkBxtuHzE9HV2)~@g)vjSBf zCDX?d+3k*HruoxOUd)AHpizpPPs?Jen)>2IC)HB>xc=8+$w84}z*rCji?kQao{Z^~OsD)!tj-4ZDa zXz)$wxB4)BB{IP{EurS}#CQFy*eKXnpy^<*Gm^SHB{ zhERyDD0UEF?U&&9)|+U-^A;GrR?nXFDfM8gshM;|{8Xbb8QSMPdEs7KOoaV`w#rh? z@Hj1YD6Fd^QdqN~Vm$riCVa~V2J(f0E{}n%%PAOWuJxwbyZTDRcev&L`G8NUu8pp( ztC?~Xsp*|gnQsu6q zR?Z$V41_5NCH^*X1K;U>1HqtY?f^Ypq z_U~?6M~$bE(EJv*Ywq)Gw*^iNWgDUlaCKE-tyU3zW3GOU0$|L&VPtd7d0<$(IQK?^ z{6M0#l7F|r#KDm}Sm+xseF$k-KpcvvHVQR&o>p;w_gU21_={`m@OER%7lbLxMf;Om zg)I$~6PKJNWzZ`aXzT0W7GjxXAPe|w*+hRYN%rs3dlVSx z_qtNAx74(r?ZH50Pp%$MoN=`7g%llHNI|n&_zkX~jvgy#5a=tK2~<*6cCI}5!nk}- zhBR5I|uNZ(!Z)5r7H_6D~Ii5`IrqHIu-G#BP(P7KcHn7%m= z5}y^Cv$9CQjx|YgL1x3(aA0mqz{r(R`sRbmqL}vZ%j_ES2QZKal^|}>yp1RCV5{*5 z;K(I|gw!9ejiMoC5=DZ8X~4>D4;Z^XCD8wBFdSK#Kn zP_C=Tc~Bl0sLi5GK04R?TM-O|a}yWEfveTOQD!s%@%{hdhgu0pAbTi1qNNBZz6Q8^JwFZPoj%^XEIYvm`YU0eR}<~6m>i1|;==)W(N&nsMAWZNDN@d(m=`_2Q)m$d zF@Go&6`gwwm{v`ESt&EDVP2wNFG6dYfb4;N zvnx*GP^!pc9lak0%J?Pk>jYN5u+m(F7xv~H)W!)u$CZ50#JRXb*9|A*kIMvvv=oiw zM3YkPWF-xFn?Vl7 z?GRtV)2K7}B!wIU_7W3YE+5l(Th&W{;S}GYp*Hp9Ao@G&;$CMfj?wwZmEi%h=PNr4 zGiI!=Amlhi?~2?gL5`O~s7z5d$NS!v=cZF{PM;o#_>_IIJT;d??=8oF0?cHJ^=M!} z>FBXEbz(hqJEcAsoR1EfpyD4n(Roo#wDm!szGeaoGweX@3XP4@eF@cG8>1=$9jZDe zf#S_G#+T-xU`NEP-X#$KeWtfojW5#9seL(8c^9oQctsP$`aB;w-r$Gv>JfUB*G6rY zu)8(44l}b|=}qxA-yFsI6wqBy6_BUw!56>r)%xQXT2%-3;(_XC^mLe>_^33vD!Pkn z=STZDoe!%IsH#1H+(~PNk?*PD>Mf@r{|9VnVcUJZZp<{o`e}72jeZLNmd;lBd9Ixr z>v@T`iac>VvYyj?u_=xBM?tf+`m;}tYS*aekDa~^2);3i-&SPPng{0eW09Tj%PGiR z%VjE}6}(QqK%DqOwQ)XvW$XQL-?!vSYO`{qp-Wpex1Crv3( z6=<3-ySG@z{Fy(WySGfF2NA>sNiNqtc^p^fj=o1~nmiGZP>BQLhI%yMH<7&kF z^os7mngW`so=u{dzYd~ekEA3-PExOMsrCBqNhw#6?lU43e)hf+8;AW9@edARB3LD5 zp&SnCYPIk~qVA%oUXCShW4}nY)W@KirvBCU4<^`nP-HEPuI+$xpR76Q9QSa|`mZN~ z28UBKGDOPehYWQy+?vm2@H*Cf7vGt=Di9WD;51=40)7@sdJWsn&g_5k7$t*`2K1|Bc=;~=B&4`xXzp* z`l*wsX{*4J=hH+%JYM$1V*+qeXx$A zLNT#dcFmdrAn*rgyk|@%lg{cr)0*bAP&A8M$l;NES4?kq-{MG+doI3JxNL2x-4}1a z%824vkEH5YLA`63<$atKNbd(ul7JinokU<%0u-zLZhm&isq48*>z9jJmI#XcqdDIU zy0h^cv1_`hS><{$J+k(@N)c=1(hoB;(vsGl^0MK_c$6hA{?OdfqG0%%|I#hKxC)9-FeOaH3 zH38lbJ+Q{+ysstEbcz;=>pxwAdSE6QB~%vKBzk3};|{-0Z7AO!4m1o4N3F7=K~USK zOWV-ON>vl1@AC=_R22Da&Fwp#9rRhWi zQDMqERcoRe%zHKQtZq(5pHaWafYKs^hgAKJ076tx_Z05HfXpEaC{-R63&uuHVHhY^%Yy_vxzFXhGXzEu@5 zNsU(x01rp2X9#OIE)%K*1qMETFiA%0b_G6yopRt0Ze7Wr!y|0`Z(pgpQEBEy8)v#t z^3Z=go9)_veD|dU{XN^(|}@Q@dTGJQi#D zBylZUm`CWoU5k5ALL;s^l=1o!Bf~^4{Y!Ios2(8W#cEa!iCzheA}&ESd@z9-0)&g^ zE3YZ5De+qW){4xq{(-nFU~>LSVjIn|kz|m!C)ZuDy;%n6U}8#Hnch)z>WV*8|JK}d zgDxr-qw5Z!wI=a0X5LgfoEdM_v`C+87I_$w#<~IhUotMyCzhz_Mx2~$-EZW#J!WLi z@dNG3ByZ+;PaWp>YwFG&vmf=76DdlZ)H~l)w}v-+8(g_Ps!3@L_=buX?7yGEj`{2& zYio%ARI-iKyzX3fVG=TQFB`J^cKW9N(jw(N{01E$6m2hYX9ALV9ejQcVsWFU^}RR4 znT_$@bvO(JcQP-OCD{L>weuo-22NEEaLSGQV#&G`JexKcIK8T#6VFfECoy&qZ!YQ) zGk3GGS;DfO!p)C094stsBmgagh(Df|SwVxB6CM1x8a%oHD??~U(p5in*Jc?83MDHC z!WWrBP3c$oS!0jP#FRXjA|_T^B-EX|B#bUDILDgMfv4}AAj`g`lkL{uVz_lexy!B_ zGL)_XXc3cpi^#dZkU6s-I~Oj=#EtmU4b}J+vDq3lcrHBp#A;UZfl>He^-QNc{7LMx z{^WPZ;_RUDMm+m?vDJIOJvSPDe2fKd9K~+L9?xextQy5j*nb>1zxh!e@nzYkkjwW4 z9n^ToX!u$L=goZVivX3%DA{G6@2?-6(r6!fcSN=87Y ztm_t?IBpTUepPq9RMt~bXfG5Y3j?K|SR7A;)zxWsn)hK@q1NW00~S>`%aZ2+x=o)i z`?>^mzyc7#kvY1(l+2g9_Kc{PE*Q7I*Z%!lvlsCmltWTzYe0gR6tX}ZsS_T7MRt8? z)^qLEs;^ycG!My^yx#`{%@zQmzLwWA=bCPO=^`UvS6xvD=F91z>Yfvu#M|d|@t?@T zdI_S-k1f9hKuu36Sr5$?DA#~%?I~@^qX{^D&F@=wGjP<@!Zz075g5*SVNol&MO?ah zO^!th6q6XK+$ccFd#nm9s>t9R7dYLolzci+K|5Xe9>HTl+ZayFduazhyGA)Dp5H;z z_`1VacbvwgpN`TZ?c+${#&gd+N*Lqu5i2oWKa-6`jC+}Dh~Qf!dtm5FXXbn<>>^Xr zLYQutlh5UVxB1g2X!SL*gI%&6eGNX=uw8=OAw3;uJ$Hipj7}J|yQz?6Y|JW;(t-6_ z#LE2Jl~wtLx1**Bu@b&D&y110U1`IGt7ym#_?l|8Dw&UQ`2AyvBitnVM1v^oo$}X1 z!w$xUBG{-P^-^E#YvBIKD|XY_)8WaDhJ*-L!4dJ3E~&|mFAHQw{7RUO_B8&J_d-z# zR}+Cs%x{;kYDV4Hv=Wbt!wL72In(eUt{Vh=U$W%-63w4taLgCgO~^0o`H=nyGcFDs-lI*k+v%4s4?j4P}}Fe0FeO<2Y5H2|-OY8rg1tK*Z|7ek2z zEO-)SLH9Yj5_@lG6~&#kZQktzZ`3=e@V|;N5q26dhMU_xujDD@^PlC9;MSbSCl;bW zUtj-=hld3F_UFx+&yndBZdJ0ByfdX?jxQg%u4Bxzi&;-8_keU0OGOssIE%Ay2LS@a z80;2=IClBb>|_Fe=)b*^8Dg!9rZ2(oCX;t%8;U4rNQzU;8MP~stSkY>fkKrF565fT zhEFN?7kH$$Xem2c9+09UKlj87&>?%CV^tII1qMol^W^$bB|DgrmL;bePaO|Xd-BEj zse?{zu-MkiUX4IMJfojw!V8*?N&>z+pX9e7`hHF)J@=WfKb8#DO3@DGSL9?!<4|3G z`my4@M*A^~%94@AMF|WfE_rCMex!Hf2`o2G5FB3eMz-pavar zQ|kAsZMmvU|Jo$H0H@2XK3ZNflr$$Ke54gJt9uD2W#@eq`42310`_rwU;v2tgcH-i z9fehxZj`n`Ia=h%3X4@vo4oyIW5dYPz0AMK|3qi&Xn2{@B6Zf|j+Szd{ZDu*o9qSb5LC^$x%2#)-Ier|zpWZ63ag z-jR9>z*U&d=X06X|4i!ZYVGwg4D{Kp)V9NhP)*Z@e%fuiz};KSYBW-4yNa11?X6|L zKTRPBX@|3o@I3U&^h=EIupwiVm>u3E^c#od(v;+)*u<`O%G}W?48$v*{typw#E(4-2T!pVhk11~wwhV26A(d;0r&&c`Bt--ziE)5^ z7p@nc{%GW=ydha_WdT`Q3gCD>=}niSd2kx{j8uNlgJub;uDzwQ-S<`By;VcebzW!L zJ{ErNOe}(PN35Xl0lpoUp#`lnM>`v2HCDZaxQRad7Y{nk!uA~|CFWZrUHlrU4>)fY zfD7xiH}J0=Vu+7X$jkC(_$Dn!8%5S-u1*GtDd+WD=1-k%&YT(y4co}d)XGMa%Rl0F zoEST_{v?aQ$Uwg~XJs^eTwrrb*MZKQ_4-c+vR+HVCjLCNg}Mo;n`AEIf!aKi&0kdT zhY%2T!V8|R>u_P#_buba_VT-ifsP!4jj-RtrE26xh`QE1v+d>#qdlVZjtcmTi;5kK z{1^#ywXPDj&rd!%Y$Q$?P1YoNm7Tz!NeWB87d$KXJC(x>uL~SD~QwcdPs*XKoo3xQ6pgpCYvkxInMUu69`@{wg@! zHveo@{WA>E-d|(;|7(yyi4+y-m7=_uB&tij!7h4m&DC}gFp>9ExGV*DjuN-lLpIX& z9NB6TI{6fsU@{m#U5oN2&vHNO6!aw;&j};f8wg4#eOPz+L#Um_D{Mi6%o}^(tz$=b zOJ{gD^2@}_3rr3s%uF|wUIckz!h^{uY28vAd2({6PtVS-j8>h2C-E)>(eK&E;|R6c z6mBlStrU*x8bs6HL6cQDr`X#2{Ng^LBLu3lpucC}?^2!OXxzz7ktn|ct4r~@@0`0O zAHrsrNDp7UVu>?@CeqvxmdS!VARVn3=Xo6f&bGJUVt8*Uzo;4Wk=cuPfUOm2ymwSA zDYex_hUxNA)LK=5N=vQ3=&MGxa%R;extH10vn46aNW9w((brHW=miY4oqxpve}p6z z;kM{C`Ewx%^fUim zK`?d#NdTpCJ+deVU0IR$zB*iWMTzj2S;wtI(QTcXpXO=6FN@W#$Cls4!<3vb(CE5x^YU_UZd$|NLN@OI^@4ox^$%KjH&EmgPj#rKh-< z3Cgp(%~OHAG8>2twdlc&M^Uveo)!s^DLrac5r40CNKSh{-7JjZ32^p$=srZ*$xMal2jSo!nG;3SLq;6=@k06%8eO}xxP z*22Q=rWaFqEuDkk2z3fr?BQCJ4UmeH!tHs_bL@-1P@%!?d2NglABjGC6!g=cSDS=| zN_9X>JtIVKOnhA*tn+=G(fdxptVtqzR{p>uKP68%IynaVEYKVNkTrlQkKp;XZYvwEm8XFDdrJl=&mmt9z_$o2R*wKo!y1Gyh-~X4HmgcU*Y?= z8Z4}A`1~c+8AAkF$3e18LR8q-ev%ZMqy^1{vc8gJ zTGpvcSoGH#XCa*HEckF0mJeU?UP=vh2Mg0}xtm58f+T@9O#wKQRld2W&X zYV(q$vBp+EZ=T_`%Y*OiPuy5|j){o{>Fb*VNx$X$ClBo6eFJ9A-kD5C`C%LHVj9Pw z3jPp~>TNT%zSUp}FG(U}gmrbG7mgb?e!vjPWetBs`zh-3MLpZg@hP98!gHHpYrLzY^N};o-?R^;A-YaW`Rsm`BxN;Nx{jIo5RtTXBX! zq(+%Ty8HSIZOI+miqI>X=hZQ4C+P$ZB-Lz0DQ^ib^e{VI^&XW-SQ@!hrWSoTMbVHp zt*L0SR&p|nYs(o|cj~z>QE+rmUDt)^eqM}JSZ1<(V$X{=rsLEoE+H~&3bgWVl47qH z#Q5j*z^9G+lruZzKi(FI56i!g2Yz}5LU+8Y6uJmkKH2#n73a+1tZ9gU;vhG1*sJ?2<$Bxj7cQ-y>r@q`i17tYyjxGB7u8|Z7q|lje-9iQ zWtQ{d=&8&-gdv$H7c;}H&B@i5E!r#{&K<~aGFyX20RGDp2xO8*53gX=CeH5-@WfrX zH~2=W;DiRP&lU{6KW&Od;i7yGBkLv0p1*#a{42wcG}xVn#_!Gt5iW?a(I0v=8~P4; zmg|pTexNNvpYIciZ3}sb(nn|jsjg+){CpnT{|!VOAb?WF4dkLMiS z7_@suE*PT*biH764LdB`x$`I5i1*wl_RF*I`!X|RKd1XI%ilp$@%P2uBMpgrP*8)( z_MUmuMxmXfRMA9^L=Ub(Jvz)cJGqN>yq$#A{H1FYNtO_a_dAJ}RSj7)eE<;DTdU18 zEUi^ok8R3mb5tL9$@FIBB;mMEMVi`?WH%sf#P?~5VA-sY8TrZZH@)?lL*^%rkCcox zAQ)jzEaTrIq?Ag&MH$xzJ~DXQ*Bv7eOm=S9Q_Ss(On>C(=YRda<=dnDa%58*J60A-qG&BSg7kM)5-j%i*Z~G*lMP)?2v(Hh zxb=_0cTrD5Fdl4Dr#N7DJzS;1p(j!-SmtLtu@p`z18E#jhkp)NSmIU##`2Jh`g2Ov zwvqY>Uk7B1VVW0oy^{l~W;e_hNpWz3A=R@fADBeBP23_9cbR#2#j&BwImB)Qt1r2; zBPP>Kew6Zmm^2QEJ?1P6W5Vs`Ttx7Vqr~Q$TKI^?Oeh>-h$O(*5ix1#8Bu*?UTku; zjJq-`qaF86X6I(&k*NhfQ0t{qH)UbN#IMc2yu&-pM0U9>8B7I`d*)FB7@qE526=~V zdd=?z>QcUY{(XJZg{~~_yNN_z+7rn~qdDVgr-QnR?kP2l7HM@ZuJ|fTqw!B_<39j` z_M_A-%JJR&Bjg(Ie2eFQBwVfyL#rDq?eknbDEkG|z6l0$j<`bIl^`r%_h* zVPpiCfzxDzoJ7TZN!p7Ch`S$ooBdvlmXp!1hO#kix~bI-evKJ=;H&Bj<{5Q6EB97o zC&FAkR(Dn$R*1B<9xf4j)G+e|-^Hx)MkhhWac(w6WtuW2@-el0H+5+{mAgeF=_lKP z%3dEv)Tb+H)L&0aC;y2E!U@_( zI@+FQH<@i6C`)rBnq-FIH{-49ocfmbKA7NAfK+V_?|jBymY>GLLO&e1CLxCyoY*)BX<@i!6sTBs=nRp*Ur(s z?)y4sMP}?$N^L5t940ZO*x#VaoUJXtxZmPAn+=bE!>8_A_Hb-a;B_%q@7o0X;nVN0 z9coeQ5S~5iz&3T#;-7B>!scI`9dv%1kjT%s#kWfIy)$Y1+>D*3DGBuVtV5;tFaPrjdR1i5N4WoL72c@x2f-dMP2{8_Q38%UZ@ECp_~?rPzP zDB%UG$WdrYu6N1etGo<^23S;Kax`$4up3OY#LS8@Gm=RAXZ1>l4@kvoj2q; zd{+HLN>uSq>*J2l8-oe^u!@{In)77U$VIW-pja6HjboY5#z)}y}v32hr z>mij^>N_bdSH8W=ddB5#M4hoi`iBiz4`&DTjx;0FLag1p*>2$0X(FGL4J0U`&C89q z4i|w3OX$wt@U4>m+g7<+nY;bostEN)`v5M28P*eOw!KIMhQKVT#6tC8CB;U)ntO?D zHMUHC-aeeThfhg(E+mG;*QN|hQX3x1o1~4gbS|*9DKLMi*DwzT+~%n7$PKE0vdT^9 zeaT8;xVGoO8c&rA2tjp_L4CP=onCv7l|szCRMiDX<(PX3r@oB^*eG|fizB==h-F4I zRZ}n6&z)--DGPtdbC5fkNz{i!qopy1|l_i7<`|BxZq0rsMLpXTq{v66D965MK;l(C8QXE#phv$;PdFH6i z>*ezz*{$!H$k1~Wylt2R^1?{?I2Wwi1dVqrv6(xshG4fY_|R(w%?&x>mJZKQYRj#W zsSto|&Gx8KF5G6i101aIgDr;(%YAb7N;=I+eZg}@t%!*B4wBm8?%2u*aDxWzk zz0?n(mFDEUgLMzFOt+q9TGWqE$iyQ@Oz`oB>+#g~K718O#p%X#Ot)!I_{tUeWIjvu z^pem(Pu=eXpQ0Uq>T&kSl{J132dBx<4DKiy+_Q4I*KX{Tfo<5s)qUeb2d`lXUkH6R zNfcETh&#W*Usc`FX&=zlNNU<05`U40zQvtFI;6>X);5y*w%bBUlAM*IC;%(#Of&cS zpg37aB2R61az{MFQ0{4W!gIUywo)t}va{-SG@)&pj~q#|ADr4+^+98$z>Thm6)R;nKR@kq+Gp^i8#fwHfT5A(w00jzPF6` z#!Kg}!Zy(}ZA_K;mZi+l?!6BgUSd)wS6jm@O&EtA+1P2Q$;V8Gt*3ic#M;o&6^{B< z(fqrVDak&cPC~7?5?16%6Uic{xuDD^+O;J-M{VL&Lnk~6f(*4ZFTt)u^%%q8LAFTE zeAsJg4V*cKz5LV!{XMBF21CZp0eAr0`#K_IjEW-YM%`15HtsmfvVFa`nr)F&nds^iH~ls(m<7!1E&35Eppr+-3P7I zjG@ySiPkGy==8?AA;pY44Mpd<#*OE=08T>AIDE2+L2s6 z^pY|sU=cys;=23!j@5Al=1@5+DEt1a;ebw=3;$7K&WGEhjYzT{E8FZ*2(#7cpC)}2 zTm`T@Ka!M&@$U_7lr`?*tQ1zesB4jh*hUgrmLqvk-&^4)Tn*F{sb!M(yg4G~>l;ZA zzb>I;K2{9|@Z4s{@6Bhr4?^!lnMhEnANP$CYu1n|h)i*%xj#4Jt7ufevv;WCTV>o5 zb%Esyp}wJ_REJbOTJrAHl%o_* z!FL!n1R3A=teV3wh>d}%`PQ!B30uvh7oBA7b>~f>fb zU|#TT%=p_HL<_X|eGHQLVKj6IS2GwFd>eB8SCiYFpG}Zg&0sR{ZQSx-)gkD&)g3>p zZ$p3os!qm$SJOhKRuFK(w_&q?RTpC3Lc_ynll=-d{j0hG>t}U52R8c89(c$<12+F^ z>Bqk901w;HgO@WMfV-F_6m{Xzc+ zs*~>)q3)-@5dL^Mx`vxYLgg}!Ji6QPi@N53@ zC-!eJGZ_d?0hkz~*$bca&1WzsF2|os!Atmx|8$zc2YJv3Cgl9%*yrB}siuD&&dn@M zoL)I|-u~lZ!uH;@xJkI%nV)#5XoLJBc!hfjEVQpVY0kNh>v|k zL-^a&5*~}#*4h23G6Ety=+=_`Umck75r200W&92i>jRT<{xM|#_mv4t5pM~Ozt2hV z=bQ*2&AtB_%XP&qYMkF=fqQR*j|DQ*4W__l|HtZvK7#o5g75vY5dF0Ve@sLFJ(90y zNRXaxFe3$}ik00GoJ0RJ|8ps5p|?22eqWyZZ%cvxKbOA;`eP^s0{!%z@+Yb0BSYrx zk*S#e0RKZK@COoJ3g|)pxsHEKvIKuk;g5*m-&06(0z)=Fff*_PaJBvoLgfyIu)$Xg za5#S1OE&L*B9d652mB|H~H}gq8EUi_rW3loPa?f+&|6m zlCvMAwjWF@`{#u7i`x7H^gR#^dhTF$OUrPYIXIdBq&=T1gHe8h2*Q7`;23k?z~9QC F{|ks9u@L|O delta 46503 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 Date: Wed, 25 Aug 2021 20:12:56 -0700 Subject: [PATCH 31/69] cht: add _BaseAxis.reverse_order getter --- features/cht-axis-props.feature | 1 - pptx/chart/axis.py | 16 ++++++++++++++++ pptx/oxml/__init__.py | 2 ++ pptx/oxml/chart/axis.py | 26 +++++++++++++++++++++++++- pptx/oxml/simpletypes.py | 23 +++++++++++++++++------ tests/chart/test_axis.py | 16 ++++++++++++++++ 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/features/cht-axis-props.feature b/features/cht-axis-props.feature index c7065b3a9..1e39febee 100644 --- a/features/cht-axis-props.feature +++ b/features/cht-axis-props.feature @@ -164,7 +164,6 @@ Feature: Axis properties Then axis.major_gridlines is a MajorGridlines object - @wip Scenario Outline: Get Axis.reverse_order Given an axis having reverse-order turned Then axis.reverse_order is diff --git a/pptx/chart/axis.py b/pptx/chart/axis.py index 534e2e305..ebf63a6ed 100644 --- a/pptx/chart/axis.py +++ b/pptx/chart/axis.py @@ -10,6 +10,7 @@ XL_TICK_MARK, ) from pptx.oxml.ns import qn +from pptx.oxml.simpletypes import ST_Orientation from pptx.shared import ElementProxy from pptx.text.text import Font, TextFrame from pptx.util import lazyproperty @@ -175,6 +176,21 @@ def minor_tick_mark(self, value): return self._element._add_minorTickMark(val=value) + @property + def reverse_order(self): + """Read/write bool value specifying whether to reverse plotting order for axis. + + For a category axis, this reverses the order in which the categories are + displayed. This may be desired, for example, on a (horizontal) bar-chart where + by default the first category appears at the bottom. Since we read from + top-to-bottom, many viewers may find it most natural for the first category to + appear on top. + + For a value axis, it reverses the direction of increasing value from + bottom-to-top to top-to-bottom. + """ + return self._element.orientation == ST_Orientation.MAX_MIN + @lazyproperty def tick_labels(self): """ diff --git a/pptx/oxml/__init__.py b/pptx/oxml/__init__.py index 3eebf4f10..099960d72 100644 --- a/pptx/oxml/__init__.py +++ b/pptx/oxml/__init__.py @@ -65,6 +65,7 @@ def register_element_cls(nsptagname, cls): CT_Crosses, CT_DateAx, CT_LblOffset, + CT_Orientation, CT_Scaling, CT_TickLblPos, CT_TickMark, @@ -80,6 +81,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:majorUnit", CT_AxisUnit) register_element_cls("c:minorTickMark", CT_TickMark) register_element_cls("c:minorUnit", CT_AxisUnit) +register_element_cls("c:orientation", CT_Orientation) register_element_cls("c:scaling", CT_Scaling) register_element_cls("c:tickLblPos", CT_TickLblPos) register_element_cls("c:valAx", CT_ValAx) diff --git a/pptx/oxml/chart/axis.py b/pptx/oxml/chart/axis.py index 5d9586f7c..5df9bba2e 100644 --- a/pptx/oxml/chart/axis.py +++ b/pptx/oxml/chart/axis.py @@ -6,7 +6,7 @@ from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK from pptx.oxml.chart.shared import CT_Title -from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset +from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset, ST_Orientation from pptx.oxml.text import CT_TextBody from pptx.oxml.xmlchemy import ( BaseOxmlElement, @@ -30,6 +30,18 @@ def defRPr(self): defRPr = txPr.defRPr return defRPr + @property + def orientation(self): + """Value of `val` attribute of `c:scaling/c:orientation` grandchild element. + + Defaults to `ST_Orientation.MIN_MAX` if attribute or any ancestors are not + present. + """ + orientation = self.scaling.orientation + if orientation is None: + return ST_Orientation.MIN_MAX + return orientation.val + def _new_title(self): return CT_Title.new_title() @@ -155,6 +167,17 @@ class CT_LblOffset(BaseOxmlElement): val = OptionalAttribute("val", ST_LblOffset, default=100) +class CT_Orientation(BaseOxmlElement): + """`c:xAx/c:scaling/c:orientation` element, defining category order. + + Used to reverse the order categories appear in on a bar chart so they start at the + top rather than the bottom. Because we read top-to-bottom, the default way looks odd + to many and perhaps most folks. Also applicable to value and date axes. + """ + + val = OptionalAttribute("val", ST_Orientation, default=ST_Orientation.MIN_MAX) + + class CT_Scaling(BaseOxmlElement): """`c:scaling` element. @@ -162,6 +185,7 @@ class CT_Scaling(BaseOxmlElement): """ _tag_seq = ("c:logBase", "c:orientation", "c:max", "c:min", "c:extLst") + orientation = ZeroOrOne("c:orientation", successors=_tag_seq[2:]) max = ZeroOrOne("c:max", successors=_tag_seq[3:]) min = ZeroOrOne("c:min", successors=_tag_seq[4:]) del _tag_seq diff --git a/pptx/oxml/simpletypes.py b/pptx/oxml/simpletypes.py index d77d9a494..7c3ed34e5 100644 --- a/pptx/oxml/simpletypes.py +++ b/pptx/oxml/simpletypes.py @@ -1,12 +1,14 @@ # encoding: utf-8 -""" -Simple type classes, providing validation and format translation for values -stored in XML element attributes. Naming generally corresponds to the simple -type in the associated XML schema. -""" +"""Simple-type classes. -from __future__ import absolute_import, print_function +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. +""" import numbers @@ -480,6 +482,15 @@ def validate(cls, value): cls.validate_int_in_range(value, 2, 72) +class ST_Orientation(XsdStringEnumeration): + """Valid values for `val` attribute on c:orientation (CT_Orientation).""" + + MAX_MIN = "maxMin" + MIN_MAX = "minMax" + + _members = (MAX_MIN, MIN_MAX) + + class ST_Overlap(BaseIntType): """ String value is an integer in range -100..100, representing a percent, diff --git a/tests/chart/test_axis.py b/tests/chart/test_axis.py index 83df8c592..e144d27a9 100644 --- a/tests/chart/test_axis.py +++ b/tests/chart/test_axis.py @@ -112,6 +112,10 @@ def it_can_change_its_minor_tick_mark(self, minor_tick_set_fixture): axis.minor_tick_mark = new_value assert axis._element.xml == expected_xml + def it_knows_whether_it_renders_in_reverse_order(self, reverse_order_get_fixture): + xAx, expected_value = reverse_order_get_fixture + assert _BaseAxis(xAx).reverse_order == expected_value + def it_knows_its_tick_label_position(self, tick_lbl_pos_get_fixture): axis, expected_value = tick_lbl_pos_get_fixture assert axis.tick_label_position == expected_value @@ -475,6 +479,18 @@ def minor_tick_set_fixture(self, request): expected_xml = xml(expected_xAx_cxml) return axis, new_value, expected_xml + @pytest.fixture( + params=[ + ("c:catAx/c:scaling", False), + ("c:valAx/c:scaling/c:orientation", False), + ("c:catAx/c:scaling/c:orientation{val=minMax}", False), + ("c:valAx/c:scaling/c:orientation{val=maxMin}", True), + ] + ) + def reverse_order_get_fixture(self, request): + xAx_cxml, expected_value = request.param + return element(xAx_cxml), expected_value + @pytest.fixture(params=["c:catAx", "c:dateAx", "c:valAx"]) def tick_labels_fixture(self, request, TickLabels_, tick_labels_): xAx_cxml = request.param From 419c9e81a5c128373f0f564f18e8fc6b320fe47b Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 25 Aug 2021 20:22:52 -0700 Subject: [PATCH 32/69] cht: add _BaseAxis.reverse_order setter --- features/cht-axis-props.feature | 1 - pptx/chart/axis.py | 6 +++++ pptx/oxml/chart/axis.py | 7 +++++ tests/chart/test_axis.py | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/features/cht-axis-props.feature b/features/cht-axis-props.feature index 1e39febee..e8ef5e96d 100644 --- a/features/cht-axis-props.feature +++ b/features/cht-axis-props.feature @@ -174,7 +174,6 @@ Feature: Axis properties | off | False | - @wip Scenario Outline: Set Axis.reverse_order Given an axis having reverse-order turned When I assign to axis.reverse_order diff --git a/pptx/chart/axis.py b/pptx/chart/axis.py index ebf63a6ed..66f325185 100644 --- a/pptx/chart/axis.py +++ b/pptx/chart/axis.py @@ -191,6 +191,12 @@ def reverse_order(self): """ return self._element.orientation == ST_Orientation.MAX_MIN + @reverse_order.setter + def reverse_order(self, value): + self._element.orientation = ( + ST_Orientation.MAX_MIN if bool(value) is True else ST_Orientation.MIN_MAX + ) + @lazyproperty def tick_labels(self): """ diff --git a/pptx/oxml/chart/axis.py b/pptx/oxml/chart/axis.py index 5df9bba2e..c59d2440a 100644 --- a/pptx/oxml/chart/axis.py +++ b/pptx/oxml/chart/axis.py @@ -42,6 +42,13 @@ def orientation(self): return ST_Orientation.MIN_MAX return orientation.val + @orientation.setter + def orientation(self, value): + """`value` is a member of `ST_Orientation`.""" + self.scaling._remove_orientation() + if value == ST_Orientation.MAX_MIN: + self.scaling.get_or_add_orientation().val = value + def _new_title(self): return CT_Title.new_title() diff --git a/tests/chart/test_axis.py b/tests/chart/test_axis.py index e144d27a9..aa0ce302f 100644 --- a/tests/chart/test_axis.py +++ b/tests/chart/test_axis.py @@ -116,6 +116,16 @@ def it_knows_whether_it_renders_in_reverse_order(self, reverse_order_get_fixture xAx, expected_value = reverse_order_get_fixture assert _BaseAxis(xAx).reverse_order == expected_value + def it_can_change_whether_it_renders_in_reverse_order( + self, reverse_order_set_fixture + ): + xAx, new_value, expected_xml = reverse_order_set_fixture + axis = _BaseAxis(xAx) + + axis.reverse_order = new_value + + assert axis._element.xml == expected_xml + def it_knows_its_tick_label_position(self, tick_lbl_pos_get_fixture): axis, expected_value = tick_lbl_pos_get_fixture assert axis.tick_label_position == expected_value @@ -491,6 +501,43 @@ def reverse_order_get_fixture(self, request): xAx_cxml, expected_value = request.param return element(xAx_cxml), expected_value + @pytest.fixture( + params=[ + ("c:catAx/c:scaling", False, "c:catAx/c:scaling"), + ("c:catAx/c:scaling", True, "c:catAx/c:scaling/c:orientation{val=maxMin}"), + ("c:valAx/c:scaling/c:orientation", False, "c:valAx/c:scaling"), + ( + "c:valAx/c:scaling/c:orientation", + True, + "c:valAx/c:scaling/c:orientation{val=maxMin}", + ), + ( + "c:dateAx/c:scaling/c:orientation{val=minMax}", + False, + "c:dateAx/c:scaling", + ), + ( + "c:dateAx/c:scaling/c:orientation{val=minMax}", + True, + "c:dateAx/c:scaling/c:orientation{val=maxMin}", + ), + ( + "c:catAx/c:scaling/c:orientation{val=maxMin}", + False, + "c:catAx/c:scaling", + ), + ( + "c:catAx/c:scaling/c:orientation{val=maxMin}", + True, + "c:catAx/c:scaling/c:orientation{val=maxMin}", + ), + ] + ) + def reverse_order_set_fixture(self, request): + xAx_cxml, new_value, expected_xAx_cxml = request.param + xAx, expected_xml = element(xAx_cxml), xml(expected_xAx_cxml) + return xAx, new_value, expected_xml + @pytest.fixture(params=["c:catAx", "c:dateAx", "c:valAx"]) def tick_labels_fixture(self, request, TickLabels_, tick_labels_): xAx_cxml = request.param From f17fb9fbe89d2957e4a31a5dfd4878d683591086 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 13 Sep 2021 14:04:38 -0700 Subject: [PATCH 33/69] fix: improve handling of media click-action An imported audio (or video I suppose) media item produces a `` element showing the "speaker" audio image. This shape has a click-action which plays (or stops) the audio when clicked on in presentation-mode. This `ppaction://media` click-action is not a "registered" click-action and it has no counterpart in the `MsoPpAction` enumeration (PP_ACTION in `python-pptx`). Previously this would cause `ActionSetting.action` to raise `KeyError` when accessed for this type of shape. Change the behavior so `PP_ACTION.NONE` is returned in this case instead. --- features/act-action.feature | 1 + features/act-hyperlink.feature | 21 ++++++++++----------- features/steps/action.py | 19 +++++++++---------- features/steps/test_files/act-props.pptm | Bin 0 -> 982098 bytes features/steps/test_files/act-props.pptx | Bin 36160 -> 0 bytes pptx/action.py | 19 +++++++++++-------- tests/test_action.py | 1 + 7 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 features/steps/test_files/act-props.pptm delete mode 100644 features/steps/test_files/act-props.pptx diff --git a/features/act-action.feature b/features/act-action.feature index e206344a8..a05ce19ae 100644 --- a/features/act-action.feature +++ b/features/act-action.feature @@ -25,6 +25,7 @@ Feature: Get and set click action properties | OLE action | OLE_VERB | | run macro | RUN_MACRO | | run program | RUN_PROGRAM | + | play media | NONE | Scenario Outline: Get ActionSetting.hyperlink diff --git a/features/act-hyperlink.feature b/features/act-hyperlink.feature index 666c158c6..9ffea20ae 100644 --- a/features/act-hyperlink.feature +++ b/features/act-hyperlink.feature @@ -9,16 +9,16 @@ Feature: Get or set an external hyperlink on a shape or text run Then click_action.hyperlink.address is Examples: Click actions - | action | value | - | none | None | - | first slide | None | - | last slide viewed | None | - | named slide | slide3.xml | - | hyperlink | http://yahoo.com | - | custom slide show | None | - | OLE action | None | - | run macro | None | - | run program | /Applications/Calculator.app | + | action | value | + | none | None | + | first slide | None | + | last slide viewed | None | + | named slide | slide3.xml | + | hyperlink | http://yahoo.com | + | custom slide show | None | + | OLE action | None | + | run macro | None | + | run program | file:////Applications/Calculator.app | Scenario Outline: Add hyperlink @@ -51,4 +51,3 @@ Feature: Get or set an external hyperlink on a shape or text run | OLE action | | run macro | | run program | - diff --git a/features/steps/action.py b/features/steps/action.py index 8051da750..c3f5de0e2 100644 --- a/features/steps/action.py +++ b/features/steps/action.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Gherkin step implementations for click action-related features. -""" - -from __future__ import absolute_import, print_function +"""Gherkin step implementations for click action-related features.""" from behave import given, then, when @@ -12,7 +8,7 @@ from pptx.action import Hyperlink from pptx.enum.action import PP_ACTION -from helpers import test_pptx +from helpers import test_file # given =================================================== @@ -21,7 +17,7 @@ @given("an ActionSetting object having action {action} as click_action") def given_an_ActionSetting_object_as_click_action(context, action): shape_idx = {"NONE": 0, "NAMED_SLIDE": 6}[action] - slides = Presentation(test_pptx("act-props")).slides + slides = Presentation(test_file("act-props.pptm")).slides context.slides = slides context.click_action = slides[2].shapes[shape_idx].click_action @@ -49,8 +45,9 @@ def given_a_shape_having_click_action_action(context, action): "OLE action", "run macro", "run program", + "play media", ).index(action) - slides = Presentation(test_pptx("act-props")).slides + slides = Presentation(test_file("act-props.pptm")).slides context.slides = slides context.click_action = slides[2].shapes[shape_idx].click_action @@ -90,8 +87,10 @@ def then_click_action_hyperlink_is_a_Hyperlink_object(context): def then_click_action_hyperlink_address_is_value(context, value): expected_value = None if value == "None" else value hyperlink = context.click_action.hyperlink - print("expected value %s != %s" % (expected_value, hyperlink.address)) - assert hyperlink.address == expected_value + assert hyperlink.address == expected_value, "expected %s, got %s" % ( + expected_value, + hyperlink.address, + ) @then("click_action.target_slide is {value}") diff --git a/features/steps/test_files/act-props.pptm b/features/steps/test_files/act-props.pptm new file mode 100644 index 0000000000000000000000000000000000000000..0a499f56de71edc891e6d0c701ace7d73aba5a87 GIT binary patch literal 982098 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�`9<6_JobeD^3VRqi)36CEG^>4? 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

Nz_ePKwAW6G1gP4a$HqfK~$5y?ZNUQjw>I< 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

T2#EPc<*!_SPEZ0*QA~XQFYoj`?k3bz&hkI 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 z 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 z%0A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Ф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 z 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�SrZT(D8>%k4%n3mE)DbdN$j+rrCUFNSdtzAM!vK0-THFL>Jo(Yhsn}*lkUqRYuhu3r-_NHuucX{10HOa`zW%)y1VJ16pUnIJ?Y$mhfHdSAwtex~G=?@aoRwrm zesVYmlhDa1-TB$RxevuoH@yGBy z*n>jGA;s3+bW%$HezMloZZafOo7mcu>L8gEr!K>}y^IszqrRSH$>M{Dxk(Ot26(^b zkzO*?A&<1?=Uy)!}ki7KV3^vHc<-*1nmK&`v)3!uoZkjZ8 zunNCWYr-ExrN)hn99!e^xhX&Ks~2UaWS%QnQwk=>>cgl6wNs+yk+j)&Y~w2bw*Rz`b}fFS%2Dd6ZTMx&h+|;97J5uGf8eaO`2s+QQNnQ9N>U6irn1qW zX1|eBn&2$?t@O+W8h2Rv8fC<%Fl~}4PtUKhkmBmFS~gcy9n#5?lY{Z9O9-2G3T}Zu zZ$2Z%|2I_fVY%tcPig;|!;n5r(@l&Hwlrh{v@-HGG3=z;p_OnMOUN0b#7r#seK=6m z&DEz?WMTcY{F)Mgfpj>(7Ub;t$*prt5l2XXO=&21_E9~ZH?+7Jg+Hhz1~;!(E>1i{ zptZ$`qNwd-80Vs)2{$ynLI<%T*F=s3>BDu*U~o=R)Nx|e7U^FUiH!b~2fa77BMUL; z-EHv(H!e9?p@>d)_U;Le6dcUjX}TXJQ>I>N-_aO|Vl2Gy$;kt}UqFRV*G`;KGN8z& zVALzpZdL}-zPe~NGwFXP&b@s=Vj1Eb2Hq!Go&2>G6?Q##!c;kez8nLJ$M5OX#jaAN z{s&`>uv`31{l;ZzA}AkO6ADYx|Fr|vnh$~_eWg24$sGb6-#_&{S?QX@V9#beK$pw6 z%wxjYLw2(=(WeG8VjLG!jY=c1W7ZeDeOd!27@YC@MCk23EBl`|!yVFd)9fARluQ{R z#JIHALt!limcI_fQYU_nJW5Pc24^0rWF>BNG8)ngUEvN_pc(AQzh{3l60VK;Ksn=k zU!Hyn$is}gBtwSTZDkR)2=K83Lk)Jp9`M!f}q%hLlp279cKj()$`HS;y9 z1&C%5NxN6=?xpR040)gCj-`g9@xKlizWo05woDUnL`I{1e|ZRQun0FEuY)`7ZDFmbQX( z)8J2iLSGLi@d7Q4AH}przm~*Y^AfH5&DYlr-DUe(e;su7AJ4(g^za7d)2e)e#5x5T zMYW(Sb%yZs$APdj+Kq@538N{g70%4!C;3M91dXGfYco`Y(Z|F*9bR{QOFQ>yRf|M2 zUt_K_HI5ILx_&RYWdAj?7Crk}UpQUo55lIz4}D|;R3W7}k@I93DH+NFD)xLLGYL#I zDm&1~3MJwQ5V>Q8vH58eo8?wtI8sf%z){$!nFD!4R(4b!-WakIwRb}86MHh)cq%+?pHliEm8U{~Xv=Vm}FW-T9x zNvPO(t+;j3my%5=*%}I=U;onhF`Lav+F8fBKbBXy5oaHe$V;S&!oU2wZ9Syob-k;5 ze((Cb@oAgs>ESZ;E;{oJ0~vJZy1J+|B^N3)CO92PL6>ffBnD)lV#MH7-=d)SO=x1H zB4ai&=ry0Utz~gX8=amzI0Dd+wc)cx= zI}-w?n}p;ieBuEiwcOiLs?4(7?^c4o<0ywRvj|`~dy`rztnLXrdoET40LJ@i9plAF z`MIQOdE+Jo)`vb$2TVq+nX*;a!5>07*CHk?*zNFAne+$!`YJoP$fWZheM-w=xPbkO zbhPVg&$tB|zXqZ87CgmU15dQhfw63By8TG!#VR75ZT|cOE;!1lzM$3?@J^-bV>+ty z6b`K2d7JRk-upc*p08Ibo3Zf(3m%WoW?mL$@OG~E^WY3`8v#KU{E6Crh1G;#ow<|U zq7uz2+b5n+Pml9G4Xup@+BaERPhTR-L;ugDf11U-#g8ZVKh6ru4r37exR5>2K;53& zh4cGG__-Eu(}EbFS*^E3qGdHnFc$tA{U(U!ty$UCoJreXorOe0G`-Rr9z&Yf{16B0 z8KtlL8OrJ)3N_m^Vsr|*FC;EPC(`CC&`1&u);A^0;Uuq5*rdejjXh{q!n({E^;p6m zlWdYUD@2`^yyD9sUTwK^4Lh;$#Ree(hX(^3${jSbGiOjNa zvuWpn_Sv6uFJ49M({4=5et39#`dJ@(^YgEb_Msd`xn9xT+rvm^m2!F2Hg*n06CPD5 zDn2~E;Zwm6i}4y3%ysPd3Oew)UHD8Xhx7rzZ+?Y-;H8W59{f+2avg z{9YX^Iq-e0(z|f$DeC9?HDK*9xx5wmT)y(L(LY9~o6kw3%yy{V*eg7YH$I4dy@U}$ zG;%f_co&}Qsk`J;(BW_;kd9gT-!ZuH&eb3kP6jPL{n8OUHCC8&K4G3-YZAa&!0Oeq zg2#oUO1ZhoiJzhcZzBwCGWY%J#8j;-HPUk)K8{!{UN6}swjxRl9NNG1Z zft5TPLl>8XH%|zG&wqoCesBNe;a`3XQdbr^c&D-vpB~Aqz#<1_z!*E;$;3NVoy}74 zD-Hyuq79;9jMA%HGYf?QwgRMn)EtW$)=8-(1=Bm)YR zzVsfSQokqSUZ_e6Ypdb(mcdgHN#Z+k)pFTyYRCe;b>swWM_yfc=ooSkX7Y!k0_&OWzGkUsqM7ph6_MR9qP zfzJ4+M&`fvBC8A*!8Ai|s|o*}9SUV^|Lp_yvCakQ1J!{-+t83_qBfZ*mz|g`D1>1K zitaEpEQEk+Oo&6r<(=$si<4%OeQNaOuS?A-8hfsn?XDDgdC|#AjiYj3Eyd6U0TVEJ zc<^u|m0~g+Zh7PF8r(C{v71@np}`xJIbAN!1VTLhgHzE8&l4^|EmPpD7DZ zf7RKM9Hr3(=kE-|1LOFF}-P>eyE*b%$9woqb`DcY>A>b zFtGJ3sToEyMezD=7ur4HKcUD!HT^Bq$(zaMdpSb{xpf zHY$34!mb8WRgX4pil(4T-Jy^pOWmTS_cDLPu0t-v)F#DhNH<~EK(FIWWax>T9F zGTIo}h_LL{xY2E23d^3)0s@;we0}l{zshRz>)I()O+c1q+ea;_SqMj=USObAJe9fl zi1YNIO|!~~*n2HK%?O*MYuOzca&3&~pF**1Qa^^omrKcB`LfeVCG|70S?qW+GQ^E_MSNt(PX7OIQu+52L z1t>3Xhz8OnbzM6v@-=*EUPXCkIu}2N4*jVN;||gWC|cMHn5GqGkCyAL703YHia9!< z9Cc?FHHxGqG#XO-^4dFzXx`496JU>-TE!;gmfR(}gm-o2A1+T}#T#N-ZBge|lD$b* zaQzYQl4fEn+E+oPD$JlD1z}2_t^l+E`p(j+$@&sq)wSN6Q_@*c@3CklJe#6MeSGUFi{|eu6DQ>j(HL za&~AZrY-ER%^Wv6tLv^lqE=}UK~t$72hoSmYrwiHhhYWURo62tb6T(v0XVyS2&VSw zggqE?ToYMlO;CjO*K(`Id`X0YSzje0S<|VkvX3?tD_Gi6P5~9g2m|AhYf0B;FvpkW zSF}JF!@lf2Ehakhs&Pq&dW9&To}|YJa_*Rj^1kvJ-|FEP5yt?VZ2@D#<(+W!EvWhX zm1h9N>% zBWpo3O|V_I47_QJVa_Ebw4q{P*Ntfi$$;xYru0kMlp?0Xe`mFW8eB55Cy0}_izNps z?+a8)?|e5!hQUho7_Xo!*3lDnYq@>w)ekQWFP!$F-f)?=mNwcCndE&h*ck zRc%OAY`zS5Cf;s$s@El)6=+z;#_+sX`V?!jh^}!tkn&(@oHb(etHpTogfd&bt4q3l z>Ze!5q&U1nOsIOg`k<6~PsIiL#qTQ+v{+eu>9J=8mKIvP+$c`d(B#@k?jo0FAU>l! z4#V8ARged1l77nX1UG{08`ta1gimCo0RgNs*ooON9$nb#%WqKfmcavJ`e8K1MwqAL z=||v?)4$>;Ts%D*(SoYsJYm#5ex(k2b({FZ&W`w)QGEKv%~{6=GenQ*ET-7?yD@=m@#~F zkqTTS@pqCAL*k%qoC|*(Q`~*Iz^Pt~QJtM5!$W1$MV~t5kn)59@7+u)dG+l+rBI=~ zbWOv+X|vtf!o#b$KtNqQ*iex&?DJR3-K=JCk~=mu_1l0*X&9*J#B z{PAQ%zSkdKWZ=i%vl^+a9Hq?wW(v%3k?4TR#Z)Kj^!D9KcVjwU2Uo)Qvo=YWJ%xQp zhXK3&vofoF>ANL$zy%e6Bq2j+w=}aWu2*pU_5~%_saLb{DJlxYgQ}~#XpN0RlAB$Z}>JZE*;XtO_su;JZ zPkNWZEUQe==3tt2?i?WlD(G-Nvxpp1i^LxiCSHud;eX4)C`jQm&^IsCuu5~3$0OF; zf%O;z7|MiBz=rZnbH1CkdS?e)w8R?xP%AIg$o%$KiXF|B|Ba7z%6Ygg2LPQ&rG_*U z?G@!SCJ4ri5kAH5+@$d{ZAIVAVrRS3+~FyMt=LtvE~=RC-wZ`3q4A%#b1#33uOw_y ze)Y6`9z-0%<4P@ zfr73_Tq;Y~ojmig@j>Xx@*IRbl8?@wwQqdLn2p9IP8T&_`_=CR8Zxv)HElYx#d4+if!}|P1V?P z3$sM*6X7Bhg#v44Gv|aWS=bZxG{`;lAH->bj&06K#*-IoHGh=)c7f}RCr0qkg$cU# zQnqBY4j%>)iSx}bePq`MFkqzd61m>0;hY?UZ@4C2Axg?doZqqS=bMWYz&MNjbX3WW zo8-K{cq6n0708tJfQ2l{N@=e)=b{B#x*H6!`x%Tx^Mrn2q+Z)xV1MROGu~k`j(9r+ zRN8Bg*9SEj*~IHl+v3>1+nus$yLe_HK*N=k+Xe&D2PRJ@mWAEz<_4s()-WDBzEoN; zYX8Y#o9IJpe1z7pTi(PjCWrTM*>~&Gj}_nzy^GRO>eTYf(|e{>f4W-n{>`_Px^bp6 z(I5RkKfY)C@v}ER-&o2iwT56ej9iVnEzw4;NNecTaN#T7U~So!TooG3mJM?>>NoVP zi+|e)pPp1&)YT=nkR>606{ENWm#0K=u(hOgIB=ABt~E2bqOEeLq81B{i-%bHM=4SO#zSU?hVpp^rjet+tHf{{xBIwkg zJ2tguSU9EPbhrTLa3Lxq-tkI6Tj=Y~+XS4n^)R6T&n}(vn4fF&|6q)f2Kuf@JYfQq zn~%g33hhn)jWNdmk3IpV6wsj_yz#K29qHp&%yWB!`;&6!F|$=$9feq+><;>xCyHcV$ zC{ESdQF@H!#+S@{N(O0xWOFfrIh9S3{C3RT%2u`UvCAD|)VdLu#2UiLUZyBnX_eLL zUv`~HQwPjuF+IWxI_966Er6r~i2^+P@6(zTd+VZJ7IV>8p)YJh_IA6sY7*2-xs0u3 z+swPeX{E(ydCz#}kTb)K3s`%8WU~}GA?aSvg6}t)HQqS>swojwB^*8E?&qD^S6& zzv8NG<}MsxcFqj_eb1Bp(dQ@wN&=4JYco}7-WWUXDF9jgK(*xc)ZQ7$#lTWzwB`M| z^j=767NeAOZZn2&O1>=D@SgT{d8r(4+@R`+iP`cAj}kkF&$7mCyKB;gE?f754rcZ8 zAV*wWgvrug3)2|*_ENU->@@3GYS{3cenn4Xva$^K;@&J#SHp~KcGk5c$UHx`sIFI3 zBcJP3It-AO=Pgd>UWYcK=qCV%#)8x0xN3%#%e5$eT$lWb7B+QpE3BiQFCm?0M1>r@ zPt@tuHXGPOsQzJkR++2ah9#=%yy$Q~!cCF$phNz?%tLqde5E9ocRPgjIJbABHcOy1 z4fg|HJ!?vBd&#rd@x6j=9o+BsuU|x>y-YEmQg`cEuN4_Pl_DqRUj9t|+xuBeOU{P2 z$hjIOd3EO18Ij+<_P&&RNp79glK<%wL=6r8#aY^cPxK-67{5>;X9q{*?11bO zl8}8unwbakuNS#OXVptv%4Tk{yERvs6l5vglON17*w-EZL4Y#kf>)VM%`G^IJh+rLYNHmD;CGuI0pW1hj96uO9#A^sgu^?OfR~59FB>@lp2D z=O~kFQ-(WXkT@UFY{LUA;4`X(A1C)M%+Q*(wbMPD(%`(~9?UeevFs|sJ|}w4mz!Ij z3T}yQ`>k_&VScJs@;AEO^5bedJm$Zb-T&)V_dwEp{|-Cvu5k4HJ7Hm1J-`OR;@g|= zQ(+Cw77?T$9{XcUhtf%H-}@RLot}~NRWli&>x~m*Y9Locd3!7gGnhR-JPktX^p%*I z8sfa^-mdH5i6T}v{l{VY;k|Uc+RZRiKml_&E0-|(p!{RL|FH$vHRh%@5t;!z`UU}5 z>>Bdb|5z^(nmzO|V?(<9?6>4hnN7sb5187_;o>zF%qrix>Mr}m^eA3wEXyxim8T|& zjUI-A6thvI=M!xPn?QDa%aK}GQ-{l9_W@YOpkzz^x zODcbmnN-hooY4omsL|l96ae^G$xq6B(^mlII!o}5>gQ_u7|vV&urnqVomDxc%+J|- z2K|85GZ`Cgn3E-Jl3L6uCtAb0V&p0zf9*8F^AT@})&BFux?wdR;^Pl%@ft-{6}-kOYo$aE8;MBArE8o`9;DRfS(7~Y$H z*QJ2q;1Wlc-wf$uS1f^ESy0czq*5K9^~i$4g^$>;6Ftv6Q}r7^Zi|e;z$WePt|c}R z=voy`XOgbivP#BPtl%XkdvDn7NWC;}9lk=fD5j(i^bah#TS)0e@R=wq%w{GxqYX1M zS%uu7vx3|?qz|rNR1;GSi4uq#$A^3rAa5{Yo!;vF4OL~hhoF2>Qq_aZek>X9`epVvs2rw{4G0*Bwl_O$~t@;y4(+z}Lj ziqdp!2{V_vhP@98&cYt}vN}9%4(T@e`ZY7CL>4Hf+BA_@yPSaIo9FLZZv&HNV`({> zWjRJ#u&)G6<&>yUuE_TvaWN4o^X-q_jZwbOkmpU+U)tq9-H95ColVdL^Ux;F(rw+D zCFP=jrlFTM&E;5%HT?0iQR__bH3WF;y}NEygS#1HRptx9ESL0Tqf1$<%1`=n3yimn z)A+jIY5d3Z;Z=F+xrm__}^(c&&ImxYFQ#aw0wVg3i>tIF_2HVecv5n-V|^OK^-1|bK-MJNo8n$9L^RZMS882i3ZqU(^{cJSK(yC z&ZTDWL{djX(goFV1SednY}|b088Jq^2mI)iahCbXTDi+FCtYrJtR{*L`&dI(BAJ#j zjW>Q$TLio$mz4AL*DzFM4m%rFAwo9{+Z?2vd)X*^CMspD*K|1~(XB=lOv^?Sk?rc` z>`_|qhtf|U!K0QN$fRyjJvB8AaM4<-nBh6|u9&Ep6p~2dQ{*|%1GTlcR#XgOBZ9n` zE0+_KB%PH@I3J2B&uZb7M`7quh+b5Ja~;(>mjfM@DxeTet+gUX^_D)j6=vMCaOnWP zn7NiSV*;@K$|?2Y18QUG5iWjiF0o4q5qE{~}L>P_QR!#^zyCbg1e@3pd7axGUz zE!H`>o2U&3b~IUO!l)zkend12q+KJ=7qWfX0sGz^bnfLdkfF8ZUjW{0M1riAU7R83 zy%pWcNU^Ev#9({45PGr99A2r!#(K>)aj}S&Tsc1{YA@rzLyRiTevg6MV5e~JtCM^)2F?mVrN+i?;HO9^ zE2i2zI`M(tuc0HMor1|J zr-|-cI^hB+)~KChYtBSDdF~vHHG2E%Sok7OP zC>e<{mU4rd>#W1s2AtjiSclZ+_yrTs$XZef?kVY;e}b6=xLCI%9V=S1XPZhDI%>6o^#`}1tM-?`V%A=0L`>Gx%!+H?-8jzESO3rs0 z5hg^TRD-Wab8Hf*pI6CFa8gEoGqh>nXYx+)q(Yefgx+FgUo59SwrVZ0?dKUL=L zAiE3Mjd#1p#$HmW9Ee)SL`|e@fByfX4E#xf^@Ij&T;9aQqJy|-WW0>rliyU|yxnU( z^TFD>d1Yzp0Au43M6X;;k4z@VlYaa}TspDgkzj!LP8wqB{a)_+mop6DOYf|KI969a z@;v&-Z<8-`65ku25Lt$BaVC9k@ zP(Na!O+*FgbTXmN{+id~&ZxegKJk~c+BSBe_W{#Ryl_|@9g67komsv3M)z(U=bf=e zk6b!WbIIY2ObwaIR*t|r1@mn_lkZsW0S-`-x9I8Z0}UoeK-?9HTywDw65Q24ukqv( ze(1{*hB|7WrOl@sZ`+Kq(|(BDthguaYfP&pV)zmaz0)+Q9pTlfw&%5|PJ(I~WH(u2 zVBPISugC*l@FWfD8xAFOFaybD2*_}ExkwsREV)RdSsX3eqiOjcr8D9iMFej7GHU9a ziP&BE+JN>%J?EA+HfZI74w);uq9cJ;DPrcIvzc>s z>i}TnR%h<5kI7ep%v9#p#D07U?Cz*KDW{<|i|00Yb_xm^0O1_YpiO7!^n%Z8mKa5~ z`EuxT>0ytk>HMJJia+1U_PA6l(qUx~%idUN6)kwCOXQ9sV#p_)t)bizNBZ?X0%gH@ zZ%G#MPDI3bBmK=tT08LtZp}ISKRiZULxV-+?BImN7z>d54h-%l2LJMgOM(9zW5mGm z-!KdK{hL(K@I@ypag@~##l>)cxVx}jtz{FNL;v;Qan2nN4Nx7RWAaV!MQ$=k^#t3< z>_JP}auoO_E6s#5pG$J^57 zDJR5-GX~oxi#?98wVca;yWf8I2NkTJ}K{;_sRCwgZP&PG+y{A-HI=qp#uBy*sCu2yATzf`*R zeCTr@&y?mu$g$OA8!yPgfy^KaT35NPA;n#HABqU4A(OH!9s4z!x<&sb+m@q`D&s{X z<6h#f@fNkrV~~|(1--rh0sp4F;{;2N%t}D{!3SOq%UYix6F*DL2!=DGs*aG`X{v7# zQ#J_jf`sDj2X%)Ljoa>y1y@mjzBlmDF;cVkGdsjMt)ULr)vFRV;5#1TKIeB-l#>hW z%n<`-^>P$C4^?*GCUN=EUy3Vbe9E1iiq8WNx{xuC#K#a40=oCS$YM}^ik~?x6rx5_ zUNxbIHDit`3q6OSxlIc$y_Fod zM^f*NT4v!@B;(>{Uy&v(WTJYG)>ChkqAGcd*yoQ+G{bSwl(J{R+K!;;%eykjRmW;O z1-#2~n|%c6K*;Uw%{{{V{5Db`pNuqMr+m z=$7V05vS%m&1^Dt85jTBA^gMnKqV)i^cuQZ5ru)KNb(AvzUt;@YhH~G827@;Z!3~5 zg>oAv4<3@5DC`o{jNP|f2?)Knvefg=H)=8M5!+aWQlmh+x|_2oI2MAvug3k+e~Qzl z(;As2pT)jUygBh2Z3v|KP(DlcU;G4Dy`_I^xu3%O9FaOpeFp#9i)1kx3h}_ z+SlANMX51)Bq6T6r7SqNas*5Z283F39`-tI-Yw^XL0;8ukZ6o#-Gc7sLEA&u6j|u6 zT+vikvwf0k(RsOz&q-&5zC4-&x-8`Dp!&md-&N{aNYl( zJ%006)gmlV?t3M}B&`Nv_~u`QO&V{5@EHHPRwEOh>_kiLSeEp5v0-lZaFlh22O;w zVBzT9e(K`q{Lfc|gBth#4Uf##6O1G&nqsHnfo7JlExZ;1o z=PVU{`O{hkuwBa{nw)1}UB8Wt=VWy>fWk6%j?sPOwanaGw%F@UX(_$_;$aiwOJKsX znIprPcUhd`36t4Ol?!8`2<&V$SqZfV$UYZtOrfCQKBuyR;hM3jfs3QO)u%X9w1J_M zjNLTGrbZ|c9~8i_qI=5!yKPb>^_jp)Cl9^I`?%5k7)(L}AKvzB%f1VN`1WozH`%W* zGCnI-36jF#38@Eg9--Z;P?6I_f<-e|Oeuk9^eJr2s>5%y1Vj>?c<1;A2UUIsJIT4z zqWaVOGnmh!lfNy<4!95|t1#qgL{`1GYye5btOJ_BCq-mmR))!P| z3NSZj_sw`FIu((>yk);_W_rsn8km4mk!RsBC=dhCbxc1rwoqVoJ4QeceWW>SxM@jS zTcEd?HdUl8tdQb)_(F7dJ=70SNxQB>Rc}=wq};>P*)pIQHGpU-Ee@~MIS6KiHDeF* zbq|HT7n)|kBq^vm!BR=7XgD>pyJjcNsi5R|{W+HQ+ES#wdW-3MHFF1zL$4IW9<;CfagHnMWvz0OP&IPcavL_qEwF|NP=Fi4c9*41;>qxiL3`5jpC9Y% zItuci-)NV|kY6H_9RnFwhNSQ04bxi}C2d14cZ-C!Hnq0@ylCmx`)`zngUkcu4y2FB z%dl{?inyydE*of8M6q(1J}Lof8&=wEMqBq&4t%*ruTVTzD2exGsvvYAkV{BS%0I6)*KIN2)h2Kna5^0##^D&Ci-L!4#+n zi7xH5a2BaGBGnZ{7!jrkmxEQbjKr8SdIn?d3C6+tw8rM9Ni|ud$<(IgF(Cy>67D0_ zEmpY4wqvZMS9{AY!#+t6be!HFn)KvW#Ins9JIGK?d;tYxVv<*;2?Kkg;Cgl;OA8BaJ^Jw;$kis5 z1E+r0K?f839Ta(Yssq|*W)yxrAvKEw*|L6UNv^rmEEP-7dZP}KS!x8%KamzfTve@P54 zb!O-OZR}P(seKptEv-8%(ml(%NH%U5$6*uTPHZ)&TEtGxAV8RaO?`^0#2Mix+^cVUAW!Esci zYAH`1*X0&o*gG?ng*tBQl`X`EBG+`@k2dl*+}7{gH!_gnCOO4K_{(3LK7|3|&t!aW z+gXw=j-^dm)zk#s@X_&NW)eT#rHrrqH*){~-X(tl8V%Egze7L#)*y~Ls;8vQOxbU# zWdGslR!css_bV13op=20oq9>dFuDQ<{Cj{s^TEz6e{2mFd4*sA&tNAMJ-<-A05vTO zkmR)IK@e@?ay&*wrX{$UY%8B$_DdlAv72pppr@&wr(9?M%KX4Y5${zz(NG~=UwYn1 zu#`CChAvH>i3(c#c?c834#_6{Jlo)|$4q1L5;E2F^?5QIm0!wt(JjHDmLjjsJOeL! z);&+NBh8o8_e+SruL70mk&)o!FeaAjljybL4CHI%DRfzZj(fZYL?HqZkpg5GG_StI zFIvzD&M^60%WWV&9nmu@BW#%F$4(rr7CO&-xjd{j$Un^wLHlnRy!MlRU1k{yD-$p0tkVl-m6PlPfp$ zN)=NhzHqNJM^=@7OXT72CU`*+(RQxJu3xtq=K7l6NKZVO$I)A{SYm8q8SE&(lFXeh zv*AH>{h}eT@5~{ggxa2mhJW)_q<~`qFo{u>huqofMa%>Klh|IiTYWN2WuCGDi z*Y&urD%?T2R~ny_B`6h9O)C^B(Y&WD2n6@MaY#&vS41fDv5wEEZ9M$5?Ro>)@T_)1 z(qt0DkAo_qftsy|^e7p8Eun~T0+EGp8wE{atq_-<HaRw&I3LA z*D{MkyGfr}ZGjWjqf!fUpQMOxQ?sqe;fEf)#5IbhDej*XmwiosKbQVW20XH4s$ehe zgEVQ*e!BMscYbJZ6mk5%BxPMUm76G@xtQA&a#!>Lyw511a3B}%GWLHj)Bpc}DK1_3 zd9Uoo#fRDfsUePM*fmG|4rRVgVtH*IaVAy(UzJv#2nJ@fp|_EJ`Vt+?B#{^vv13ZK z)6uoN%!=x*3!;=>YU1QEJsOBGfsxs!5{LEXrbhbu=%}z#HQ>JFL=Sf|x^X5~$|6ac zQdS4KqHww7@Qtj#Qh4+$7Zq2&sWH>_u*jk$?#O0xL(r;SI7{vwBzRDeXo;qg`00(W z1wNnMu2wP31XIZzXJZ(Q#@HcL)Ei0V_2W#lD{S0CX;?0??+>fE1f%1hm)9tC8^Pt7 zTfG3aee%KHY;w47-W`M5cuvyls8`0fh?i9-4;9Y3lo|b|e^fCYr|QtE`A4Gc@aMVq zX3b2K*$Kv%Hhj3dw-3dTpE3ns90AeU%}d5kaY3zK7xP6Ngo zFWmu;wqo5&n2J7XLNPzDeYDeCER?PfM!X#O=x-@;DNr2ADp8LP}jtIq4V7An}c>V?@w?*WHvlw#!rVcp5|B75wT{!D9j6^ek%N&S*6 zl_E(<2Lre%O(-~OkrUQnsq$-|K#g~Z$DTnCd!l&Pod@*Y99`!ktT4NX zjP)B{)afzHC#a6%icQd7ileI(_SFevlRS)d-VXOFlg2Jv5ev0gVz&U>nH!UN^Z(=R ztb*d|-Y$>3yM^HH?(S}lySp@Q3GUjsySuwfpmBE++#w`Df+S48Gw*jZHFGod{pZ~E zx!6_vbnSKa+0V0ni@hob0V9n@x9D8!Cc}Yk{oCHh>NA(nte316Un{*mg^_-}y8~n; zrvc)py?-^q&=~Mf*8C!BTmvL){)2y(Uj4vLT^U#Hp9Ikkh_&Ns#=7%zIb;HBHw}_Y zPhM&>EB4`cI@L$oE_cs(7ydki5js&a0y5|3Og01%!zmVQvtP7(BR7o8HkLij6z=be zTnV=_LRn-hof4+ezs6{x`~Dx5n>kaYh{CvLrdE8ci4j}A+2l*?Q7V>uj$A9F#(uLw zmJlzEm^SvYOr4FLxg?*zg;CuORdc9b{OMuj4O0S)GXJSvyMCaQDGjFk`J9H*-hWf4 z!;xHGdNQXpncnZGW7Fx8b*-5a(1*#7J$^Yzy8->{QtaE|KoRu`JKF5 z6={3Lrpf_rc;+1?7a37bw~d^P(?2VJ$_LsaaD~j%d?~AlGG?Jp2v@}O#E7Jf)4_?d zpa<$u#sO~v@|edPG~(!J6Q!){PzPYu@TGR}8;=FjSYyLB1WzeE`h$d%2k0{e?N za^+8`%Xar0Ith=VhYJABRlAg5g4Zc^_sJAIFYkAVxq1I`)z?S4+a$$OAd@XVh7qw? zE~7D-SKuoV-H&b7`aW*ZIE@A61kpbQrQf2uyAnRzYjIk_k6 zELRPoU0A#sPmxG{?^#%b<2meZ{6J%o*U!p$7o$~G*WR?6A(M!TEvA%TtaSof|ST z2-3u}m~Y}nFr*Qy`&;N=lwn5Pj@&qA$WBG($*b6Ab92@_jB9`ydZC5-Kj_NQ{>>j5~mVd6g@n|Haj%R7dtbHFOoW?nSiQ27gSZ1;f$Nu!b$BWD@0Ihyl|0{*ZoYD z1hdmo7eRGA0Cr1>j0QxcDmTu^aG5Fzui1~p zHqt(!Q%o{nZ7(x%LhFkGDYK%)uC#y-27mkqeb`4PR@K<_t#A7B&wuq69eudVUoyvJ zP+2EwSWw512wbVg3v4yoXq9*=j8slzn`ZDipjJ1b{Ad733B;3wvLb$R+_uG;Q$>2T zvkcJX-8~m1HTt`$67ev?UAQKVS}Wa>(=CKl8Gn75rk26Pwt_k~>cD`myU_+$$1;*E zFl$-auT6R(1ZLZn5j1Mm%+o&x*3ZZ9yr}~uCWQWnEq8CnJ>El3)~Qp6$MdikKe*wB zIJSM8B-_fEy*>mwh2b>t;a!cRXe~2)sC_lBO9|UX)l1dtGS3sUQHbW<1KbnbqneE9 zt4vn#P}hArR$zCprGLq0Kv4alnin!W|G?8!q?2wuBcv^%V4%xMgC1;$kT%Tfe`^sR zCQ_yg$cY(f+7-x}#0qeRrbI?*WWtEeN@h+*lHn!AZRKB^cBF+OnMv!3pvt%Vu7wjT z&9fb6dh5CqL8MyWLnJ?)&zqO<_a*0+6DviLo=g&Sh+?WEp`BE*oSTYN=Q+5BTk z?quyimr}h%C-Ge4YGW>s{(b)6eF7$gsiME~k1sOFj@YH3=6}^9OX`@sA$|vjX__vD z9f$T&9jG6IIDEPYku_x((KrYQaqgSv`X&*SE3-cp4clPjOIKQ%HH*h$wbx-QEx~2w zp==RILyUx@F*=ZNQkT04AhS*db$P5v)!_-aD!Ez*tT$z+AJKue_nR?H21!x!DMcts zA$KWLm}`mDKYgW*it2+YqXn1%qaA?~6D|sSIhYlUDM?NBO4h>~EJm&!s3E5nnS;UO z4+lets_cz6lCz2&z*5*Id!sGvwYz(5YSxL$V774L6LV1oRlrOm6KZ z{F7voGF@ckMxLvBUzuX-iwurSmUA|f-(PiP@9x2}$DI!>d?oHb6`DAyM!&2`Jj>BH z54a+I-L9)!zxvTWq}O1@@=#-fxz^f1kwcTGhwCEHtdT}a`MF%WaRjS4B|xFRCcVS< zXRM88NApXWHX3#k8wn|FyW5eYMNhveG_{qRAx-NVYC7ejpZq3AEdDMB6ZCW>wK8G& zM#!cOXC^BYCf|;`7B48$qsl6cLXt~UUVD-wV&rJ4DZmjslnd`;37xPd6Aj2~kC*Q~SQ?+cY zf0lBq3TAh;k)sE{=3UbM=)m=(wa)cT<9Rl3$|%06 zR|}mYU8l^BY8^aW_8{j&L}Lo$lvEyzrOsvP*^9q%psuqdcSeSPqKkSu?OB?9y$YqbGy(D21eqM&e$ZMu=`%oLqgutu^#P(ilsC0@*e%rH~{Fj_M7bw*VU%A6I9F5)7Q>we)+ng416na=8~z?kETQf@M&KY={8>>rkUS!x}m z7HQD|O=s&D9bew%W_H{}xu!{%P{=fl0HqPlMOx^2$Dsg^er6)5!}E8hNRyhLGGL1o z&E4ct%ZJGFT0faE&g^%XArHc!5!DdY6)Ug#`IlkmTddXEFJi}CAKc3In&YRS{X8D0@-pyVfCe0Ps@9M zQ%5$oy6gLf1*Q8KF^HQ0M1YLZ;OLa#KO-N|8Z&qC%-bm#P}JGhlf>;N8lUmAA!Ft} zu*YiNYMKw9>Cy=N+s^6gE7yHlWJ=6{5Q69`3nk5t5h+lyuC#bk>6=A`>DEf6!YdDA zvRgmVaz1jW6S~sJjpQ~=ookm5yI+qs-~ztozEjMsTwd@WGo1dXPjRu43~l1f%I`n47EqP-QM+LT9N$z zA8TZ2gK5-Maa)iQK8gMAxf&58DB$MQUs$=FT=X6>SDAp0Yb;hD=SPsb1@FAN*oH~7 z#de%B3TF}Tnxw1A@hYQRbDC~N(;{_6_KT@4-Y2o+j{Mqup zXY$rD=cyZ*^|R=`AYq2fWpJs7rdNas&88laT*u3)6U9}#MQr?1K)u3Om!uqMlRU=lxVX|hm zUthgIpQE37g$sCt1huGL2X9u3?)VKx<1^6ynkESv3BM`)s|k1jz?FWW{WMctgB>I< z(j`qV0n*1f#`Ov z!84<@+l?^eNZt#V&JH(yRlcYqGFZRUHlOwpMX(^3qYN9&6MmsC0GWh|h&Waz6RyEz zP8#OibijLuXJ<2)M_I{OHk*q%Il|Pg9?}A~q zpT=NBj_A-Rodgt5ka-f@F8;SQ`yc-KLnklT3#9CmigTJ@Ime+;_i1=1;p!2tpUXei z^*;8k@YS-dYF(5*>XM^7R0B|)9vrKxV9v*hXkf2?RX(2?ju*6l;CLihXurVPyXcumq)SAeDIqY8F4c%(-Afu6=OZ&{dujRVgx&fq z8feu$SLL^gsBVp?Y^c??=)hFf#} zQEj!Rv;N$nCgS3uXM(X@(~!{3Pa5AOXWY|I%7=7wxXrUQty4nQoiOzjsd~d=gEd>} z-f5j#eXAk)%)7bPgKdVIGj9DADp_7VGtpT;dEJ-M-z!X2!7j_6O7An_?idDey$hmk zn#wspt$;Vp&xPQ!Oh8~We>6hoHHA;{WM)R)R#M-84h?u>zK^}XcmEat`o7-Rw5O+q z2USIu*a29Rag(4*WJV=6pSe%vSWlxzK_SGFdRq*A3f|DJP4gudOjr1uH2Y^zmw^!@ zX%9S4CEH)eqTzbT-t?=HELEV?z-EfV*1@!phdMNFcB3GWP>!?bL0T*9t`ZH3+GN(? zMR>yS4XgPohvBP%caS&N;g1r*@_ zE4?^}v*46`haOa7XC?3cHT@Le=smk5e0vEHo@R$>+hJ$E{3c9zHD$iFwPoixxBTVI z-VYX)~`H!nQSN!)RFue0Ml7 zoEXwT##odYdsJuzV}ik2Sq*;P(!o1ET49G{1Iv)oeg;K4QdEgbnkx-;#SeNwv?WUC ziGu3f7P+gz8pmH>qpx`FeEQ)o2kzh%h^-os{7ZFof6-=g+qy(l4EXKKDd)XP?2of_ z@qWntTy5WUxKZRIaV(*6;?v$jp_}E;L6pzQFnwvWApH(4?%Z@5VT$R`K~-`zM6-Zs4lEVB&DHi4+9IXD+&dQ z4xb&)99L7b89|$xzx_hgj|I^u%F^l(zThsWN?3GOm#O!8xcU}Xej!vMOr#+N*|z0%hP)pn$Av)B2gdw_CTwIdM5{Au}T}H z3dv-74qNVCL`dslz|AUX%JwvjicV?XDqu+GU`?t;4o}cHo0pTfAI;>9{VvulC*;v) zzTU1Wa{?c>TZNoluf^SAu_kJL82^&ho!uOv|8rXFMHk6+$58bPnd1nG6eM~;W+hSp z$6Ax9mB0I22|ny>1ba1&62^PuDlYBMZ{tc}jJ}Y|e!*g7i|AUNR6hVWlE0=u>lYZ% zgeF_G!2@eVB$a0BSC;gW-uf?$UmcogG))9mX@?cDo8-vW4l&DAeKJdRk#mExu_q2| zBM=oi<=IZD{*0ZLh@Z4bN_OBwm4C@i8MdsYPb_GZikRO?X3C>6iQD}J8aWF6Nb?}q zC5M+8mru5V?`Y(;Es=tI^5!FjHeAlcrUHoc&hVsHYBx5XPvE%L8uNk>Bk*~8xgC8U z-ld#ek9l`pedb*})=v?e1Inth+i}Ww>~}L2t%f7VcO_{MG}6s2%aaW*s*EWTaPVR| z47@>ux;@(d+eG581D(CJf-}yb2G{3W+?fRU!+|+i3&{cd1zL+r1bps1zL9 z(b5f+(c18gv)Nb*oCF*&qGxiNhmtF@+)%#Xu$1oJyg6HtM6;;uc5MF{>sW}8lwCb| zM!l%~cb|Z40f4aA#+-L+dkY{QsKNmw&3~9G6SbGT5KVxlr)j8!@Dt3*c;8B@vm=-_ z^Ril11}R%msZ!C8D{Ja+dptc1V67z%=jrQFMvDC6qt>O+^HeC|QW?u)e+*_*y5oH? z+NMr_W`5!ccXLfGnM#%BdMd2Tgce&$#LwLoI_iQGPwuNvASn&!&4G}4Wd?lzRv8 zKL58#0|dyfyW-KgJ@Roy5QAkzAs#!hBwW~j(SEuD)D(uJoTT&oS$8%7X^|MwF#@dK zoPWlErppe^ri~2qy*!Od9}U`##`b`XFFN}a)!EAqVs)*f+iG5(&yoG#ClXhWH(hpV7M3^#djob&ox7#=jdF^ z|Cp~-fowZO3a83=Ia#uPKHVV9L3;g5$_7i0+vg(x34ABKdF5=H(DQr#9^SL31wVkm-oSy z&`E=vIhc@k@4=*ESF`9EK)nO+@7n zw?!<$*yobkMe0#K70a+p%|*{Z;wK))txc1tK|!Y9Ep=4`^aIn>pK7&`koe%r(rx9V z47gB+B5eW738;_F5PF+q!`5KNT77D(XG%u-IU(0MWid5JF#_%BIT z*9rJx!s;I;wLOH1n2h2wwA-J=P3D{~b$e!fiPgHWmwd2;2}pHeDdl|5$n3J#8ZjMQ zoY{o%fBdQv7IC=RP2{HRq>7&@Azb>)(x`z1Nw|qOhxCo;6A^Qm7yxq{ z`OzDB-GXg@4Rv`63s@-1@+$5bWSp>_^kkvIA+R`2nf3)%|NHF#r3(ne9J8N~b8Hg5 zb0Hu3tPS~g=xboV{D-}UO{419F7H_Oh+X%v8=bLku4*8Yb_Eb(f#2{ zg)+{<{i7Z9WF>$>Ic5YC^rDGdT#dbaDvo<`92f|L_Rgr-Jw{^P9HIciarx$uxOS(q z>SyP&&$q6h2S{ptT0iER-;xoh*Zp~t_q4XH%ZzT1Cx+brR~O%MKW17Y*p`DF!n?YskqhmknM zl_@T1>(2zgX?2#D4(T~$$gPgnnd%2ABRqf8Uj@@DB4aNWXD;)oMY@}3wGKB~s*c3? z*DaK>bR~OIWK)N9PnzS5fMj7GkVo7&Rt~q>tmmktrrEHTF~qPQnmr06Nx9}G0*I6r zWxrd}Z?awJ&W`#gNs{))n)l+@whqUW!4LNq$Q}p7K*72Pp&lB#Wx=5+mb?lLV*Pdy z!*y7ITM^FNP(|^(H-hw&5;XiYzT@wt@9&GYM)EYC75!~464Mc1h7zC0}v?$*ZWy8$oF``XkB4j~zU&QnWh(f)I4|)0(lw1%I zULoxXC8LqFK#9&o6sjMymF;5>8<2?L6>EJxloP$+i0#s;|JSMcFV%Q5qDemH==~KROQ`Xx`MRv3w=$ZQD{4LU!=%I zTGy_G7kV8327M9MW4XOl$q9YX4}j7EDY9yHsL=s4<#afE1B+V`$`p2Z1pA2^i>jF9 zae@q3@Y+Jbmb#|AR)#A)2^Gk*112(Cj-x|fN`)HJCVaS1VhgojEfqcDXvCBt{Q&X^ zFhcMYismp>z3i{se+N^MXv6AYKihyfVRqT0o@mC3Wsle;xF2PzSla^eQUL{rAdt|U zza3vgePO8OLVYIaoiRu@Y1n1ytCJk39F^-&#gI*$;0n@DYi3a%7)>ueUg_yb^ycot zGwLFhdYpyc)oeZu?`vNx+8li>0a~+ank;!UMgw*}@3(eEf(3P%zTPw}?zeE5|K)+2 zG0<_i8*`klV@iZ*!Uy6+#b`)>LMkUi`>%l(=qFYrY z(H;i^`*a8FICbQtnzcv6ajg|1c!+h>Kn&<@Kz%M8WyTc8%ySjg4~P`dQ6jmP18da z)zLbG)JdZF&dGBU;T_`QkdvlPws^A$>2$R~6Yt_UTcjm(hcn9u#0b;Kf_bUZRK-`F zhxOYN{wPkcQEH&qeeF-+r5`N1X-&En2Vyz5r=Aev^ZJ6>|7{Kbf4hI&H@JO4<1vHL zf(+xJo2AaZ7MmC+a(|qdC;Ko!imWGRfhx6iTvq|sObs5zF+)ld9Y3Z=YZ;&YDt#VC zgb7W=c4%F~dKl3YK5H6fY!T6;U8u-i{;?TTn?|Dq#hXZ6t-}BTNEC4(*PsgG`uSnt znz!4ncFxXQ54T+i_3%!*{EX#@z_!kbGwS>_%uG2i&Lbr)3-sX3haB1(Eyb3M>8Hfk z_9t|1Bz1l*(cen4!m#xBubFfAt>O+Vq0oJ63Bg-s#d53jEVyaf)*#UHB1d%0WB&MZ zvP^{L2Nd517PaDa=T1u{s>VilEPljFtjsl`jc*(q)dF4Xgg+smRvx+ zsv|UUa?r}Q&(IlX2NHio+f|Ci5gpecW`w&P=C3e0y8OK6q$=YS)M}Qqa3c9q$v4}r zIN5eRr#L_#3&+#LPOPcQ`H>4HG_#K93vvi#o_CVOW!K!u3XD-xyvZxm%|qM}O1+OoY~C+HjFezLNdK zbjU7I`X6Y)Q}Dk@ndKDc-83nTGjXIZ^4F{6lzD>wZ3E|da2 z{*p%5Ktj=JK-8>qHs>RBQm{;{81wPu*}OsiXVQQ73CyM||7gNu9N0wkw~NViplTH) zFS4=8!RyyS2bJzg=VY#|Tu=tJr44gh&g_1;(wtDE{GV1D`FZ6!+D&tZ*4nq(@%2yh z3&gQHo~0bN^!O9U3PxYhyiszvezQEf8C=$?5DZGC!?!1?XF{ zBDb2wAR9{!+VJ&Jt6iBsv6Eo`_{1t^5|fScre z)M_;SQ%977caQAXzv+k9K0m4a5x;1@;g`QC(U`4yK8YE#(ll!GwZu)0v6c909Av~T zb62>r!WXDb4f2Etv@W8U z@z^zd2kRp_7Qq>sj`?u|qKOVA!h7}Zh3)Fx$m@vjZ*i^3i$oJ`3YY!PAX)53&tflac5xj#B5+cJ9Q4KF&!NhkZDN1B;_1!t+OzW(87i^j`6^JSILG(czF z?Q`;RXlrm{c&k(u)Dr!ev|m>*{^NFteQh4NKAL;|1w8>mt~NhRp&OLW@Gx|BF5*Lf|ljb#~-U6(K0%yh6L1 z8IxuduTK}p!j-*ZHNA##8jK5NoN;YAI$p%ap3b>Fb9K&C@&iFDKuQ^i;Fa#hB6qsyGrcL{(Hf`Ws(^|=k z5xcZbK1s3yN@+BPut74P7nOjC>?>IQdB;v|8Fw3=nl-V#N7~DsLrZN3+B8jrj20jt zSIk;qPKJ8R8IY15r7|ju)HuFIEfTQS%JI7Gvi}@AAT2n!gtAeR!y4~%wB&ChLPZk7 zm1-faMXQzEbK`EtxU$EeCqL@lLNnmnrpVgb0S`+DjJG@@WsH%PYp$R4u(gU~VTxyG z8-|jXjFGq)|Bh^LUIEM9I|XOJ6g=d5aU1eIW^^osgaq52Fx820;8PW~vvIQ&f_=!e zRs`BlPzttzmuyX~W8#szs*hAL@_Nc`+S^yEI%@lSJQI7>wM|uoA%;+5ncyt20kV|x zH)3duSmHq`&cjSh7#ZSzl7TEuBqJqwhtHd&s|$tElAnbgGrWY=`_R<2-uM8f7Ywuy zqs)R%p>hsMLA)?+d@jFEzo2Gm6Z;yFlGRYM)XV_6oh zco^}=GKG<)xw2v7u3yuH>kqF%s?{Ao#+b44Ci(e$LdO83o(GBtp_L%Ja{g7t(NU&z z*{A$gM7e>NqyxoM5NMfDPh_wWe>J={HgrDZa!XPRt zHhBXWfw}eY9$<+U?dqjPRGc7O5D2S`1+xN$z!Ww+Aq={nOTL5*0Vn{VltFJ#!-Q8p z-2POS5ni7~J_8! z%!GtL{5M>0A?2;$T@YVdr!GJwRTtGcq9e*F@5Ds^>})wk6h7mA$j7J_aa-0e%v?Y4RAkR5NvU%X^i@X^+-gYD9n!Lkx`-p+I_WcP zCO(}>`Qs?#S5N`H4D{QVW5eq7z4dChw(Dm%D|G7?Gm&OB42tzMDfjLg?(<}mm9M@Y zzAm)$@TL7)J#zi~Rre3EYe;Hs8@K%Wo|U?J832q}+wwVn4xKpw83!fZ-bziwMrwBH z@l@BEiFFu&x9pt@gR*L<;0le?FO5g7sVRw?O=gE^&qD~~pqhpLH7rLZ1jW|=FB15r zJ7va1-s7Ym#;gNDD4zwrgnu?8Lda^`i8W&O@qB2L=1Rjjg)bTmT{@B2h+04-5`rGF+>Rm3) zXe5nN;PqiEik>(|&RQhEta3v5Q|9$;k%Kqw8)a>y2KH;jMKPH*KTPZlbI5)2l!T9gya6uc6QLYRXk0_yhA ze#l{G8~ieR8*Si0#o-nW=IB@Rr6**#R>F%yp311syw+S6W-ArBn7s_9iKE6D`@-!+ z^lmvagYFZ3SE$K%$KCmkp6R}hYDZ816Q2eD>Dh)2DAbe>iu_JK88rWD!j!JoKXr$t zxZV%{Fj|-N6d{_>*2m27Pc4#h#`V?2xux_5UL{vSrWzSknQo33fsIWPkl5yrng!i7 znOTx5=abZ=L&_F@5DW8ScOToaoRVI#sB_&u@a?cgcD|5_Y|Y4<2QFmLV#@Lo`Zlol zJHz7r76UC~`rwQjsKtBqZ=b|Hi(H~mh|g&8B~HH6QUf=o)Wi*A+M~prVK}tWP3^N` zL+Xp!cc<&+%(UOTIpa05Mh`H9DFHoY7P0csO+L+LfGt6B|3M$S3XDl2Icg#;<)W#F zaW*rtSLNzPU?YD`52x+?uSBpA$y3#1l7Qacw$b)N&9%+ow&Q<;9{eAMfbU`LGNMVu zx}#w9r^B)&Q5GJ*C)HSllwg8rUtx3YHfHY~Obt|`}RlKu%84eAesgh?t z$SrdmigM4jHCH+WB-J?crhj$9$kQQ68_r6j{0)tp(c#$S{tW;o-(=eAL{*wUKEZZtKBc?owYbtY#24@U+ZBK7sL%dHl|m(>8|}Nj zWi2CtWP^OY^chvB*21nX-2*GiluAfkyb_8|DV!<4n}suz7y~~1lIQ?=W-}zB&n|YI zr@WJHSjWM+fQ{i`JFvWn8Hxa#0)IQ57CHer=#s6{_Nwfsj06m0^Ef>e4xW22Zt%R? z24i^?_YN$hv~{gDlNb$hCoX&>A{;lb8v{v?eD!Z*!fYo(+xwHoB_fglE`SG-ky1}R zQ|pt%{NFK`@x3dXWrhlto1yv2yuYboRAWfcQ1Tiq zacc81nqeDMpe*8+`$)9O?UfZ{7?g?s_=N2E47BX4xBL|eqBV3cGxqg6FWq#^AsF1mqhCKLMf%M<}_XkZia;UV!R9waARxnty7MjMM)-l2$5o76)k( zqHoxW+&GDHSFyFzZFYV*)IKSC*VKwto6(J%Ua=@^elr((H$g;p7#atIBrh z1Ib|`n#CxxBtFJ7%{%%w+{-9Lsx;iwI%0pvR2UodN@T>Nq=%`dE{s+6!jCy|*t~1{UqVh&#`6mxGA1#ie_TwM&BBOO3OF7rNw&dm#_t~>0F`iaXmMCgMX?XiJAb|49 zq46Efu|J?pDijw}a_E{P;CW+=90RsO9F;#`Sot6kpS; zMjASLaK?u0GsnpgON0o80KHbFCJBe-v@e!8=VB9nN1th;zX>|7=jjrJ?Tg@+>e@HF z+9h;s9wR0Me6-<0`Tp<;NboXxqUSLR6{*C_T-paLw7uaCYSNT(+b9_m)zZ?|$+fEf zJD;oUbMyY{;%pxltA-(xBEcJz6gOO38A60MA2t?Vg%maPRo}vrU#CxBl2YVYT@`*Z z%$JHt_N+$x;QtoNzc>l-XN)2Zq#m0U43Pr>2l$5C=$W^k@Mfdb^Q- zN_@oqXY!9>m)K*xcSk|6uGsG7%~q98_rGV1jDV#pNYBz{oYDtKe?mcpf$~4x=5%I; zhc}B(AGd+wbRhe0Qeh)nxrHKB;Hi@L>L%6+-zG;nie%IjN zpc%5d11+;CqO%ZKtC^!Jp>+}Yz z#s2V1o?e8OQ!T_&A_7npy-t!6J0xfv3f@D(AG?4maWg1I038@JV#8}mh1&LKhQc3H zsAO0g1}E?uh|f5ydsJFC7L*&0+Y#dPDlTua;_PZb0>5R8s!En#yc+zv9^i{HtNK>n zklW88V3C;8#o$Ae@Y|Pcz{2H;(f69f`cG-zGti^xy1foY;*yHfY@Ldvir7$PD8Zsx zic+|MN)fh~>u|>GGG>Kl1qUy;LMsg*gENne1<1fHK`MbF#Y8Y66s0Vew#CL725o?X zhqmd05ja9oZFp>ntYAR{gJsQwTWu-A3twpf^POY;@_0lvFq;bM;NfS`jOo?^``aDS z3cR);*+iyBX)z6RwB#@l=RUxWYJTOb-20d}v(?9_EhA>@hfJFv{P&wS=oXp0Ux3<{ zt>+$;)6SAA#MB{k-yIyHHctMO%#)g254`)Hcs-uDjtfTl>1Pdj{@!mW3|`PmT-ez^ z@_tnh$1Jsw2nmC{^o{k)cQbcr63ZpvB2yf( zU?MGfS7UAIpkfT#FZn3k&wMIKbYS3$&|Nq;US-*y6Bv?eW9;jrW{9EkPd3^qaQ+zB zLea=*)${8)k5IuNEaq%mWtt@OAnrYMCd55L-JnpCn8bnYxdgZ(r{;0)^%;(Rj?fDI zeLm5xe(a*toMhI0ZMZ2zZk9gIyzSZ5PrgCT%r@E z3)xm(A7F9X(g_&RMjux&5$$W|{Zx@P6C~yhFT4je~p=MFEKCwAki`HNh zZ_S%w+)o*hT|-Ip$odh~!t1Big2E#N7gJ`5>3Q{N9lJLuqG1FzW{p?owk+t8);zhq zV~)8%@t%zP)Y{#&@TUFXakHi))g_&~Gz3;&JVfd9v#e!AptIrkYkC(cP@;D$)F5qwCkGm0f@T)gm1 z>S_D2kqeb-+IihupNp6@l-zn{-cqeyAkUgKbDT8$KOgDoLVoE3HX-vR#ynPwA1>sP zxC7jSmE?3)8TvkdMim#PHl}OiayKsNBX+bh{Pw=kG?HpCZ zu06HB!k)6B#&=Esj$NxSLv$9*L=k4QU0JpClU>nB(OoF1*z0bMJ;H&4#fNwUqJjwu9mhQitOwG8>3$ zt>@Y3!6cnS73#uNnhId&V#+-uT8k#P2F7R+^^Wc<(;{1!y=R|^2%`#UI1@tygX$)- zkX?~vC8EaMHA>xdma?>vh*$wU)q{RX#qSPP1{$M4=?!6>JeEqwkXv$xZdCgb9(uYE zoUz%^tfT54Cb$T1lIvP#Sp4sTiruYfE``+-CzL39kS?mFNRMElH74!s0?Jd|RDp!W z3O8jNgCl{dJ#2OCm2S;)d1muTkgN%b@12T7$Iu{qzENO|Y)UvVujn|INs%T=VcUhJ zZ2&ImfTH)^Y9Ut`DL)D2E0Q5g#$$o42w=gvH#pGP|3noOnaph6k=^nVAasi}CZyi} z<{fKfe3N)>eJ~9#!N!1t7ixU1+0q1a&ObLw1hgXkq(N}Wqr|((!4pmH1k|e79!?>@ zo-mmt>H3y)c2YasKtSdSPQ0x_^~S;8io)wT^E;?s6;)U;u^-5F-cZ+Np5G;P-bz4v zltyu33L}F11ZO!%TQ$~nWoWX{Cst^QbJhx-IIt7Nyq!MhILs`w2Z=uBw z#6&_&XAGQb9;Iq-Ncs1S5$VQs^~zw(bvJ%{0OElvQjFGy@EH4UI3OOVgH8&3(!|aq zhjJOXGj=MX*%g1r=?1RO`LB&n-7(jYcgqor$PthExQe!(Xw1k$;E~i&-o*r=p}R2? zK0Lb)HZMs)W9!l+j1h;lq+5>41UHvnZRDm*M=*?`m*%A~b>qD#TBlqkM`RbWvs>EY zvdjtl^k22Q#u*E>&dcw6TIxV(!&ja!2qv}$IB|r0h+uR8!jL-G_pbT4y*w4V>&flQ zt}Z9HmMF|g{}s*nOe;tOQY3Y$EcT$133~-lE-G#t-w79+>TJ!YZYe>m;nb#&m;8WnSs8+^rh}p@ZqOwT#<| zslJ-8rr`s6xmA4>p@cgPXLEW+7M-aSp%Rsf1U(V@h?ClXMIrajl`CbuN0@Z~(a#iH z{KnBNTFpvJ7%a-H=rA`SV5^b7koG6P7g%?{y59xl&3i5|>1uD|chA)F9?g%mGp*oh z@&@khoq-j19}WA_w}YD3bp`vyf1HZ{BXqtwJ-zKXu!+;gners;i}dv=p^T4j6H-o%Q0m2# zDWSSy_J9p5PeL*RS{-SYmHB!N@CHIA%px3E;#x)=&7n|R2ovhGl8a|9`>IT?j${%> z+_40@0H&*4Ma-)H%>-10TxlNJu+svMt+lZt-zj~qWTk|4I6Kdv7*`&K0nWI*L2%Kc zC6e%88BlA)c2gra6DQ5+UkWO_KySxLQRSxFBIL6*4YP|YUr+TF4r(1gX)C)~%`6Aq z{3;sy`}r3c*ZWuB{hzF6XY+Z8SJqHRKt0;j$&^i48zlTpCJDF_vhr*av{)__a=8i) z|6QW(4O&Zk+i?(0_?8$BVYI6D&Hv+-K7r&r93}k|f|ya+ zNkS%%SQS8mZYmkaOX8n_Yph_iS-+xclSj--X;6eVMjp6Z-J7Zt-`yITRHZaqI<0n$ zy^(==EoXP)K4Ok|k)Td%V$k^w10I8x)K2?L9Gk;f?m;9#fftw~c%U?d?d z*y~5Oegl)Udc^oSFk*eXwa}L3yLq9~le;@Z*2dDrw~Af01#f zaK7TEE@cCB4{NOZ!*f4pigk_n1MU@jESO+CwoJ3Nm-!x!*E<`omYvppZvxSSyjBO% ziAYeRRyHyqGvW~qA;f=vBt9T`IhI~m+buVTqOON3J11sjxlUbm%9hnNSgx|tabY<1 zg1LC!1Zi~o^H)A^V{^B>(J9JxMMICbSn1`NzRCt>iC)9j8N3d0rYu30CiRyA;kt}> zq0W|u#CYHPF_wm_ppE_0$4A~;lueVjyDcc1$le0kKl6jE+%Cn^*lr%)|U4Q~yH__fm!M=>Y`+qSiQk#^?4P%UZvS&NJbPFB%h z9eGlI4rW3){dzWS;CnE4#OAZ}OY+DKGSrZM`o0)QF8OzR`{KT{rR$d;Ebi9Wq6=17*Yv6bq)$L`-tLG3+LCVZhNPo4@e-Pprf;%TRrOIDRHb@EYa1)|5!ZDu@uuJ7t>^^!p6a0VbQGm%E%k8rR*-T$3^6zJ`86- zjqc0Xu`H4JU3dG98-y*IcKpR9JJBK-(_y{@k2p-Y@pPi7%!po((?&qZd!-t<`hInm`4&gJ)D=c}{YrfB7A71$2=43H`2Hj_t8X zjxmP@*$s#6i;Sw^M0U-$0Ma*H#kgY|KVu2L@M>%<7Gj}5d!C1ixlZkF=b&gkpy;}= zqD`UksVt(a)XYyVfg%bA^GO|($mU{0VL7>gz)^)9$JHfOyPZ7NzdE<5*WOETkl<~? zrS3o|;XKS2<5M6;^5N<+*3~2z5Wq zOx)`^jxZpmb7}44ftaIVn>Fr4L+M?w+4lSY86N!ij|#$bX`xG0Tnk2Jr0q~H z#nzUl##`xKZ?(-XD#$d^;?XGqg3%!Y<{E9+u~rOG&nVhhtW+yn-s9MNBoH5HMKmDR zd`X|_;*FVx-7%9}Je~^DXIQI3W%@!6BH7J=x}o}sb4@fZD;U)2sPn#a>ihQNxKC6= zq8&*Y=sDOTl^wS%v()aRLFW2Re|;zu1ewp8nh@v}R$p+VOo74U(*!*?V4Ke*FJif* z5oiG*XqvtH%ebV60@F)_ovE{n!AkiN#U|T!!?g72zJou4jWxRP~NlW4Q}FSnb>?1L@lI10B67H3!>nW`7*&WZ$w4oP59BC zQHL)0MD`Pmf-PrHxCLo`#-ll0Kym|x7qzJt&OCIaleZd+?nX>&WL$ZkQ2B>sA$|xL_ z*QTN_{VR<*n-j#R%}JLkShVGyRoP{tz8bcSh-NKyQi`{4Kq$cluc)_{sW2#nKGsXorqOUvn zfOx{SA!YyVO2+zdH)^>CQfo4k{gJu-UJrB`6m&Re~RJxUNokLr^%e>c!qM1-RPt&ma`?_?; z*Y2$xF9;7B74*1~iQ2!_IRxv%2+=rDMtl@Up{zB!BK9{#jZ1C;BeOOY{SDXPZHd#C z+KuvO_3a3xjF8zSLIzQK+2WdXnt~Y8)u^;}uv28W5)7Nys$(B-|Ep2j$fm;Jn_3C; z<%hc-fwe)>tK(>Jm2kBQI&MUbO}tHJnvjvWEWg6$HyoO%e$R_re_T$-*1@!}vKYze zHeke~KTkF~Dohf8Odzda*P7gi`Tm#IvR{7O&W5DzlK6+~+v@GX9rlg2e{=Ob3fq1n za>6Rw`bexOFwniPwfY@=AfU~#^%(tC&^);zv;;=lcAXA_d}X|0ewNN*kyywGK)Oan8tads@w+5 z78CMd>b+I85*>`(J=BjV!WR6@Qp^Ajen_jrwydLGdsVPOR;a7562#oM(xaT2Rihtk?omXYZ0u!ZhmIEL}7hT)bT0M z@kQb553IiI+lId*G+68H&tbxhYbhyKvwHiRxKl;#da-9{Q74M;v#a$MeXkY|)wym2 zpPd!1Rv!<2hfO-Zf30UqHgAvsh9yKEjU0dtokQ5RdI;A89-4XphbS$)sp~HFBDlB$ zco{5CY^%arIJQ6;X)HApK%`N=v<@O2Y@)CaplMS6O^SJsR*Z}_tLy0tp%tJe%$-ib z#up)qe)Wm-cF`H!_E~X2ahyIwqM#^kNDVbdcdsP>waNmXc+kbkcOAeaHk@Hi+5K%-7 zxXxflc9x-hM4Ms37|;D8I7k1?ARk;g46#W+it0^QYh$$#j?;xcH2qAU=HZW+g}5e0 zi_$h$FTCMktD&{UA)Wj@+!*O=^PcuQ!($$SuPbBRuJH4R_9heFXtV z+9^~&^Du3bm7HaAl82b?DocS9n!`i6%&v=Zx8_& zewN0NFaQ0!{{Qz8KhRu_otu^S)K~bzPIXygO3Tx##76nQDnKBSbT?}q$ENJi5>7L% z{5(wc6vcWpbC19Lvsz^mrv|v=mJgi8+El9+FoATxR&3&u3(^r@HT-?4;O(KrlWBD! z!MyF9#=Vr?NsiPZqdU_>?AyCC&w#X=^tP$;LB&i(F9{bu55QUcBObrU20nfo^GJU8M5eBp_ysT+z$PtxGie;;K(-&2Fs1gOgv8N7iTI8&t59MpG)OIt?vs@Q4>PyCj3NE6#_G1c(vH%9B-`9lqzV;<+Kzs z4_R5dm~gZrA{7Lfg7DGNc=~kE!l3tb{@Hxov>PP4>ydNeP_Ad~XoWtQ-8my9LEQbS z<*>&fs1lt=OV#h}aOQ9tGp@@7b1#a08gdJ#s<65{6XAMtp7G|5Y*W1w-)y~aiN$DM zOWHH^Sg*coRVz+hNPa4WgjhikC6UA-hV4>3N^nD>;m;9R8Esl(t#X2+<CN=A3jD5`$ejtZ3IQ)GO9Z2gpyb2!Ng#cH6oEeWq$s!wRsaTn<^o%_<1NK*b= zicV>ANfhEiC<{(t?u^ z(x3#7>kOv zd*QNisjrL7S!xL6bi76GHUmmdk=N_Fuj0yTHP>3QpEL@W^UQq{$Z0A$xU|{K;q2aI=$m?~oOw#fDD+!6Jb+vUTE? zU6N0~oz-6?m{_!x zl7@h2`kJ-Utwi{;YIusLDqBn*-%Qh)OX}iSyDk0O@!_XV$buRsr&w4i)K*S9 zz#DXm5Kdfr*&#e?;xUG=chvl|U#rzSj-LkGXqO_^kFQhyC}kA2yI|(PT*todyLfQ> zkwxX;C0tSCk;%|$I}we=_D-{|RaIa=Ny-I%WSqpOB@8Z}*4Aph>J`~p^;A_;oRb|r z<^}@OC!jKf8j7rB4Gd8upi*`-1IN^=cjBr7)%rDdw6`BF&+lWMc5@15-pwcOL7ShB zm$pVhDqN1LtS*A62I%ulC_8%vNU6xU%tJF&hJ}1;Ga=}UlfoPJXkX)1c2y#gg>)W0 z*;BA_^zm3O>b_K1aLrG(etmr1Drrydd_f4c2%9!;?@&0MqY5s$92acCw1R+WXc*J( zAi5b)`G$-nMLMDJx)g3`#L2CQMJ$3RGt@pD52Sbbsi}^btW?^bTdfBD?Pg*bCA>~Za z{@?y`E-F`EYlgrz?}F1xPcuQ7ZZ0N_R9Dx5jlw0O_76rVp7A%Mzj?HBB zjb*UZuz@L7+Yq7*yoA$~x1T3*((J!T6R@H3h!>^xWfJ_e9@8w6w?u0Ms&1w}7Xb>F zhFryT&s-04n2RNrNAg>`)L;1-4>v18F;WM12(N1o} z+iY5T9$mLHM08q@Lzk3zPdX9V8Vu@ z|37>}Zs31rs`PU@BD2?!z0%o{CCF!oujw!3te&9&5|c|aVXM%##pekEh6CpY$GYSu z>WgK)+GEd)WG`vg5RD5qP6v}oWl=UM=qS9lUmFc@r}U!>R7ic3ZoNg?02)j=Q~0kn zw#T=<#R4~}yPfTznW9B2#@rd^npb+`Y>aS&r9A>Y;>oSy;1Nsc2FP5+>pe50E#_A} za&b`mZ74SRcGF=|7ip@uQT6(}3+3?yN}tuSmG{L7S-AtL>*#^c{cQY6@W)H2L-$ch zYHU&A=VOd_+Z?t)Lolp81-uhbBa^ zti1TctjKApqDn?W1($?Rn4Re>C)9O|V+=1nZA+2Kaba5`x7zMn2{mb=^2rW~I(S}| zle1t$aJ7DntO|j|7;W*SE08RR3A;ax!;KUhU<7SIC12K`%Nov z5JcBNQeWo~Y#ATESuxl2)Ar+OIfp6QoiH`#HU5{Do0_WPe+Ygql%t;nsd9Ed!EdHb?jgp>9H()w*hTh&A!?Z#J%QMFRe1!bpl!LqQxlt6WGtHZ4$g0F>btmhq4-Ujunz~LWMUwy zk6NOv^<1*u)T2z*=1aJ=`L9@W{vA}+A;jiw{s=)^DVJhkt+qW)y5NsK$;$&&4gejM zTu|POJAa8D$y@0Pt$wpAEIKx4>@x-0O}p7g<=w{=j4v?=b1QjF2=)Mj%a%=&Xe27$ z932-vK{zooSdMPB3f7hxO1Gci?;vgFg%aJPftl<5GKQNBkgSBmh$O=j0~A zz(adN#HfpVG0%)+ML}chr_;>8FhS&>zQ_RQ^|A6;BFIVT)&a*7~z2Nd$6 zShjtKCLCmqrxUvaZr6kiSvv(f*mm!n!%Qjg4F-8D zu8u5wRXE=Oi2ysI+2cZH(jBgn(Dq~nJ^ZK&r8|Wm)ebo@*xe_QbK6&?1?^Ek#- zJRp{-T9AU*GtKdlHwuR;f#Z2|;{r)Wjf_S^OK3)qQ$q5$4=D>b{i?5{AH#0WL&1sP zkLHFbiO4V4=h5>e*qoY3o-iy-DXnALKWQ7ba_S&Rk70E!&3PiYy70UqHWdYu_RoCJ zW;#}LQbLxnNg0&PNVDh9`}UNE_W#Jozk1*Qmr;6$nJd+l{IdtTMQAv^mt}$U+hA6H z4wjcMBPe;6G8D%k3@kyE4f0Bhw92Jwe%dE^oU&Aym29T0XrVpzsg7g%@zQ#QeeodL zQ3^6M8;@1if+faphZn8$sSdl9;(1Vn;?VuG$1HhGj?d`JOqc{U2Sc_=~S@PbE&5~OH5+{Sqa zV+89NkiW;)R7|V1JXr1r8HNJ^YLJl6BqqDlTVvVv%!s%+4?(b@Le$CtCfPIT-nC=Y zZ0{h=`kzv2fj{w|=jsv-Eh4l;mP*Y+P)M15WNh-HlMQS0S%kxiz@_^Zfter$)DTJRz@-(^_;p{81X|GhMz>GBXTsHNDpxd6 z>#ek#SBUcTK6gfHfmgA{LCh<~KkfBpl(ePs8=uqD7NUpu7X>;}zSgC_reB_=aQVIP zFHB(5({(ZkaNWTDk37b$$^Y<0Htcd9H|1Ei0GjvfC7lz+Na(AVRqea_h7&LE-;5P( z0vyfpOf|L}B~;c&af>x!eN&UcN(;<>hRy1JVZeEQcOxNF zGX^#7pve&xP(ucd2T@>D$vALlKhn6xnqatCif)Y2+o2quCv*Q&st-FagOF*sa#d>1 zmB5Z0*4(fDKzmA#z6`Y`7kJCSI!Bo%dL5=y38m*ua1#8SRv4+a5k1b?6bIF?Vc%-5 zfV%u3n#VvSK&t$c)Jm6~eg>bQ`U_d#z4fRBbPI$lo*|5INj`>9n@zC{GuQ&kvI;dFYa36KI$P3I1+A*@^Ei#-`MD+?hRoGy zieB>sFPc{FIPf3D+zL_I%4d|Tc%X-}S~W>21Fd)F#Pn30-rSP12nngaJ+<^D|EgTU zfWMp!#i}}TVX(l$>+R79i%_yqVmw$Rsry|nHH+D$Q3DMj9)D}vODOg1$OXrN-6X#* z`B@*SSDc7YfXaS>$A^~oY2le+w-a3n4Q5L*EuIN{!MY-Yo!$WgCzo}oqLW|iY+dOk zSCKF4hxEXp7;U>ll;qS+pQVw-_>u!M><#dYJ;0{yR83xh-x5VNF^dcX6b5)KguW+Z z`typLSeXLk7_%!cON_FY@|6kWbvzzQv0q4}bVWU+ZLarAHNm~K9?mBJYUaCz&YriE)Ai%=W@_5zcxSykLwa@ zxU@VtS$_?=3JcQ#FMLu=(aIvnt|aI7j37)6lUGWTkWEu*mDdX9C&yw@O@8dIO9%>K zf)^EZ;&#yEE`0q+r~sgngYOjLF-C@1iG&f-5?iSDQIC12v)3xn6GqKEv<*PR3q1=k zIz(WY(;6wnHu-d}ZzwV2ctuY;B7M?O(xUM-Q7`~eSp+ONaSgNj28k`~G*;3)lLfCU zZ^ud0L#xCxQPt>vo_TxScC$+*%YC~y+)Pl zj+4{#;;B3Ljp*`w8p=Hs9(X*^Z;Ixb(slZ$4@;~e_g1#gP;`jJ)bGHj=4?7(F+FpJ zkQzni)J*_mWZgp73>T`EBOLUS7`woitNh>o)(moLh|NVob$>tWj7APKM|yx*4NCd{g_VQ=U&L=+Luw3m1!7 zLzkjfd9_&H!ES@oilX2s41k066Ft{1uBZ+Bvpib2nsNytn3WT-weQ3P5Ay(Sz2n23 z+Kdqd=-KJu_7q=#F140Rd6sAGGfJr zk5~hh6!AAp_oKvMh*UpN1(6HPApE01ve-0*WWf=tbgid6&sb!*95v(+gj5If4sPo> z5wBbXc+8b2OvKgBlInYy%mVIw8&d%c#KN&hFan;(ixSEQ)UA`ruvuy$uKYsPV;;(8@XI1_N^{USB$jYA~I} zzvYGHh$DwWuVod;6hsJ~Jm{+6H~=p%^T@4)1zH~MZ3y=UIY=)2K%H6@`byyc}M zwMo3^0VneNLg|7wTh{pWcpUjyJ(yp9QE^>Vr1!S*mxtpc9#qja71G$>yQ9-@zur4O`Bd>S zYAC5}QOhxr!IhP8$rMH-f7i~)>Towfb1JyF`Qi%w4ib_bg|E@Vu_^j zMZIr~TPjpj2A++2Kyh+zc_?CD_}u>WcIslmil>6H_|%Puhr}%3`=ML_w*7Y`l^?_W2g6!nU`kI$DA zAb&|ji~>x5lKsWdY>SCI2L0Qw^Z(lu-hJfl`19p&U}h1vSc`%b%OqZHD`8wpzMP>5 z_Y}Nnm;?nM!@il3En1bdOWA}tsaOtE!nItNn|nyhCw@Z<6XgQyUPDeU&wn5hbXcR6 z_iX)kOvASttT#fEI=+(`&Oy-u6Q-dE;5pvMeftRJ7cHI-?eSONd9*^%ttO5wG;qFG z=bZvIfopTiio+x`#!kE0vkhl%j1VETvn79GO6j{K#q z`Y?D_@^aphF_)eTO#02gn#JBEd=4UAtRmJa4Uq`N@b!bt{5^-ON zR;Dcck(w4dty@B4dMv|_6WqtZOtZLcI12S0f5+SYO-;VV(1wXmaMwh=4_e{~As*7w zL)&aNL@`s9-2&5#T}Hh~SiRYgTD9hmHKy~18^sK+M)A+?7I7e#rl2wxf> zVzfPzkagg|ukX82XCN!ij@MutcAhH-_QE-1{#w*G8p>I(^3xDDsD5vOFlASIM?%1R zKR$NnCVh1usZ;S(QEFR*{ZE)`<~mbOhr7r9pKg^Yyek9TM{rp#dukW$#AT?BJ@srO zPBCQ>8{g*)3APfIc&=_zCAJQ1S#5Kzn60#vH5S3Mw8~G!7;xVcS5pzOlr|lssbjOM zSZBO6XQSk`PX_d0VmFwvQY~!i?>0HkA!6sw9_qrRDv+$U*1G7MzmJaFj~~R( z|DOE)d%scf^q2niU&g6P}v+%L{TFw$f%IFd^9EYaOb`1?IFs*ajXOhA+TlUx%!2R%*u(iTU_i(a1t(sl!h zEF2yGX5);6abC?ylSN_SgFV~N_YW0mi{E@7giOJE14;f`PA)Dio15Zy#r+Y2+1gcyYNI--^j1f$=At`j(msM|KY6=p z9&q@e4z&g>FXGPuaTc)JwA5IE9Y{sbx#B&45V(bzPlQ60z})AKB5)Bcf}4jWz0p(1 zg3WP~)|5U<8)YQVtn^a&R4J!u$33q?=R{;)-RDwST(!uCrN@Qt|DE4iitL$}HX(5?h6dsHJpmV}LkuSgtC9N&CvWjv2j<%$~gxHs!DG0@PpI9i)m9IvpYq%9kP8Pe4ID!a|o$6 zAwwfz6U>hkhM3|Wf_%yPPSW0`Ry`mfK%i9Y=^Pj;BkU?)E$J~SB@lN@09$jYa`O80 z%}a!TI15>s>#_a!Pq5_0?H4HLzc4{x=RdOCXU8)C(H$7-jJ5ya7}Gc%|LK7OBxk#R z_~R8c_2WgEvjK&ULK&!w+M!$Sr%^v_OlDg12rA2e?ff;(hYq4F?%ExNz{h$WL3_@J z6^1M8laG1bJ zgmS{XeA>ePbqK4_8AntLGLeojz-yAm{oS=k5=3tL`nca3%GP-#^4%Skt7{;*qB{=5 zeJ5Z~7vM>o-^5wvV?1ih__1Sr(;t>@lB#8Q{fYji%Vve-M4bl z`~S!536%XFZ5>#<#>u|B-FWVH_%udxutTC!CYkqjgokGfoWf7?t^&+9Na%2 z$P$AjyyJ@(j0hfTC(5h{6cjh^$r+?3mrPSo!WKgnZA6Y=kll^s=uO)~)cjRczsCQd zIG*7-z9v2`K`9Hu2^7X?E-Tw0(U#OYtne8&Y<2T>$Seq3jpin=FYH{-qbOugs1*Ed zImHxo?7I7M;WQuqDwg8QhZ3gy7+e1bZSBPk@vuLNA51^3e0Me+Yl<{cO1M*nbLzOmenCX(~5d@K*{f&)$ix?#6ZFq~o&@yci$t4k$AIFnVjRW#s2DyMOZZB302ErcW4J^9t&cD0?4K zgbt+Ymdg_uNHI~#n~YF^rPUWQ;PJlWY~n|h_+M32L@xd2Ko@noQV=ufBTP%7reT_0 zVSUW=SYEVNUfIRT@#9MWJPjZNS)W%njajJ-bgs`OPmj~Z%K(1*;(jSxSbAgp=!buN zYr~XSJ&dD|mTP9i>A&C_wf6*W(Hmr0Q%}21DABeHHeA;>kPi%qe!QiI2dj$Yv#Oy) zscnUM$f?cJuR061Stk&=Q{}w%F!`&%XZi87zERPKvy~Q}-|8#-_3TWE^HWFjC1~_6 zY}q^|#!k@x!~~qkf4brNoi{l%tB`X-uDb3&yUh)qNG%eX+x*YX4Y`fiM1wJ*fjaBh zsEa1dopKmdA6(h%&MZjJ{FQM=*W`4xnD|6_0%WI=I5%r+c zG#K|EQqgV!Kedv~+_3|daE}4}jI-wiD*J@)ruD&%q1@*7$rzS&6ozkD1H>!{PMB;a z2wL1OI1Qf5pMCko0H2ric>D~S5P%Dm^cDU2T|1z!*S_f@@_kFhTqn)c`LY`{a9CZ+ z;keTD;ol1oiAGv2bO|lA?Nqak%nV(~4GX}Du!fgqAZ{-n{y8E1`hkNajvGf#nD5>G zf0aC|Uh_)2aLS}ND2xvywvp#NaSfU3umwn)~e za;20Wk3-8$r3P=1sl@9XHvF5Il0})Xa6rVN_j{76;|MK~e(0@NBvQC%ycsn!8F@8awQ- z*KmJ6DL|9pU*EqMD*o}OKdP;$XU-lRi%`Qa-g(o&7FW#KlOLjFuM){(M=J0gF-{en z^K+Ox@M3uP@_xi)+oiSf$HUis@S2n?qeX)9s|71psizmQESZnYqM)PDLu@k69zlWT zp(6+?kktcebE}GAk|%}U>VyZyokFZQ!z@tyY*hHi^!?qWBHW=ra0aq8&I(hCQsFvT zbW|ufDkcfoQH+2q9*=^6k0{x3bmFp&73d`K+!8+AD%p5H9>=zC3QZ@yNXZ#Rizq}( z6vwVan_V%)>7rh*s=>3J5oq&T&)3Lgp%``zS*CO@}C?K@dv39Y;*-Qu>niSCYy zOmCj<_fJI75wKAs){_KO7t12hRE>}PC@mA+9m=5FVIn`jbGzQpbX_o{7r%^ly>Qw1 zAmo3`VD^4-12`o##~)P zEj1*c(8+o1jm%U5vSho8v&J?N5|+7O2tj|wtIrYdSGLwgP{edAw^yvl$6MgCa2aq| zXf;We1|#&a=wJrIcejFYTuP?PriCw-j*#ziRo<`mK-dYVai(CTCxhkK(9Cj&=h%Vm zYT@}yhs7TWSEF+GXo>>SFi^Z!DD#ljvl7)J!N&A%Y z0MUcwI{&EyuD`-(8=oBgf0}d^y(&EvP8M?fchU?PiT{b8-0C~^lq?Mk4jRkHYUhWJ#H-(J50BVT65B z%~PUHO2v}TJUz=Py8d}FXBF?Hh^1;<_uSWnEuIrIuWF7RR-2#f^3qgnFg<_oPwV;- znoOJ^7JFXzhTzwILWEv=8%GJeb&JkKrJ&&~M>)Sh4(soySt%W50m6N&w z4Mv|C^Te1IN@zru(`Kj5=&V{65uUWhi9kAPkxWHZ^6S{8llJ8%Nz6%zqDjEnE9?N4 znt&7>n7}H#XlT}z8erd=N?uP(0I?A+^&|f6KV{Irm`irr>L5Jo;f$;nW8KAZEbf%E=fTC zcB_KEtgyDjTF9$hJ#~^6-1%Hm1CnSPjSY5e zE9NFWmM4yD2XX(XSNbD1hQnVqx^Se*7-sEM^>W@KGVd3w#0$1Ct7HE9>9PW0F+ELe zw+tBW*NFDm5QgkzF=g1e7jp#GS$b23Go5j=R@*zX#w_=@3wlC~Y0#LQd33{U?V*(3n1|@T?AQ1-!wEUR__n?C#hA1Od67?o5WUHotu|K$Q@TMR#$n+5A?gh2f>c=!3_{LU^G*x&Mj-jcqnYS z_EB0%EaOeF&sveo>(8P!$I6k*_;uX{ZN{8cRgQuocv50<#PMy-B1fBl82`Pq!T;=R z`)EnAU;dVhhSjnYCdjXeMt@zGz`e#FFr6ANn!(gJd<4Tcyx;sLYp@}YuI9v+O{fBs=O@Pz5JzY(Qh#hUzPHu5KWyPQmI zV1!XRHfE>tsE0uo>D3zVbjIqZML%WFqhg7}VKMx|vSM|DmgxH5VM1xi(kGTu^qL~Y z0pfF4v>Ve#dYmO6SM4fb28A+dkg}g_ux4h#gQ;_^EyrI28akZLP4P{%=`CfEfKpcX z7FP8VX?$g-BY;{FcAflI3=ohbW&+b0OC_E?qMelY568gm&hxODPXYj9G2SEu$)U*i zATAYaHZUC$U))-j#jM2%49=oXh-G;!8AF|T`%N*`jr#~WD9I}*8S3jJaCnwb)yNl} z>TRR4qYh-UKF&M|t}tlVV_wo2081*2l4I|pB;BMQ_l>kU-&y_8L0M%~!gA4SKRY!w zZBqQooqCu%9$b)54|1DVrq;pu-4T7O8iN) z+JVN&NiEjP$5$UUUa7qsMfqU7{G5`BG}~qQ4|oPk1eJT0J0o*KE=5rwb2QVHw!n|t zq|Vrp@@NA~mN)7;hA`4ctGqMgB^z3ARF&!(!HuIZ(H37r1Eb2IK@MTI%zIs*)vSrr ze!@eG6;_Il2#v>RGEG96Y-It#6Lj@!X7nq?%7JH^e(gV|bx^G*wDbYOiUS zq~Gl7m-|+r>9_KWcFls{PWYl`@*?M}c{U%kh9GHtWMD^P+@$EX+p?_2L~Ux<&9Pj% zNLjn$>PB?B3Q$mSXTCF0l~jGxiO0CXUU_n33yxw6sRQ*uk{IyiSoXb7P&d%BWX{;Ih2_cSu&1=ix&A(c13z(H zO#&a`rDglNr}OG7Ur?Is$K9O-@w7LG7AM>ecp6N&eP|7NILyZWZ!;MIrN|rw?(Bu=7x@p9CUYRh3i zfTS9FCrs0d>6OPO+^mO_-Jr+AAe+dl)*&E(+Ha;udR#kN><}~dtuDtoq09tEV>F>4 zlCiJ=EEXp+o+uZj+|3)s-^d!(IgLC@CPhsfK5tGtex;QjJ>6 zKW>K*XGqbtHjR>lxsxjHNoN8ssO-5I`3H_deLH5U@DMhW(5*g0Y(J9c1u|eU*4XAX zmOO=z6utEU@wcQSxYc@Cv>F4zyz&;NBu#i$D2PI4G&6HFq0XQW>*Fe`H|;A_0X=!4 zV6TO6c_yv^3R+IUX=f!RttK1gW4*L=t%o`R)(Vz$6=nt5YjrKY^r-J@%YOEws(EL; zI=k9b_gXC$78LY#yU=4ZDW+cakxJ&1(a*4A=mLd7KZSv^i~C3x-di&TK#}>PP+S~J zHC56~-o#l)|$_ep#aGbdwE9^uRqkfc4#>0Fobry!7Cm>(L9k*T?R0*RF{SH*s zBgwxksz2ye47xJ1$JM=3Cqy&@HzyxUGopv$%Ne4CY^SVgDIpc=yRvu=ms<7>{E!kH z^fs^ALy;8Y)@yktrJww#cKDqTgs{X>JHquxJx#QqVTEpoq~`pk-@VSh&$afO@KOv*uZC%GDbNydl&qz(jfQv{07$o(E!!$uf!vdv3h z&(ZIsCeIaLO((Z-6c(e(TI;jcrsfdzEjWa6ysH<-b*i*NQvqJ(5?`mD3xq$zO2gGw zY-@j8HBp;=Us1pIg3w<(yWE#2kYdZMq-IjLooy!EV4=KdSvC@DyXE1idX1gs+lypG zD~(}VeSm0m8w9k)HMj(gu`ul>xOTKecU)r2Rba4*CnwxY(s&joZ-ll6!xyP*#(Mj> zl{mQf=Any;m||S}nK7U^C#w4gbcL37F-%Hb7^Z$`evl{wPy{8vqWE3Q#Xmr@`?mt&-@9=9L)j2J2&Wz{^E`V9GkUgx$&dd#3w{-*e z*Ro-AQqZX0R!=jBn!DvZ+hS>6odEl_)wiN{C>30>*G9aR5p1gBPQXW}a17t2!EB3Zj~nHMkpU3lt1-?iK35Hggx=@L~?{~KIP z-CddA-z&n3b6fa`-#a-yW^&prZg8Z_W@* zZwxM~_8GYKS!D2omA?bk#eyr18aySvGDbjbXR54J7eRd`FKF2^LCNPP4cNkkI8ffz|Fco&r{X~;;E zPp-tD?SYU*?}r-Nm*;E4Q9i;4p}`}`ObkTMed6eE4Cf$28F%k1r)0mfGlZxi*a9dV z1ayjuwM!y?3W{ia{gT1GCQbl~xb!*TAlkkoW=zZW+7R9j{J z{2;X>m-g}7<-^_FywR}f1^@O3gC|N}P^$IQZ~gz-xc@&s>JgruXw2;}N`r^jFI`BO zXG1&K#ThV|g>R_Y`Kk0wo$Fg>JDvg!n`T*-Aw#DPHE!q{kh4lIP7%1}lPfpwrR(7+ zNF5h->HJf6)J7&-Qy172|0)@%l`c!g(rv0k0Aq>^;?^wJBjq_RAtm-AcZ1qqE`7WE zN!I8e5lX+~N>Zl`ZKM);;Y`|pqts6l0hG77c}37)`R954ZWi^S!-8*;S0?fM97i$W zJFL>G>p8adi_e|A4;x_DE9osj@k{UMB>;emD4fII2Km?+3047ox!EQK$Vit9*-{LV zj;}h|WuAm5^%B3QW8x@V*jR5Er?(OCF+`ORTse~*`XlCF)2RF1y2gv))#1oN8jMG4 zC#fNdD(rLF^*TptEj%XvoVHwY%r-^+IHs65)QONBhyqeQ*n~~vMWIlvNbHv=4jU%X z1m_%H3)n%pPsPjQPaxh0)gAUlZ28AxXq;+3f*lbHC;jp&+{1)omqo+?y$VqZ^s&&7dr&d0xVfkljz)IZQR zIWIRkHf8uq!UI~9q(_cygE2(yeC^i_fjVH4A`#y%YPZ3#YdR59RPE{w08r@OFTXJ=3YR0A=Ot@kTT zfuTe(Z(`P+=L%Am9)bUre{!k9dC6=hkEX(J)#+)TMq6d0UHGt?L&A`HQ3+Vh6$t+V zGtqaxSpPb&Tl`L(OF&5H3-$z93}~yB>S=|g(MHyD**{C!+LNt#L7fx27XOnj4H+=E zQSgsOOvdTBmkA4Tp&1z*tx_rsUK5LsLrthz9Ca!Kxpy8{J3btd({}2j@hnvHY}6 zKwlUt(Wfh71L4Z1+09Crq#doQsA}zVl?2ss6+Tt*%Yauw_FTnbbKeXvV_YPZSu7?- z8+zk~>%(H%$bHz++9zWp8q(&7q7+CZh`vwsITT*nPn^8eHYhYJ?D}8Cy=73G{qyFF zI|O$KGWg&w!QExB!Ci+0mjrhWGPt|DySuvwPw)gyAPJm#cAtOM?!MZs^V@Ug)lAi0 zchyYIx9{$&yFVAU{MOXaWUq|o8gr+7!V4PO)+(VVW@P7aH)vfTG(^+tT} zVr9_xY9}W-!!KhqWshRh$(K)dx$`~;HBFuPUZo2w@0>_=4bT6qw+vJ{zOIY@U&|4G z0Q91~eSSmQA4z$_N8)f{8`F7wxwBQ_eo5!DL!5S#`0q4VCXYPQ8WOEp#N&mb71bR% z)#HiDX#?l_rt5k|vVyqUVWsF%-5Fv{bDH>(LUV+sSiv>Z+8ce^KWjsVXXDel>p>=6 zyr8kvMH{4^P6fMSZr$5KD;&O^OTPOWJ$Kf8OhPX12=aB;h66fxZEsA_v`uedVdZ(l z%JxWi8qc+F9-}9dzX8vQCNbUZc)_pP^Y)s9TJWl~@9!@{cfO8}CRHne?o(+vcS{v; z^e}+Ce{0)XWi)A0OeH=iYYJxJ2!~$_lO;1iTuu{L$b*I}ywARx6v$dmhO~?BTOtCF zfFe`+1I=!beV1xXKP3*Ob#W4D%GbB~k$MNr1s{C8r-{dgVvfHQ&lGbMvDv7n z*l?=dEazl&#-T9}Ltx6b1=6 zNjxK#)~=b%c=xrSJz!S-FAFQ))l8)QQmL;==U4#->veDSZIuman6fawYQj<6jlGDt4Nf8UF& ztEzl}=?M&-UawcJZEY`UXgJg2_|~0_Y1o(QdJoE*RS*e8_-(x{WGj;iIL#Am+;w^f zEO-v2;Go>`4#ntCZB_b~-?^FJ>E=74-qi4cY<&q!{hMszaN(kcsiC}^;h8+(v4N^8 z`_m;(L4P^?zXK-c9-Bot)25C%-znsuFVMN5l|3vi{vkkHezz^)T47m}o?x>sg6E;;21xe+2*XZ!wZ9%x(a87snq zR+JYnMBO@Und_I2&DXwvjl9oY)|89|F)#}(G`nl%{@-c}|7#9$Ym^RY-8Q0EKQkXV z?;BhomG0B^63JlC?eEA-3Xz`Nx+*7EuOl7$7^ik0ron>hj+zYqoZNLYHnHEzaC}9} z@+F#6LE5+kx~Sddtdvg%XAY3k5i?>Jc5KGCAZ4TQw$Iw;bviFezhE&a#a?1T>Y9~D zk8SN};XBS?2#(Z;bgM>eV_2{}eO+v5SQ}WX*!i<@ArLPCT^y$^Qfu&jGQx%HB(wN9 z!!t14JKb0y_f`q{{pYPn3IO=4#x7DF{G5q9=DXCrvd*7wQvr6Ep5MPiAFq#c85wu2 zejB1MuPM6+wryWnJ5GRt7Kre;lkbg;I6D1z`FgqVRQc^ug8DFYxoS5{?pUaBqmESF z5(x+NtqOKX>9y>zpdD;*jhlzD-Dgen*+s53tf6%tH?hp8iLuZI zGHIphQX8L2o6!#lG0o;G==gevpkk`?106t6wb?*w!q0)mas#Dw1rXJe`Yo+@T2F+T z-9$2aC>g~-a=;93qI1_*krbZt)f1N~g!73Fx2w<$Og7u-uaPxhK{FY@tClP>w$S03 zDUK1O1>HFCt4YEQvgOIwkZ5@xDkhe>3wg_Zr8AVX+K`59>SiD(rnT(oYz?EdKIHZ^ zB7m^e=U&6OW$eT;b7ce8keHRL%1?T{j>E&2-$lK<+hz168Utwi8^NuwJ7XkF5gppW zD(s$DSN}D~sBEJ14^w3Yqs!!tRpqxT%N1F)FYnDcEguqOA*?V5XIfb@k(=N*F##AmNij@2gr1(wUeaA*AhyY z&Z+#Gf;E}rxGo4>>WpN26t59JK^F{3#T&fUV|L)&BPv zA61VgP1;@m1ROi(pnk`+8G9cd!Hej&<%HSkt-vbkhGv0^6qlTKJ{! zd76`uka8pCe?8Iv@z0a$yM5a*dw^DpLF7q4nP9Kf((cbVg%`ofH>#o-Rr>>V_#bOL za4Rcm6NztIOD-+dbKAxm&@C*$`yj?ClZb9Xiv|w)T5k2i8{KfVhwzZSXoD(JOzZgi zZ+Psaw;A#j?^Zy8Wb{ATJdmZ+v?dYm%-K**R$~P14p$<5j-Mr=2$S4$gG` z%rkXerz+Ho66Ux}1<@sdCo;@LjRoxXgzfAsxqp9nH>FIWv+~nre)^2vj$38NJK@}` z)U4U^Z=GX`m}ie${pLpaZ7ecMd(c^pW{kAMseS!Y+2aIK1vR6e9ZHz&w!+=F2Tp|!bo~ndqms8q)f73&1Vx>keVc^t7qf8qke5=urC~i;T za}bC@#XK1Y*_T;Rc9zhE+G<}I6D}|kJoL6jI@qiDGY(RuuMiLQyL`n)Ccwq%C%dY< zKb1j+&lpD(f5529Y=kb?vbn)2a=nmF~=0(;vD@&&xg@deTqbxG3aT z;OJt0?*2TYJ88n*@mGwz%WQ*T%a6PQQ%Wz3mjY*vgs)J9TI~vB^BpS z1!a%jioMF775M!A@khP8x^&uaGaFrUEqO!Vom8zs2bJ9ZG0IGT;Q_uRBw+s`UVP5X zD4~R<)bqN~AA3x(oFyb#1X}ulViu)2#GTG>RYM0#hzsbDF0q>869H@>WlZW|**ofn zH>#nXx$z}H?T144H6k}$(I*g>l-cXm_lJZkiLMNcojy5@#5>w;L04U!^xmg>!m|I` zw&SE0O=&MhltEAUQba&d%Ulc-H%Ry8K|ycJqJH(KZp=(fmZ|({%x40!fXW?@EW9{2 zbDDt<=sqj!A9-W@emH&U+>!grI+y{U+5``F`&g{<_LV+>r21#%D~?ETraSPvfs3KPm^Kg+=k+GrBpd(&alx+T-?^OEmr8K7sRQquABO} zYvgleCi1l8l@Dx~%ga$DoAXo~J6%xG+XWBzM(Rv-nG8{ialxipnk~JxE^|_$tt> zf~wE?*GT5jijI6j%k~)zup%rB%*o8`-(BB5@37OTDg8LCdp5YJ&`QYKQc1MH_iyb` z4VzC8o_7Am+SO|%4B0i**Zn6Z6jfCI!)TSwWN=Xe*75BS2W?fz%JtDKGa;a9-fS8a z(@e%U>$M#2N>{3Qg_U1aBRhogm$u!KBudl$Mx24N(_&{jJ%si&I4c>F85kf&f1nwY zY7%WxH_;W;KdJEF7f^kc_;G*S?zwF#ol0u5PbdgEed3qutkUH**V0rGmT0FRYXM_CFja zPE#40lo(RSww5a2tV&1s6JS1brG8ed+m_{uX8LIRA%Ejk=-Y!_+JPqj((1boVO92H z<)DgqAWGBj2Li*@bc2YrY*Ql~(kMh%P4fs^SN}y(PhF9poJZdYtmlp;-T{l+-Mnn| ziL?|T{f^pTpT9&b<~+3I>3R6DTtKmXovQ5uLx4*s2qy8 zF@@r*@qe=oqcKzsZv7zzQ&dkLqTqQ(XYne>k=$|<3IoLbZZpP+-i1}>*tBH`$Z3s1 z2?HHbM2#=so8YQT^1QL#CnVJm#GtlUtb#VAw8OUzn3zWp5;s zJS%e?cfBfjUgw@=ie*lzqApIkn)^(_&v||M*Oywb)@bY@CPUplt8sgC2$Q+;W6QH5 zG7BZF+Z@edei{2bs=3VTwxbXdm>Mj$(=o~HY!c68q#Kri#dN_eUr}VU#7Jr5TzN2# zFC_nzsLc6B_KJCV#w5}+uT;zV$YQcUOHZnI^|SV>WC1`b1lk4Z?WJg!5%!W|e{$=D zP2rw_hx5W?Q3bAYjjIiv_O{(QiJr7&(=zLsx-|NtAMd3%i}QApQlZ^=Wu7#a_{Vk9 zGTqsm;9=&F`;4E45M1ePeY-KyO9ek~FfYlwt5K2xcqjFJyNnv`UJ;56n zL(41jGBb{yc+NCSkxu^NH&)rC4HwN1k;>tlXxViy9d$=j9R#~dvikV@?=y$WH8wu= z*%B0nJ||<*`p9XoG4JMnv-Dp`t+fSC7y^~%n*NHbC}ZZwe$iRskZE-B<0njWOj|~C z^rU$l6lG`xrkzY}SXt?_ZT8Q&R_47;|M2IvU+ewP9>}iW#*otPM}KoT9T)lCH2I=v zb#1;Q9HY25S9HAcNx75CdSXASz9yuuXVl1A97oBd5P4++5M(Kl$~W;O*DxdHv548_ zAWW8=_ryHRlFnQux@*OHHg`HC#K@mo8N_R+Fb?SFz2cYENxuMA$NC;>4+;foE`xJ4v08q{`rBsG2LuwJ~{CC}S-0Sf|e4 z=0R5f{JKg6<8}hLg9S+)O^;lKaGDb}lTy0f68$zhOpIpr+@=giM41dSFg(&o`b-tM zks!7JT1Z3_NpCi5Gs7pH@L&EXR6%4dP4Ma-)^hl>?DF5}bz=ku;T7pJG~t+mc#_hG zXfiI+N{E;UEzG!J!az&aR!0G^zc~fb=`ypf8Gx)F@Mw!7gLA|Y7r|Q4>SI=;Ee%u^ z-rSVfXTFrft-v%!mo4Qfe=8wK-a(}PKQW=c>Kvvqw!@?N<~qk&^?Vx5CE zYwoUAhlw3nVEN1jv?}#4dl5Y-F8a>d=(Ng5c6uemHm}C+TFP?Z{Zp ziK$nJ5^_xv!m(cr&fU(k`vN{zf)HVL%v9I}RRCHx33Yp%l##qTEKbq&wPuEB-*W z<+lTsz4Jabf9N}+pGvRID3NfJ^Uc;CaBp#o@e`JHqWsx>wW+_I}o4A7@B5viWRIOj2MFwg?T{rstfR)mpTu_$?jPnXaim0w*tau3f8v z!=IZ$B*IuTLNlhB9fOTUe|Q-`@R<#Z6pt2FR|AibQF1nm+cM#3m>>^2{5{3hFgb(H zJ@vTJ2Se(-WSTYy?8F{vpb|GM_nAjc-6MljNiv>&S_f}bZ9|0vfxVz4lqkk_w*Vzo zcR9RGu+^UbjE``ERrtdy;uESO!KekEfB_9Qk5THo@-7OWy_lanQLNaJYhnT`i|k{1 ztJmcojh#gAJ?qAAR2(I*c$0N?S1zW&wxH&6Ho+joFzj2q{$ z+5xuja0AN|zOi=?{CAc+Ai3CWlD&za9f4Ks5GlMk6swa`jvP9*S~;vbh5E8-$C7j= zO@Aa4!#QvJ)9BY7Pi`{%LkO%sFf$z;nIvr106Hv3jAkHw*YwM%y7?RS` zTD!vrB%q^qcgOCEp{!Qd;j^$qap=3C%oIb9EQ03;J8tcmv=Du!RzW?o#DO<0tzA2T zdp;#iqh@85@BEBd=0){+zY1&IVK3}#;0;QZ$kvp0{U>RRu%K|9oa181&?aA0D4WFn zA(lhYsst@kF^*W3Y1ai5c_*hlSyEnCPN_RnvvARKD{m90RNcU!Y9*PRTXu!m*MjY! zNdkSNgeJyDO_rH_U|2i+u4c#K;2Z6tt7Yf1l)&2ukUo~-blRBZynEQN6&x;G>54Jq zlTljNiwqGSe=lL_R$Ho}m43X!^K{{u0ylU@XvPxnC!svoL;?X? z%g&D6huzE#T7*l|qJk)MS8eHWx&k+g!OTl4RK!xf2Ii?pU0^gX(Q!?@m-gkr%n-SG zQ_Ga>kR=E^ld}V=W-kGptmMtOf>qP$V6OuGlN`ypOJDY?o~Zr5DJaRc4!ZiWvw%oD zvq_JpccwU{t$MBYwH~=S5mKfpx@i>c)*Cd3frJYNT|~AX1G;yW~|EB_a(Y zRUnHiR4SHBr0scyXS3s+`}Yl%%+k1eT8;K^nS!*gBHUpzens2VlD5m>I}@HZ>*#2njEl+FPt@(S zyP`Z@5?T%4 zFmsOh!Q;qIV)iShwlw{hp70ZvC%`yMUoc=XK?t&Iq5I!BOZin7|M*J7A!WO@9mls? z^AvtqBxR-a6j6#)91+mvI-tdJ65u+i3HB~%BY=<@Iw?fFMOo?+nB0WsVFx<`*jzQj z`}wAil&HrRR14W`5_!Kq8!Fuxbc55EZAjPbbIOvG6(PgLu~MqNQdK!fO z@u1c2#)cEo1{uuv^x68FA>lGNlh(ei0a^6j_{XHTL-{_es1BM~xu%NN$w1=_?9k~F z(W&=Fsq=%09t=m$-&<<`X!%;j(lruIuEa|&tb$o)r*86}vi#OAj?`bLD+;;0RDnIi zApod>+r?=7e>f=o&l$%*^#lj($?76acJQ&1?%QE>$*_u?r2)6O966KWQ9^czosz*W z>!MQ)2RHJ&mNW97EfvNTav^T5sc0Z~RjL_5~!$4OB0KE&-m1M{dW4Y0JIsLalYM&Zf zT_xU8B(&)gDGOO1b#l6;)l{*G;>d8asX7_9O}N`$Oq8;@I--eqZ=TM9EKNj|%{$(A=9 zU8f{jTDEX0h3kxM5xZMs8;|pxq)rk0L__WsXsTqs#~yKAi0)Ol`J2O}2U_6EgTKW8e1!jVpI+e51sPtubEI=rBb1{wkFiuO&{q4%bPbJ{ zT4mRWJcFf4uzzaLWFqX@Y0KMDyx*iV5_No-)Glx7pF}7dIJq2r4-FS255+T^T&MH} zonx@#P%qWZffv1^K;-8&+*6i-X=coSoK&2@ z6wlYgvC`As#OpYt?b;bK2vfERGBlRl^AsaHPi>Kl<3`ZI^<%Ap!jiWHV;}#z1p~>L z$(r7)Hjk7TvpS6c_?V*i)R2O;Y;YJY_lcVA!6=H)e1<2y`pJMD$-JZU2(FpYmh+kE zZ($U+H}_mlZ1|McdYczz;ICPWrVN};7_2*a8_AgQy3oQY3?Jmb! z<1U5`B8~o#G8qk_mNlPT>O}p5ZXKgCfEbb7d)oLg3gNj8b@q=_JpWE01Hbd=_r2hf zKwx@nRfB9FTIvA%mhc;sK`8{K)Q9>Btw&I@(_RQdg)IWfu)uXEk=%WdeuSu ztb0X&v%1g zu`i~G&w-s@yVzYP#wN9cBL^)CPYb@(v5_O|o^RTMbx5r22_X^xymoBrH$2urnvsYt zYbQP##-{s`M?wxF8Dwi_r3?IIXq)`YH_tS3>6zM!eNAs$O})o(X{2@yM*Cx7J<_us z`7t2%_uLBn4~2n0$$806z_yL8 zRJ1Yn-SVEc26L_R%+L~qUMS67t31`pIlT1`{G>SeP&*u?253YnrH2Uf0pe>?w~d-H+cHDx)43 z?w3OTMr(xISO4f;JR;g|#hi!h&$im^bTf?gdwb0f4#CnS!8*m& zh5pqjy*+o%7)~{L8;~}!OeTCf=8>UF#HK9&S7L7*d@nq3ysUt$Dr=EvzM%S!Y8iM12kCpcHcbVM-wx)Z(ai~9bW8?<;3S6rBn`$hgxKum~et~5g?|r-O+iLwZ_9O z8MDs)#DS|kF$&rZ*%n4e496_Z%6fV+*9u4qk1#oV!Up*a2>+~b3SrUGY#DaUFrRY1 zUvBlmp3q?`tgRnAoyaLgb=~vwOSyP7EgbEM>!@{=%!a0p1Qx1NWnA@tp6$3Pdl+_J zpg^hhf}eAIps!|*Q9MaIbrJuOqlW-e_^~ zjpg!UVD^opU=WcC;~(nRg;RNQRgjV1hhs%S`9L_VWI^l7g#F6gKGuN!h{Pdkl~ne~ zO4Z;&MlxD5fB4}28)jhcIo5PcRA^N2!o4t2NtMMn@hQ-ePTY(tuZZkV{~pPv{St)X zc71+|lg}8fLz#HooQ@4lI-G&lF+)B1ax1SDSyl8m+*i?_+)OsPl@Hhhy{ZQ|l{A@X zmlq@^Z_hw{9=bxBZnKC8rEVq)XN_X)Znp23JnT)ob$_J z$Gna*H=(=Lxzh$;b{X9+qjMX_7cuMjFEtI7vWpK&FKvE2yo zSOY|oNH+g`FkVe4S4~RoybcQ`b(-)sjc-(Z%7l9w`N_8Q-2Wd63jdq8`2*cfRy=kSfQnel^RZ#8O(0L%!%5LK ze7Z#Uy~}Ren_p@{m$g}e+MxL}#G?wax!Da7FcT4cNJop9qLR%{7i6|nzN7=_aS}o} zIYg5tA?sTY^hKrL`$0&KPMSNmo07p%$ccUDr)U6K>&v(%JLY=ssCc(VpK6T?Ak`bF+d#{kJ>13yYHFv52S^&?S-?+7D4X3XOJyyBar zfa!jM)EXb(AoZE8#z686C|Z!h(hcb+8olOg>EBAl#;tI~kK0a=v@7L8E}Q}ON<>`k zvQv$lES zole>rTD=v}r|frx@|0k88Bctg0?dkD;-g+$Gb*QPtrtxaw`~I(Wv>k6o`52bX>(oe z`LgazEChLJ;w{m)9J;J~pNx=X_I^I%xTtsYnmJd>QNtXPy5IjfneM4~r`lW9nD0$J zzdHNWr{nY;G_B_u9&a=@hEPvT4(uR;F8%jB0po-9s?(c~j#H}Sy%wJ#RI9PNmlD|3 zB@O4jkq52t3|_yDoLr0q7)qs^bLAQ;AdNZJ+|`E5*StU?UNPOFJjP89&}hhZWGzPK z7cmI0{rMt!rLL74qx7P!FZ8Qh>&%UaeKb!;W?o^7SWu63#Kds%n)Iu$mHcq24x<}!h{dhMF9=j65%U3$%u*{`j|&# zk2rf%<&*UQiH#%#gbfmf99zUHn_rC$iieuh*u;R(U+E|L@-mZB?^P@^-RedX-$$uy z(hbhtIR3(GCg6YZI~r^F-#o+r;ma6}`d_bFlAQzqCtbZY&FGts+8d3m{at~Lc3FWL z@x*yP;F1%ZSWQgsjz%o15fvz)m=Wc^;+iV_Bzoq=WU*4jKGwtH+9hJK2E!7%rci-- zz%ELLM2l+=rR9pO%D?n3QC2r+OiLV)(NS zp7a)onBhO7vaW^4rN?6B#6 zxv|UJfQISqphUy>@Hs;s+{~o1zgxvbHYs&4ra6-tLy!cHXN05XB)*3VnV+4P#XPKO z2pzhmh{w~LsPWiHt{Vt>WL5nz;aI`=#-rf*0k}C9(Q4pSvd+Y?C~VA&>V|Bs+WB%h zydGzzoHVM2v}f$j%7(+H-Na4uWZK+rN-eb-HISFe9eg;Osg~D9fi!JGa<2Dc*sj4# z`WrvmzlOU$x59Dw&oJlhNEbcZ_6=F5+gEpm@Whz2ebv*M~pyKg7<%}}yTo-;=eW*4HWm|Vd`naQL zQ^6)%SYNB>cETT{Bs$LW!E|GWvOra7sNq%6;Ln z<;W2u3x6pCWrQmm4(dt0LBOvPnb4x(l-qxf;#c`7G)8>rW0@cAzNzjOa|RJ3I$_Qz zee;0Oa0HBZ^T=bEV7{v}WzEbk7!1U~CQ5pbASPTixFagmm1iJ4W5|{*@M@ zMl?^cD(mxq_^|(<-R{;pJMd*og|R`hkT?uu%`g@v=VR%LuT^(Tr#qP|Z#915=Oir? zBFZEr7~D|TXgErpk#HtVG1dVSaY_LyuMxS60>h*C`7%4LBZ0efo?|NC@h&opE=6+6F25mr@Tsha>u?TSG*2(Ts@`yfugxE zKb`s#hFc}!$+2Y>0b96*JB?0`!VR=tw7s6H+n*TFQ1A=S9nQVW~>4BJt$j@4dMtjI+T8Nt;lx*70jX{)A0wr zqFYveC8#dkuTmy1PU>kV)LaeX4F&2WJOC~6J=CGnsheBBw6{9es8dM@F_x{blvJGR z^fL_{JGtn&44R0=E`m!a?R91mnZOTjE1mugBwoncU(HBekMKP2s4Q3FIv*m)irsw+ zkn83sFpXn+77l}rEwz7^E^34u@w?4ZvId}eXJF)VKQ`yY@Dj|PI(;_QeR5KBc&wPq z9{UDw6yR16cv>vn_s7qgJHnM@ZZ+P*<*`#Et-0eD9=6h92X5L<(>8??U~P!F?6YW2 zCRRc;dIJNXa5u8B+M5)TMJLq9O+{L4R2!p9B92O;h#y zZ{a7jWNr*Iy7+JlTD+=}$eHxToc(o<(YzVBgy&nsD{215NZe1f+)!5pWKdAqNFm(u zw)xI?PxPjdaLM$l#*K)uUV=J;(Hh_0_a#-kcL_DZq!U8fm6v{Dx`Qanjb%jl0W>r; z@EFV8SnU6;VUm_*c$$PhUafy2o!M>d@?ibkK@OHY7WJIBNklcQI+ZPLaVb%+ z3}2rrAp9BMhB0G?qI2~{qSl@5BHFTyQ%=Bzj57mf>cq#Pe+mJklVTy*`1j@$CQ-bI z_`bib*pg#EfTIre|C)j z53c?d!^S7tebu`C(FaKi+RA9VoEPIDhHgnZ$Dn~{&^9$!DL*U(s!Fdee&Sg8ES@sq zwK_5y^p}xY+9Hp|Q!oefsZ4be?3_adQL-hy{B}tX*U*>)K%16StyHfwq7{E{g1&@a z2|oPQCufm*#_^u1ST>vDU`>2_V1J8HQ)((0r>jnTM`@er!@|9TC)1QS^0bP@b8Xnh zBStBHng;FBr~|ew=eQ1{o8P^mn_J`?RN8@Yl&4mr1g=(OkJ<7hS8}4XZyS3|<{^Uv zjLr);Z`#)8&y{)iQ3zO=@;m_hTeR;AfcKRgLs?R4{Fp_jfIzZkFF8VKLl>e);@~SN zRM2L}BUiAJC)rhOe}ctet12s|obnK^EF`13)Jp$x07<4_4g550lwGS;uA`+nNNfKE)9F=LP5T5NEdt8o1%FfiM~ml z^%ot2o`685N$sM7FXy< z$40UZ_uNo|3LD^RYTxW`2=~KSil%!y+j-m2 zx7}3B1lbH#@BjAS+sa1~=eOf+tfmZZ5AeCw6$O;$L_?&Qo8qCJW#rN&v!c!hlNY0Uda zrw);bX2yt`?NE)fUQ2>6ALuWG&|!%nS*&)~{cx*2{lvH_h2DCe!8caDkU#Uw(>w)? zh?|^2RYz-VDl@HrcJu$<^~}HdcG4Hfz1n>w{yzHu;KcurmmZfLCu=!pj8|9y>t91#|y-`x=+g|F=0L$FTEqeI7n(D<${8S!BVf?Od z-^RuwT*ecIqdA3Dbo~th4F;+*6TEFD3A01xU&@nS7xAgrL}#1I71#VR754J-N^KD+x3UlPx~?<5HLd@q7x37t9E*HV($a~blB`w@ zDalc0b!S1IfiF#f*Y!YEiZ!jnBU4a@ZDQcUMc`Gm(Zh3-Hc!D>|M;1RPosPkxvga> zSrX(&N0O#(RH6NjStoTUN~n2wJZ@@bPEz1lx*8j` zZrdGe*~6)WS!}|G;^g(G`D&T*h;Z{+gGgs9jW+Hc=~>ek*oo9I)MaqPB3+cx%%gVn zsv}zVF>T5k5`81? zfD)FMaH00bxsV+ju|yZ+Jro{^04>f~YakOYh=GO%?q+Msx^kA{m@i5Rj)JhLs*({L zUgBmbq#LtxxJNB|pn zKXM1rK%DR&zmHq`pK`x{7p`*(%zp{|^NsWM_2t}#_1~E#3>4kfBhq2#Ar*`q2~J3U zbpAhH^W8%*Mk~xO63|JX7P#$!hM;3AYuy87XFS7ZSPuHixXrIaS@@N+Eav9{HVbQ_ z>f2Zkd;IlxwmJ+e!dO!1SBUZ^OpGM1YUG~?Q|3i4Mc~`_KH;V~hEwmKLgA|KM%PLY zdE^}xrlxI2n8XZ`{cD-Oa7{YWfQb*d>C_1E6|IgUTOO7fB1F}dx(yaBZns{$fbDZ9 zKjNwzDbJ@&Rh!&zACCvmAOgUCwzq}{NxHL6M(rwSjeg+EVf>ZJ!kjDq`TTU9sme44 zH#yuJT~Y@TVDbNWp8r33{|7(EMd@W$6TU}11VsB8e#V#tMN5cI^&b6q%AfYL%8Kpu?$L4#K3$BQifW?zI+is8po{Kk*t(UOiXiKo&>+b#n*##saT0!a$y z8vPLl86Ki^i#4L23Y2$EfBGdZX*H+5Lu#i444F^ZfRqsD)*mB_=ag4o`ipD%pP zCsKAHL+@TWJ8&ZN81$lbZiuOmrqh;RXFXxhc=!;_N92rFx_0x4)pyTF+wYgK`AMgm z&-RU#6QCQi*0u#1TMWjPE>;LjHPQIZl48f0~8ywRuibf0bRKTs!+)P6(OR4Bn zrL00yWJB{XF!RC1i?y zu%lv}LDkStYXW`z;jPF$<`m^q3H?`d&>bV;sf@ch&#mkn{?!Ox%dgeTo%joCgippQ z>0jWKE_i+ey;;FYhNkGlY#~ur+M?W%1n=%l=o{jS;W?U7Z0$js11sZ|BYC?ry&NmtFtNtwQJ6 z_F59yP7)ttlcg=15@ePB)*rV|Dk?>d<=JuufS1uJrLRicGCc^@BJM(qc|$}7;AXHq zMATNJ#f;&CeW8+Y1}7VhMLr0fNb7zLq$_!2V?!_Be7{Ob4iR$7Eid%<SP&MWMPv4=`?7!s+ ziL7TgaW?IMN?_Z>mcu9>(m4@=S<*s{rX;?3KieTq4jnSfu_gS%TnYl{bLA0%4yeeR zx|A%$Kv?P`1b10K8&19yJQYrpe9SS6l!TFEOh;1%iM~x zW^;$fqcdk-6e>M-kC{KR<)Z0?-+Z!=w8Upytb`grRYIzKy22M4 zYOK}L$|8ukctVZj4<1rf(Vhe+A&J~L=jndr*HS9=oidFs)h|B}mrpNl8LJ$LlyJf# zRv@CeV07d1c0uIjy(p3Ei#zGpSsl4AQLOryy4$fe4>^R z(zdH1WjV_Ch#*qr6H0`?1=CvqJU3x9C^J?x7n%FDae;#+O)39+CH3BllBBfjv~si@ z8^epS>q!oXey)KbRQR%LklW(IA)IZJWi?`uc6dl@IYDoh(9j(UBw zN!7se>h?`jrH`9}c>96|8P9AO9{qF>0FfL&0%BIE5g6$}XHZJEmS5-|{7_*h5mLz# zz=X5X$4A5oD;HKS0^ldn6FXeH-bj$A<3^1$x_fc_KfJwVP@LVj=8Xq;cWc~TgKOjN z-UJ$V2@u@f-QC^YJ-9<~w-6+d5Qd!doT-_5Kg^jq|MPz9uG+n;?ylPFUVE)|{jTFT zEC9|ISh#2*`yXd$P3gj29cUK_T9v-tj^F3*Aqbhah3R0}XMVQ&r8*>pPxtlv4C+=V}e* zeeQgv!;htqM(H)=@+CK9i4f&2Ah|7RQegH}#{v+Q*`LcYoe!FT!35x%PEYs4XLPM& zLd6OVyDvjL7!5a(yk@jl(&3T@m|gX7f_dth3{jMU#(ww{M-_l&to>J=L7?;T0mp<9 zV_MHs6h`k>=+Rl${rEpMMv%t8vD_EVBWvDgn{za^Rp0Z3ch8siyTe`^J-o zIUt~AbWyQF%@)rd2?<5DqWL9bX3B6<>%AGjV)5L7%dh(+?k&ssxqCGa#OXCkdU z-h^(qEt%KGPvjsw`umZ3;jUz*@k74urKE+=nCtGuT|kE77Urm(!LN)ONomMa%N-L9 z=e2L95|D)p{w6ebqvtcZP}Se?gCqYs-4?e zar$+FjM?YQPDY+w$my3H+bdID3%a9+lapi!RQ!lNWY#6}ZbUH@JBjR6Iy$lY7*g^6*BYati!uxMZ7LjJ54$Kj8>_Vnd* z8i{RI(ZWwI{_#f35c_+-Ba3|M7Rs>i&uJHibXdFsZ0a+S zf4OndgMoqz3uR&nSFok1S-wkA(_D1A-11g7a%UxP?bNgRqC~t_9Z=ZGL9kEQ{i4a1 zC=yeyN%t$6>y#_K$vpCv=$BUakpT}P0K)jFSDQhn2ywJ% z+NU*XSOx|=!<1NKApnBSvWU_cz1f0t2m`L2346@+=-#c^j8d=xiil1UVtWNKH@ca< zQw$+zg6#-eP_?7b-KhXVg*#E@QX|knzDqUiJigrJYjjB&h~D~c^L!?Uo3B2xrygJ9 zW6@D<%BYx_tWjgW_{M=e0+i5_J`OD}ivCh!uNDonuHkLy?xd)gc2v>yJpuy+^^rg& zJ`Ypwx{KLE@>OjgX4y_86_E66GtU98V3*ElhfK`%W0hqE_*E^|aFx@uzd7tbb)5gt zi+(asxwlF$s+Lz2fX3bYshyB4Zdr~;MU$9GuFZ(5g;02VW_7vQ78CWs zdIo5v!QyWHNTsNThf7i^$GLxjYm*sANB|Bo;(`H&`6^R|PF+!|%XHeb;-iY?=L&Ye z$uTcOxK1cUk|LQ5*G5Sgxd@>8Ef>*u%BB7vs(kJ+CEiB~Sy=j4!onAPt5;r^4vRWP z9bnY-*#+@H$|%xO;X3uLU@$=0lHw@I7 zEa@jm7&=x=%Y_J2c;hydbN#rk+Rk3tuqbbBU2i$>QkLHdI%}U9^X(hQ7$8(UjwlvF zGPVubF0m<1m>KL}7$rJ)>k!ktJE>=U&u!3xu~)PfQimp8PY;urGfJ6+y}Dd)Xtf5H z8_=48O55OT11HhlP_4}GmUlp2BjT7DUM=?*&K37luBSKW@L*2N9RH`EhSQX&&`<+d z=YB8EiN&4kU#?$}Vd+I`N-r9JVL?&X9!zO`RUwMXe{GBG&zVXID;FT>_)cSiXu<$0 z)+eVb7{E{?xsG9nyUtMzfv`G-dY=*wPOX;>=fLx2U+5H_%7zpVM&+wkjW9VoN&jr3 z6^2q)&Mgb!IOR>tRY&y-Ts1;ev+1!OOE*wW5r${kPxHxOyq*l~W5tArrX0THv8pcF zt%y0ny#I~=M@(H6FQRm4vUtn1V7D1pHvO*shT^HQvz*?JDz<1PyAx5{Pj>YHohS}0 zCh;vhWl_%bMP}3(-EaTlX3ZZ&T$krqmlJ3bPm6VOfz3BO_fJfS_;=5jh000WcSdU- z#rtgYJ3nFiT?19~9upXC6L}7-o43S^kOlXlyg-x`3!Lz8Q&tVdv>5!|gpF@@k zpTIWg=T?THZ@l^&L6}mxHH7Dr&V92&cr$rh*weqt~d)#feR#aCA)al%g#D$>_p(X+zg#i zMi~enX?7G#Y&2N)dqq=b}@_et|-#iiZ{PiHr)DSafV#${iH zKu#IcHq-8HR>$Fy#e|HJaxY#fXH0p2l3S(ih~`Q7ZFZCD#kwz_LRX4i=$ zmaEJE=tY)mXe_?h7*{IwX8vlWeg865n49|F`3dheMu@&EUMxkX52RA`jbX9*o~>A> z85N7hm>MZ*GMaiEUroqmjZK%T@rf`s8dRjdp~^1}X14|>sCwi>z$h7_IWGkg%dNaR zuuEQ;CZ^{8-XGow0M71952NrSp28|eN zm=7A4+Y=VIM9P+Nz|Tyj>7cnKh+m?LpYe{3Nj}X&e*xd~!szQ*OUie98|ozjkd=BT zs1LbAiJ)|om9^UV39BacRdrpSX)9uU>aQEBfLkn@T?6W;Gy`r6ONs(T<^Sh*|NrX> z>NM&^e#Tf{1<^zbLsA0x1l+-BUU$)(v~$pChLQ3GC^V|}0bZ8`W*4EO5{K)P!;wI% zF)Jjb4Pc+SP?$~nIZ=f#sU|0izp5rwq3gPa3d;qf79_SKv*Xm*?I3*ig}2J>-2RwP zsUHdIK0>D%DZ4A;d`7LL7!@U=$pa^Cc;hpdeVrY3w#Sdu~T_KEg`R2h3znhpTdmmt6Ltk=pEbavjqtl7uUjxcIlA=!i;mj$Jdcc!b~qY|ke6`%vVw z5nl`X$TqAaO^jt?TsW3H&Js*pIh};a{T4w8CrmLNJgP8`(94mZX~h+R@?xny&h>7h zZNxecLL$1iv7*kp9a2n$^3}KJnpO`1WLL3qgvwweB zR)e2Brvl)^RhJdq-UHT|EI`Q^H?To$1um6Bgys(BmFA=tCJzSs$0#EvuDRtgPlqs-5b>YU>D@!Bk)okuQ%~(YbE)g4y5E=Ev>t#w{c* zxk`Gf>#N2Grtuv<0NXcQu4^$EAQA$se&C|H>xxNax@hc?4!bo-aIGl-5McnhsU}E`L6a7m)hQ zi|<=s1uaQhq)Uf9B~)ThztTFD-z<8saL?nV54+Miv)Sdpx=b{&|;@$ZxKuhyp zN#i41*QIHZDxK4CG+0i(LpCIw{M#YZ6$ft>X-EXA-IkNtWa#=-f@xY^ADu{2!!*Jz zp@dlV^ebR_?rn3?bC4AinkL<8hEgpeV)*MBv0}UMz-j~BTeREBl~T;iiMX-E1#~ZG zbWmfI^r%M^jF|5pEL(aX9LZx*{|R@qVhVZ)2%tB!Wb2@2_yRYM z4z(sG56fE{gZSGYof{DuQJyZ7IY&4zY`L#8N)bSA-|jH%wc0$hIRfLnrhmmTwOuOk z)O6BtWaji_o`hA8np4i4UOU1q%Hb1Vz=>PcJcQ@PmY5uSnZd>?V9zuxKdQA+*n zvb435yGX1Nm8UM-m79$ek<3?-+J&_jS=Lx&s`!zomza7rRUmq~gun6B%4)BlfeFyw0+pQ|^fAfKA_{&*B z>DsDd_!|=-U=A#j3$3|f6A^GuMzbg-I>cFfs>VqtFZMSP!_4SfSY@AWOuZI8XYuvO z26tC;yik-Nxk*y+mf5!`PKK2A|I}GnaYkdeU5tViWhqjwC*mU%){%|K{C7UdHHhA~ zoLy;0X>~yy;kuk);lS*(sKX4=ry9rio{p!AQec8r$s(KHJ$1XH-^N#>rK;r(VcSID zx}0RyM(wMPS@uQdweb6>kQyUN$}AGjwGLkz%QdQc8aj0+<1M5ms}&|ZD&kVDfrrHL?h*Wbv%{bhZsvg3jim3MZ^Ilhg+*#}@%sX=qiS3JVZ#Wsl}o zx&Y-kQNS0%?+k(dh%lHV#JK7(s#b+0P^0EW)E3o~Ks4@W5D$Cgm1D2- z70+`o2|Da{Eqw<>)&^@yiw|6q5dNh^l4aEu_?Sn@9b8zL84_DBA~!umXK2z(ga&=N zdWerDs%wEUbH!Iyabq=C_=Gz?oU7lwTVG9{LCo*-K(X{iS#XrZCtNoGY;B+*;kdAacbfod^GF`ec5ZW!q~5Zgh~paZdXTT1DI zdeVUhcr(J8kO8!_>#bq+GCBn}6eC`99Kn-?JXqY4P zHItDHfdQ!xq16b)6-aQ{X4F9n*-VkJ0U0FJHXZ__sh!p^%odRTMRe(sotRYJ)=aqw zJEfObiqS*NbHtPoaBdXqi;EZRwMrL; z@hG&+wRnc;kK1WSR1Vn6p12xgEFay{NlF(x!_w^o_HTPt^g4Tfeh5CSc>y5+(P)VX)x01Ym!(+SM&HM; zOv|krzHiZ*pPN@j{%>CO|GKqvkh}znZ<-u6h5aP}62yoWQVF$uF(c1=5}t-~-6b_p z!M5#+d|6t4c>05m1xL~kdRa-C9$pFrhPNhw7n8-!ApG8%wH_Vuvs!A!2i2xdd$<>v;R#Pl>fxvd*c`2K2H5ALV3klR^6qN){=M#u&QkxqqD>qik+1g&B zjx>~Z|0^{mihX0ZT{o9b%lgl3dw2J0K}CH5Th{I650A%qR2iCmMkm)tJ1Uwjv?axp z+}cP*N)0Xd0FNFzvX_)sS9A(;gYWq&*vzQZvQE@YCIrEaagyd|i*^=eFqqAyL_+Xv z^9Q3?h|`8YmDhrHK?3!#vuQ{G(-f7?&%iYtiwKHg+aC$|OR^CR!}1_&TgD$sxTwiT z>jri|!?q*r9o*W1f3lR2xS+(5*}_Ap#vO9BH#+akSkx2HlhlOb!BwrFUP*IP z94L{|4RGD33(j1A*(>~>G{3dsw>*`1op*=HaMOR3LR%Q*=n#~@_!fHfgPrdqiNa`S z&^+MoaGb87%kkw{cPeGkUMZ-2Zci0|tI&R_&7pHVto~c)n>OwtTroM!mPO0!kM?k> zljhK9L!ZLo!wYwGeo2c2opZ#=^~kN*=8CXRVtfIy1tyz$_qq7nAw>Se9!RHBv+p|7 zkPx4Bv>SZV3YI=!Dvah+2G*I*(+?q7rjW3FTy@Ec>PpR%l@j{h^E;wEi8&IUI7Lhg zw*-?|rUDI5Txn`61i>#KDUmStaEj;){R%;kv336?4P$DVDuajC)X(3*U9LqhRVEx7 zLbi;u?Pj|rgoD~0U{(oE$*S+3->Ff%x||hyaA5P0dHt}OrIZW`|BXe>qbtdWomgi} zGm`vNJCS;B8-5#f7>?fu;jaJ01cASK!jSzp@Z`VsB6VH<<_SgKjQ{Qf1$ST-TxiYp zSRsqBP*|5vWk^>z6G}pnB3m?n4hN#j1mc6JT=W4yrPT-U_+1=^HO&Jf8r%m}V2(^V zxDVj9wB)d?W~msAn;S@aP=Oqt8CA3@xD68E0rSBg3e2)pv60-FKv(oY7320N z1u>$1`MCX!a0#kKx9#n}kSzRXBE}qK7ok-9rX3gdyaL0BgcQAoI3ucg|Me~;j;*VddLSq0(rPZLmiSmIx__AY1G1cXTPej?P$KJ3_bI@TE&Rkx^mO< zp-ZXzI?dILf-l#KVu}L9MOz5e)vp?ghe+l_48-Jw0|{Q4zXRANG5@geQ$Bs>%sC|D z7J8M=6dz*pILAS$2W3xDT-efQ@xT?K)Z^0-g{{FYNE}u9ka~C5ZeF z)aYwp$}pzFH4QY@wB4%6jw1+Rs=^pIDjw( zXu?}eq=T=I_u6FnspkKDh_O5OCFS=O0pPCxxd3`esX+uVOlG%U$19dEE^$G~lA8Vo zRocH5|DrWDNhl>6F;mF_=l)UBx7`t6rzS$D>*k`pdo1#hjT(WjF?pOoomB~Bw7UCwPD8{=b<;la zmByAD^*a?q6z+(}Oigf>1rtDD3p3-v%N}@|8*;(-O+?VzL6SK4v?WPXqXgA4#`_uZXQSbs`uafi-1p{}t*sHdjMgOX+n z;K<2TQbAf6x8kc`q!Zb)X5BUPCFWP+IK|KGAd6bg4EdwU1D82Y-2gmE>y-1)%susn z9)DyIDwGw06shXtw+$7NS3o2Z40M)Kj#kxwbdgA;CR#Q+pRIbPz%`%H>`1vY{&JS8 zoEvQ1qJ>++Q?D8_Y#TS&Ahf*X*<{%XF#XvJ$(?!P*3{LC-AipZliG33aM07zDibkWRwg6 zussAAnIAJcejA{!nG3WB;e-gd6wt~uT@v2Z{*1D=G1 z+^>`dAq;&GukWz&GV(10u@MdpuY>zP-d+^-kc#*6||^FApNL{w~tOJTWN zr;@5C8hQ@tG|rmcm>xrXO*>e77Gq;bYDDMvd3kf>C1_`6SSr zUxH6>6h@#B9ZSdlCBTAfD)D6BeaNiD=Hkr>!M5gpn`t4e^M3x4{$qD(65>KdMm8XU zrV72d%X(%O+FUh4qasJdy%s!*S@(x8@pbFdZLfBE@eRx8Fl>V^8}m-N)(Pr<6K@&) z?p0|OWM1x0IfKaM89_BLm=EKyS_~U!W3loVw`X9eG?;0fD^FW6R*Rxyqk)GE%R~2& zr+{TZC7WZK4Lj3d@e*--@=$+;L&7aQyl@ZwlD&Xt0Rgu&okxv~ioa*p4bD;$-Yn;oK?>p}F^R=x!Y^vOm0W9aKdgQJq3o68&DWF+;SE|N#2(2~zJk(O`aSjmsA&$j>c zBG0YfS?)tt^V1vK=4f_CJw@6z<|swgT<`u4?|zY~hjxhvR$ch&s_{EwBV6ZQAQwj_=8A0K8Vu##&PjXGDdb?Ot+ zrpP1MLNoMI*)E4(KUEV_r79kZcRocqqgXu_)qtfGlS>yjDT)FKM$U5Ot0b0UH)?Vs z>6~?x#0^_~*=hXdT9u~KIljm~A(vN|J_td5Gb~K6+c6Gu^UEXR`>LH@diioV*7i;A zl*25bemVR4v@N+3Ep^g~A(mG}1XSgrP z|8_+GgD#m1Niyo3!4Bdq>pOKY7R=ydj-NBWCR=F9I2Xni@4+(@{o8?gLnq2~9cil!astbTs zG8Gy#1cJzi1`LUkdxRxJc$V=V>2*fZNbrc#2{n`rM)b&_@qHw9#vhW@d zL@Vc;X2LC6p3E9Uo(ln_XW^BWa6_TQC=w4>)8BP%c~yVKK}t|-@(@>i6Rvgb$ihEq zX}C|W{cP6=Lr-MaQeoR@FbWfpEf#6QP@U=WlCH>2MQ+(Sc313lmhV0Dj>sC~_1s;K zM{^s|s^8wW0cuHRO=;0g)l4%qYjI-N;i|5VipRj2mGM4}=mwJoV@X(sNR z&8MK^rQJ6wQ#rK67ER%d-q5#8l%O7n09sWL!LSUo3>*2jr1Y<^EF1%)mTt}4SbM(% zny+91j1%8R`aAT*r`2L>MBdqoszhOS9>(Z>wI;uxdbi8$&7EqL9KOfDxzx&WY`dzh zTXb{*)`<fG|T383`Gt0bkD#E{BXu9qw%Tmz@a&U9xl_L@E+D5)S<8>U0I(*{r@R zWy#F+$inhxee9FC-RDcrc(#PwRyifV=q9rFL}X*v)qBr^jELfgR7v?14Q-3E>u6)- zoQn@~fZnDd_JK0$N3J@Z4_6l3>m{}&=$EOMh@Uh%)MTtVZ^*0gZg>GmOMNO_hr`oH zWX^}54R_dL9f;ySu|4~dRa=X6RsVB$VEgo!(F*>Cyz%$$V5pd-{XU;i70B@(6F@C; z1okV8ZjDCK$Iqb^RzA2wJ}(wf1JM*sixtw&QgUQrJ^eoCe6@k0$3QBI?1cDn!mTFQ z(DNoiN7u~7pC!|()ekb}t|+(B*R?nX>04kg7oq#*l+fv*K=ieg*&>TxqUffd%b8P^ zJP$4I?3h7k)UCVx&XHB)V4aOY(^;W{H$M&tsm zO@bg{F^wH|7hFan^P7#U^dCSNd!5(#zG9fL_T-KE9JF*6&AHupz zPcEiDVfh=nO2QY*)%>fBA`|B!o68KF@qeyWzdQU31H%8h6?I1IilAMi!Fc~HEJ+3; za?O1N7TX%>vz~@_V0v&Wci94t9F9K$ntU}I6oIs75o4rDzM=Bz?@$Rac~53gQfBPb zDbZeHAD`Lv*Wt443SWDkR$C{IOZ6R~yk`z0C{5?_C6wl<*(y0!mD3(6iq3{9P1m|U zedL&%ax^A5fo67&Jm-O|^1SMkhZZZTUUjXyAlKKmlngfwrUskXuMQ@I1;qIiqp4%& z_O{K_^{Bck|ioO=vUOD;U>T_d7?@?`({r0D; z1VHc}DOwin!%F-K_l3#Jvwt8+_UzE^HPmKTH3((4=}1{eVBsjq$Epf&z%3Zu)J!kh z8w_IDi$H%Gb$=GrhVt@X1n;!YEEQIzd)y2g*POAtMDdnXmKouz6B8{+D(=n}p3d#eDtznk?EDgd8VijBq-dNz z#9~X?&nBo@-Mw8 z2CWkC$8KhzvuR4N8}Nwd?&vvkT52k;1j!}Ct8;<*<2~|7U4L7So^F>N!Rn|!9i7?? zz>F<-{rs|Wn??aqe5R}f?Dt4j&^$?~bz0b|aBjeZ!d^2>_~CrPsk@PaF} zZ0ps;e$QhBa~WGd$6+Y>phM5}<4!X;JFp)&e#(&EU5FiQe5hgox#ee!#$K8RCRnRC z>RXbeT5C{BaQ)E_RS-k>6ZMB-P>L~Zum_9Eba)Ja|Q8 zAyMe}ezvRA6IJaSxs}rXOiqnNUJaluK^F&sW3A#gRMXySa{Fl5)CwAP?O`{Ohz~ zq$Q_3_h<-7$Jn5an!2WaR0h4>8|$x8k)JNxywtvy2!flo4mLI0KBdp8P{g7M*mkf` zS&C9+Ff1Im$_pi1C4-g8jhS_}j^c!;)X8YsOqHLVl%=xjWgAirxUqRIjB00Z`m}V_ ziKBT#{zzqv4`xV#r56~ep7NJN4^V+~=bFN(|gkOO2%0)@Lb|Ya_rNZh>SSxp1KRfPzm; z(p_>;c$;16RvEqm1QqNsxg@ENA=)>MW~1hO^lz!n%o=ue&Rv2RHJ}tOc+*5!amn{b zPZ1GHDPZ6fh`QwA#0#8)4K-gVNK-bBZR0<)8077mQlP`-dhJ_OC6Fv-DvA`5|GMvz zpvNqf{PlNxoceI& z@Y7PMM;QEUO*4;W9TkCSY;tX;9v$PdxyG$9F1xQM^KLw{n?fHXP=h zeSnETJPmw?sm#2JuFF!95FN=s9yU6YBpy%?WT8YBsT|KhX++%(!HzDFG_OhRBlGmr zSE_fkb;jfn;^f+;{IZ8Fo0e-?M zbJFd8Wj)*5&-p>9V5Cko>u@k;rje4}8EN3?()7i0dccIqU!?MtGwEM?uOxM+KSY;}ocRbjfLYq^HcV z&ed7`yymMV<(|bb{btA?8cVc7RWat+ZS5UioZ9i-^k8ZWKFff91s}FZI%G7}_A2|i%Uv$ya*?5LiHtdtB>q)6wr_8fPWyu0_fxGjJ| z*vm#zyR5?7%5B`HgNuoks444&h^K&y2?kkk8;^}LW>(yQ5?6`9wpS;Q-=g)E+emJV zM9pulxaQw4z{flzan%qY8WoUb zC=J4y6{R$oi%=-@;;1*5As48)-)#uKsu9IUpl~^HN#h$8R}X>pR76L0wGvN3C1Ed1tuO#jQaYa| zPp~A~cdgOLbSFvu1x(LtQO*ru9stD%sJ+XSoXdxY8mDoN+!duDMXm8$FQrYmYlLA` zxHcb4=z{RddWcf1N@}OnpG|*gTfbwY=G`GS#IcTf42bYWtE3a%*_|>>fzm}Yna2Ww z9o-i$*)K{rOuThlsV+?XgvT8f+czEvfDR&$CtrM0DM*ET>7^jp0G~CWx)1c^=gmfT zVI|VE6+(yAPOO3z1Kw<>7k>iiI#?Lofs}fyQXg~;HHe8Mk@q$MZ(VbO+hLsqOqUv* zxGPF_j>`dMca1wYgA|faiQbpCd6HaD1PUBT-@KEA9BIO(L5beQqy=lqJR((Kt9*c6 zF&A*!N-bTJ7>yxWO@amLcbUloKK+60h<1*<5f5P1Ol8*U@TPpv-2ISk3uHt8IdUA| zH+5TA?|!%^Su`nXB?pqnnwn{p-7>MstjBVR;Isbn*KZeZZ)k6?Z+~up9RI;`x3O5h zc&{Z50Y|Q8Lk!zX@TGXr6diCWA^-FM)bt;BbH;BG8@eJ zU^-%8xFV_Dt(xFia6#zno$eUvyhk^9t+b|Ef0Innd!+?W&J5>YnJAnF`R&NqZ+^)T zrz1s!icdrxktoS_$h#dj${=j#Y8-ZC?KOklNFj%_3=s@$j$GrE6_^l+#0LKi>d28F zXA0LRhS-RolE_***U-sm>|!a#M2gzI-OGk#*|-|F*P1ei=~s=aKg^M=V{mU02V79D zmvn7FM{Ob?b+*C_Y=A_I^kgY_`Gr60%I))r-FtQ`uxM~f%c#Z07mC&yc1p}rQ&N=I z2%{}-%~eW6kN6 zbxaY5D@m%iEa5Fopd#Uy^nCjy9QW?#Q?GGH|4h$%xIG84C=SQk-P&TZb@X zM`s&jEIJWG+*FONJBC585aY#Yi~X;2e2yWz8JQ(qmuYc^OJNh+oA#MM^kk~5U-i$q_C>;rSh&g1mbh=AgGLy_Cf!*&`PlP@FFWpy(1jnmY;M_`HBD zt3&wyY>1jZuTe4eTt@W*LQNti<_}G7XOgH^4_A0;XcQ$8DF35rJE4p6SuE%=0klKD z_=~xaITf_5;3w(y?PXQ9kpPZ({UJ(-$Kdv>7n*80pb2OSIfgM&8{ogRepDgdW!8S! z1>gZ7ba_9*KYjL?%K*1B(hAkpv(7dn-bm|4X3Lmk-a&Gl{iG!IvUUJ;hL2M1hte$- z66o3ok${j3x|+rSr93b<<~CZCM@&{Bk0n$oS~j;>S6DU5`~*s;s2Z0oW3vJ?JA=2i ze0Bizd^Z)&TYh-D;3Q|Z_JVx~W*G{?ycU)CGKT)4=9)ldkK3vl_m;jXVTTOUnIjih z!<&=ow=1}Ex79BmUjj(*kD zaI`H+cgrQ;mS1J`(@(Ar)RTZS930S*g+23}OTJr>TN-&DT?qWxlo%|WeH>LM^)!_s ztXFBjIl0j0AYiGtSXd2^q)2zx5(O=@dXtr|=zEn^{KA=^$1lRJ*})IWlUb0zmp2Y) zFX~}olB!|&w9up;HKZ})2Y{5bkv}m_-iVkJsop!uF#awT5^&x$suQ3)j_GU-GR)G` zm@*>{CE)roPws(RO_Jvn%+gUPJY@jLJYRN$Q9EBsPrz-K(nQ|e>VP3seB={yMTJ>c zjVevCQ28M%;fhDzvGm&7)r@;8Ll~{Rpfs)>3C~V+wH2RZ^_`M}M}j-=M6kz^oZiCM zW<;bg`$}u#Qbe!aHPTMg6Xp!xt0@O2QtUgJY|!Fk^3`^YpT@~Y^a_P7sHY}bui{T! z4@^S56nUSiZbz)SVYWj&$eqklgSKTLc<8$P&Z#PU@i8q*F=LxfpwK=O_y`kN);O_P zB#gW5_?OJLGa@@~*S)>1-n<#lyxpBGy{q2-m6^i-+>1JO)1GHDrujR0JJfZ5e|dv0 zdTZ>`0lZvZO7`wg$dYUn+{4qz926<0R2p+#SCMFTPoD7_#tA-6rMk-pcF z)FZmMFE`Jb>4GC}8^krX4^6R5T_fDo#ue1cR-j!6|7yoB?5)eU;b-@|<>GEM!8=HZST&tmpM;1u-tyt~=dGSyN(dC(Bfg>_?AW ztBfX2<>52n=Vgao0ZmyC#5%ZvxGBbIGbs9xAf7pR#S&_}=FKP=Ai5C?0zBwh7k#yg z-xxuIoB+R!hDcro6kva&$PgG6lGVVIJVd)J1{`YD0G~J*B1)=ht&&Ux&2I!6G9iV+ zEDmq}f8t zvfYM+PDRn>Te#c3l0VSW60@O;L#Su_04f*8!Vp9I=A`8lFYu}PIg#`Cu;y`eh=qcY za1zIEbjJv0J=_FmZj8eOU;bRYk@1@MD2dv3Q z0B$zEX=)a|S1TNkQQZ6H=hQ|KTyjBC#EyYC0T7ZDs}6dAb%!haNF#WC_Lw z-gi6MEWaxQ&&yspNk1%)MTf_0mXuz?Y1d1 zp~eZzoYGThwBR#vtV`_$twke|-yRBotzfLW6BuQ?vN)TQ#5qTcZ-t>ljhC5*j8`1d zNgd?U;wApnJBVGd{64bUrkHb(z@wRh)|{%;l|e1G9y7IvcB$on4o`PP=*fXU=Flqi zPd%Yf>;3L9WII2(@pnEUZzfAQ`hdBs_FV(@KF3Jio|(w^u8{pY5ruC}nLiAj#LY#C z5-B}qEf>HDEUhM8({tVYF=DCCW7(NS_!?91N`u89k3NnlKdYXRT8u3Yt)Y0ZsB1QG z6=?)Za(UTJrj?RAu$fY^%C^A)Qrj0_>$0=?w0YiIW{ux0O=gQkKdYy-Fj^2dTu|9= zAUAGF`oN)tFDa+FY#xR@Gj8x;Zq2Ks79^EbvWD`rAa!InJXQ7M^k#>V=wcE20kvsD z6{8c@l)dqNC4#4rO+2nsO;ELeh^7klAqzN_FTaL--63x&#tLCK+#Z-9cq>)zwH$yM zJlPrfKTq?2{lf$c&!Ig}mK`sb9+X-1J~=h>82l1UF56_H`WzvgU3B{r+m*JM+W~rQ zJLgQlXjUM)^zh6s>{f#&(KD-C_Jl9TSxAq@Zv4)+cF}+-g8|3y%3^ra23uK*=2X7( zUh^0|BQyFCw2vR0%TwqgIQ4!VTZx2Vx@DRT12iK|Jy<`yq{N2nvI-g`N<{KMbNH&f z?i&h?gc6wag$Pm{_Rm>hjhO)4UGVcL?!WAqeI+`$Ev(ibVs#O&1>yPT4ZFUUPb^K5$`<^Ll zV0lE506bvp|Bg8sH<#%8Xi`AMSO=TVrfP%kuM^*I+U_;reLd8wpN#^eCy~Xy#L=NG z2<>jvZM^nTZ6))ip>`B()VA-{?Y3O|>#^gT`^(2`X%pTnTxvz}o@H~-58)Ir!LTpO zit)?Q73e0gJCG(ZOx%o#&XnQfq5gF9-kOKRwYcXHI2U`|RX+_{`pS+899S+oPwanO z-h3X*el@(@{h;qQx~Vvtt|tLSeO8U>)Fvrph=f{%0lC@14#&ClO#?#{sgF`xh#W`o zGhs45p15vVJpzNR;RCWe(l%9^MPWTjyZ;1pzv^E3r)bb(p9|EfnkmK$@N zY}Ehi9g-Ls-kB<`Ff^M9Z`*ey+U#R~lCT1iundm*DIfQSE)|HyqU@5~t~465(D zXp(LW9Th6`W^nIfwJZ|~RhDk&v%^Q@DP7;{wrOUO>cI~4hhJ0w^;q!#-QjoQVe0?j z?Ja}a{M&6|+}*uMfDj1o?(Xic!ChJ??oc#%i@UoNcP#|BLb2j4R;2dkzxT6e&N*Ms zoOz$U^C|Zv^Sfv6$+dFHy4IQoqzc0V+so!QK?A_HdwN&$l&Q^)HYzgE%QmWEe1GD6 zD%Lw*#gV(|19uW(2nDs!B8uuluc;JIt&(??g%{E9uZQz9;mxiT9AF?H$wv(3}pF8s6eQd4p#G3jXD=m42JD%?5 z>AaZK8$(o&JKMZSShv-0I$QpL&k)eITLs~N6sb*Y;h&{IPA^b!0XWihMES^{)E_hJmIB5pNiaL#1I>yiQEY-=R=6b2 zxQ4U@ubQZy43W`x&utE8Y~Sc61H6#Fw%FBXphu9n^>d(dukeI+wFKd&SVUcSI*f|p z)nlRT#-^^Du3&sxVbC;oZQq+X#=LLLzZ^ryDtDTftS{9g4rCuSU}XTW8NBkMd3r~d zCY#0I6%jyIUyaxz!k~?>PtrWce6Y&PEHkO7JoM0jvZygsEqiKz+0(V#u<}ca<_a0j z{Pl#S^P1}zk$S7Eefl(`0eKbzTg`3yQT=R5kO(jzQD3LFv4Q0%mU-h1klU9Vd6`mL z@6CP}BU20>y5Ab&*~kt=Q5j}4;XpFp6p!hDEs(Bo{7}}|fBHw8xWCVJQqmra)1Niw z9b3X@12rOHM3>DL^-BC5jQWZ*SGLsBWbnth!^;^x^j&YNc17MfRg@RLEwX-eVDFCm z*6hcNA>UdRE1@<5H+=4?qTpPJ8CNXZSq-bxE0Z(!{kzQsZsx)TLqc^Zt}iPwft9L^ zZ`~B(raFatTD#1bevQpj7YHi(&Ih|qGg!Y^*1s@e1D+>{%zEr#%@qEF3A-i+_3%6) zuJ*hG-eUwnVXea3?qfwI@zIT<2+=S|1%ub_Rsfh&d#|@Un9Bjy6|rf|V)?rUXD(RDFZH)d&S>h3Z z`>gL(Oz+7m>Lzc#Avo$p1%_|(M)RrprFF|s2)(Z^i~l=Z8y3-2mN*f-s)=g3C?*g@ zc~Lyi^ywp|mhG~6*k)hNt1hPGwQN<095Jsm^KGit+nf{$ZpVkV&0xhZpA6;VNM`*xQ`A6-TH0Xe^{deXC6<~fx8}ZH|GKw3VCM;sChXWP_;WDz zRwD=5AQ5GHqjB3p>|DRJ+sM}MxZrH*be07A#0tH}3b)N`t-uGfWpVo37m8u>v^3&~ z?m_&aK`*;1dCbH%?rkZ=3pGR5(@K-8**@De;v5 zl9=$qTm^EgxAU_rj2TjW{rVFF+9ytrW>8p_EAWYsLil53zg2FtfQCJXQ82RyF9o4F zuHg3FvDDInA|n{iY0iXT1@MsPt|!Ekt|D)W;7ferN9B_8vP&?DkPq`((-iZ!R=|>*O6neu34TM&v*S@Ga(( z%g+1ak~muD9ij_fE~Oq`YhhgZCt+Or#ZdhMD)Bm7`Dh?Lk3M_oaOoSH^?rOx zje4_LrhjpkuvHBVT=7RCW9PLE@O}cVOQ2Exm#aEcj&p&y{&tkuqqXghZC3(1#*EM; z#_;LYNSAcQo?y1T3X!(~TXGazrklzIEE||*axS`YXkI26W!lp%5(bo1VtO~^i~&Kr z0Yrv;-m)UcS=@TrbfL~W3FeIv2DLD??Wwtnp|ES%0t@mxjMwzf4&pdFRKGkF7~-h= zgQA|3fBsp|xKBqtLIgmx*%QYZxXnd>FY%n6<@_G6LGF9gBOr9U5#w?0amD&d`8Su- zmkm(k#jO47BsGg}A~Cl2B_rt*P${@i?T!fE1RE!;K~Z*!{FM=7|DNZ!$A4qw|GSPF zQB?Eqa_`)lA+=oyjX<>^swh8KqdH+z-_px@ny&;11=n;K^blssNa~MoIOQCb5`BN) zUXm}S|Kd(}MeRue{(4YeRudqBZ==&Dz!a=gtSlKOD$r9L=A9Ps`v^SD^IO02UBeVM46#nF!Hy&mCsO+LaulisC zf_rOmXLeviB`MTqbjw$eMwenMf+2y$&YPVc$Wk|n0059{0y3c>)ToVN5(Aas+JLLR z*b&Yc|QXR?XB^_zU{ct|u(*m#As%8_zmh9!;M#30*I`6V`11tPET!UPE9PkOVj;6eD_TalrG0?FP7}3S7%Q zvEcQ$zw2*y@h8A~Tp(!%{uu3E_U`z{wD;Ore#H~#?LMrJM!up^uR4Ew4L8;$h-#oC z9{BH0<1(h!+oyV4PG`4wlvIaG=FVR8@QosGokjFcO$lUykDYZ;zK&6LdV{IrN?LSE1>K_MWO6cPqso= zRjut9tIlOcCJvi)>nu)lovcnYwC4dj$vd*whz> za*9B<6k$}m`=t^RTHHkY0md_42jXM3%B28`=_7HK1e;Rlq9(q9FZ?S(^8TIIi|Zk2 zIp7xM_0raAOgXd5FY4goimb(D4VBu7qqH0P_qC#k)*Wx^)PU?-oJaD(ZZn@}vI5(3 zCMtMO=LFCzRmb0!teiidC^_7>iD+@QIPR?WSp}HUe;72P+=BXAahg{k$>TJNx|PN^K*sx_pLE+4%Qcf+_+T((fA%ZjL`L*5Zpbwmf7sh6Z(M zqOYBfmWH!TiwskMJ}-F0K19uQGM=SX`S#8=3XAD4KS~KA9@a z7_iri%D7Zxg=foq?_W4{%g3!2xRE3L!&EZCj?P2zabXhR_6BBM|(| z(NeQGZI{tJ@;1%be8hbAUuk6Jkt}i3pf!iG;9+*jlau0`_RTF@8{(t7EUT~9@0zQ) zTaB_cp1W?%|5B)3oNB2|=^kZ=XL%l-hYMg{B=B==mQu`zGBY+ADyx=0urKglJ*H`ZK#jOS&#;g| z7=`#!mBKt0AIYAcP+Qf+m>`&gZaou3dLEFQbz=hkrlr6cz&Lx;!+k){uxUJ_nus2f ztxA_E$_vZ1dD^k5Tps*(-<#c$U9&@zuOD+FxRS!O^Tli*OfoJb_M^%&x= zXu=VDq%R`MM(0kXP3 zRLE7^gfkp}^;LJMai@CQ7moFx*3^y%zxtuJJd4Q92fB>bylc_D!@I+rq^X3fA%{KVehq2&znU*~ za;pD5J*`guUf=h6tSe;;$HL=2CEn2F_*}Obaa}Aw7emX(m`P&Ie$89b(crFvU>nd-oPvm3!Cpf$en>A z9Vyom#MwR{FV)`U7RFX^hei2eMJez420~DxRZ{-`zJCKk(idUgrk-(UwSFLBL9>o< z&ODFFW%UUipFzbb+iHwm*H55lmvL*+Y0lPMeO2-6rB|a>S~HvK1LvWBtX0hYPg0T# z-675BL~=^K#CiusSjFGESK5>ge9|aLH11``$H~jmV+;Z2HPt)|B`(-C!&zE=Snl{{fRa~CqssqMK7sk`V#{t_H3^T83*7d{B7SuRsMdj~ z?04^;+s7V|*P51=Az*AVk-R^MJ^cgJv(|>lH!W^St559eptzj9UB|3#Mk$>zZ434f zhaHD1IkcWtj(wdr8b!0`nQ~EBbKNGfa1Yq=_hnCb;;vP6Aw8a!KPpvnlo9=|P||IO zwrjU2L&{cFX;XUNlhNaj?p(>Ka_U%FY)r-^Mv^Qx2ceB!FTJ(|jluG;H4?d8^c7wb zCQZ$fcZvi0DJ+f>P_Qi~Lc2-f_8pQp!}$DDkYu5=vc`DKEXv_6174JuhLcQ`BCUVp zGWK~>t(5a1S4n6_gINKJ%~!L$;Ut`7F};xlZ1C83^kvZmWoq?LDf>;Yv040tE>0MB zN)mf}uWq7v;v32JrLYL9Mo)-?#$R(Mk$oVb@3Lfi#=j_xZ@(z%1$^g7lIGJa#n+GP z+_;@g{@U%H(b~)fDxga3L2O-|&_^Y}?04cL)nYsjR^vC5??r2ajb z!T;kae-hZ30UnCWklI>s-=%$l0S0PD7@s*dD@!<10DV(c>JnrV#(~b}rki2EjM&Yl z^aV=@uhpX)6L=f_4vYH7im>psPHw&IRQyFL{23a@_I`u`5VS<93s4gInSubGcJ?E& zzu!zrW$|&=_9c6)>^ss{TP&4g9b=wJirbE{$mt2ikda4lppp>Zutd+$! z{qnZ{_%fhxb9NSqeQo^fHwM<*_8?I)nk~w;WMHmS{G{XEO_*PAYn|HT_E^IR0vpRA ze59fxiQdDp1$&hO1`|e}Ff$TPMeLLW?JKjx7>ixk);ZzSnZbC-h;+$YQXSIt2E1wR z7kXUP14INaU6qN^;n_)2~{af*R03{!gn6c-Z2- zwMnuK1iC~+bq(y*vvaU5_)35 zpI-K#+C`JUb9xccI8>ies~J7iCu492lx(3GWg7U+V?!RPwigbJ; z1B}g6XEbJ6u;OH&6~uD;ZSkJ1)z`XWbj zD89H~FaX;Q8%ILcTqvu(GLG)}8f5+yhJSYb?Ow9xH)`L+yB1fbj;L<`okl#tC-M6@ zhbb{bw!&=~Ci;7u2mfrBBq&zFA>|`8^U*0W|^M#I2 zYn^g)$W(4LUH`QLh5U}bxfMCDBFr*8UEo-c4aEb>m1oY6%?{m)yLjcg#0V2IQ5|MU zjufCa;qee>;!hFVVE9a1nZ?s15UjDj#j+0ddGYXZG`=&6(z}oMYaT_MUVOOE@m+PY zoC+BWAl;#Nam~719Gr)QD8eX`@1Y#kCoRdE6Xik;ddtUv0H+$~Gm4nC&Zfdx?Z@Wv z3k42;_Cc}Xy)YUKG@ErV=KIP+j>4Xo1b47uhuwoRqGDEh=e8!pP!frBuj75$p)50% zO=Tj90D^NOVmtrG-3&ac6t$&~VyU$zbApYT1FGKT5@6I^;DweeBQGwgddb;~%iGC9I_1c4un<5rw(cHy0yvO#tCWTzeJy7} z_VS?@O+@G|f#x8!O+7lJw%>@R3qxknvvg$L#JmpA51XAksDAk>sSoFb(qOIIwp8m^ zVdj&kuSbvSOq%M?=#izf*rE;dwQP!C7|RY3yXRUnPARzkI4nj!c5y6 z%adQ<2<6eT-~0LiRYH{9;STEwQVNmu;&;7nIwOo6tM<8~3o2kXY5sLDt7z~yYWeRW|1^sTDOHkxP1_-HCw%C$Hm z{)-Md9C=obN8%`Gx@ZBtkiM3Q+`HCX?;unoagNy2F}lKZ9)Bg%9fndRpe?d#(1-Y6 z0?Lgi0PZ}yZxoTHKUoq~bD6X`D2q5Gc{k)c=`vwEk+xnrl7v*L3S}D1ziU@`xllrR zYsy{9+o;S0UNhu?JB@sz+wB`(mSQ^VahvpSB|)Yi^8U+Lx&f}UB>dWKYGx)B&SRuK z^VQ*i=Lrd*^9ML45FY>elW$Eb>#E&Tl-&Y0wOJlTmQ`BAJx?FLBy0!moW- zH|2m(EgF+^xhgzBHtk z-vHt$;N#$v#xZgCh$=cNNvI@?AvQjXS*jULb}SkS@??JGEiCns(_XpfubHdToG7yD zsJOqGt7AmJN1N@Q@OE-Ej&KZ2NnK+JsB&~Lkg;OSO|lKTP`Db#(yiok#<4%gMgp$I zr`pfOu@aV6y#2q$@&DB4PYgdkcYf>aQ$FA2Q`xctQ%)0PF@LxHSfUuPhRi8m6N^cg zp0t0UYMovC`y&orK;I$t+ln01?OW)Iq4B|xWPtsBR&${R{HeYHoUUu!WPgqCV$nA) z;10(x91b zzr0y}G*zz4oVnAUiQg4VzUKPsPxR>oQZ&)l(xwoI+JW-)%T}k+mr#GCa=wObHCJ7O z-$49M#Rs3_nOIstYkgH0o-1SeCn?8GCFJmst!P6ig5x{=BY11QWJw9HUbV6muN!li z!b5Gf6-}mOdH!h{>qfyS`p}-CWTWn zlmTQKj6R&$ZICo{xJLPTdLqyj{4I(JN8`-h#-c}9T5&1{T?XAJa-)}MtZfEyUyI0Vv zf3LT6`c*&(+hL{Vrb>1x9&kM(VjHVtd9V8>Co`DRiQ}?c^#sv{U|U2l{xevl9o!(n z#ln2|qmW`hJ|RK_ah~(`Yx_ys`j?X&DRu|7MwfEwRODgl;&;E*2&e-(f7o$_0=$re zj(b0ehiK1!&Vi!G_u|0b&q_^*-}Juw368-_j=$FXHMwSvH;w%wA4DrKyYP2AMbuSh zr2#>2*6vyH5zowt@VVNS@!PD^hxpLAgI!Abo=CkZI}<7BJ`Gby=&@IxpC)n`jWFa4 z$pi34l90T}Y2{zFgO#Da9lUnfn5q4TpRgNRU;ytY^u1|0_l9$p5Rei)4D{5%h(jx< z)$wD$)5`pWvt~e_fk&%^e2~Ol%ug4_Glta`(HQ}stFl`=mulpAVm}Lj%um7S-5zB_ z&-QblUF;|!d@~X*KXRHQ=j}g`qop~pu6mN=$FU_k)|crlt^Te-t9~VqS9K79ZQ-KI zQf8r973NM1)y+#Gu$53Unf1cCtH|q+G|0VF<@s1lzCQrpi1WnP@pY5jc8hm%w~@_? zyLIpZvqwKL=5w4IpA$daaBFwvu%M@rVpm^5;#F85byCwUfb0^2gzra!imxST-9nVX;<&y@v#*lo0sh2EK|B!AlUNc z{qscD#M2+S^x&rW#x*J8>}|KX6aRrGH1VwF+T~4cagQqjU<+Lo!Fk3Kj`BBwGm!dhRIWU2W zvh3S+iFA5S2JETj<{%SiT>Bs;5tq4KNclZa9#Co+}<3vc9I(Dvt= zv7?dpS;B+6mxiUsicqc-)t6%EXtz9TDbH+DZy1Y}Sv9t#d(mKdRkxXHdN4vb4mdS5E_*hC#_fjhn<8m!)nQ`}Dw@SABY@&7~YdZ~=V%EdYw7%2{aL zFzKmH<}DmkOB?!TCuuC48&T&A{BwBiv4tbEWzL70;6rI|(#uc4=ZaJ6L2y7Wlb-Bl zYk1&18vwbVS@o}^d~<4%-sKaK1yZ)PS;`o?mTPgm6cS2Kd29JjG*iDt6fJBFM&FIY zlO~$U)Aze5UHw=jy3~xo2=e*c)x9eovhj#SVPoAl*1$r|X2s?|+S6Z);~3g=X1|9r zOx`va|GID7{-(uN`A)wL7*a|UFW*X8Bf(Z0>r&}*e%Q44*6PFd-d`yk?oU$RL}Dq- zUL(<_{R0?8q zfk8~NRfKP6ERIi{jjumwZheAf7eO{(p4efm%c6@vecF>|Ln%dCF<~w(zLt&Y*ZPC> z)FdOYWQLS7+KMv!CY854fC_caDvf|WmI3aY$HD_p%%$Q7i;bf`s0ra@gv zITKrIPHUwZScjt4V`uvD=X%)%EbrdQH~J3%c6CyiTW2hg!2ZjYj}{fS3*a=sslDtG$$j zRnr@h?r1;96mtGx0()DR#qqUen$k*CypjbiVoh!eLvnoaLTNY-@qBR7p|&O4@Ok7h zM)_%#oZ4Bw=oHeSx_^HE{v|@->&ws24>zvMCNJ+^k*|981}(-QrQ-|{9JtT^HPAsN zL@uw2SA<;mig2|bL&!5bSBQ+<3{*k}7tIeVE1LLhjTLnOdA9Bs7IZYT%Tcw9hpPHb z2kH~R9e0hHJi{fl;IDre#DzKhYS92#d_cWa)jl%RfzMhJR!$n$=cd*$OsA(CDA4i~fB)%*hWeX*#+c3+^rXx9(nN8*UErREJfh=8i`deYV>K10f%#bA=}tjCtTRur_zefENSG-W zC+|LYAzJC7j9bij7=?fJuYupEmo;2EWVQlRDa_SAxE7q^FvB+3Vq@$}7+uv{6XZBN zncEH21Fu}wlTfo$NDPk!i}_fwU8L`1=^yf$a2B*}ApQA?J-vo00n0(1O zZ)}YJPfWm6FgJkjK)Eep^`^k{gyMO)9wS`KeGu*;n+*4m?J~;{0i?A5t#s)ZPxdRh zIC9SaI!vI)rL}X9wsS;rqM8@emESarH}dJy8|$;rUeN>Eoo)CB#E+Hb!Z zo=Q}klwoqfNf|~lsH=hfOagSn$XD{sa7OqXJ#7=mw@s?emw$6tPFo;Kfc-T{8kI?G zCv_vfOuAVAm|B1DjLSz`sfRRkyzVxNvspMzTfK@Z-swcG$-$5=qV*=ME(TA&>eO|V zVy(e8e=c9o5GlReQ*7~|WBtZcL;794`1cu$4J-q=+wtU~kHhtaLBRg`+uvVae!qRp zRzQfnSCZ+W|Nl!@dPY>-N$;U?Cl9sEedlhHO-of?@4U z{Km1t3N48~5>}=fu+9X|pQ{Mg?F}BTK@OfulslGI*c@f@k|}V{g-HymahPI50_Kp# zlBroj9KQRlBpOhC{L6bu2leSCrlUe()2l{U+Rt-!tI}F-kj=z7TMBe`4UHf96XsY_ zy(0UbkGW*vV9JEpGUBMiqDCHK)aOfbzkZyl-!x~`yppnAmRl5Ohc8%8>Z*M`uLiC6 zc>kGe=OH8nbp6yC@K**=4FFYh&j8Z5?&mQ~Tq+QAaxt>wg%9Szn%h}q1x8Z~Vvvlo zXKm9}LJ$eiXmBpcq$Qr*ib!UaVbPQ$YiU(MXk z)LRldDNgtL#QbMkL~smy3UMu*#;2 z&y(e8l}yrEuOLmx7BHuCH5O9gd1oK7OTqIa7Wz}jx8U&{!$tePq)F7sqOy(b?W;hx z6L)Kh#FuPY9g){UBw^v?o4$r=^j_MyoQMH>I_jms^A7z1qHeQDrv^VV<>5#wVq{hJ z?C-O0*1uWl4Zfj&MW$M7S}iSRpTF(Cra;Hh9Ye;bJh6w799?-*Q|8PJ@tRs`?al~t zDuR}<>sWi^G)#90LOWF>v0KaROS1DAjJGwvf3ZgS9^a3pshz7kBTt*gQJxqPgQ8GZ zsjQWUWVbL@2oZAlVsWWB3+pb-If1?`bK45>mmfytXt*)KD%~Pws@y=~spWjRz*8B-QA!rX%{!;Tt^8#NL*d-s_I5J`M;BpcL)u>rn%qfO3u5fgYKG8zQCX({!t0@MMX~K{GzTjpgzr&(111I|UFs z8NsGxaKXo?C1&tv%_lzesVGLT+Z*jLOK?%=N?7~+HPI4pv;+u6AxD+U^4;lF`tSDm z36t%gmgHhMoJzghFR(r36&_qA+NmV-+&U?Q1vy@0G(G_}1UZ;69#v^&&frx5Zl3G> z>hl7Udt;Ble*Q`;h5MJX%=RiMSTN7B+ky?cnk$T!at%y0iTFQ_!yR5Sa|jV%wC zd(e(Y1&Il!N4)|>nI`-t&5aF-$BRfbL7SlFlGbu*;!pEl6c_wSJV)5W-WR8@$SMDN*$(oGPlg zBgZD9a^1z{i?0o2dwr~WGwN|Eg?|fNqYL@O&YoqHoXP_Tb7D8@>OtKKwsN59&-xv* z+{N30vt6fz($`n%Syj$|n|H0FH!Q5S12HB;`{2F@eFyn?YtZoEu&Kkm%e=Px&?!8{ zj+{725uL^9JA)f!3%QRb(SodS%csfOm&OYOW0^JS|ItH#Q@m<%!j;B1y~uZXVX*xw zs*Js5!FWmmCA@Q3dw-qY5nb_G-hO5)%HsfL3Q;JUE=HZ0YJO|i5{El0tzHVD-+>~D zW;xs5@kUV?LNHQ=F8H0NfY64y_IK{aK^uI`aL8soKfRx2Iebvy4uh zvjESnnJ#hU@KLY}|A%8nfS(c2^>*^_&p45reqY*+v3Iycp7Vv=2*d8dY#`3g>lMIU-u)b5*HFQF$4h3PfHQnPF>MSCF-p3md%hKvP}c+|824<;B$M zX#Jo}&zlY;8T)M#twBScVypu9K*3mCu?WoKPN;wy^H;bMd-x$DcanTG|Fr$dHE99W zpXwtcA3=hewTPqF&To^)SC}-fkS2fv(m#87LSA2wVeM^aGq;W6Nj9-pfU|ouud!E^ z>sua!(B6qF&oNqBe*;I|Ee9b;kwQ6xdRN+Yw?e-^cNGodYh{@k2=Uh zvP2g^4A42<*(#qJA)^&wp{xBx!<^gx&$O};7fzQar}fqx7rAM#?2vJ(`sm4d6I8rY zrX$Z+;i1rU1!2${G2O`oMJw@Ec7wK8ecm{!_4FR9lX>}lUYes5BPxHJE2mdw;an1q z?Oj68P~qviqwt*WVmJO#@@(Zn)v-HCVz=6qXH7xPuF_9G=l;rMdESd|GWc@yd%TLH z0#q___=6_hKeqG4uUu%v*??Juuxh|w2b^sdDN=a)L_LOj!d!f$aVw;vYx%#tHcSVt z-8Oj$F1sl6EmVno+$*K)`;yJnNn+1v?_NJQgc|%e7ZCqC{)s{PxKqsDos1y8>Zd1U z*4!HfWSq=k%(7M$hSF{`YwlFv(^JgiB1(itFn>_f+m%r`DOiS%bbqU9nI+zW@~4fz z9yWGjpXTN&}PF1FX-V$q~r6Gs1?NRJK7+bsv8QQ zsp=9ES}Meatebr`X^2i{UsdJLg`lREI!JMH5lfiTHkrGZIvVID{fIWF7tH)fZ|p%xHU{RUVRrF1?R?1&#?gMHO{B2r^sJ zUGW;#QU*5V=YzFFOgk{nh6U(z*xv4FRRRIE9VJXQKJu4a0dMyY_F6hW@Iz$3S*3EA zxXE2WP#oMwP|aK`MbYGbEb3WGA#B+Y9HoSJU|`M2(hFRfdgPA|-JZ~y&laLurbeFn zu*_O_(h-s`bcjIBsR(Tc04JvV1H{p$@g;=&JL-jpxL6)s=oxh8n%NP*+-SbmlQ}Ok zvO_6e0Iw#*0xN zGGv3ZGz9lG7UZLac}(X8%0(q9c<(eOGY@(1z5ay>3WoX@qOaZNW_msCINWL7;ofj? z&eF%)8hDSflb&L`Rfx%*v0Z0TR9FCQc7J|tamYy&KTA^L_kk}7xJk}it)Ptw zl#zeyal1a9*84~~yNtu>NAub^Ust{6#Df$;n8Vow!48KArp`uAw*0Zb*sP1a?8DeO zQlWYhnf59stR^N1aMWGI#^|S(F{3}S6*O$X@u7Bw`J5;rq+&K{`6_#hbTcIUs$3>5pfM)n-}qy#0Ag3q|6c1MmHg4yKOU zOU#9KQx1YJ&+@A}URRSS{m+E||8ke7aN^o_v2TC>p3WH}h;*tS8{&K$VmL04{waxp^Z3w4ZMo?{ZEiu2z{er*cK3xXG(p9wUXWC>|l3YhsPv zhvQDcn~@|ZjwuBz?o2{}Rs*YWLzNz?jF3ho$x?*rP`t9~d}}LNlACZgcE?}AXY=3_ zJD*8gR7gF<+$5<@_nw-PkycChP@(t*xje@15h4Sql_OGuO{W`o<`n80NidoB+9y!A z(AVmwi@}w>niy8;zvjpw9E|BmjLF|s7t7!o5c>sh6VjI_;NdcfAykyh+DzX3PLbrQ z<(0e?C>cs^m6W?%lu-G4wPBn_JB^b{3}^PWR0gDh=*|6@l1ZgYFZ-?Pcvb5CcRFl6 z5_9;W&L^Pu?eh&P)8^ipq9Fne=3LvS*Ls*ilbd>^rjXxZ*o3aiPyn+nKLuk5Lr8{H zOgvj+gRuJxRFUO+WS`~oO3C^u`8`v}dv-^(>`@&Mq-W+?gk@vYyhu-Kx|{&Zo6YDi zc*OuHnpeKe-m;S4{UMDulnO()fi*Yof0!aJ% z%}<4G#<#lMw~xHvtIeWC|40*3WI-u4&{KB9_9iJsX~Jd?vcC1jABp%(sn1>3v$k7XMG} z@CD9e-0={+OjU%I15t6}Wh`gUNlag%VFFiWBwwQqF01}cyD!0W(iUI+GVU1#l^6ex zk;KJ-EozN~D!&X%S@gJ5-r8a@;UgE*s_0O&Wjn~Hg4%k>jB`X+r)36Rq?h966r6v5 z(kj8um4mtPQbZtYVyTwPY8F$Y4?!q3e0E+pSQ_~zh(b`lJ!M*mgH2U>X0>UMX21m zxi0%s{YHC%Rb2xAJ@yU>|DUn{C2aru-bNy63Q_y;!#voOO?;d%^inauvKuQdY*3sK zKEhm7Zs~GqDOWU9se=h|y@2*~rx=-j(U&3$h&TzQC@G6B-i)_x^3=eQyw;$30ggz* zbVYEN_~Pw^taX3%B%_P!R=1gMW1RO}MRFrvQ`zcx`b$MMHg8Al(dIOEt?HX?3*}it zD5#b4jOLu!M?0!yEe7j?t&d+JjbB_qX*T0+7Zqw3H2g1VNR^G1Wy6z*IjY$L z70UD`)AH#v&Qs(ck7|XZp_aK-)8Hj6T7Johv!PXH@9OB~o zsGA@Q#ZbZ!@AMAPJRZC3CU9j9I&$r?D_(NmmX1I|@R;t-k5_hbXAZ&#v~9Iz&6iY= zy)H|*pbozab$GDI6FrLEI|g|Wz(tZr=YN6gd8(dW*=ZkW0xuQlFlZZ9OUQsgmy^Tx zo3ri+Z~8UUWwguW<_KmXxAZPI?@InKJcN>@oDTX4WSR4`2M$FAt~i?N;4~f=`wVRh z!-03K)8JtidVUevx}2IQ3M6*jVO;JtB04q5<*61`DcE@@tX*j=wNHzjc|hIo6ZqH26vy;(R(`0$(2of5YAvz{8Yy8 z>K)Vbjl*WfKYyi`pcX^CNO@&(gWVhMXdNM?YWSs?vO*jt3O)yI3v^CO))k0RCU!YS zo=?@(?5#8J7M1uHvx)yAl&D)V6!8tTe>{^A6hJAoeOZRQ{qKW>|MTQ&77_cD9=jB| zA)d3!BUD7zf^%^~C_RgLYfRjy5$cR;{<(5)B0eSqU!UV&v(VmQ);@2p#Mc?+gdoR* zu~OII1fJ)R@H!&d2IkUq5wn^wh1AuOr?No3HM|?tp{l%I9f^0hdc@@5*fc%@GmX0f2 z{-99wg0_B8QL1_KqvUmw*gNEYKac20cFvLdN@O&~Kkv3mi;#q*3AlFLXL zw|l@n?N=LRqHSc1_G=(Kepfymhz` zm*F6r8iQXM1C|v!%)C0MiT3C`I9cG6L9y8(a4d^&huQiLh%s+sA!(zu9ey~ld?z>3VkQ8day1LGrC zg#A2^$ANOp6zgBR-79Q))9^CSGh}r{3oswOdz#kd(4GHT2c8$}d@Ek{8WQ;!mp`(V4Xs}`!r6!{^Qn6gjn|1)>Mj@hgFKx4( zu%yvP8@UWGQhS79yj*vk(gf~rgu_lobXXmP>efK>B;7ZL+`tVMEzW}X43h_AB&{jw z0Yfz&A@3JF2|0!;O`Ys5EWfY3_$IN_i$C>>Zj>k$kdyxxKcNiXWBh0CF!LY#4(}QM zvv#lr)%GXDccAj@8*1P^#$B8%jo9%N?B+HqT@`I!x>g=(z?CiUd+5I6#JO3D9u6K} zL?)I4?Pl#Tqq#1#7HxT+B@f0N|2{G@tM<h1;8s^Wut9VpAWZgWuGAjF8PAm@{= zaY251pSr@GzB^(N7Osq%IR>SNdVker@q2h6%@im%OfKu)HD+uXO(Uh*i6f8AfLZ{Ra!oylbyVBi z4Jv%pG!J!ChSJxsufl$O$dxm6J8!Sedyd%rXfvc{s5_X3)m*&Z_l;ds=SyAVlpJ}^ zYJ#4oQIh<+d$wot>?@P$rfZG{denKc*K}Rz6si zo~9&R4S(bza*K%#KVSf2ojdC0u+8MV0Wp1yAn&bW&STdLRLjCQh_-@qTK(xM25Yf8 zbeX5En{Hd?ISn^QV*7g~Jo)f~S5_-b!!1zllqRD!jva|HVN+Eb_B4yXyYnOGu1%H5}cnc3RC2K z^BY`4+i>Qt^K)o|bG!!)-?f=O6tfu`rSxS&YC%wEv2Lph#jS;>DR*-q@`$;sxJ!ieEDZR_{ww#t%aN zB}H%$7LY4bh{=>KAv<=Zez5R~v50K!oVSQ0Ua3n$&LUSWD%y(?b8CQnz-GX34Q0JH zZ(Hq*!uSsJ4q!~+V|yeLiDCpe|M&M6|6m5b{k!+O-c^=96}+lawfn3jz~_do;Ku-L zD4#YUyPaJd+{6SLyrc^wlqg8oU<9LDTia;M{%u=}qp@(PHPulXZ|>t+lf~#tJ!d@2T+ein~`YQD<$Ho4d@&7!Hw=*i7y4-uPnsD~JNNMeAu@=a2s7 zdH)*Wj9^!>pd58e!Ll@=(G-yZ)z$rVkYY(~a1NCmoew}qNzSXcWxCoEwFx)~?8@>9 zG#wrM`FmEV$Kn@nbH9eD{<8ZHV*lXZZ-0Jt=yRfojvnaCntoGJ_=!X=Gr*%Qd?1I0 zXZ)!+sK7&n07gDHGhwf40Im;L!V}I&LL#U3GeA26K#Lo9(9Wl4i_KBwH5l!b%s1*u zaYkoQviv|&29??IdJMkIv#q91L=QRxN5Pa6x}zi3pm2Yohz!JBB&MN{iS%R;Fyp>e z!V@x96`Aa56d5Y$ASx}CW)5r2gn~yS^4=c*7bS+u4@1wu{>1ht=MlCb0|7ZgCYa4_=;l1mO<9}&FK1G0T)f>DHGlaJ}cefd+ z(u8;)%fm^LMK(7~A}ES1LHo|y#RHzH<^xq65Mr}0bdv0)zIkNEit@{$(9|<&`U(SL zl@CLhO>1IAYC~;=E?dlP<-p$=K{J%}`~3$F^{WH*Q#j~`ZVPd64P6J*s<+0J;F%Gp z)Zu*<@)Hyey)Qb^T(9Os_>cX4bG;%U;j`f^sytu$Q+G!hnS=iD4J6e$gxjZ(hdNm2 zeF_hje#2`F^7mjgIIa_a<6 z9d;x@qY>^J1dPSy4tS;2xiHtE*&c0PrHHD-W>V3F07nHXgvBFq)zY%$GPyp~4Li9^ z%g@V@w(E^@a;~vYJVaz-od$>X&;%0{5%a~J@-IXdMq5Dqnh?qR8_~o@_H7}e6Lc!t zLb0Vi+Yt4ZjI&`PV!450;jovS9HoB!+(z4==sn6}Gp1M$)ogRs9{ zgTPw{?+p?wlhJqshcua1qgQPdw(ja7fP6&@FDGM$$x83UPhPcfj(2ow#o9 zwg$fNraaoJ@Obp^hk2~>K5h5|mDoS4ILE`dh$+EU%59-CxYF+F zjlX4&xRv;ntgiw2lC<2`)ZDaAbI%MsebW@gdkwVlF;x~&aX9=FP`y9DzpDOy>OP0m zhZO5`-|>B*wT*~Qi*wj*koKq;8|=RO4(7UfFZDTT{3T+()(Cr`XOeR5=2iz`%cBq< z4@(3!2IQdjwg<1Nxp`&lH-&ZKb#+&J!T`i&>sMRHZ6W!)#_yXhPwzhe>=~yS9r9Zf z#lak-|GxR!(O_hond_n{!uZ=kl}$uLo63~{Z%?;XMQ&P>eSx4+@jv$o7)Hxhh<=A5 zZ1~jQIH5}azv6^?Hbk6&=#_5Xba;ArYw)a=8zrvX=FoNEMf04AVn*!cM|oX5hq@X^ z=KW#W6ANrnOiWM>mEVB~SX^EPw~Ji_(F6X3EZLKnm1rf>&qZNVpEjV_@RHy=wcH7Y z;S9&umDlOYD=aYrlv0(NBg&tm8Gl%E)scwmgY@Q=-}(%_oV2d9$pn0Op?29L9KW~! zI(|Pikg3quPcMxGt9~27w3>aFzPAnK2=|D3DZ3^82EK@0Xd16o4)-QMjo8;J_Yxzz z^o6y!Q=j#Jps-#lBo>2}X%raw+?5X5p#_&k{Q$ik==oogfd9MgeaeP=^OvNO6kbkw zg2kjC(~*Y^^|ra<7ej`cAE1snfi)`Nlo)04qMhJ;L4B0rB2C}S)k)Z`tEzCrFcB;B z2qx=8!)u#g6&UkENwYp7_Q^`1U-1X@6PHRvlV49o=Wr4~B?NIry5GM}ZmjV?o+|H( z=C|R=#m~&hNP|UsX=TcW3kk^#mkpulzcEd9e9O!>R678a|Fvm^ZOuY4aDU8o(#WGD zJwpQWtEE1U=5YB!y7jK=q&e_QO@KP&hDTctF369R?cTbP)I3`d=7q6Ki_>cs?O+RN za;4*#(NH=F^CX)GGx`9yd0e7|SAmf#+obJZSo&EhLRmyzvg!&~kwWM!kEb8R6p<8f z2a~{?Nw|Jrgf*KMmvJhY4#sPCaT0mZhmfL6B*uRrE9R_=CsJaNIVx^$%l5b)dnZ7F zq0v&c`{*cq3Rrv#Qg!X|@W~IlyFs?LJj}zA(6c(E)2{Y0lHfk}Jv1wuWa;{)+BE8K z@H^yXSw)BOXq6l})u##uuo{zdq{m+U3dQf35yo2Cfhi@?g<_(I?jIvwJ@79>F2k%H z#2Icc+r=&`Wcx>6%qnKtJkPw{Xx89BGKP*=rzubxVKUGV`vl2F4NFZz;Mk@@5=Okn zadYaCLD@S=p=rIu5uNBMUTnrdVJUMGKo(ctV{04=*Cc+{n}rm76o+1=02~L*&=rdL z=8`G5HmQ1(idQV@K_{eP{kHiF$(q63ETZ_)O*U235j*lN$W2~9o`X6Gk%wqmt@vHX z0TIuQ2pq>lK=GJxLj`QS6#jQf?|Q!D7BnWn<%P&py|)~?+z(Al*9HuwN2ypGWtUO~ zM8LQtpbTE>i2(}Vth*P_TrDRb0kt==x3>H9nbfY&DpJ|oKRYWh_*&V1@=ck1k^M&h zU%A61;y!_5zu=owp$R3MP{I8KeyM&rl#oOuY6g_s7nlA( z?#p5VL1h*bauRyUk_-xDIaDHil4?a_J<#18f7mqmFmICpKwbFYm12n#Z1c5y8D$t$ zm_Q+0e{>;*CupyW7GeP%Iw&sC;-i@Ow85`OdCdeGDBl1GvH0g=azG%A>aPw)GY50? z%lrVPm;sm z1Uwe2zm6I-6rOAZG1MA)k4HeYCF0Jm7vZiR8fx#c0^%wUSkD!p8{N5O+NpQ#|0~LH z356OtK{ps|g4uXMCFU5>yQs`Wjx0Y2in#^Z=%mc$vGEv3Gx~4=tS%-B1p;OUzhsfJ zW*Rw@KC#G#M}k&?L(P$bt)z7HBpHJeHT=I;9rAk@G$sc)KE)M@lFpk17i7<`4Q(Ov zX8!0RnKx*_)lj_(aKynIs$6{G$tAgoy{uw9ER`Y_xu57(O#9J0PM(ctbX8I_lLQN8 zz_H2AMawFH1SpZqKoH<|fQL4zC4(3sr4nbTkGnGAN=;JH%=!6YLCtu4>b`EN`k?FW z_Xbn5wgM430k~Z0vJNyGy2aQ&K@f-BC(5`A!Hj#mp1vn#%I^(sOH)xLp-sbc2^j!h z+Y(2wmpb6rLl_pX5`w2SluUH)vHb;C)5{#gr-MhOq5t*r*@*Y|B76 zS8P?+fq+-2r5c4_=(Kr!3s9sDM^mUt)iWEXG)>y0l5m^WaDMK*=JAp74>1dMf}Y>0M>7kwsbn+1$lHt;SL#GNux2@3Pe=8yUjP8}=N#U%wQZKv22>u3 zjWz0}4*@-}Gri{Nop?GW)Z+GSazg&t{Z$){1rOD#&u>~EL2(ke1@#su{+VYR9Y3(v*f7WTeq8$`h8meRL!hRuF~^`cGt zTb2jJl7D~PMCO;{^l+lOCj2K(K>6=G#&zuR+`l|VcZ2_~MZ)1;e>qEIOdBRm^|HbS zU3HaI8N#X#VGNyuh0-!w#c$_H(b-m$1=7m0e?70|=TVC2?lexJ0F$~EjhA%vNgI5N z62T$Nef9igorCwY`=GfT(N*>FZX!vZDLqo+OBQc3yb>p^WFgjW;aY!=wNHj}QbQ?N z~#d#rLX_d;4RnA4Q!h2_uZ|IYiza%c< zetdR9f{sKn#Jy7VfB)+L`PG=-M)!tGRXk()Ku}3n*s}mC_`H@i0_WgJon2yzKqG&KhodUV>t61J6vQU_{FGN`PUzu%&zSOjWtrPt-VXv2 z^z_>VrcZ2ZORlr`leUhW%MRci?p1@p+@niDSoP#-pLEgz?PPSTNt&t8Ml5u1gu1o= zJEn@FGdoXJx6&j3GSnnoeqr(WF#-~%J7!vO+?<>7!NK7%k|j@cJVBQwWw9`7u};lH ztYWJp^<$5>MEuFh5pd;f;VNfF7q;A8oh#OkiSP98k4sZL0fr!ns<5zg+qd>PT*goh zk#|YdQ4PFtf6Ak7_$PvqjOXkzcG zlXRNONUHS*flaa14=wo+Ay3-m4QxWom)EKeu`k3|b+48o3a3yz+)cJsFKKSvB6-R6 z^HwN4HHS6Zb%mA=6E71xneH2eMC$`AI;M$K^;C^}-mGlrqeCiXaI&1j z*1Pivb(vJll!nEcEp}zXI6N(aD>cbmrdr=`F4d5abDCS9peIYI(Ho~PCMQQjit=fo&60~0YILhv&PH}DzLO%91Ni`HrC_{3-l zd^(}XM<_VGBic9uXc1P>pV6O0@KPTCMRA=wqtk8`&@Z|{r(Ewl@=zPK|KJijGMF(Q z=_}NBpM)n`n1^ao&^L@*8k}rnMON^Rds%jQ%e(n&J|+Prb53taFZv;MY~=t&R$obd zcz%{}gd!%mY@+zwp}h?^g66&Su3$`XdY^_)9!>C8bVejdf2mroa&l%Rr^io5gD9(b z0>@*j%eW)HqiclHptIsCt$sj^#C_yf2j1Nu_J47fbP=^kgtvJUgy@y#a>+wSNL10J$BGPKbhxW-T4Cij!z2Pc5$7 z8>u6aEQzhQDmzPyM8)X<3w~|jSQQ{&e5Zi1s>fjBuSWs!u62kflgj7TO~NSRh0b(L zp=&ph{dml?c4%E9Xtl)LOgg)jozfM5K&#m+EyK@esxgeDXxd{tG!UP zcge=NYbc@!1*yq4WqLQtcPyYZQ?-yn5+FKb;Dov~3O{$tuCR+9W|Lzr&$nw^OW{n9 z-;RDbT}Z2(mZQlmn`mKs&)r0rl~vRgh+f0HuiWl-(RKcK^Wi@)3jb%vpR60r2z5w; zA9>)&)qJG$YDP1F@NXy;QyVx14|DXH1AwniEcXQfdu-&ml$en9tR9d$EjKnkdO8NF zbiU%LOK6AtIHn|bnkK$%)`=^bExG#Q3#Yw8Ok9|(u+-x(EG{>QMceGAH2QZsoZ8Zs z8ak$|_;62yevBP`x^u-s`~J77%oLK=d>`L(^l5_(_`R4_)1*Cr8Rxmku}x=un-SDe zVS6t=2G;~NK~hE3VRoABnr!UZZXX?0dq+s^wB)QNAye1Ru1yT`U9>{ZJI;>Jw>92F zNXk~tQTAJ0T>d~7M_(mrYf?}Wv08;;o2ByP$R9@uH?v^^h+A`K_({Cx-C3kWrT zRS*LoqOp-Q*$h8CdGZc^H>RU$@#J^T!QU}FVgJEU0Cel(x2G$VJ zFQ3m5o;yWZG^EH)HZh&n6vkb8YkXZe?=ADsiDgjNT{1$EqSuQ1`5!M6Ibwy(;9%oJNpJ}fIJ&nqNEt2IRuHQ* zgTg-(l|IPwoAR>(H4c`S^=QNl+&jX$xb%!F7CZv1*A5ZBhq-%&iQzn zJ~TsEgS3N_T#6x&gX(z1ka3(FtWBT-*1fs{FTwky$wFG5(a{4@w8WM?Wg;!>k|H+V z;o9NxU*Z?U%g>bV#*5>y(p>A895t{%77m8pO?C7{s&x|qZkbXn2SuVci z90ZR3!Gekk!6&7Xy?3+TH*=>fsMym^{z?^h6?-#TA-{C5k01YkPVIkwfi{77dyn%1 z{S8sF1vVui*}Ulm>n{x8<`?|eSAoe1@X+{c=QEBMaz95gAc@YtiKVhIT=FKdsPbGUpIwy}sxpfsU} z$WH|Q_{G7J-4?rtSEbFE8e>Runw_KR;xw6q*q_W#_jM^c6`eJ@e}rnQi5M?$UbJEB zJ;VUTT%I<1ZU;*w@9~02eiC>?f3ZMxs8tU2M0G%A-&K)lu=c`ui$#j;1$tj1_YNqe zEgS9Ci^TTI%kOj&IS?Qz+3@fCw0UD_*w{QuD|H=53%QEIl$JV@m=l>v~azN0(rva>AZWM{|PU#=ebA_ zOJPLuG+k1O&G_l$Wbv6WHazX8l=Uv3sW35r)`cKf2A7C zy0~N-y$YXkn+;F_M3cQK34v^@!n|k6Rd}>{vYCk2bMdu9P8K?9=}REr`MX9eqkWng z>&sVm=^Pe@APWKyg@$$dzo`;vgA?mnFysuq#aEm!wH22^Lv$x()nu5VhhL4`{Two* zR170_*1y|OzfJk!@W-P|bHe&^!qccl!!NIieP;UgB30bjx$z9%k(208n|Fzk{#2YJ z{v%g~tbd&U$sLk(7XQ{ACdVEB&XvwfMR1nBa5T8X;eVMbNNp}HMqc#-bXdmCvq(uF zJ8cw5&W_*AlMI{SBFVVJTN@`6y!e;;4?Q=qw=-O}NhX6@mV|P`G0=r+ga^H%w1+<_ zKCeaf)I&%Akb!zf;2N}m9U`qC>ZVA6Z)vQ{sm2?$jzxh&7y5lmk?Np!T^EaPy;7@- zvn**amx6P|fCcc_S@HbZ&7%PI>;nZVZt%;pE8*mBu|-FsGE=&@bWzeF;-!|lIuHmU zDtjA|wCx8P^v*MJRZGdYu(>$9%R;K3CqJv-E?&Z`%1QUfZ2Xy2yC-h3Gx=+-;7@ZG z>6~{uAO7=N@c-Hodhz+`_Olb!YW6%xSRi6A6`l=4Sx>$W zK!LAH!nYB>TInFBN8|g7fDob>@kfd{WvPr>*alI><1wD%jSNh>2Gs7*!oD&@Ub>Bo zL`%Kln;yb?#ym_{jt)-dj8dg@7EwoMTZq6C)Qg3Gtt+J3>? zd34@XujwYg)b1A|{OBc+C+_Y`sJ>Y64tsWQ^taumgfld|yCQcT_ol1UPwOW52rPNE zARJJVeDUHW1zBx`+EspCZdW)T;XRfkk2DY?@yczJz50d%f(iVjiR@h9h0hc}XLq=8 zYC)a&=)(M-o{Yf_T-YLzr6 z-c!T^n&Uh!#MMHulSu#&#$&@Xnqw93#>AAaPz#uHYZ0CZtx74GwPmni5E=?6?)5n% zwjmavQ(F9$(Qb11b5dtXUizRS6mmO&Gw+H z;@TMNkCv`H^a6&wmttM-CcryGB$}ilv>22U}<4Pz3y!u>N-6HsvD z#%Q}wpxKY(|$C@8EJEr%}Jcy10nJ5(uL-5!oMraRgwBP7APg>ILzyMdeB zSQZl5TGaQL84eGJX-$MRUcBYDjNp%W#+;R=X~9`g8aD)U0|hD5>L1$d#(CtW_O|$vw31il(d4%#=0Tn+ zb_!Z_xGj$-0jPkQZM0j#$~#fmGAJ^4K;kyh+Ovb$!G!lSpT7lAiTL#N{J+lafA|@) zO3NP?uNEgRzBpwi+7_$i@5a@yR*t{YJ>UKPCZ<(m>1x-)k~wAo>C{ftggVagX2T;u zaP*S%Q(MkAg0>V_i^qIllRqx*{Iu_yc__O7ND6BWrVRuQZ2w#=oU&iS?e;L?BXJ;Q z*8&U*zjO$kHUo zc4%F6mL_ZIfR`=(sG-L1E!RP=rcRY^#^9q(A)edx7xdW4$kYT4Jmg*T|4`C zzg4OiJ4^g($KR6MJcA^gTh5H@iJVj0BM{+52KJgI2jmGt-kKzzoZc>aEq8bsSBCF< z=gOlUbBtRCO;~e<>1Cy%YDK|pYg2ain!MRBm}Y}gsuE;xB^r{QD|Xdrqsg*V6r86h zK9!w|+iJzc9OuY5OkYV(%y~&^qLCSOC>`PPQi-dTa9GE@Uy^6KsT?+2;q>*5#2Sf5 zbCCd0n6YBW@X6pW1``p=z!s)e-q7aO4VJ;uTKp8SDMR*}nmDJcP_9b5^m1#5t2DWY z{)EJJHDm?uuJjEz2&KKQbRT)5b{nDg{pf!0+q3m)<>A}oNdapMV;X<55|gEks;E7I z)_0hv@I+x%aUP!Pq3Z>+I-`hZo{wK%l5xV@_^CKV6PgHBDVD1^7Gaa^eL`U1dc*bRa+aDSIGH1 zd}6()fJ!cC1?!~Ojp1=wGrtc$YzysCxn%`V{!l90G$w))avwdE2xO{p#LL7YjW)E1 zJ}mB|XBW^FLr-)C(C3%?RXRxWM1RV^&5T#Jsj|h=sQ6`v_zn+FeqEW2y!h~V+3@H7 z{l}{(uG(eGS4jWl4lVYJU!K2ro^bsAm(iNPTcq)~?vM>SY05chMrPVb@Z!_PArs;51nsM*d_NC=rMVgm~%=(VP;V#qbZQDFiF`Ru37VZ z<5bsSSM9cOeZLb4`JqJ8LNm!29)n`i7ap!l)%AAiCeDenk;>1s&cZSM=lRv|FNEKo z0{+7%|KGNIRQ6B;#y2iF6e441*X;dnT`I`fG?gRHuqU8Lu~xb-vydx@xdKYhx7%^5 zf@FUq9p@s7{$Q(hlxBW(DR%myy~NCfdVq6VzjzQ0oD7dCN$UWvQ$gx;$3)$Z-X#RLK)`Ij}~o4C$HgOfSlSc zk_EpdnFRNfnzLM_z8r+v&#U$!J;TzIKeVDiN@PltmK>xdLk@HMU6@Ra%tP>iOLfS^ zi|vvsM{A9PWSp9X!GP+Wc#dB`*DJjda+l%uq7g2jQ_A(V9DrWWWE`S;nSU<2>J#}~ zjd{#yqp?#r)Gkh|JV%5)e&S5o1R!OtCV`Ix-=C)PuokWCTS)6t1A5j_k|SqVDeJY9 zH)`47^Bspp590dUL?i|oI9pnCN@7ypQT!m*rVN(QdwV%(;Tj?!!J7io)t7Z%T}*3;o(6?YK3;fTb|AFQ`J>0dp5i99v1NqhN`cjiy~ z`UK>$_QFd^eS@RffFWKRrxNu384hLw>jM`CKfh&+e8_X0xE%D)@lC8K`NlD?>L})) zG#wp})kH_h=RuY8J0foL(JE$3fTVW3t_X1DS8TxwR3!t^0@o^d*`3*lIVhL zw0~PWvab%C<4#>Cli^!jJGe|&{O;93|0H%IjISWLvoeqT%Q4}RklAa>&t(9A!#MMu zN?K3WKI?V=OFOI&D(JP03+CEon=yKIUP*)k1xHIgF%z`j=0iny1n>dF8@4KZohmm=ViAnlPxR{dW%34x-<|PQ)b-sIU3g z$C!!Ggum=HqAwmoR1S@KrAcMe1ebGap55T)IN0~gaIP>UG$d}UW>G4xrC2Uq{VjZr z7_DagU92fN$XvATu$4P?s;Ttg+|X=NF;F-v=wO;pbujpAy_GwdB?Nt4ygm>7DY=SX zJc@usb?bO6SPtu(b^Uy~7ySbYj^L0{-U9yvqaEJjj zyG*K&K!_TQnik!Q2Hktd??#t5WeP_kRCPbuSIiCfUB8;BG_V7E{_SM{PnMV39*6)_ z@rx?|4vo-Yj5P>9bPOYRR3DXfLRJo~RcS$%L{L6OGk8$4*Zyp4L2lq3puDW!3){3V z^LHzI8)^cqI-Jr292G_{z9l})ExrO-QKsE0M(q|R*F@=da&y8iKz(^Fci5wxNo&z^zmvZM#d)_ zrSf{pI=`L3A+%1~fA@k_gEx+{p$>zKoG2vC+Wz#Li=u{DyXi(%yED5^RplPp#cIg@ z#zobQ5~k6g`5rJ*DU~~dAyY<6RHB2C{Y>LT$H+iYwmKBa&{3jpJacmm_6pE)HfSJu!B zwI_Vbv&Il zFUrbP$H60WVU7+NwF*riNL)$9P2jy4peVp1_VU*s|8v*Pqc^vo4$ri9K@+$Kd~L2n z{xctg#UDesq&n7TdH&Ia2LwMs1Q91-EC20_%yTi&u0m+SFdV^cM&u6A4UaJdXDM$4 zh~X8E!)j^PT85iIYvpNq&E{eF1znR#lAM}VhavpP+_}z@IXT75x79Oo^d~oo|CC|> z;s-;#Ti1SeA@q4tWg2F5g4C{{j>=#|d0rc8Hz*)6ULdphh7FZ*@h!2ngNT?S#dtl$ zSam0rB{ow&NH`70vQDzfr`bV*3-y@jwF*{%%*>a;#LY}ri!PaV{v~~)>oaE-vXt_L z0%4<3!ex!w-9gCnx4Wk8Z;~=MRu;}LO0E+wfiDhmAqWWP=EnAXtGRE0>ve(O|7wBZ z-=*Xy7AsYn5~B+XcYXMzLIur zfl&$A^zm@#?Qv?w2?SQSMaimnJ%MM5!E)TzxrD+M*R6|F>#LH4Hlf-vanrE;B376a zIXvZ93l`MCqr<8y7dI6ZB417rN>6!}G}sTpx4{-b$(8v`Svt|El@11I>VXLeZbkVd z^;iw(p3z(XlrPm9A#_tOgl((=H1_)A4UaC==Cihb7@qQNzmHZ3!>6!O{TMTF-l7R? z-b_<+Nh$?kWDL?1;<_aY#HIzahMVl|xaVo?VZEoZ;6a{V6B*+d}y z5HgG@@&r~7IX$cu6?A09!*%TdLlP#Lq%E$l;PKI>&c=-iA@xgxE26wqNJ)BVr6b-Y#eb=9_;&(h`> zlckkx!99(8sIpU>h2RvHk>3V(Sy?B{N-IjKK zCDxj>^sysf$pHJM*+ci|e|eS$bQcgz73Wn@d(~ghlAOVRd6pUx^Bw-)F%o5I39-i) z&TAl}kaIQM!wIAaxOb7%e|xeNQdVL&rSi%X-O4R^YsuOjNLJQBeuFDfjy0rfd$|@# z{CkcsOCmDspZ-(N?xH7>``z_)ve3Kr6{wP zP9_^uQJNCG@3bnZ9iKNq%#n@vlX*`#9yk{0;N&1@Vb z98;ZXs}j+?zj?&YM~nJPAy6#?Hw z-r^Yyv_AM>e)gJtdSA}xFgl*N^$1l`7jjN^VpJZrvMXZ+D_h2d*B7X-*~%m15P~rA zF&<&72lLnRiW9gg#B#UIqYRo6mE3k^ZV7zGrPloYQs7=un(H6h=zhJlKQMJXJ5JCk zTTDMUSBBfNT+#&TDcG)Q*N-kyUtx2iHl~)9T+$l^fYm=%i#Qi4q^RMV&DDb2(de z!}R5VX))X-@A1d2@UnP3Og@Od_xm<)qy0=hFBB_w;UiT&BVe)%p}BM#+wF^TUr z5Lazl5y-UBtW=d$nvc`u0TFpGNtcVy|H?Qi7ze16);T^yo&}15@-DMtPa*S=Tt7*v z+xNY+=;yN|Z1Hpmjj2Tv6u9118Oj=Ev?Q#79u)IS}YQOHyn`2YY8bC16G>Ek7t5(|h_0QX^)^OFNepfs@ z>L9J_PiyS%niP4Nljh3C_Nm77oQXK^t^m0iL^|t$SN2bwp!t`xG~v9ajL-yfmrn({ z+6W$FM>pt1ARggkghD+O5kAJkQpGcWFo~el7*UwALX2Z{#ALFl;^Jb7IKv-y4Hgc- z_G0$CoS)eE;IHsV!5DNRO<)KqF9|gfDayX_#1b)ca_Wv5jvujKR;*}~mCl$NZY3gf zMppE`9sH!1+Id6~KjiWBx|N-se`q5#6F~hSP>jwMSkEKdi%GI?XOMx zu;c39!YRK&F_d;U)QIb*5nz-U-2R-Aewx$JEyxN3se`B>tyEf@oz2BPMSXza+V(}m zC+DrIa<%c!y7=P+DGw#alHzFqA5n`Nst2j0-)icca=?E;5K@v|CalxeD18P9WgH#h+}Xh{i&-o7VP|K_qSte%TWy%=8##La*TC#++NzN z2sdW2^H{Sor`2D}G(<+KjCWwIR2B&?)dGbj~ z1AS`|D;@nkS^YUhGU}H2v7eDU&xZlpbQ*y1edjxE(G~dPb}r3 z@fKnpM-5*K)RI^+Qi?EVp$1`u;o=jfmoYGD90sOq9nzApi^nb7ljv#{3@_zckD>!kA%qoJ4%z9h}rB0tfhb1^<#NyYF*lUKj#FZyw zBk$?Y&3~(Dym#uYDnMF9k`Vl=AIg+sNgE`lXWW>o1AR;w$!YsK9K*T3GWb^K#uS%#bmi4G+`TNgzz!qHFP2T35YvJ#uP-y zmvek$Zra%PK(;1|YjJ2ZDRx8)hP=4-qdGl-S_n0mF(e1jO{0K*+kq-~^`U4w&OJ#o z?iKtYs(y%Ab*1i&K$Cbpq1*ZRvcF43Dg4+D?+<15g#=W@@+#l5X2ZmRwX9p`Hz#}X z$cMSi`+(o;GS$VRpW(5DvT0@0S+WDr+Ge^L=y+3n43&X408q})LAnj`_fG1W-hN6<7#8Ee#ZzU$BNq29- z+H595v?EpIKWelWa5DXfH*(MnTl~7Zz-d zg+UF#J{svpn@9}Bl42>&@u|wLbnti6Wz|xZhqzbg9Gxx>5pmyQ9UPEjLfs_CscH>D zg~}pGw9h?3+=*rHDKSz5;}Ep!UE6~RDHO$mLgTK<6UUbw6vO&&KuR5CFlZZpQO}32 zaRa4|KjV*{%dUZ%V~UBHJhE++hV{yIh^QhH+3eP(IeiLz21S>}#w9 zozlc*SU=h*o{dO{DB!HX3_qnfzUWd3YDkUwIx5(A6%~nITOOX}Wg;k>;K)+YD`!q{ zcmU2k^-#LK_?@2070ZQRb<7|Taaz9$?G)EOj2aiPx!4ZxRcLowiZmef-pMei1Z^o^M;ccimIha@_T_Seu}Xtrb-ec2VA=H z{3N94^3ot^)1kS;Dij1sZJ4lzLggD|GC)L!jdsUY`Nzf0_ z39^F<$@(DSnueD&xr1-Kq&KtdvybTKB)M^NQ3Y)Mf71yf$j-`cXEpYCN1POd33`Mr z(11Nf{jbsHES$G*o6lvINe`8EdM9Q}5}Yj~aOxb95?l1(6KnORg7G%57DO3G4jOUR zj0_kFOi?tM897DJ>RUV7rGvlgzHQfH>p z>uQihzs$RXd5BG?^iWJMPMm(jgXGr8xQ1*g-5;`6n#8r<_&V0GEvLHV{>;#f|DRCU zekw8ULzSG9-~{=1QG{jY!0sMsTcE2@r?jA>kijU^;yE!FTTUOy8mNa;8 z#2(SxJf8>|S-B=yJ9fyktskRT#!QKYDzKzf@T$o09X%-pk1;7xQ| zTBy(I>7jO`S!fdCC$$+A{6*pIp`(R{%mI0@FL6JFZ?-2cqi%^}vX=zOFI*qm7w?XF zr-xR14pMWyLyp8i{kZ7wdzYY^9Bow1Bb1ubL6M_4WoP7lyaieZR*FS@delIiR53KU z2!88g0`qkg?xZ2@(!{bi^yPgv+qQIJ+?eChb-!dqh2iP^zGA_6o#>T8{V0e+1H(X< zM+xXb1dpOJ`0^X%vjcWz|`+$L=*Ktbk9fKj7lt%8|{B6>tMm zL;#~rSkko%?m_|brDS-c_c2%(#6)8;D%wAMso&z{j`OJ@O-oIS+iEK0beoA27LxkZ zj@CyzYNFFZmDEPJngwI*@c^%A<86_wk%^Rk;yM5*{gb?6g->?u(Y1J1pRuU#l#Lmh zUebr`c1C|jOE-W=2$v87V2)ZNVZnJNH=?p}7xg(7yhvsp51@KRtu%@H? zw+Ct*)xR?CNvn$rl(DeLhU)Ct~v}?iN%i%_;)ze(9Fr zv{dCvJew#PmptV7+3|z>ST*T#nxkb0`8|AUrAE)yBGguxC4q1mvhU~C4h?O)tqUKA zybWkp>c0B>=}U>Z6_8ChZ)2~f*9YO13>P%hXIXD{mnXBNdFYEtXdN0fGh@kE_dV`Q zoQ}Bu=$(56W*KNz>%NQkGt&88@S|YUW3X3Y)ye*U3UB}a4=#d~IE5FI8pmKL@tnF{ z_gweR5EWPD^C4MFNtghJmN%8e5JT?|>NJdsQp+M)CVh*VS4N7UT+)>RA z;>|?8gg-0@`hJ%4QI*|97m(lS-LJ@*)1fd#%)Y#c2#?gnXxKU@;LOG9~FebpCc z8Rc+T^Okpv&(DBXE zmG;`UE>8y8Kvgf>>mYGmSYDUDVo(zGjTW!;u|V0C(Odh%A57QXDg`eR8(T&GtPUJ!M*Y+ zvx?*4hiVXEPK-c09t5r5{5ZdNqu~eE2O3)&dXIzeTkn4$W~@ zPzI07rE)pMlnj%PlPFjdEO3SQE^=vE6w?zUF*wMrC?JQX?O;%%IPmw!)A10c{`b9r5CqO(Sv+?nfazuj~xZU(lrJcRWYMz_I`KJ8V}Os3GP>HWXE`3LtolK*k&o5tn)a403FJ-eM7J%VKZQu+t)A5l;Ii2bgm}+SFfaZqYDZHpJDch zfuA{LinuuHQJ)rx_dE|#(KJEoG{v_N!>;4zVy#OJMrMdq5piH?rYFI1HnMUK%uvf6 zV<^rnq~XKK&kp`7i9V*x6BqVpy)|vY%_{2qIfiQZGM}cvAS7{qfyv$fM6kSb%zuG2 zZ>RkOTbC@+SjBc*%TNMhbTpE(8p|eBcruA0YVk9dCV_#7EmzS57E%Zt*wL?HvY4!t z!)ED5L~6QcCUb}(t22g;jEhkc!oI%Bae%=h8OP~(5PVpqBtK61GtN0cit$_%#Jr;* zp;RKhskm-S;&N7@X}}ss_e^3YVolcIt7SRod5v6gnWhdU5VFw_Ye~SPPiOf$iC++W zz%*>Y=4KNmXePuF#Da=s_PnrD-+=_V)NqKY7XwXF#Vs=JxcdVVhVk=DtKJF{a)kL2 z!p0HZkkaF%B=x*@2oGOeI#7c;m~x}!p_#>l8Wy>C?KZga_;}ON?wxN>uP-LDNK(7g zBh62wq}D{0CO?-U3I3(3q!)tjGOOiFX9^%zkWV*}9%^0Cv?vIzjRq{OU?l}FPETFe#wMnH1q(c$31a&BpsdOS2X3%%R z*1|hh5qN*5db6CceP8d5o3}y3vm#ib3NMrnLhNxhjb9}-{?RI>82_%$L3BksQI_n02a zG(FWRAM0|2-g;R8e*6lk0zCY*8)F%J6*o3z97YrB8Z1fEdvlP(KbCn;C%KWMW^$m+2_znT_ zn$2WwKJwb}_p)uu{<2#*P+wgfJ?Dza;S7fuoeBaLY>bpP^L>WoQsM5EKSgpLuN$pZ z7I*dMM|8TKpuS_Df+|LH7%09tN=3_0%zhp*_qq*hna431D?H{Mi;40Rc?QMK_oU7u zJ3fo!;-7V6AoLFAdoD3`8Kj)0hNhW3tygRXTEU;*){lBJ#J0gQY;5r$hrVhrZc`I6 z`*gJlJ8R25WiLY*-%j7YdM94DP0I7_u0Bo`i(23HD)7_4o#+3^@)~h9$I3mV}usFJ&B8sNwcXkX|5U8KBW)rN5m)(W+JRv%d!3#bTzC@>bV$ z6qp&rEv~|rqbWBxgk|&HaHfml-J2bXi4>8_aOI_pCPq4`Eu3#>_l{>*#fB{iFWYe= zICk9P*7l>@;smEf&B70@!icCL%v1{`iKcxpV?js5$fZGw)}&*C`k4jpf#&D7G-59C z^8^w6&&zS5)q*!#`z@e!vG*Ug%#L`^-f-AdJw3KP{rUF#*VEGt&ASss%&7Eq^Y-VP zPGVNVm)5puL{a$2esaiDWDYwt&qPDw?%NZ0cP}m>xD^V;A-Dy1cPLt* zrMOFRr?>?V5ZqmgyR}%0OOX~IdhdJZ&V0Gg$bTjul9MMn`^h;uYn{E;{s}7N&HjG; zuOPa$&`hf~h!CD~Xpy|vY9O)A>sT(4S)KXTnqE>*6Xo!+P54fAl0yXboiDb+#T{s~ zIf;L)KL`>BaCZCpFcBI1*Xp&n4PIN#3@^ZO9)U*rY0?5zC`|D&k4wXlI68A0Ay%}# zIN92?sd`LxYu>2T_Elag%hDiE0x(n!(aC5Z^$9vP3>kb8fhxW>`p5`}ym_c(rZ&t+L#v41 zLQNN)-6YVO8=^>ofD zg5y1I{fib}F(@-dp5VHi7imHTbPjH@8*>9-B9HkscP5-rz>oo&`mmwfas9-F?3h>w zTa*D7``XN@9ljmZM*_W@7<}{X=PmgQPMXKOu73739T>#4R#ymE3h!RYQNLT@6@ZM+m)p?LgBl9@757M>oVi6n_8buMw$9j8&r z&Vz$szldihK!J_wQp44l?t)^Q-3{8KLc68PhvXu6N#2U+@mt6#?SsCWovHx0u7>kl zH=jlG=To)r-_IxA56zlcrfUC^$p2p%B+2jv_m$YVs#RDV5m?7=%&>SUC3+}~RH3XQ z2xm)l+#m_5JsD+nn$>DVb&HHTh1|E_;Dbu#==v-7w7Skp~LQR?PQK!L>@pWP% zay7W=YabBV?$U}*b8~@9-^}kD)oe`o#h@1-Pk1PGjQl9CC@mrLbP~l%huirhCG{U} zdftlp!EnMi7__ZvxbfKn#G{?vmwVL@vk$2Nh>E zucg5UW-2`_pKYyW{`v-CHjA&sD=u^26RoTn#lne3d(~HCY_imeY*S#wrBAWTQxrpg z7&N*ZW4{`ed0d6=&z*~+O=?87ETj+71TVMs&F+~A3(`RZCQhsN+i&?}c(18HL?~|? zo>g&2#vCTm%h?h#D^R0(3 z*d!cW;Fdjf8K|i#WaZYl3Ntr1Ano(VLMEG2H0Su4A~H+poj%Kw_WHEVvY!OkQGI83 zOw&4_WyNJ7FtPaMnC)=4M5WlrUX0@p`6gi6uAgMz*lgi78WOOW@YyxpC%pUMpch1y;do8=-T9C(8pLm zxnR_jjf>1Fi={efeMWCZErmqYWwl;YDH~nS(-?DVcx%c-NO@r(g=2k)&GE zBg^?0o`nZzy$|*t5#!S@v>u>Z8b`spFkWTEq^Y+lo?B)a@+C1=r(@hvdp;@%K~i>M z<)5v?L!d#MH z`?C8@?~6Do&K1>UYMm5|I=_mxNiUajYo~?-r%m!Xz!bFN0dK0{GC^wSx39R`fic9E zyp0>b>7FVjZZR*6qqPw?dy=!#aqA=YrQkE+h5}PFFiC;Yg)LB0;VyQ(fDI*Zjw53f zO+&S?YCCdhJK1#}j*1A5^#YLu?No*^tVKlttOUSUgh>UL2HrVUjjU4VZQlb4nzkYM z(^C8mifuk<{aD7DKfSE^!qnfd0^@9dV=QdpuEGW>Avp{uRco}O+(49yP&^5XdHX`b zB-pFb4^1-))p{25zpnL;9#^8uip6LHXtoe?h||fOBDc>)CiJ~p)cEIqocBQAkEROA z@EV6`+H=Ugkrw>{WuI$2_~xah);dm{$;2*d?V4-``L<%av?S|;T33P-xR-pHFc|G< zf22yRG7tQxc2Lv?TfShHrT{NGOI*%*8@hjSj1AkBfTq8-13`}c#+=hub}I=x&S!{C zGCL$gf=XL>jhK4~K$iq(IX3%R4tgdE8P#W^*`{UEqZt@BNw{11m?h<6 zsUUYw{B9ysM?0bGg|j$raQt?&{>Q*gz>fR1?OhgVC8zRMg`WQ*Oa1eSJIQLRtORUv zCDs}I{ljlI;w+`=(VXTRIu>7~*xtn$!@y1Z4EykYEc~+q$7!mjC4XbKNUs!jLRJ4f z(jVrlI=3C1Mb+0I+{~#M83h^tn8)q%Fa}z?liuMtPErMVL4WgFXaUpx5yRN4O#k=7 z`@jD7sDiLnJZRN00HQdu>7C@tmY~>&))0kNaC;(c_ z_DCi5Mns5ap76f1sq+Zz0}e4L^>`25XhP-?hQBtl$0wJ_`J@at$lneu>dHANmY1yu zr~EhwRGA3V-?m2|9rW8Cd@UM|51*;ePgPAebq?pHrB#V{7$*#Pq|+c zPGo3WpK`o~Gq{;OrlYO=L^}m4C?`w@Afpyd!gNwt*UR27a`ggt$%P78IfK$iOqL}H5O#uk!sa^}j2kiZxA8DKX#2}$*s*;`yN+vDO za9rujdPv{YFc6<2l0Zp*5%k0PGAWmgO6W@n#00NnFKfa3cDNV(Hr=gtaysr-GQMHu zo0zj~k)-Gjk-TLbt4`Zq$Jm|KFY>fdT%S+QT$29bideAnh)M7TpKTD`+3R-5AT*o* z^Vl4!bq>(>m(0AGPR2bf1xQA$Glg6G>sR|Rin=qC2z*v zz8Z}ZI|L^VUaA~gSrvN!ViTae-p3*^?`Q91?<89k-x!RRk-RJJ*K>}D@0KuFG&sts zQk`tbP(Lhhu9kvn!pd8+cWt$U$bR)k+H9M`0;wR=ur3ONj8T$VYLgrgL-DovV9glE z>7$e=E0JF~uN7*#`Ni7pLH>F0WyUpMBH2Pm#pKnp)QI&VuZ%4iq)))uh zKV*f9Oui!3JVJSeiSluk!gGd8*ABbXhKdzzl)u-X*wmAs{YQ8hr0x-#a>VHP6s(hx zQJvTVV|k(splXvV5jjyXi2yt*h#^5N7HD*tu~i2)VobW*-UKxMQ#&xd^ceN~owgy6 z>KAXg4cQm%wU?aW2XOdXJ1}N4I=3>|H*RMF>HBAzP>139nOKXJg?1$b`5~tSUUSwo znJx@nD-g?8MB zwZV3c;hJf#F|JiiEmJBY_p#DrPeM&sIHH^-@ao)D@sJkTy{P2(E~Bx3lq&ru;!SE% z@q#*H8i!-4m^q88IxoPg6K|h2drkuNT;^(?Sm%)-xEReag7Vp0yXvyNR*A#6w`=Le zJ889*)#>z9=>v2X{9-b9F8bu2eC-bW)2{lJtDjVs;U)GtQ+Twy#{&NAi2uL(^p;7< zDRGq1xh)@a>F&bTA%8HLUIAMuNXCQ-_g&%>LROUgT9OWzdrD9Yxst({7>PFO3b->+ z$l4c2oPgYU8QH1ts2qs`*CC=O^=J6vGMHCtg>(ru1z3f&&&^+ya z?_EAzl$q1*9QOFh`N1GE3Ls1wKeFTeT4K3qah69nOp1MAhXj=T7$HO(fgdp`y<96+j;Zilk2#l?Zix#LrOuHguw0LbU#v8FIr;iBzZ<>Scx~Y&DG}Fua)r*E2uj z<2!yGTqp@)HRfo-k==1q z*#0I$@OxG~=3%4K+h{glj~@BctKX}r?qw{wqb=UM^v>liLN@k0Fnb$U_x+F2Ny1G{ zF^j2M6#vW<^tBiM;uDr%e4t)(Lf*>DJi!sWy{VGzI`?HQvWx>d>KHFeP9+-8qfTT$ zrve_0FEPk&54+I0I#v&d-dm&#n@z{HD`alZbARGKOg7KA>U2vGQzN7p=lk zk_d>abhzO)R!=!g=t6an8f`3PVv!lEo$y`sa5!ZfDban(BM&G2;sN4QB7ZM6Xn9-0 zw~HO!;YEJ0BCjpZIc^;Yr8*jWHTGw=jh2wjQ}u9uQeR^EsG&lI&sV#RMU*EjP5H_J zcdq7C_1YI|aCZMzErX**sQk+^2_yfPj8;`g?ieAPvsYdw_cDJ(_uEFgv))CBmHQWl0j12p$H=(@!&LY)21I;S*vBs@ubwLnPf9+qaZ^u)#PG? z-f_J%;dp!hI6Y;ubjYj#TU{%5EBzFj6tME0bp%@?9D%}^hj28NW!11D7;f|nx0iSO zfmFWrR{{V+#tqlaf&;7FYb23q5Qbdij8FQ=2)UC<4oF}P|)G7<^gA!aV7FV#2xO$5lcX|`0gj282;KpEBdKYk| zeSpP0gC&cZeTFCG1p6Y5@YjDNM) z4#)goa>AF+7ruidc0&%N;;%QHOqmmGls}#yG9=+fNe>TK9ExyS$zV&3)oGTYxV@I+xE@vS&VxHz?Q z4N=pxF-7GqQI|if8Z}|&;sjqfI1Iq=zOD)j{(jffGuSEaY@%yyU@Ry4~lQ3aqkeU@cB8N7T$TEP7SpCX%1ocHwQRL{O~9;+sj!K zKw*Yg1No3y+;$XB%_{LB{Z_G8jMV!S>g-JJC>b!GFPf}AqoXaU+K_JTi%l6hrnwk} zUei$+mRYgB3wK}wXyaVC@5iQ7=L=n#Mei@LIPS$Roc}5b6}+6lp=lGM5 zLlGfiY3Hoy)T0Q1^3b4I=9ga^pXFw=;Im~h5kbKW7koTJy;XLrZN~pIz<6c7C&W6< zKd+^xvJV1o_~7~oz(h=dEjb5oV30Ix#qL<)VC_Q;upc)YFJ#- zPok9M$i7*1G>AP`R-YbPAFzaA-c&6zmT-yp6>ea{G2SSdHE~cnG8z&$m;i!-z>6Qs z%0}y6+QPhMnFODT^~YC&+x}eSO)4&b2Da~@`MPTmVj76S6>}U*qrWz*I$HeJ)N~#w zQi?mKD|(@T=UMWroSJnl1#{nQj!5>&g-2E}qt8^wRIuyS%Q~>0A>pUQ6f37gJuN@`Nt1i;f>S9ayy-lmCSo4wXrQvJSu$h%zP?! z(z@C7t2aAfH2D4b`9Jg+G5(DM3Am-PHy}Y?!my(q7D#}AundnmVVwJ4% zt}L!BHIENfW^lD>CC9od%|Ab-dH>6D!v8Q(kK!8htSK61w4-cXna0IO|hpn?@yKabRch9DZ_Cr zr(cL1lTDM-o@-S-fp(WJE*F2NRwVR2!?UvjI*!jr}t1Nq$ITUZ}Dh| zLhySG(8dm#5BDWnsOY2nXt6f5nWG}5poNu)gv$6O$kR{xb6N3+k(}`6XoPHrPN!Ji zZjKWrUNY3J`QE1N^`*yQ`!dnEF^KfkE#tqscBeu)wsoX#xX7+LAm952^XX-o`f*ZjP5ZuP zE|@R$Ht?G__dKDiZnk>j2sY`(IY37)BCS#k+J%Ye<~HjIJ~9+wF9T7H+q$Rdn}A`v zGgRE>sZEd8HH48#)jXZfn;_~xx+nId4j2YK23D(2?`izJyNR8%`A#1i%EDBMJ!mss zm>Mjquo*bN>U=6BwDc2}Qi}{pnpSE~~@Pj_)gb-w<5!@ACu<483_rvVNCU z2ZPR+obbh0U-z$mLJ-@5*I!;FQg+fPU%Y^s{I)gYFP$opM)@U+RaP#t;%#~YY$o_Z zv?6E^L-i;pQhKs{rW?s&GY4b8rJkT*K|&xa0jk(PyXBc@m=7Cf z_<_K54M660y@Y#O86VnUSBN38Oi{YYyNJ(_2{PkfKZ$F^OVoQt)0)zLp>`%@IFC7l zO^6Z2Y3GI*uzB)GN9kx%Q!ZMonO59)s)g*wwrYQFSPtZU;9mG~+nZ-!8*`7vv0Ug` zMfMs4r!nh<{hRWoOuO#u{y%p&#<*Q6R|$i_Zb~;`MfIHH@7ivv!XneDqowD)(#roK zmj6|oql!w-i{G58SEw~>JQFBR3S~$#&_#6=6hDGw0$*!Lz^n74r(1Yg^A0$+T#OsZ z3;6Pu)Ld!GETuM%YM0|?+p>R|C`eoTWH)CBEjA2y?MZ_1ANM3^y){@cKcGrIncZ=6 zqKBg}a_}laRh8EWL4WH2SfWdP<#dFyBRo=iGby`ufU%M|zBQA$oFa}hTMuNon=0XQ zdczj9Q+uqUY}`uS;=dShs9pDa(8`%-BYxf_jvab4&JX-2JcCV{B6MhF4 zW_zzOr^z_HNY=fP9Ot+tv@EgGY}ERck%JWC;LWVJ{ib2w+Q&#noWg5$Mq5OYoSp8| z_;MsJMJ$ictAHAvGL^yFzZADG4eW6Kz+=R1C^xGg3)OdP zJH(No*_tCb9id{EQC^Uwm!|NvD#OiieG4~@5ZiW7yrHzOSY0~mcJ)%es1ED=t!Lh@ zXT7?_+w`f)vKPHfI1J|m^dGeYc-HZyc33`a6SIBcJLu~w{pCAk0?M1JUV4lv$w{N^ z&V_A=sHN_3fu}L+rXL5T%L7L+NU9m&!i@r%_+(0CN>9ILch-HN@qDk^m?EtgY|5uo zp&dsMuZONq64_WPIjR|6#6`rZW`rUOdEEnWpkodBHp4c;va7n?xExM3YFXhH+!5D! zAYh3L?Md9`F=2d*k}-QYBgk9rW9x)4>gEqc!btU4jB3(qm)Y zn}Q!V$cbf}VqB5=+q`cq>lPRYl0;?z_S)>nGQ+7#v=q)a?i0tb$INeijYY>M`BQ+F zS#xDdq`)J>#=5{Ah)TQtzbXOz$2h>^wPyX7vv$lvZWl8|=#Cp7{-@O7ksQCedRadh z%pEd|0M{RE@8BO2WrIqfnPsOFghVa%fxv+6meVI$%q2tyFLHW`*P#U$cqccOw*oJr zvmJJ6QBUqQZy9Kw{>1u)_(h<8x%xc`8w*5+0yIZXGpKmgKjKF3wyIl*P*p?>7Mb*& zmH;B37}wu$UYoP)q|}zAza}D>%5=+Vu7L6%jS0v)U-|5temQJzJWBFOeV~U5RJ@*K z5%=q~_1P@U?HkhH(VT2Gm?Ua-1pG`NNsxLlzDq-68g)*xXS6QG(R6uHN>-SZ5Vk&_ zP4)%Mf0x(NBcZhmW0TQAutJ+Qmib09-6wEs>eMNgj7DWZkerLlV&1pRnlCX`kgnNc zlxBfh&y)f$x(;8+Y{n0^IA-(ilCV7vCO^-<*M4pT~381I*w47zkL9}6e z3kWQ*#;_XyLXTJlPE`=xXBBozdgY{og~a^5LpzMeut-my2#G{Wbi8nCfa~XlAlRTR57@C~QqQ{4io3b%}lc0cz3p zgaQ(3VuPZ53v%T^ux7;&!P)rJd=V(ch@VEob2Uy09Nd5y4z-wQChDWT0?ODZhKGMg zr!#rQh44+?I(k3?MG2+%^^np}EJ!fAVM<9H_bo#bEuN)WnFc@}S9oP1VB_u_axJp(6 zb`~yFnlw&l4+2Cw!?{gg3St)swcsd7Bkc*J_8zw5s~+2ogeH_D&^!udpW%-RwesP zYd$JzqJ4QHT%<@pB|8#b7`&A(!To!edphkLc8Q{j5HM?xmh~a0KXXnj&g;HmF`Y1i zfe0vUC60mi)Qy$6qtOhX*C;^gn*QNoTw4>~nd%1PyPi*;op1zS6RwkTogS{}L#Bna z{Ya|1GRH!Z+pl=dg<9ZH_q1+lbME@dV$LZM*nRO=g?mk5`Mdvb0xK^W<4G}X?Ro8k zv&)}RP$nEMG9J-e&Q&^^@N?u5Ufwe$u_%6rcL}%--&@Fx;Z? zHV{Em!6{jARjWWYA~lSDO^<8ax+iKj<4BOCLU)xW2yKpH0Cu;JC&{3dsYe}w{JES@ zU4;lyRs;V$V(Eh@0v;MZ4ogYszAz1lx2LmidrkM9{eJHfHX(qC6jRi(_yk>T9;Q!1m*= zIgsNK&Ju+c$x-KwERrMMmCCf@(2Ov_MPu9CEPwN>i1h7~-x+=McPW}F7zZ$Jx5gM^C9H{8OI!iCwYYNZLCr|fxcPtep{!)%l<5UL@y~65LQJrB- zrj6XVDXj@$QFH_YLQ0-^uQGYxJ4;OceA`!HaP!*qNvfFRs%F|)&|tVvlGx=T(jFES zIjeCfMT0JvE#Dy_!-U#oQHdwK)V8WQ+x1`R-w1nKjiKZNy{&KcMy^URSt;`I;*ME0 z%d1IoRxd&YBAhR?(&YB=?x!>Dhw^b7Gn1=+ZSuY*i^Ls=ors8*#Exx6PP%$A@JCp= zp}@YiCF4CTv@{MBge%!qu+Pex-(?tAs%_3W3}V?liPzS;!jYBx7Q}0ih`suc`}_Z^ z*KK;ploeV5h zc`nL)%;rZJKNh0vnI`(lrbE%HtO>$u(~|DL+3>3stKw!A$IYjY5}gDzMW)pMv^oev z?pr7XJkQDaD#g}l*gSB#DLCKCxS&P=469e2xWj>){nYwp;}^pLRZU^5Mw4?SAY@_2 zbN*FWBe99!a1V?1?J-Ft|I|;=n>9yTK`TF3%a!&JB0`q<|6&NK- zEImZklnGS83Eb7M8`1SJ{*Y9`x@@Jb7Dd)%{~imPYNJ?+@*tn-OazyKHK@kuCtPhE zt!!h{&6erD^l5Idy_p$fydYgBFhmEzWL^vBdQ$4Z&ss14|uopPgLbBy^$X?4tiR{jd~gstxZ4x~8EsRDr>nZ`&jb zpLd<`L?k`uZT`tNO!CA}s`Vm!62&JyNu{)CQOvgDIY9`Sm_AhGb76n@cvrMi{k#4~ z4cl<3f!w85?c}d1AcK~=n5XisG|j5fiLvd(Awvr^bvcWLWR+J7vxBUJ9u_H+yi2}I zRVZ7$i^d}BN(jbMYJBj&iyZ?KN};xe?w{nK@hnACfWb}cB*kf>i4v?7?Khb@L5$PA zAB)Z-IVL}4iSglr9~`@vX!o?WCNCd)*Bz+ADT7Q4Oy^!TGBpORB;6ml(%v`idBG&c zZ^d>Wj84A)Q#){(|J%C5tfJpvO_hA&h`*XD4Qusm*DpR$Oe`F;LNEA)tsHk|Yk&dT zz-x|Q4I+aZmdJP6pk>+w!g@3)yv87JPT7k1aOhITuM{2)yq;)Ao?=VIl#)v>^+MW} z^g6*EP?uKg1~CIFA`{Z|ES!wHqXNzq2lM`yN@5ARRUbP!N)HSKA*E?5ifs+UT$gEJ z7BcAP-G*qF_2(*d{h#L}OCy|;Y;HM+HXpGE1f-7pxIUaH-^w44=py-Xm`Pcj5AGqO z`&TO!U7!54z)e({Ygry<+S>~G@xv-PkGw*G2O^L@z8cwxV{1Y)h29hty$5J=ea32E z_v(J~xScu^H>la`@?-ly%n46Q?kCkrOH>XyBn@agXjY}z3UQ`fj&M@@*wm&nc&3SO zlm+5@@$IO^)mt|mj^R6H@aQpu`C@&vb-`~VNBUzsQ=2I<2ibCGlGy8b)l~?%(Bf5m zX*N-iMhFBvOLp#E5QtyeM)HX^E-q{z-pZHR<)yg9*>a_i>0NrU8s9naeaR;)9^d?_ zCPd3oG{-#=E}fz{x5B6ut4HjYT1^TnM>`h}XbLQ{f zsqCLab+W8qZ@Iz21Ea>A=W8S;j$hZ3$J=BOzC|Kc&M$<+Y#@5SKEr5n>V=PACB4Uk zT$nLV)}yfplOUfF*|JX5z~FSe#>b!$Me4E@`lNw8ZV+DZb5O~Gd#shltxtCx$sx3r zip+m4Xd`0Fhv#+~K0Iwm1z$5Lig4TI?M9XO?2)7h=Oa~WPV?y%)E>}w?l`(V7qxFbtK~Qo#uj}!Oi>PA?T!xwQ`9_2KS2* zxu>yk-UmtrbMzBl)w?ikg-D|_x?8O!au2?AAz}}#QGL5P?5F@Cv;ABJqb|X(J98MG zKL=^ithwzebC5uN4$|(oRUV0RTJF-o2)m;l!4>GRh~me~*!8l8syEe+&-c&wk5AX# zkN!fmU-16HHlst$7yi}`Gh@;JrnB^-z4o_uX#RH(*%xn*#Zkt1nKJpfkZQOJ;^j_x zsRkl!_NgL=I|_R@bmldc~cn_OK9e ztdcTP-9(-=oP|*_e>wb=Sf=0cL{bTI=LM^OibY;qQM+N-~Gi+AzS^j@zOiXfjYc|s9LjgzwM_haL~r% z;o0=|S^PQR`QdqK<>p`F6gTQ_+&QZRj1xscC`v<17}Ww>yC(g9uavW%ys1ngqlw~W z6i#w)jy!@vFh&|8qSJ@M%DvM} zW0mAY3e&n)OONh3mrWo z65ecAtl1b7FAvyLKktHO)G`@`HzS`(RvjFbxQY_1??sA)+Q+Uc2U(5L#{RWo$X>@~ zf6lOR>U+@-aTDCBwtsXxV9&qpnZ~^rN1dQUXRjut(gbIdz}@U6?}ItNm?4y-sbi0L zRGW3NEk<)+nt`L_q~P(E_`SSZk(IfJTnhT=FsP;R~cLk0@pWi}EtEhJ2SzXz;G9WnVjx2C%daYEC%&FYQz{;cDzl+#Mg+EEun zQM#$Exh_9Wr&qoPrG@uyMk1kThuP|w*_A*{Q@+&$TQy^nYCslCbQ#5!>ijnI1~3gZ zIQH!gQ^u#pZ-l`o?H{aQh6XLB1=9_JHuM-dD7-!s;39CPEK>CP8^MpcOl$Bc6yP|w z02U}#i)Om<15mPt$D1fVG{_XqMuhF)h{M@vWX>+7c`nQgBk@ZUwKZRTFDoPL$Wc-|Eb>21(7LKK?;1wiX^`;CLLPbW%fe~ zi9H&`tojocHilFj$g290WF#;;5@A42j*oE0nU67gaa!x$kTs`urMcO*Yt>K|71}%t zd4sD3C01q9j)HA1ctdfacDYrT;t#mww5a_=$>7AnWjfaqvN)#m&pbh2``V))X(L> zN>y0*D~}mlCL%i>L@m2%U>QjNl@tV2mn{TiGaB<0;8zbB0=+pe!-0JhTIABfXTI(p>M-miZMX#QHv^`Wa)G9xL$DA@uc7gsj!n!+057E+qs+_nPbd^0XIS4uA>2fth~{jV z78xyKmi)l(d`Oob#`jRY)SdgmXZC&id&d@!v!6QetL6$AL`J*<{tqVz|I==PyKP6# zZ7URkvPe{R*2eUAwJvHFoE+iaA=XP?#0K%NOe(Iix+e}z&hcMv`rKOu2|#qB;1OC> zMjr$@i-UJ-Q$pS$;I@D|~(qoy2GUY(_)I%9hN^YrkwWN4RfXY3Qu}U*QRy{ zF5INOHGk)Be+!o(tMJUTE2NZ=DHLE@bk06np|^W9+Oh}@ClM%eh+A3eLlmRwWlq#V zrY8v=TWGl;oQ51*Ano`^6Ele_<5xOaF6@l{uIHfJE3r@acywqjchsd$S*rr+G=Vo-o>d>5z0pwLnCP2PUD zF?;$~i`qvL`s`A0mo{74n_r_c(2aY6+6i^nq;}pYW%M=eJ%{bcR785O3V2)#bq#kVI02po%kL*x1w8W* zIFp)Rtr`Prsae;=P4H6$=8PFquK|hx0$7YJY7fOc6LNmYn(Q@3(qOh8rI!jz%|(8Cs8r z)TzZQXqas{zBVz+!;*eQ5*6Ov}C6QTP+s)GgC-eLB1;ww~DK(E#D@yaEb)MY`zV+Spyx4%8d z2_Bu&>Gox8D0S>Y%`&sQHR;C?ZqK)&tQrl*bdKF?sn87K?3(M6rRZOvZ2IVR^NGiX;qbqd6~JED_$P4{e#wM>sbJCvd_96|4? zBBD284V<}E7IFY|r1++{dtPdQLL53MFi)paW`=v|lgB03rfQ`De*7;~I+`J@{l+HyW-_+I z`svQl@2w8J!{*p3u%WPX5W#aIGD-vuKP?$C4hpj|(;YD>D&LXoO+Op3!xp1E>l(-nxZ`xb?tQXS@Oe;58;4?*N&BzsvgyXnBu#2iZ{cbtt8 zpH&3()OEt<$(Me}18SpAZDvzO=PP|2`|x?|+WvsIb=+?};IpeM`&xa5*7M;%IRVFN z-jVBrQx7ETujfl%eWrHRj!JuX1Hdcdq#0hAF|N4Cp8f%j^>v^IqW~yFMj_{oM*6BO zw%kf)e8#@h>*;C6@P6dutCok z_JaD6FDl{+qb)tU>=`yz5?$|P7%W@FiglgOE;ws;$L7<) z;&BmEscb*OEImx<=D4>c2OV>i2zmDn@4A6=GB0azGV9KQnUdWK+6nyD2a;sMK?#KG znX>kc5MVW-3G?=&e2yK0JhLZLxs%j8jJcJ7vXJA$(X3x->5a(diB0A^rGRF+#hrE( z5xb1Kag}O7W!065(}}+(dj=iHvZ#Ma$0eRp@wPw)lAho1fEd=ha?!4Sp&iW6T1wQZ z5%ykmDZ3Q5M`DT5A;0oy@VJ@N#m{of$9@?olYuS7LU6AYgP}0}K4=MA8 zKcB9r9&VrSetdkm-w1eq1U)#!h&MLi!D(t%!_Y|xN*F&cIKehU zj(RcHc|&DR8;S?3DNOdK9AU)6%H#D#l=aSv{tc2tyDNGe_nA}fZ~QH|xO*W?%_t1t zeXKI@A+^bp@0GWCg$p+kO?uLbDbT%ocvdp4!r7zw(>I})R>H&Y`=8Hzj{z?i@Keod z=V{76IRVpp;fQS9>04D>uKBI=rdOUy?2BKd69n+G?f`p1>DHI-*f+Mr^RW+TIe@JV znj7Zhtw!T}XB1MEb|$vTL1ez48(6$kQNZdQPW4OB(N)Wu^tiU*umMJtjNqEAxNi5w z(94!19Me?42BS*sD!9VhH)27Sb?ErN+A7_Xq#}I03*eX>=X$DaG+3#yKh{JxoSE2{ zfx}k_J(5Z#B1t(Oso)o_RRAVz0z7_GU`L+7U-u+V_jTX?rQYbLtun(ip>l1!#70%1~E3kBY&f z$2#>_J~rILa2WY!Wk+{F&8Oeb58s}>9-o(fKj%CzY)HZp?Zz$4*@HuRz&}i$sHL!^ z-_5#~_NSc{eDpUc!4(M2ij~RznYk?u{(ymvrN1LqSfIc(vAbQT0(RLZ-Vl{1Y&c1P)xD)B!6 z7as@E6m4nkXozVd!o!EwU3LwS6mz*Z8{ZH_lO3Da^EXc_@-hpY^(P2$q|J72An->d)+TRUz*;&f4Wr|hrtx6+)C6@y=3z`qpjTe}peBbWPLjZ8D=RUB zFX@AO1>e!Euovvt=y830{Ys%X0F*QMG^s7p+?+Q^eDc zLd3um`a$MvFu_|z&|Cv6zKqGjNN27549^Q%|02Nf-&p&~pfpMzDFF0 zzp0h|1G)EGYR=lQ47?>C}*~d2y=u;1f3&z$JVNRwE`z=KQdN=e_NQ3o zawAIfD*%;a<&FK@Bzj!aCZ_%A)L0q#T49HU^m7YGE6r(_*o+ty`&xuoQ zWRpil9HUkPI+#HWEL5B=mA8f^Csv4qc@34p9P_QDe2gcuv=3S0>}^clh@pNL_-@3c zy3GH{Gv^B*oCafRq`G+@CT`*4doq-;w)$V@#)Pjl#bpPGsgZCrB^!|19qCG4^>}b2 zQFRk^T$!=&7W2o44Dl7!zp~jRkdRszK>#-U2;!4_j;>OL&-KA^Q2uk_p9$n*nofCx zD960Gy7K!A>vH;2Vo_u|{OhJ)HcAnH&TGK8sPz?!xPOCsw`VO{d}d`gVgU(=?@AYg zGl#w`66SxS`AIctBEK7(_|~U#?9^6QAm~E%<+-Qxx9^lAaR0GkapCULI6_l0 z`@3a2CKyb;l9vINK2WwomZnL6Z}?z(L-bm}{>J7~G+>`0@|h4!ahtFxe%M3U`6U&0MgT{hIy|g0ASqa!xp$w`s5#5 z%KV;0Fur$#PCWrib2|I*++h|WdZtD?JdNK~H7NBEGM~h$#AE8Rph}lmDp`qzLLY#` zghNw42I{Ex9<%w`h!k(|=}R<(%64*U=_AcC+E_v<>x(zjnNhKw$q=`L2}z3Uo5(lW zH^a?nflG#bME>|#HHw^tP8f$2>CkAHcc73LPQ;9+K%PH|Z0K?1f5#E`eh72fmn?U)orkq-Q7<>zHZ8WPd51-Zaz)C(!DdOh zpOy&__8sYH`Rd;#iu^x1!2iDdjkMLWMv>5xR_CtyM?WX_ZE@A@+Di6Na1<`*(3%YE zeP9eXbM}loJOkX?Ud3;bvr+FcA71`CIk+$x(x=!(lk})KH%ZI8Ptlpdj2|X9^bt^e zvd^7~O`hrhwExH^qEIt2A<VLm@O1l(yg)J_l zL9z0*J3%~-BY~$;JP`<_&@DOdlL=3{M@xbl05L1EJqRmFQ!rz1`x3_W$fMRu2{wbM z7buHvtT~zEqnq%=5qjKcI?7q*pSiO>EuzUR7CIyceLV#u%teJU+ zO2~*wFy7BugrnwYEYV!Vg-WUMcK4^fe$K>j44D3Dbz5~@dDgiG*ZHN<|pj_-cb$T z0yhDk7zYnC^2WQl4~_o$Kf93m82w}PYb;q@Of&>|iXMCZ*%MH|!~7FqyTgX!KQvG< zt+Y<{8L(r!3gDdp`iE9pfG(kFwZGuig?u62E7T6!0LftBW|s)bhkb$odYk~-luGM6 zcWEVVcxuUD=dADD8#FWwebY$7uZ+=jGcXD3is?X>mf@h;-Ef*{+i~MU@R~z=kTvQQ zbz;#L1&v2E`x2dPe^NwL00~2+`cSsx4DHAH>F@kjGsxu$e|(InwY8U=oJ0#1$C%d$ z=G=2yI4U-_*?yN+P{ZGSFw9lBOsOCcV=>oCYO_pMd$^2ncCgH{DESm#>T(!sB*_JN z?|jH!QZIx#Ipg4=VcUA*kubr&{`q5z^J&)u+~zNPe}T89EN}jICI1Uo2W5xrYxM5J z1@41!Dv5>o;?PD`mcop?3*3x#gIIsOwMF|&E&9{vmVNS!T7Mig7Qqi-V#}noxQ+cva5*ki=Ky|U;X#W1lx(9ZUOtm+t`PX&`u7$s&pOa+w|qU znYrdq8Z1W54GPvCLOADer7J?>X9&rY)#9k;pwTp1MV{_4FXIlUm@*wa1cTybvOa`5 zLUa|i&r9lc=;~Iu2ym}HP@@ndEX>|p5E_V@+fp}tKijPEs-|li%O|;)z%ei0X!F?_ zGx#W*XR95T-Q<LBm6Gop+=?$%9L9kxGQC~J=Aec|pTww%g%;1JQx?p*58(_v0|$G!BQT4~k4Jz>D9A(QdW~dVN~_iFLyh+R8S~dm-0aLF~{t z5jrS)Lw~Sh^6P=+trTR>QG!Wp5Y%-^<(?MlAUUu58lbDyZKt3@_}*kW4lRNH0x3es zT<^Z5MCpg}(MmQ5vs;}G7O2IiKjgRg)}6Sj7eZOOxM6*x*)SCT!9E$}SrXp%Tmrho z+!h=I|54T`Cb}rY_|EEBm#@JSyI2>9!D9jqjmH}Dr_V9e(gXfZXyf$$eVv(dnq9`5 z@hi_|rSZ+hK;dkoch7m){=+-XIXyOZ_qnwJa z-PX#OSJB{w^;~mqSFIR!=woyeHy zkIID@y^oX&*sukwFIbv8k(&F@;Ec>#yzwl!2x%p0S`8##jz!=L! zO@j)3z4$QYaC9jg8q(&%F6!)&ApCnR387gO7~_vzUr=Zn9RU!HI?8!H1ji#Y9*P<| zAYaW3hm4~U+}o><1BcQN9w%ZW%4$Xrl^YyHW$)Us27sByuU!fU0Q=EBi$x>@ztziL7EC#{rUYT zdQ@XNpKpe^rv3{;j5E1|`=dAGBqKDxWY{`W?oy0(lDJTW~6 zSnsR0k6y?co3Cp`vD5KNUj9f!j+WQhTl9B!a}mpW=2)ck!FXHqr)8Gjh?RYKMaK^C zmUz`!d|)FU$af>*eWi4hd0R~HbY6=hWompoindsQWucf&hsS}Da!W(>j(A*Wu*{ta zM1_h#&>DaQH?t=EiBlGVhSl|Z44RV%Z`NS}xXvU} zq2hGtRf9x1AiSN0s4tfZK$2^O?(o#;5e0m=Yfb3sYjGi&=qFkD=+Vbgw##ngW5Cax zQR9yR7IO9rYVZs24Cfn>C=|rJr=QRDM`Rb&N1EbIMba_6yDH%I;G<+YLx11 zOe}t*O0is7hd;CoS&{{@s|kSKI*BD7w0B{q?t=Q3N@N&j5h*Iz4rz=NUouBm51F}# zBo_gidz(D>%bl6DHz+XzeAq&tl(DmFI+AE{u;b60g~$M>77e~Eo=jo~{T9_NS?gL& zS|12Eu+(13sH|!8;iVhcU^XwPdxYC>)=Lo*Y$(~WYm0vQcIfEx%9k@(mr4cr5lXLX z4PG%#S8AJ9s1)_|H2JL7>8j={W7Eb%X9#duc}<;p0Mzr7$fRG<^-3; z)5^eiFrOtt9T<-h*cbtTHpkny&_fPknJ)=VotOra?h=cZB_bSnFx0+^#sv#S@k(C5 z{U$I@T|e8s^n28+_QIJh>p`lta_#Uja4Jb^(& zv@Kc+47*rm9px;e=RZY-&9ie>h``r{IGuLMXf9}?6Dz#)aA}w}=@KR#5&+F8dI3e4 z33`nTdt^@PCe`u*mIOAyW}OEq!tX-XNll#at2Ko+o55q~w>M+=O2o}n;Zj}%YKsM_ znmO~NI$71hif10hIp7 zAx6TQN)2@`5s#hTL5mDr-wxIBDLDoTw)H|MZKnqc$2mR4HyJC=_zCGE{h_Dl`WFbX z76tFFmw1Hz?A_~A70(eSvjViE%!qd%KMB|yw=JZ1%a6CM?^DYV&j30LqIDvZZ!#Ng zPnAd^xP*-%Yx&dzq7UGnaulS4?Pk5Ug5KPSBGd`wMeE^ z^O1;KQPqe6dB2dKE$1$?Ht{WtPlijSaj( z3ngQOse5fw7j2Nc9Fo?_TB;9`(6j5c7~RIQPjlX? zk9is7_?U9i9U4?kDjHE(aIPuwz9&f~CC+cU?BEb~kTwPAkca8q;IsakcT2y+9xz4A zSQb5ViM8qP=ehSpcpIOMpcB8%Qqd&FeLiozc+c3Huvw|dYSLy{0~1tt4$4jwKiMp` zKY4l#Ow#9Ul)m}@DE9yUCC~WaEti%vYx`&_$?5>~Tet$aJ+`teQ%2AZw|4DWW@h!T zQf9IZK`a6jpXkzgyKHw|13N4QlJQ1BWn#s-KmQ|eM5PEa4t7r+zI%*%rVfrZ7*|cx~Xv0En|>u1v2F%XLUa!jXXGpN>omFh=iK}?ZK=1VX0-~h_MeQ zI8br)L0R7W0p2VH{h>8P!w_@tgDh%srT%Tq*ppGCB~CEZ4(#!Rz`D|CoGr!O4zAN# z5s!fJjS+)5k(Rt=ze4MDw7G{!ST>nuXq3fiJpn~19@O+LggYC9iQuP)MN1ZwfqC7-3bSc4<_-mL*ePjwt>`IU$2@TXf`Tl7BiTei) z>uPb~I;QWHRjc@12nh5T6jC`opdn=iMX!(reIIlwuct~*cSBY`Yr2gB61fbO{EEiK zX$r{qeB0fa$w|BqPO%7L94ou;N#IU#qo%&}T!MZ``ZlK(OLt3%^G1aWaxw#hjQe)e zqsPk)`!}TJr3QI(B{re3Yz2qIwo0GK!4&vtZX>CWu?;auijxh%}S%j zN~N*qFP>oEf-C@R8FNbHJ%_(zuXmf>d#S)@3>kxSR5l1Xvo5-&K1${+<(>Ch6#`C(%8u;uN@+voxHrN| zOw+367_ae~mqg>r$HlF>ShmH~h-Bf}a+G*CwD^^L8|~%1!w==^8!yOTuAh2ul;{a> zs&rMqk6WBBY>p?I5G71b9O_nB<1BkEUe8o4p_7h?dwi9fy!baw3(wl=S1lWk)-LoM z$dT&2sLc`HtQH5M%4s8Ku+P7zGOmUJWRMR;6Z|;IT*c-PnXc%uGVadNC7AiufnqAb1R<1PKIg49(d0grkeA{EQL(m`$HFRJ&uxp^D2cYw65!qQZk%Z}=$^Y|YW zblN{SPP0A98<)yT-)N=}Uyo<=r15t=j?ePaAsb(wuG8QW^1r-1d<`&kl$)PUV8I!Q z%!C@6-7wVfa;i(*4%}-V2J2HfdJ>Sxl!6px+&E_!NP2Q(L-OiqUW^qK$QlSXgF7w;uQ zf_8zw@rZA*U6FIRb?i3Pr8&$n3FN6}8{v#5-I!Iu2J4C){dL7oH_~ zjACODUMbzrm<4W)qyG(+sNVlhh(n~#M=zgS9^Wmg^2K?G+)BH$x{EgyJVV?B(_s)CeVzZrswZ+5B2oQ}l_pxJ<93Ai|NO^Y%BLc|pQ*TC= zI3;`uyo6t8tP5$1$*_uB@g)K}(qDRZ5lg)|c!eS&1<(5yn=4Lj9n8o}yQ&%PmRO4K zez2`id(cJXe>45_yUKR__ssVrp=X~~k6z603msg~!4w&Re_Bl(b(QJBJMYs*Nyt#= zR0?BBvc}?#Z|d>pDQMtpL@h(O7=C*7lb8qBD-Oh1l>{Tn!Sw-XXj@P%M6&9}WH81z z&_mVKNZ5P$q6k7u*t3X~re9MN+zCkq7qs-2DmIUSBzyCHj?59649&x$a&J)qM;N@G z6q*%2jmk}kxDEW;xfesJS`yQC@%k0LUpV(-%_A@1BM>7w-M+}Mkuipcf6(CUJ;$8k za;%q&uGYq!>?=&|gcB!|*0=QJ)w`*Yo7M27lQxl|%wQ?5Fg#MX4Gb4zt4XVhu&haY zO0&B2(=_QQa9X?IZS~rW_4ySG*pw-|B8r0C)wxo0YQC}S#ZW_~{qj1FR zeY%4jUVJ#L+|b1s8i*Zi$ozrX^Y?_R-r{a}77#K)tI`KrX}@Dc&_RL2b|?3`q!n7R zagwCU$Z8+sL03OvIsTBxPxXTkjy*C}65SX)hFQT7Gib)JJpFBFVJq z#|TP~{watyX;x^i*vw|s?quYKgMlT15ZN?5Y;7urb4{(ITTMK;w+%!$pyjv~@~6OM zmqMn;97KSl?J%doRrh|U-j0^diDXcmix|QX4wp0OKo>|z;<$r_x`MYAou7hZH;M?L zT=wn{lMAT;NY)C-YKT6JW*)rVX9wQ_I)1ih8Bu$JWhLj6+oELh?UfR!Fmhwx0Z7Jl zhlB!6Wc1IP$mTOUx7tLNNR>CfCweycwKqIvn&r-5yorc+ht%gOn$G*o_-$Vrb>!8b zEJxlgx!a!o_glcfcmiHpN8dNVbzRmGc|AIs-E3qCC-B~BS)RMBPJ-EK z_ZC5xhE$!DrFzLz+wROwxI0d*kaQY^>Y>~IYEFJ236)xTgN7NsvFd`XW;_IO? z7`#qRhUw_;g&CITo5rLdrr|l{chJGm4Pt8&Q2r{)@%B{7>FtctoCj^>(GJ>;H zo<_U{7QPRmdUB#;Q7$(A)Y+7GbmJ}gK#7tR!m|a#**rM0v z?<2)mcCnjGVGkH4;>)YGd<#`ivC}f5f%$D7&BcD?~G* z`9Ovx8~eFv6|_Ukl2I*`^sbu)d#%{18E#e!De|izx`3%k2f;j4me-B#aMHdsd0p@> zQNo8OzBW!+$-Df@BXOy%J!iQph_6N)DL1Jhbu1uTwG5J*^iPcOH4IOHJ&POxRGNsv z^bYPYegZ5Tt_E=GoeS#;9WEV4P7Qq6nZ}fcI!q%B}n)!HW(fI+uJ( z54m`$S$SawPhl)kb1$*FU36G;#AF}dp{tiuIStA~ba*XQX!Nxuj@0UUIpv+(nJtsf z5?8(z-BrQDepbwB&tjm=B-TOIl!(gjD#RPjpdT0&*!1w#Hhhc@k9B2RMi1TQqsVZ+ zXr&?Ugjc6D92U|_dI=`Dr+)zkDsCgxha&JcV;FsYVk);trtkH}qv&zNe|%`K-o10d zs1N0wQ2XXes^k?q4kVf9d!$f{&65SdLuA z8q0i)0x7r?rBN7iEgaW(35~wz!jDyX^ND9jSVgCnactqebyvkVaR&S_@%{WG;hJg! zupmAu)p@SQT=!J#>IpGyomqw6J82fGCCzVg5N^OmC{DXN>1#Dj zs4i}(w$a8$i!vPNTeW$%SqhZ4cvU3z(f7U3Fl3l<*4U_Pk$s8sH&-dL*RM*uFr)!S zxRvrqyCe{4x&>i~ik@i;q`t`Jc73L5{R#sY)9`I)(efEX2e4HEMpStQ``9syjsNh` zb5h-n znb^C^Z_XE+c8^8E2VOK?*2$;#F z7;vn5326;loI{%arpuCT*_vS?h9t=9AdH^_LtIp2Sy8Cc^X#-Wg|C3T*XAy?Rm(o< zDGsA@*=^tpi}~htH@(3K79`~8#F(fm%{oXwc}Ic+9YOFikxLNSxPj@^^rURj#D&FJ z4MVJ!)c2%wTg{3)fM$TtH~Y!$Z=k?cha}_WGp`l}a?SZ(YBAd7=bM?vB#p3?}k#XZXolR^uQrv$PG-n)0!8p#q`!mi@$AqoLM0f#j)( z($8(Gsmj`Xt5@Qf?`Pg4L8S|&u$T!gbtdby;AuiMa0RvoH$J9=?PWF6OgWh-L%M43 zbZH1(#sXeJwec1)oIS2{vurR0YTh=b* zo(t8h^k@b$4KUK~N2cR_KkvF&{j_I51h#MOFpl=C6 z9%M6@)cK%N%s>z;hGBf}oVu<4ki|gh^bSsFD~fJ@DYfi%mqcxQOdvcjUvL!2ia2gs z$c+_zRL2W7yB4|3lcG^41heT^0`jANRvFK4o*EwSx&+@)Z#~XlZ z2w^!6Iw7l0_bJ`DM%n}qW-FT>zx!-FhF&Cgd70 z&|X^QlfJa+{y6}+8^Gr9v~cM&orLeR@i+fRMkCC8QXFh8lZ+0>ZWxM5WV~NEB#~ud zABl~ZpqfJIr@k3|YM3eICul(rzBaaFux3}pCk=f@qNy{Bq{IDzj^GkH z|FvgE216}9 zJ~l_Ddu52^pgA70343fE8>H5Z1FTCO+jT{N0hxd#e3o8)Q3A3?-!wnl33uH3}kDEP5M}du^-Dj3pg+f&I3uL}FiG{!=T>_8&aP=}JXsSWn1r zHcuiqa3fHw7^YGx&;Ta3t1{sVGfq8u zhc2y^Yw27plCnivh0HIqI2x$o4&>M-Z-Y^_ zXkdS$D4Do2g!Pb<CNQcR>33BSXuJu;BeX6gczG;)TxrhPb6_m7; zJ%CeU`HWK10URtRi%1Fd;pk4Z=7Q%Z#Y>xJ#*q9#h?$T*d|FQ>O}2QBdNGlvb8LLA zWth>Xw#3^INdq|RfVqoGbROPi4b!K;{Rp7o(|Y`v_J6JLf6t9u?eq;Nuo4;P&+z^# z3$)n0QR*~!Q+Z@`qf^t*$){&Wty$u|#Fef5ak&85c(dQA5{w{>W+!|!Qv65V!LKb% z%zne~66nR^6in?62M_IUYepw|e>SN^D;0(8_A0ZpuTWLq>c+t1gmc;r2oErLPC57h zE~T)W0C}Y%x1&a9QUDI=w-0ROeEtQk;4_AtC?f-kQtdSSIkl9E1F6K6Dyq;Y5srQX zIsYyH`Wh`;1-ktwk3SB}HR~5!t0S1K4)=dPvVt~pae~*cUhct08wO5bBSVgp?nP{z zBJ}>W!+J*td{R`*P|3ZB!5r%1!EBMDDxWAi^0a<8ngc^@9Z)BFcv1&S{Ht-%X{7L*wjF<<;Y`i4gJ_ z-Z%D+nlmq+rq_SVAg07VnKijxo3ZRzNl88Dg-9veG6*5OCUElL7qcw4UIKwdd|4eS zgR}^*Ky9F>%ZRuZ=+LlSCnD{^~Ul_bo5 zsSCzB$&Jj6v!l;1$04nPY`byqb1Qu#!?nGb?4>=a#*U&48++2D@wgQ|^n zq%Wg3m)V~=ePurO0BLesr`IMZa-FLwaW(NtHv`yBYiLx3Ts($^(y02A`7k@|B#q6q7K>N=*;ZW!A z?%=XK9vk>~P7s6b4zQj8^JDz0f%->JaBkthCNm`tZr+dhp#eI@ng`YE6K)zZ%Px{v zzE?6xVh-NL=dwr**AOZwCkd#Q@MJ}pmRg)he~kuaiH`RuALqX!-szY`W5e)91Y;T~ zKwgsV4N>u+yHT5tYKp1v&Kl%nF?NHR7a`2X^@_QM6Kz7KHi@m@XuVXI2ryTv88fr( zD(BbZUJTbApM+hPb^-e)!sxUkUF7zHP~_TiQqf zps)`{nWQCkrL_GboO;h@+)CDH(LpONIm;VE4iFnPWRlz%3|ea z$1(AiC{@QqEQ!ZdYjW*}iCaVoVY0U027x4X7K_JYOTy4mM#nX;DQ99Wy%?^ermf;@ zP$KIqkQ2A=@D6D8_t0^(h@69lSf!!U+3A$ZuA|VZi-3w*JLKut+3PEh<>o2ZEDPU0 zp(Ehp+i#FkJ8$VW;KPP<%j&@%6=El)xmG&VW7k^13?u1_WU1Xw1#xe-eDT;U3NoUW z6j-vJX|5*{Srg*)t^`4s35Pw}u2-9w?We(~4pyKvC(0dXa8n^ML1EhoR9~ELa%%ky z#WOAmc)nILnfoa^S(^$ej>*HRGMjl$B+(G9q)x%xzT~wQa>dGab5b+I3S zL)+Jk9pL;K2a+$JDLXEH8yu!+&R>DszT_^wx3r0Uoi)RBlubI?^O*?Cd&SW(JpUO4 zQt)toSl(z(5Z$Z5!wm}QPm03+#bSa2hmK?7=(PC?mt?6~xKD~J%ND1ioei4L?c+w1 z%{y+aZS4&~o~}g!3=~qhX}PS+Dj^cm$a=FeYI7!>@9oLfmIraJ6oDpb`O3Mznqw^z z;(cqaYiW|HN1teP|G;@2!k3AOL)kGf4Ya~q=Hk-`XM<8uf*p{eK^?v@V+by8OJG}|5U4$q zEv|_`#UfoPA58laN9_gl;6o9J_-)})?me4Qot}jev`lGHU>)R$P!W{ZES&<4%{gaK zG5xkQBK>1`Vwxw;C|z!EN~KgwzNDe0yRd&o4`Yp%?ruiA>sRV9b<`@eiiFSX%S zK75qnfL#i@6@11;gXTaYB@g7JpF-#iFD@k+?aTUPTHUaLoX=)u3bRbR{V!YY)`)J9 znym=<{GDH5XT-hPkFRt8Rl)!Mi=7Ui4M*qp;mWdkM^;33sZffNAxkqH%R@s9eN2Ho z;8Nx|fq;0@wSL?fhq-e?J#uk^NAY(WuH$AhC**P9svDZ@3e9ntIC}|NnVTt^aAdBPCTM`PMOl?B|;vKWnF{j&c5`@55gynksr0l^>23D`U%uR&xHBlj; zRo@6ASCX9J(}ncX5=9JUK_q0EOuB8AqhEop?w%OYO}2E6R05rJ*JqVX9e`dXL@nS1UxL~8ij za$#>#oDZhFy#>Q>#CKE`Gayfvl1m+!LQXYY8S`oEAl0cfRxN9%tgk+Fnm@UQ#z}q6 zkbc_LlhB0s-RN1d*}E+T)c?NMIuV*VGO&&ORYmD#f?4na`6tFm0>c%>-qq9kiFFaA}TkWSeng%Jxap zl%DnUy@L~e$Wf|lfsJ`4ccElB=LiHazKiAjc5H&?6z_Yriu@N8uIZUI>#nE7@&l4t zomd~vDJ~uVYa>gYGmZAX`LT*foY>iIf`&LDQG8r}#37Yhg*EJ)5W5zDqiXF|EwSYt7%J{Y5e#YkAF!zW(;EXrJ{p_Nv0`BzdW4{&221&p%6 zxvQw&OfBSv)VJwp0u11^=6n#Jvzfr09<0$2CTKZu@4&L5N|RlaQg)}}bLGrkuESfs zC9k0$b;QK_5#}I83W_B4#QGLM`FtPsa75PX_^!VjrJ~K`rA?e*)4lt#(vfyu0lrA& z+}&?vLyqk2*rGyiAv#qsxlC-wYAnL;;F*i_-PU%tRG?} z6khXWVVzW_Z~`|dW{kf*zj+)v_q>04{!Vc{gUR*BLQhx~Zxaj?inq-B%EWi!3-K*B zXH9n1-37LWmID2$q9utP@okyE3YT7yPjLk1GrDqU>HDL&r!}CLnt#_9xGtwv9U)2PqdIE&3e6Qf3o?&?L*&V`1IvZ)3Nh!OMsr=bL5_y)CPP*iz}83Rp5bYIJ~Bs z97zvmtwjir*GX7`Or+)LF%qpbH5SQ&2VjJnpI}|i^k;anfn@O+0NrctE{X6jSp9_l^dj=FCz zeEb}((fW%fw#+LeX|>m$W*RNcD3*CbfjJ)1c5c9fwTB928a?u6pO=@Kg^0e2!+}Ir zE1RIgi%$;Qv!Q$Q;$047(|0i#+?qCcdAWnIH+TIOOr_ZDYEuaE0-%hbW=a-+`_VGv z%*oBHoEpY^X`8KbkLEb6bRYLx;m;F`x3bXd1K#Te8ITG)SuR9>X7Fj?O@h{f)EC7~ zJ6YcPafdlHFCXo`^Z9*8upUiMdJn;Ej@OcVN&U9(%~!XT{jYzX1D~&cy;NUMu;{>k z4}yaKrm+8~x1SN)zqWQb*UnShpl8UuVa7v^?*w3fn>0(l7oVJ7Eew&hVXkWNRkW(1 z{-u4?`9YRxs6O>lj;GcKtZmJL8@?2686K~%HjLl{-!HwLnM&NQfol62Tp_2lYDvR} z8c-M#WjJ0pB^E0WwZkzme=}``m5o+h6S?zA09@)-tvKNjlUa{5qy#w8AbZ;|nH)ic zm*;O6#Xa!Q(z?=QVxqlY*&88?I#r*1ANH-pk#ce?9HF$ff{i%dPoX1J^7dj zbvS{H+XP{XczfVIIvgANI}$~fXw3*-zI8N0q)|mYQpXhQvH3O^A{5iPNQVhAs@W-b zRCsE%15>UC<4qPkRR{W_6NIfjHR@00%xsyst8QN_eci-qw-t#+R-fC*H3rqxX@l2v z($N#Jo7!v~DwTF{4( zs4D@BdELYNpe>AB`c(*uqR(QtW&p)LQpnrL+lVbcgX%gv_Q53{4@V7s$gua;UOz7M z#>MsgKG%x$S*IqbZqqhzyt=XN#k+fZ(-+IrQ|RMzV8sm-STk=8c5Vg>ZmOGSB?YQy zi1&>B9ZskJp_(4924QnRIDmAx($*+3Ig^mXK)5*7dPF99XyeWHk&yBXMlI2k*9qnU8p3+7KAMi>X zJUN%AXoz7ENBdtsu;w(u&bOQ}K&qLGPuy3D=iQ@pd-U@&6N6K*b zs5hK*GNM2f;!+hqU1ot&R={sV@eGwJBNY(pH(nEB%C{|5kBp(yBC|Q?j>{BJ0*8|9 zWsVANEFn4KC8nQwhFDx#d$B}&%;2S9VVprKL4IsC^W&A{yP^21M+>EfX0u1gqq2yOv_G~T#N5?s1*Y24l2 zLvVL%ED&6p#@!M;xI^#+O>p-RKJIt!`7u*dQ}xX}H9t?EI@hT>)qB;JwbmZ55dnHP zIgo9L$%Ze55FRmVB{fkHR_e{#sJC5htJhttElTpo8pOI3q)xhw&)Rnq-DJ)lmSvQ% zOFPcJI=Y(gV0G3#?$n#Mpvhe64yqFjXm`1rD!(>(tSz53Yn-3)7aV;H_y$;1m{k`3 z!SG3`H;o=85tx8>jugesE*IKF*BfEBbm$BWZ?=$5%6P9_rO%l}<(F-zSGbs;QB8-B zgQZB2T%fUL-JF4|>ogS(l2!HQJNh7N>xB4~6^YYZa79igiZlBjx_2;d0O(_p&7g@d zzo*<0O-H4a!LGJ8qP23$VPuoCli+Hl$av$O#>WOjXVIIVKK$nQ;d@hR!k;pYCK=UM z0*e-s$7`yLFGKipR;clBk8K?D`INc9SZ7jsMSz_`$`c~agWn0(dYNv|`R20*gSxaJ zKpGd$d}_vQsjtPn-Yt}`^rZRj<~E?_@~suTGtXtCQ}g|O$gY9&ql1CB?oLlrs76IT zcTX0W93dRJDI_3@ThsI2Ybdx7e&%^izpvjPkJ!+>32oE(?lq;ULnn>h6H&03TCc|H z;LSDGu-`Rnr%++Z^GUyrXXZ-#s01iZ?KB^sfUf*zNR!CRQApi1#Ts@NrTzh)Er^f6 zQ-8u%7LPLtp_ZX$^32@^Y4VXrB&GI8(C%4Ncc+FX$>TYd1zV_hlq<7yAf;GIp`Mr0<&i_UH zWb-@$#4{z%_t#=A;OY3sYc^W{1$|wae=(uKW^VhXpRfjc@l<*7Xl;MdO22ro{iBO~ znLD7i?4||^xPu9tgJ-Xi(Sw3Rq$@Ej*wTGt2h@2Njgfo7zyaDmajgt<9zlgueJakYmFaA6Dn?EJItZ6~)8 zvyVXQ!DO{&yIeA!64h%ajjCEJm}RKS>oet6S)=kJ4SF1G4iI+I3Vmxe-D!@M<*4_^E?e0C##2;LoW9-*`PMp#)R9n52K311U!!d3; z57_Z4V-{_{P(Qw}37p)1&Um`@{~PrD{QiDw*YWFrZv4=f3a*(ITuqQvY)FFLB|Gk- zyuwC75^&iPLuzT>O>TvFd3u-RkQcUKikzs3Qn1YM{!-y$$p=#1(ib_&njB?zT7+cA zfBB@ANi)8BW)a#OPCxpo^}HRtbP15P&D$QSzAalDXO^}*~38?^>TRcIHwT#VOM_+4pl!~ZvHk_YTo+KCRn(X}+_KoA*W zC}W%9^%(KHO8J;yxa7CvnDEgMeAk~*Q8F+09+Xkz!Y;nc@kfG0K;W&w3HxkA?#mD* zp=C{0O_^NdPN7WKpu{FAT8hAvs&QERXL0Ezze%f9{4kjyFVK?}nLb;`So+GL%$;s% zU{>SEwjWF@^B#4&ZpXS@_V@fOFY)`=ShI4_d4vzY5LD992|^U3kYCvD2Zmqtp^Q@D zX1H90SUAt|f{7f($>Vctg$5iS->uu^uCO9T1ng=+4VzxO*_FH00~!hOAfp&6=~y9{ z{=q0!$eQqu=zi1qRJ7pScrh_16DyelhX_p?A8+GTWiT;}OJ4k3RHHrO2aDv+QS=CV z37~Q{bS1sRE;~{gSqRejG6mqNvf3cOG#vyTDihZgfuZXyi*2hZT-+=NYd|e3vuHLY zB{^D#63#TjBMpt^EJ`t)sBu*LSu-n!XnAJsT6+A#Kt4M>ewzryeifQ}b+FM~Qd3a{ zgCl)TV{+k#ifQRwq)3sP7P3?VaCf#*rWk2Ngmn&$=HbWA;J8I%F>*RYwubLUg&bnIIV3>O&DB;?DgyK` zciV&$Pce;^F-WaEK9h!zlF3ieG{udL{UwDi!K1<(6VgGoqeWrWiF}`3z!^?h$e|EmKGNvl5I5lnLk6|d$V06bT zBNA6a!-nhW$<2TTa*&YmC_qi=QV+Me=Zw@1jIEQD>aNJEMN6VqTPV#;bTFl35adkLPh$DiD}k6qQL`#YBaqAdG8w}j>2+MEU+((} zG)4lx>~ByoAs{U4Sit3!n>a3!59*(FnO`H^T?VfSor z?x?_4W<18VzwGJYf=O>qu#Y>l26a$Wp=-`CiB3vce>=O^q7J-yOs> zI_eU?LVQmzY$4liqX2zTkm^8aWPN_2rN?W!65oG>N9eKYNh5)n7cA>;qMZKLducJB zG@0$d_peorm}dm#MObkq%x!cX9_I6zirGSYn0$8Ak}oG)Qns$}%Hq4(Y09r}v*qrU zf#k|r<5n8=Wb_6&+*S)HvipYfbM?$XiZ$qpb)#tH>%U`;?;GMS_2z^57WuCQ#)PfV zc6FexW4ih`H+u392u{mR4T7sF^6_;;5=|te_}tWT(HvHX~} z=BC0j@^TB6JEl^kcsAd(dZf(k*=vgoX2OIVp7rk%Lbw*zvT!G(&Snud0p zHDh`71Y5`hO}(309_EB}{Z5feTD;_Ex=4$fnhb}va!Wskep-ZLv!?N%|BR}1PDr;C zM5`Z=pleFWVW{H%b7ga)i5s1a@-fo{p@JHrR5J2vRg!6>Uq*aPWPc}lHg37YJ z9buc!!SQKASPr4k-VO&rZ!$k4Krm_`hO-QtA`fg~hU`Y&k8#gWOyV!mKYd*wnep3D zo|urRxHJqgYhp=Qjd95Y#f^#D0ZtIGx<<(#kvy7#eD#2BelRIaTJJqrm?2MU$X#n5 z7qgPYi4|bijW;`*N&maqkJ#jeS^ABZSlD{DO}s@)rA5(>SM`y>1ofcp2rqQ4fGufa zm0R0sHGOLP_BOC{q+xQ<>6Xg<({0Pd0o&nP-xck@n6RNW7eL-`I}3}YewjPaOgMpG zcEdGRv;Dq)=`p_0bgiQknsZJWS%r<*JO>>Ps#U6%8Az8kQp%xfs1$}>BGmX<0)IG6Z;t|ncF!pY=VMq_e%SD+5$0g`AyP4^6~PDd zfqJj8)X`;YoCJ8E?0>n_TjlR*Cs^~`M3oV*j+qLD9U+@TBnEi+lJwBFoWagvlj2Zf=^+Gr& z^*00gz*-SnB)gmtkKD^bGA{7>6zg#)o|Me>L@UD$^Zyvt|Dmft=z!K|r^E?jyll(` zBLibkSm@HD?R-~=x*bko#d2h}rwB5sUUIz<=9gS&6-&a~w`WSU$#Z_C3{si>CtB+F zif!J=)I>^Q#K&K4c#;tnh!xQ7Ad+63rHH5=ERmHDmh2>e1HnVp(I&sx@hh3>X7|85SK<=Xt`2_X;A zNWqtG7Rys5(t!@my7I)E+8(Whp3my1b!SXc1>PLb&!AJksg77t!EPc`Kyj*SnVma$ z+Q_dkxtPTxrK1Fin~{bw6h&{e0M4aFN2~AOmwesUk0YBVd^Do$B*9*|3?c_5v;OUElW zdvCIryiUsJ%Q0FfZLAH}TDw}iy-$FInto&qCD2dqU`km)JSmmb!UESNie_6QD!Fv{ zdMvzm&o&VV(rq;I-V$x|mN=@mR%u8h9rJ>qj`xOxz>wk(L zMf?fgvXt;l?(xqeM+H8P08Tb91r;M;ZKtMKNII*EPjnO}aU7w4s!R{+m;MWPSuqHWRssglE9h2Qax3kHFv|~*+_^aju@diXse0W}ibX=Q? zsvw#Ncg*4x#Y~ARs!wfs=lr*Sm7ybFX7xyY(t2lA>guAIL8kXLjqZteq&hG*B=7gko65KXV5pA?sGqcEjFMYMu7kXp)eX zc^J2mR8Mr_$kQOc?$TyIXDC%5&W${M_FzFhOpkn(T;fnxIEZ$#Xi1*1zbQ-(5*oqg zPfgE5fg{y6*yL?}RC+2u)KLv%fu8MuEQp|PxB-2(A$UsRBk=&rLOvbL3fhKarEq!1 zBGa&&=bL_Z#^y|Ge3!({kb2rVQ-L`e(;Yt&wG_+N-16{lw>eH_wc&w-Siq5Y5aN}G zw!-BWJXe!#B+f}u3tAPgA13({#N<3*Egepc+!SpnC4w>58jxT8p>9*V0dMdQMtWiL z9|C~?{V{*BoWWLZtyFex9FkOPq%p$ZE7&+oO-to%ZAj=z|5EWKOQ)2*WtJ3fqH%;L zZ@eYSeNR2&c|IH>OkC_Yl1|5}IU~hDklg#)k1B-5>bk@bm(X7fjbawHL}JG3UD~(v zWLsEF^rj_|7OTcmj-yswY~_dw5p$CTePeUDodK1BHs%6lrD0c;w60iOKp5S-c&L-~ z^vJ>i1nB_CN{)JOr;uoq{Ip#BhTW<5U}qbPJ#G9u=~RsO$T>2Y^K!mb?Sm0$u|v_V z(`oUA;4C0n4hVfF2bREb+|SwPLiu&|i3jwM5KG1rUt?3~K<&uVG2|?KNk!b|mLJ@5 zhOw~kx)p*G8QVYRfQV3|Xx$SK9UOL1P~;^^riF~Y4XSdPV&ut%8>k0oA=N4V9s^qn z{!%Y4vEwG+;VokwA6b4pqFU@)BKT2-GZGgL+>>NPXs^wTXcw)HsgY)^cBv%B&{EO5 z57dflMxq-fV?zrG5?Jdg!Ed(~WLM;b^QUeW-wXrks+PtQFlPHux;fq)mv@_m#=h9QT4<_g$`tt>}v?vX` zsl*7)YjQYPIL^4bC{wBqW~c$Q(9kC`ef&!Rs3%i9g%_QKD5~V<9XnDLl|t;UiuDt0 zQ3i7Fft7fn&5#oQV#p{@{LRV*cwixGkjf-&-BWc(+ujE=ocO86rr!iljG2lim#J1F zvqxrBBwK*Cy;`wsO$0x)JHN_F>Pp6@HCOW(6W;k!a}A_fyDaUFq%13|b@5{N>HONo zdm_Z4`FP_UC}NrYZ=UcIqH*}rW1JY(=Xl8zK38i0*Upy`cGiFT35Wt5&{jY~F@>CE zpV80z0$N-d#35qs{6Rz#4JXv}_es^*&^%8CWQ~D6>}4;m@<5P5V_V8^aEcU%TrXIq|B#X%IKC27o213< ziK&&~R7N>!(L<>pUc&^lMHQ)DIh&nzE3>qM;f`}=$|IKLSYY)|@8iX9IDVR$)i=*< zg%~a>arXcFX#T&s%lM)*ZwDmcC(!ed*^$0VUx5Gd%tHtBdoh{_HG2o4hP0GD6*N*x33!VJIR5DR^#j+7(q|L866;fUN+o zG$Oid6-G+o0S~8Fkfe&j9-ryVT3N*sJ{-#zcciaYOD@MB8JSp;CyF-@A8ZrVD_{S%Pn0(H70v4%Q=3f99|;1O6H&4x3uF zl%=l-kGB_Tk9MebP*VyuT)N_ zp&!xfXUh<>T`>k~QzrGQHq4NMnmVl}CCjalLU&AK^^*%*`RXg{q^>jFgr>FqWpana z>_Egu{H9UoGtNl{`01ciG3AvN=zCrYG|vV0eRZ* zz9~KZcB@f4`ipm6iKo&6D_3{5>?fVz_l)2FV#3nPuK5=)_nFunqksAd_j;N~zc?Bb zVU_>r9Z9DMad-XleveDzn5^355t{m&F{)L9@Rz1`& zp`Y8eoUWa8?P&lqPmu8uJw#sv9XZXAY$e;KhUo5G_n>}OjH5tg=EP` zH~m>wF~?{>Zg1y?o|-5Sg>q>vbG(deSOE(s&c~H2d%P$Zh{liT;zDz{uGn>-)p1vU z3J595=)bW!gDOGljhl`SwXVyX6?aKTNn$gxlYM*6&H!_)k2S)lSK8lA>v4eZ>Y29j4#h4G8)6NvR$7%= z;$>K^D_u*nDl?Y5taO+6$Zx3~s4gehm1;MpTHEuK(cZWEKtc%Rt$lr-pN?a0c^XM@ zD(J0<03>wAB(ZrI`ocumh$iS5U^^wPFYy2~Ql@oM#2RE>N`h!E>!Q{)+8T`kw4Pqy z5}t2JRJVkCv|6 zNn(jx(}HBqlolu8K|~>+!JMW{ip{*Ff$!tWA#98q8I6#WZ5dW~N0c%K_F=JGf9y)r6S0$j`66e*N7TOK_i#0Q3cE1S{(0t#3cyq-574>_gMyhO_7> zQM;td(u*-!>TEEM>MVi#-P$;VNQ=~h0BC@H{NZ!qqwM}dE6in6JP3X6L%#f^jlVXqg1_U ziXi<=R%1_QC1n^B<10*I$c37O>m0eai7jSGJv#1e`~gi?*p_Fj7ieE`ZCU)iKN#|s ztn;H+{p`rH7TeqJv+q%)(>0ThiT$;yM{UxzkG%Cp7xy?8(r0JXHCINhWh1o|P3Be? z|E(PuAX@+EA_rjpM*rjq*?Rw_l`eA7R41ou$);1t)!$)ewNg3q%<3nFxOxwdR}~C+S@X6q<+N2yLZl6 z_``5m$#BGKKq$hxQThEwIgQxll96k0ozAgW3tDvNLJAGqQsK!qKR!ZH{GcOiwH!_T z%B(?lpD|zu1F46{&cXvKG0mPYk!9Pe)YYj(*?1ERL(7I|RDZ?47!J;qlCNr`gih!w z>kioKnKLtYGCSit`fKb%(!X8`_lX7htxe`PXHnAmn;;B|*{_`lRL^eTocQh3t=%=d z6AQfgUq<$S;K!d7H9wwOTKMf!kg%lnN*aw7kr$i36U`uOF~WIG^gi3f(NWM00gShf z07Vm~RnUvl579ZFHfylssqUpCa;I2aGL5o5JZxuTl8#n#B2($J39qnk@ngh(OyhoW1wrS;Rmx{$MG{;G_y>JtCXZhU)pB9)uCw^ND{0yy`U_??*1a7N zCY+y-U;YTDoXmi%v19CT*~}OP>ED}(j6KuRcF5!{y<@s%_ITApMUp*G9C_KJZ<&gw zyz2-O=)&gd$@H?7CO@-s#mXB&lEVbW`I=SGy z`%%8g8$ZU2Ta}%UnL2htV6YHDHTrt&T9u>@d-~n%1SETTwgD;5R^$8E3S@!K=a%ns z@zGM~meER2e4&F5r8aDAp031$lc929B#CAI!t^;N$-!qk?R1(f`iFWN$+)QL!{TT@ ze6w*S=GXRCZJ_T?_*vLp%iu_)he>_{yox15Og49xi?u?HY>+h9fU8Ax&tQHDiw7A! zl;O}zRnB{UXWn6^43HJhLS3JZhev?blRQM(XBxw48C%!0LqTV^{E%BUEK)pCZzr2y%yj&ID(b%_SEQy(+eiN$L(~5Re0WZw=jxv zt{ONs2yhUL0u@ptuC2z91IwCKnB8A2rwJm-J~iw~BGyqzO^U-0w1Ai7%1FrEU+}h7 z=@hgo+$2O1@n=3Mm>=Q=^M6xm<~Je9cFgdBdnN8xZrR4?afsd@*u@365e`~NdWa0h zTcjcdry{%Zqa-hra{rPbk`CTb5v)WB#1ugeo~A1UqS_~4!1O*TBev0n zO$*@<`jrWxy6U3Vov{*QexturK&5Sw&@4B}u}uCcL5&*HGspESr#^9c?0960TReQ| z=?|6lQVfwg{IR%FRwPpWKM?f)zmE9XZq@1F)WTmJ-o-0Li#|L=u%O@6x4q>0CKgTl z&kJ!C1EGnuKZ8U|fvW{&Agxiu;^SeMGqq@7gH$F&2TRuU?I@y8wGpZ^tG z1|TVCy!-(mQ0v(;8)O-JD<{>Ptb)&6I7)54HyZwVtWB3!E|Ga!H0DtG#@RV8#T?Mo#QYJ^l8d-^{>Vh)3T1I!dt>xMY`5R~vBoIPN@zhf4 zUP~YZV8h zD8nzU2oumqmZ6)7C2D%ZaFv>mjNunXf@7r?UGW=ftJW!5dIbxy{D&{G*_~2 zuK+jkx8=fXJVsP? z#HTy~aybp74~1MSGnOL%eZK=%g!aPWOYJaP`QqFBqIZ~i@o06x?ewjzcuIKN#02JO zP;^WcP*9NRwHy)c(H846?whImpqCvqHk9J@w4`KgoZ~k##kLsT zeo}&+xS6Sj^ByJ!7RiAgOFR^}E4R(Iw1Zy-l86L@0(MS`8)SCBc>`!|P_t`pot`s# zk4EM?7I$I8^<&O(ZyVN(`gxhfzK$#p=3sJU>uzZ@Q4+wFJIh)FJCRcn@%+_n6{OjI zy}iO;Q^+FSZ@x7m=kD@#r|}A&Kv4#{gptdCgt_#;MTmP&MXQvN*N!D;)F}M9n4>_$ z#cw_BQ1K*Jrx4t+l=bAH-GhzJrBCm1Yp%4^6sio|Hai>bH_Qn}6zVW3l|%5I)m*6s zXp8oUbM+nA5Wf2Y$|>@3gry$ zjH2UPS&63~68=WnJb=)Nhf8+Dg={8%=!BOiX`mxi+ncf2lpLWhnf_EE4F?xlcQ)0U zCFh_N##Rc-n(6>vHC0~Z7byl)@Or#E%P7V&W`+G&bzr5Cr%d6`8rS>cYNFtlBQY6l zDpBGER-+1GqC|*`aZZ+s#6uo=_f*@uZYVt}dh^AJXPT-d~36;643H>=vzt!+q z$XUZTzdolW&zEhs&38^YPZ{CSHN%9+jvS~NeAyOM-lcAOS{wY+Ct6w=r*q3W50}0h z83ySK)Zd|JJVH9VZtZuktVraM+12$J1{!m6%I3D;aI2yBu2s4D8vR^_1 zC-JjPYjOm^=y2})u@C!OSH^SpyQS&{D#`NMM~f*k`LSDTQ59)2fR-ZCN#C-~zsXXR z)AQ!UmR&JexC~%@W&hPvk&xT!KD0HkN&=Fex_|0_%M-A9^t8dc{m#Q|luiyWv&d$v ze>_#HxU1|-{+P9*Rq4ZEaKaFJ9wm-uV%2)f9%F9}QWf|$B}3$-{zn|@@QQ5#l?stT zmn`&lUavJ*9?fr#OtT2($vmCRT}BQfRrT}gySBh$%*5Vv;q8-cIn(ja%NqTaC6rWc zk3yb56c7XJd@`oOEKiL&#dL#dG4l3*AB@_PxpzS?q%;B9&K~slMFL#^+aUgbax=9!wH%Oo3=hW(VUpw| zEvi&GD~&$iD&rgVQU4jazWfoVJHIZ%KUOu3z$IJ0o!5s2N2(uZ*glSOwktH7>d^_) zojzeXhu)%}k0CcJa%Q}GN$U&8ZXX8!X1;C9P@lE&Y=vNR^Ph@IS^DJLHLu|lM!?7` zDFjzNp-H~H1fh-|;qT~XcT8dD2w^f9<~&RulsIe+t_JuEUc_8Eo9{h>#F^0Regz<= z=-#{6YPT2%d|Ur%Ag(#0}096NhZ-M z#_%eo;vHnE0Bx+e&67vNU@=P?r7S!TT<*%OsZd@rBl+=O9DEFHCLT<75g*YMG8$G; zX}nhuTjx9XEO4vBX1fc6{X|B1XQ3DOd>53T?fmjxx#B z*spST1glgd`tx7+s_b(D5uc5;wFZz)6~Je9t$4-zcv2Jt&KPS|8K6n4q=Q2XT2C?7 zZBCh;Ey!-gu(6kz00Ig^=pS#uOydn>z-P%!@(ENTG#Ld=C zJtn;SV}(}z-EW}^Ri`{#88h(cH9YhqRGYFJcjo;mjw=T^+5?i68C zbZG6?JX!ByMx$0`>}j$K(EWeYX&ozNHI zi%;5(+e0&l(o*+g*}t_zgu#M?NPvCc=;*{tKjBS6vGxnMxj4A~;Dw*i40s{n2p**# z@EV+$ER#-5d30_s1Tq9NqD~w7V+tprIyFcK}JNQVnEo> z!7zJx%G$J%IxIStSUn17*mV+V;Oo5Zu2(FkpDk_wP3CiUSBDJ1TN z4beIB(h5Sa+idK{YlO$9^$~u$wxzZTO@GcSKOb+`gqNh1?i-7Jw^M6Ftfy?pulEqp zVQn(^l0+CzzT=2}&C9r!K6p0b=sR8!ru`^x4UzW#)pSG_9UzANA&9LZBfKa4ZIxG} z7b%w%y;CXj zq1S}V0N@phB*D%h-MnvhkybQ9oxCvo=1}HrLKKvb9Avz+?v?50@>7hvV!+rntZH-V zw+{8Rd?j-vI;2-rhV(Ia)GFLLQHCCQ7UI?~qz2js9$-rl6V$%Cbt&Yl)ExN@^bp{F z(a!nW&qy%ELJ2qQ3h^@h1BkG&xG3>jk;G3^Fby=uv&Y}g`VvkBph4%K!^;)H)pzPCvQ7w*6bo&0ZeK(RXgwAlYY2{lCG>JTc zU#W#Hry91h;wW+WDfaHZm-CA7$#LHJE)oG_MabfK5mJ)5wKK#^S$MqCsq-?0+yhX7 zP=&Q@kB)Qt($+74DgAZXvo6!4NEqmsizbMxfjtw@TkRm^Fn1TP_Fah(I?fjg`3ug(g-~Er?VY&F99kRudJofeRZ(G!A z?TA-F3yI!LA0?tR3B7BCJDu2Vb(RMT>$-F79-KwEYb3TeRf&{EA7L1FMtclW60G&a zlaJPUaSu+0(OvVwdk4ktg(>cbrfyj>j5K-oty@f`wSdpsMn_B7u6>O5((W|yn%$&1 zy(A&6L5rj`(I|7MmJM`}bFs zTMd}$&GtVv9PXU$7n{9GvG=w8Uip1Tv3ynD&U8C97IHIY@)cqi1D;U8%hO8K;J!Ke zg2(5X2IO}>3VpE-VYwI*XwgIo6u2Ktfmt&HgFY{pFSs|87aK!o^(+hx1nd+wH^ibK`R*?ENQLk)rodipt8y*tFirFk6PK z&gdGqlaX*NLP0F#sA9!cK)^iDgrFqfLsv0giY=lFkHfKs%qmj^Rb$EbSlRC3gAb|T2#GdGNmXffH2Cj2l*A_{8TaDW}&Wf2cbm>W}RT{W^tbT8} zBHx+ocSoHP{{hx+*PSyvsMQ$4M=9dt^d#)9>#1UCO?*bN!b*;KjAFo4D+bNa3yUnbF4;GUj8mP^cB;UOG zMm=F)6%xc@JEo9He>r*;5?ojOTD7I`8(k*r8W44iaZV*c*gGHcC2^Adt)!fH2yAFc zE_ljhR6b!wEL6=HW$9|wZIS`q&7J1kc)aPo5ud`|pA&gF+zwyG==1NHn|U3Wj4z|< z4NJq)rtAQ!!ydw!KRiR?mBFfUdVPifx|=`6%wXUjxa0)`{-pOgPA8BdN8qG@=`IO} zM)fRQLd1k+D2D0+oZ~oYj~MR@sAZ35&CkWZX-O5V_vP@pf4skcyi0m^_L&a&@0=x0 z%YXU__9Iv;c`rL(-W;d>qk*c5g2T>VI7LcdJ+1KfQZP%FwQMWEg`+6@buN z^SOs-5hvK^IAX|(t*5L~bZHrUiZjrbmws%`ay9_$RvLXf)N4=SLj&3S_1^V3(gI0h zYM?`wtr^%Xjmodr$;BgBYgA&j=wo);dt8*8(yr+i+8|6Y1TC)jHS}tq7~`{$>Ho%j zEy?o5ngMU59Rq@HF8(|vnT%RSxFZK3@@OQ6iiuqOipiU=G9UkA!Fkz!aD4n%*Cy#8 z;Q3%#C?M@WiVFW8_C#p82qVjuX6J!6bS$1k_z8t@UUTuy2BE zBm827_$#DRHK!VODqdjXxc2LEe?`N>VuZz_)G{y=qeOSxWEeo3BAb{M@L(o#`CaH! zgU>23oV=P+4@ClzZTmd;{8U5mX@?*onnXG0m*P_2WDQm>hm2UIGeo5M6y3=S+lHYJcWDA@ zNAK0|HK1e0#amIdAWi=rk;t+r<_f`w0t-k#25TpQL zH7r&2Oad4bbgSf$CD}u~O=vqmPRMPizr{6_g`Fu=Ntvs!F8vk${_Ppky2kf(+k6^e zo0QhGud`tw zFZDsWwt{+tBTdAC)tH4lAwtQb$dkG8U8q*r1qg=%n+udJhktRGsZia#M7e4kJ1ySq z-b4Cx-<{|{d_j0|Q%!0uy3MWP!AmO=Zq!*V$NHO6qq!A(B3>-vO2tmps-^}*Il0Hl zNNi1!idPtwBVpC?=f0e!&zog&*+nX(K+W&I1Z6jSR{Y5^*k#Lw(IsZ$RGD&+c5}j< zfTh(10P@FXp)Wx`>^V3IaW#()|KLh#~Gi0qP>o3dpa{YMEkhJyT~h!7$U^p4S$2CJ?{M&+ds)(BeVs{L4eEd5S!fjbq(T*Y7%0CfN~ZayscK1_v0EeKL=K%q!qUkDf_yQQ>h^P01*%l zj`DLxU#A7BqKMyTDMM0396+aFT+{9DPoo+o>r&u?^ZVHj@9c@=2N=jL8fzJ$ZkN4XZS4s53=hQ_Vk)MvB{Ru;j?yVum6 zrQ@*%_-~{~A$-X$`~#}72Ng$huS%rD@hdg!>%X*6)6y#i$Z8akql*ko7ONCt+l`JP z;Q+lnh4y%<5=9Y|-919}hm^i?CX>`^FhA6gPg}|_M^i@OZI$O63zrLs>;v4UK3yGI zru_A-FzWDdUPDe9xv_iSU5Ul9`|gJYS@MN$N6v1cT|4OO6Z@m@2m>e8UL6w^LJ82G z92H$(DnvG_>B4Mkl@)CDI!#hCgh^}ORyW-2nKIuTiTmoYIwlm8e^fs0OK+d`B6v3z zsDg*+F@}H*T_%L&pLn+1Df^OV{nLnd_R48>BW$ zxTqS@SMbJ9%}9?$g0q$7bM}igrthv`BFkvBB+nmkZ=SyALaB#sJi`tl zuu!S;(w%wG^Z0Ykoq=KtBQuWF248PCE;xaX4cj{@RvPbrjN#PwS+x(PYBtEMxD6C3 z743f-pC7)u~nj>uWAv}fNpU6tHMj1=5Wgv&D*tQzi$R;6fz$L$3 zPyK70we|;38j?!B_jS4ePH}C6TlIhQ1Z)Pqe>70G1EVXIFSWx)hSz`f7!}#=OJ8aS zln6kg;O?yb8lvPUoVF4gO#EdvTvlDQ<+@0UI7>S&upL0bZ?WlZ7arY;;;Vj-(Kkw{ zJcj%!lGzIUO6%zb(VS(pcV%O=q!g2?Hj@&!9tnKtuSsH0S6(b&B#Z)103Ev}RWCh@ zG0}7W?reV}-$zK(l)>aL)TVYT4*$E2$&dDw?p%`NyPt~1cypj)&Rx~FVfv2Z@M?}! zxxKP1&BQ&>;_&&8JFiCzt*iQLp15FgG;3Ww#XYh6 zTd;@PiGZ8G<=vl&1Qje4EHbqTD;4FQ&;KWI@E>~Sx6sqVsuD`*ew?qX+K!tUtQ9>6 zTSi5Znv%R^jwmmmMNEuMG&JI|u5S;qB#1gC6r@IC_XLTNCUkp4`tE_1l?W;9mziP1 zf$6q+S?!Zm8C((Ta&L}ncTvX%uh%5cTN7fs_fuZSw|B{2CMen!#^Dfn!Z= z;z}X_sNwolbA&CoWY+BVYC?@=o7t9h5hcd+bgZUaZy~kfb#6lG8|!5algx=(4Vldm zs{r>H)ErEpjhGfm{bHqCW8OxG6LD;+2p~7$qfZE3wkN5KPNoRD6+$41B8|W{6Q0%@WhyIDR0KQ&BQWh9dP!I#IC4uI6CaZh2?@n)+#KqW z^l58`!(x63*?LK}9M>DsboyMhDCST3T z`|6LKe+P&L%?;INLnTzO`P#MZtI_Poi?H;I)kjc_P}GQte^-gIWh$6ef}3&rE6lgo znhIebCH;N|QV>a*j;}w!qG71GawV=WBCu+K98LMdUu^Y$ez-C1PgM!B* zFT&m;=uejQ1(2ntY53qn3Z0}fQe96vXxsv=cC5mbdPL4M*`voX-s`h#Zl6f0b@;B_ z4-C37nBlp-*+&zNTaGB+FhQE> zKN9hWp`uO-3?*4R>oyrHATmsUMkK)ffyLln_N2^6T2=Ukcz)dc)sX(@um0RUg$I*i z&h4JdL>g#zTwSH}UFU;V4i`gU?`@$hQWcGx=n3P@QA!my1x8~op)Y^NN-=$GGh|im zO$aSD+NzsV=xgo5ry(Vg3~uF$)nAWzXA_!R5_iBQPK@{F7dn%8JI0aHT>0tWKK;F= zS>CJ*lPMr}YdQQH_#X=I|LK^k*AE9LExZxEB))U)`V1o^=#{fuITBG{5+KDc`x`$x z5VzVm23_T{s;y{@ZicWj1axM#^2lzM%e1fRS+VfT6!p*g%LBYAm2yH-VBNf*Kh7Y# zSKhcq9V?&ZkJKsNTE!m>)ZgT{BS@c~sD`1WysH<YxX@yXb84Kw|%$Re(?_nGe`+xo0Sb|GlnZK5l8cHuC zL>uFk63EY%QvFKoIMLt@B7Td#Cns%fz){gr$h*jj7KZ)?$o+{8G6`@cN(;($PSHIa z4Ue9?#uIZ5M8T;&(mAjD;rk(0#KKVfB9kVIE6+5Oil(2`lbChM>rPz-MB4TC)@@C7>L9=@j+;TZ@4umN=t?)=RkOEp4t9=QT z)m3!84u`B-J&*Y!td51GTJt?FoQ(X-#lSn=tQp0?3{4m7>e>0w%}5`@=vK77&a;L; zptU6}*{OUd(?8pKa^!ndM25vn9nds7o zjI#0Fc(J4zNNGavS@`Fjwf%8D>?(6EeuYb8R|Y9L49O2CX?AyarbnqWSASS@TgK^Y znH((atShI)mdmM=Rw<9NffmPwpeB%!W9473t zA9yn*U347A1(Y;c8%=|&10$l0JKf6qaW{u2u-xsCd>Lq})e^@4kZ?C_#TwO7D5{4` z9jRhdZsNJfeDyT(-y?F@h6&G-c=v9IR@B#}Fi#;3tQA{_27Fe}tBCuOg;Pv}CWt4p zYJwsDo*{8zgnw+0uMElieD$QI$S5P8l~G%p6E_nHhsz1+waJflcPay(+i(NPSrlo; z!DE9^d`5)IX{rM#A_^`p5`og5J050Dgq(sLdH*gu(nxMaGKXhH|6=E-D7(@kJO7w4 zUEz;3x)3czX(@;C5>6m=QEEuRX%sU|nJoq{QrbC%_I4tQUS0fw=`Dz5g-dmtduYA& zH|!xHTt&$ZWlrsQyhQW+n~oG;l42f^gG<|LCfo{^#?v7^z5kIS?%S(S$U(1~J>QTC zNKT@U@?k$@srZm-xnNOP^4(7uB`v`_i} z(e_nQakkBtcyM2L_MkqYTh@*c3FQHn>Y_evV&)5hW(-=UYL4I9KiX>;*QQHwjaJ7o%ZcM8qL z?5*+{@{Q|rQATQ9%>oaf&Zpk>ei0YK;CSOy4f>zS2+wz@Dql8SJ2n(AlZ$O(6LV?Y z+e!9=s~>#W-R9VW3u5iLH}nly{gp|}gyCyLsTo$=vToy=KK`NJQ$ zBfUZ_9jVRxcX(dXZW7csMx6$ya-ZO}#Y5vuEEiT>N6&AmlyoFD2rgtTom&z#S5z|o zia~zl$*BXUcoq;qo48Td4VZc)^koI|pk$90i$#}9S?WC+wTgBW#{M5_M<6K?=Cs`! zx~f=Li!|timk!G&HBCVhw5}Z|t_Jqps0tW`CQeYf!gSFfB+!pBvIHbW@}42=7cw~v z{Bkv=LL8nPIFHdbWhE%YH?^JIv2abY5Wsd&$bhx_Hvcm>(@le{9IFJ$JIinh!5j1U z<6nd#j-2yrmpX}YD5#tBPZ|@1$tmE9|Me-XjPTF z8KRfet2Ld4mcd`pFgq-JKsCYsD00A(DjK6;8g&)ji}zxTDy0vQ!JO)BtUR;mI{KSNqGU3SUPQm3XjQWw$Y(%t06_?S z`RLmS+{pm>>TnoiH(TYQB!c2czH6g-=IG4`>n`tlQh4gxS-dpXuzx3lxRx>3F<`A@ z{UQm&Lq4NH`B+yd2FIttPwr*B8mG$3M}An#S^uYBp%!~IBwVfq;Fd+L`8CpidWT2L zC1>Ly$8q4~U)|=yy?0s(a3830xE>=MV??Poeg67q1G0*Yo$XX5&VXV+IGSwhD5;@g z=Qsnlt&!|=;w|I4oFhbulnX2Log7}eVHb=(Cg<_Qlz`TsAWCcII{y@`cb_ZAU6l46 z2=BhOgbD|{ju{1!r~-3W9IEtF8|vvY0t&15E~68XKzXBFcvD~d_15p*I5j?>j+Z}i zniN(Px(V{2ghsP`CgOwpNSgpRe{eJzk>Wou*WyVe1ud8gVlBP@MLXFZ(YQ2qE(a9L zRuDd!X=IlCG*7Y;m0wl1RYj|Eu_#ArXRsIEl7mZJzS(2(@#fj{_mAmgn;xm2L#X79 z{=cU(_&;yiO=t970l31709_~=6I$QCe>G+pd$&?A9`9)fr(}|OM_F>Cha*_N2gbJF zw#E-+cgu-!ay`xsfiq`Be0Pk?Ka}C-l}FDJu{1NAOo_#rua-Ez1q)52`!yf7Nu_wd z>7m7EGh`NVu(GZh)=4#f9i_3I?HP%^75o9pq#$8)HX*Ql`o--`z+Oe5={-)1X5NDN5+&_hamXni)syG)eU z2m!j8_U8!7OD((l3qxbhXmT_$*8aLg;DMga=xeF$_o7ndNf|d!5i&E@CLF(*m_hn+ z1qbbeQ4L;X58v?+8cfV#6^ss?8>11PG4HBr#%~_9Rz~mEidre_$VmgaCjMliMSXb7 z+9)8>ZBdSIc3Fe^8SkSuRqB{YkS?ZHV3w9JRM1aN{MV6zkw6FUnIe;O`JBqacH;%2 zw$7}P;|wFADjEIDo$AyL+mrsEm!HFP6W4Dg*q%BfgK9JvzgAD)HqPLhoK;im5N>ri zySAtjUUCl<;i+MerXCR^Mec{sszgkVQ3__Lu%2I}M!_Kq8s0t`yY``sc5!jLjm`90M?lO#S?JijqBie0vN8d1fNAB zpe>|Ye)X_IUBX%9)8mz(XA@1;J~EjsDC5Z?2stu+@xpc1pd1s0!7e0YCVx^sbyKCk zsG*iYY$%ulSx3|N=vG1dS4xMZ$8yf8tm6;tMPqgx+L!^9#mbKgZRKkqQxexjx(rWK zADy7)1zizmjaJ9_rUMxat4@$u-q^QF>!q@Gyn)8D#IK&O8MckcwqB%9rLbmjVwsu6 z>Y+BcfN*ordQcXW4{O>a{Edpsb81vDh%>tAU)K3cpLC&>T%5VcxSskygZkg~=?(z~52`-~x-cx& z!*qK2esi@|^FzI;HN$u5&#-FJ50l7Zd0ylJA_xwm zzYL5?FPWL7+>KgpI`|qHteClpiRSsFsn^XTAJSlv=`1MLt10BCBu3P9BH%6F%9qzbEGEB{2zjTomR#V@vs@lcKQ&HY`qtMalcR|mjT zX~QM?g`_VmqAsM~34a$b)EPOqNvSSmP_e|r`#w_I868JgwZR+WN0s(wIuylc*cnH+ zwzcV7wHkExi`eJw@z)t1&Yjv|x$j;6`tF^+9>J5MN3U92+tdC$!Sdedyir(qifZh2 z#dXWwn4ICKB{6rXP8u%NHNJthE#a($d}b7_mLhc`yq1vhuK(omZ7rxx{ zEltSLbzmeaPqC1rew=dOXX+DkGXKE5fO5-J8j()bu@^uPJH)5>tqRYL;QJmG1)t#1 z(jdAkDZR)Lt?aCT_Olgd{dTZUueu+5?xUYrrjY4W8SV7A3My_?d z?}8Q_5WZKyRKMLpBv>X~?BQ3yWo)F(I3LgAt{u!hvo`^NN2 zvz}Osc{)^~&qw@7{TKN{=n*Z{q3-z;ie9yE$;;1}lrRWq5&eI)ujq*on(1>UWyi^b zyhK$kV7|vQtv2;lX3F^e%~1jI97Jqi z)d-3lCIgJcNMNgZWLsRdEE2e>6^wnSJQJn}>ZApOYoDQ!p%C^B-Lo6LfAJG~ES8Sp zy~7}8&fhxW>;2omJjNc=8hD)mpF0rd!M56AM`;r$R_CFbF*zH(A=z)X7|Cfs+91sgWZtaw^)0K8G6|u`ud`%JYLO4x66X`?5)}Do2(x)!Y@Am zyE4Fkde1IOuIuQgE5JkmsO-2qS-OzVxk}H;XsufmL>^{}o2!$w=ep)!!o9T9W#G}o zj4hM=GyOBp$OaVV#A!ekRnzigL1|Ztrf{T;9(e8eo^;$zHPR+B-c!oDSl1g50+@rm z7>^pQVEk=Q2?!?>%wFzSSAiQpQw^o$`v^OveGDiKnJ&?5DVPJr?GM$V^+$&w$c+Js z*P*X{OE<3*1%uwl=CfP2EU%SR?diWfNfI|OakYol&mEj5X@N%+|0I&wrmE_+?YJ^$ zCv&9CFq7l6ds9Og(jgwa>Xe$IfI2QpfB6H_hHWIiw!3ZjHj%B*86p(WpQrR5bbeQk znjW&&KRAtEtJ0egf`)47PPue2boBS!YG-=Y#;j3&a@ivEruHy>^E{fVFyQ{tig6r~ z5TkA0cwBT_Pe;9{S>O<-J$t%|p$5?_u8lvRGN4nSNSviIlx@00WS{?igP-YUxORrbtgpe8|I#-{C49e*qVn z4)fXFibDRb*D%*S&9$Xat}_oqZ6e+|;(P^l=~oits6=u?ImY%P#ZRMMR|Cd5)grNG zs>*{a;=1U5H652Q$_lZ4Eshj5&c=wmg#hAxvG>$WyvlP=s*z) z6*_A8;iB}10oXG=%b(=$K|OvLPl23F6jY2lCoHEr;8&ZXsg0^4i9n;=0LVXef)!kk zQ8W-T0qoBC+dG7M8UD=)0e^FXH4G&tx5M?%Tc}Ua zbM5l%mb?Sru!#@<#!|Y0@MJX(XL;XB8bSR)xGU@mq#VEf_;6<`*;GqT8ljy_n>PECuB0;-Of3@%afq(146(TRHM{CTHGaDj_mfXE8k5!8vQ zUMT3=RlR{%OF=Xbk1SqG9q+hR>K4k<~-a+|LDyi>PUFdlYhr)?IC z!CqGc32<7;1QxGE34S{?vTxem1TT>S#k_T%9b$iIsbRDVpU7wvGg-=?Z?MPkM=xt{ zts~t=fj^1IH7R>ol_e>R#07N~kS{)eZ46?u(S4SJFX+v)RcyAXaVGMT7>LY2*&^bE zVPGnDvAV7MN ziW;FA#=wQO4Wo&h`U81Hm28lwJYh6|-J(cqLojtvf9md&bwx}2qP`!G@MUSHG;PEO zK1bh31$w?j?WI{eTOQypz~gSEf4s(?_V1v;&6_S+TlZ zX5=nnlbo=$LakjirBw}MuF-scK`eds3Oi>rkX~;#S6(+N?swOFDzUJ&1s0(R5EgPz zroC?0v@P-XiKxoOe6N{TpAFR+YTecRu{?r;*Sas-v%M&#GaI;wF1s{%y^^a z?8xN;g8bPiPUr7-h~xV3+#AMel!56}t*oJTZpgCsu9Kc*-{zT#5cps^FmvaBPr(1* zZn;F%U}5wW-O%_CKn|S=hvbW|k-fndlwsvjHt8qytAN)+rEabju&V52l=GYD4 zG{hSHYSbKlY7Y@{VA(_|=iWed^Gh@;>&Z@0+!YtU>ZMwHdbm=<@KlCscF`x)62!n% z6sL}ChVpHt3DWPJ045G=pfHd4 zO)$kmnSV}D^`s3b;rFY(!| zjf(l8e*PR5F76rE`l;7w0Q~Dp_(bZgHOx+MY1xw97{v~!hfAs>%FMszhzzm5Gci4# zd7(*&Op-T#nw6DH4V2onYFoR4>2?G&-FosU|9M+LGx{g!r}+N!8=F9wD=S?aFr|aq zl{h^|Ae|1d=k>)_-<+++($+bNj6*2dvet3E-NqtCP7drl6qP9e36A-ow&o>h@19z| zCi8jnV9-LyLy9cF?peMJ*&Q;Xz!bgI^^HGF?z-L(>D9M2P5V+0srVGs@?4j}El2B7 zgYm`N@V4C9a2&aI&G-1flJB%|TdsV3_=!ioYzWs(`==IBzu45rdiK0ViZ4G3#bF{Q z9K4L~Z=vn=u^hkR&(dpNR}FI0atdOd-eC?hS*ke=a{BRu+cJz6M~%(ymQHmwnD3=G zk2GJ8;N{wk6sUf-NNGrrEme5O zwXH=VR?-4pSRsvXE(+UcsD0mFU&=`^qWUUd#;Z9q zV;{40WxEp`8!<&0Vv=EB%7psLzNTZ6&@EyH9~+*Hkf(`4!<>M+tAZy*etjZOQ;#*% z!BYp7gW-3{yyveK?}c~muNeUzQ_&`?mw@-c>%PxcEcX)h?(#&Q<-j8e4aNu&VJ|N8 z=|jcz$e3=P&C%1#F_FUWTSiJ*5g2+hRv~YnKmOmM``_^0Zbq)>=x$0|c1*KIiABSu zQF%#KJC6=4KQ>cdx&FF-7FUt1uUsNhHMEti{5UVEmp9n+#ahl_SpsC#+@(p8Q!tz7 z_>9~fM?8bDIMr7Gt=s2E1($Jsu3brivd{9L8nU&#S9930fqF`1O^<4t72fef<5uwl zwOS1wsJ|WYHXde51zS2J4C+_gffg#4*+J$3DrkBZZu(IN&fjZpHpt!B9*K`3*3$ z1-^qCqrBIZN(!0z6)9UeAuen|d#$E5p}Pf}nP7z$<2||IjCG7Ki4VldR`0dw!)Scp)Dm3+$xH^H=*D@(6}D!S zD&C9{I;L}FNuA@Q;27KHsL^`sEv+MU+nrGgOJea5;XW#gh;o$nhQNaCSYEkjo~Mw3 zi<<7yqoMmW|Bt?=4*Jcsy#)WM(41owQ;$`QyU@IL4kr&$kcF`)o7?Ng)>1-OCrjvE zNZNefMx@x*?ANox%=P8B7uhv>`je-gA#$qB+it?C{B0Lft;!@{YI7Da2rrIQlsX@f z5x_ptq1}IaYDRBM*Xc!4ewi>{38mU(;Go%kh#!cP%eIbZVNq2+?W~jB9Aa}m!c)(j zW4u>sjs14VuCTT~*UiBoAWd6NnUxx%Ev;@Kdu-T(EJ+#~B90GZMPNwc8>htyg(?V? zV})9?PEls*PK6fRP1d)_KiKVYq7q?*4c}b|ePt+<<1RFWjW~omVkV4vlv1uo8U?mA z^Gn!BFL-u!Z6h;6MRl?)MTb4)0Gjj-9{FR!^c18du0HG?mfHXH4#R(Y2RM%rv)lhK zo=G_bc37#r+46}eHRHHOpc4znhvoxa{nK|;07+Q-aH`RvtcvZzIKtx)2g8dH`n~8_X(>lN zoJp#;$GV;DD-o!Ep!F2_1y0g7nuz9SCw;&P5VVyQM1n(h|I;_0xmQESwD@j%I^d;K z%ZdRc*uP?8DhCi$s!wNr>fghCGF8iz6+!_L0o!|8N-KtoRQfr9o5|uv)(+~KRMj1@ z56X421D}3ziTo~yvCE->kk97|vk){h?&juh{}NxYU?uB@_SR z_p5M;z0&-A%`)FEeN}=Sl031!jR{;QM>m9yx98`YRg+JlnX1WXpm!;JdKz#h&mTDyM=obFStr9$<)WO ziZmtB6^%mtBhOGp?&(aQ8Bbk}k5K8|)H-$p|7mLkPGTqiqolzFzCKp+%1eJqtUpbpZ~-_VWH(N@{BtCGwa@ z>);~RoqEcMk)oIg^m((kJLgX*#oMKYadM!yn||b_x2*o`O2xPQK52{m`d=c_#l5#C z(ceN1_I+^9K8*Y^%(@Wno8V%G_YaJTvgk#aNDc1TC*c_~d51=3Gzdu~9hK5<1xPQ1 zjn4Uv%CIXMf3b#81ag5m6^dpV1Ge5Q6zI>kSSk(Qzd9R!JK{zF`*sXF=Ix2;Vib?W zOy`{=nGj|5AIhnnMFKV|Xhu=*q_d9OzC2cwuaC%x*e34bQ1r)UUr*TNSt_K#b<~-b zRTu}eVB9AMCCO>pgi3l>uPIJged6X)6{+lkZf2P%aG1T1LXX7oqKu%xFS9Rr)5@?) zD<}pN^=Ir(dY_Q5lf$xe9is_BBYy1(mR=P8Cnsna!e^0zklvc^d>d@nqUv~^M0lN$ z$ieOd*JDHhZ0K!$e0(rM!7tihP>S8bevL+mN|Cdtp;8trx?Y^?ne!@U#SP4pi(%ZJ zo|q-yKsCO#0>g>Qe==Z4FFtlw=chwegf5Izaw&@SYQo>4F-c*!_h zLCGodlfW^5FHSO&?wk8A(t@R7v&I+xn1coKqZFgiX})VARlM zn*!*R-tF89_ry&?tR;zeA8j!sE}OnJDU0Yd`Hfr8nsHunyoheP$?$6GatP79O=X;b z%sfG86hEpS=-#_n6YGyTv5uE9M>7(8jR9QGi3$PI5%!7JH zK_Tz&a$#fbHdXLM>aP8YsX+2qiPuwNb^@nD?rozLXAMSto^mhOMLrOV1}PYQct|$? zIV3y5&&21SJv~YIUCiy*;3RbIE12|Kxzg|cG#npOGhf{iGuVd>d8~o@cGNv7?9v)a z9f6FTN0qOu>yRo=%rBz=?8pqdON1=~a%@?14b|M;B7vOj#E-g}1~>fUm|)>@+_tM- zUkxn*39fV+P;Hkbr=dx`k6SIrC|T!F%H9ZmPChKzF~H| z!h46W8gI4!&LX`y*xIANcL2(AQ^z-x6wU~koq^!EB?RPJOnK5A^t6hpkV;3%$^kMh zBm`sK{q9-ardEEy3^=v?yLluZiTJy9#h8aKBZl9L6<0eN6>x)fkik@O9_bI@xCZT% z&$Y^mRF0^v#Hn^rrh$S@zXia7(mLJx>(0cm`*v zEYP$@PfFz`BYq%09*DM$6f2XbMyUBq_*54{>kJ$|v6H-q?qBq3y=DEchxz~O#3fL* zizBgfU8lq*w-1#PhD%Zh(D5iC@WITeU=J1WA^`!c3|4ecl{1FRIWc_OS9dVqZO=rW zCkZEKb{QE^aPO$nh&~L>KGV5yL?N@{w9X`#Fr~j!oWzy6sp4;g8BH4~zS=Z3no4~z z3Uw-RYn8QM_#tGx#@_x4?s6J&@T!jFE=0im{B6e(@aa{{KBTuO1uFv3`>_9>7 zo2FS4Tfs}|22)liErtwH-eWH-bxC3bm7Fzy?E%E;(3Rk9_2S5{Zn$1ge-v0~MG4Y7 z4$ZRpo_6@6x$5c!zU*+n8S->IDb~#lmb+j@_1)hd^1|?v)p}Q$(flM*=wIk}XFl`( zL`&LxK{cg=D`kRFHLM85iQy6~>%|%ukcxO8u4}zyRvY0BUj;%`M~UG3Mu|{}Vn-*q zaLjt1{hAi#n@5fEdGpKgP&|?gnRj61|qs=$hqhl-yw<~Viv`W z&`13)s*DROaY^4#UykdU8SmrPO=2nijbZveo!$T9Idt>hvrFE))#f+~sy1es@n)#2p+lS{FRTtx^Ghl;$ z?ib0tuGh^>K1uDHpKEWpHtSaZqfWr0w_DmVnsOUBs(JU<-+?Aw{}jF}t-yXHl_1rI zLY(X7^Lb$%nM!CKiC&#;iu%LseoZI$)zYyD9fcBk+u~QB+bBMlv^9mpe5{{~nM_XF zh6CNT;tP&HoGy2jr5dI{4c_YrpskInfi2shz=Ld)yPDr^(Ozs4$vb_S#)>&A!v#r6 z2K9O~^S`%uLWpN66WboJJ3GzLEO;{Tvi8O&GK)T231H%56Wq0gTx7Rb)U#ytP`~eK z)iwuMPXz!rehSyUW`BPEaQ^A~{YrJxqZgH7`%_26Hp>4i(Eo>?g;3VC3XE->-89LO zgwmB28E8flsqZuSB9^{mO=uQ-5xQsaW)I^bx`I*OA!4&st3IFY;L#Esw(T}r`g>mO zmFln~E33^_SM4_%M`O5)xb!p*$2{gI8kK06tTaP^$&$U_j@x*+l1^|iw!mYyi-8Ms zq9*(Hx#l?i7x8`pR74ye7o|`Eb9UXz?t}0At0XAXji|=KjvEp-F3w}Iv3VP>MDp6j z-x8+W6gdYq{b}l`q!!x2-_3eyam8U2EjYzah^lYrmk3)wjVqe2Lgsj5%+!_-I`vm) zYOBt>duqSQW1bw6UCdTm)n{xJq3|j3TIO{~s8D2$oLwKN1?;|%@XUFXfFtOZ@`z$< zWu>Nx9Vg99>nMDP{G@Ms4#{)h+)B)G^pGbaDt%W--|g0st9-qbzwK=vfvsq(sBjVdebP4w{ZkQR`bnL{!EH&%f&+n@35>Q|&h}=6B3`ojF14Zaj`~Bx|p+V~y z(2^9Lf0|G@uQ-?KC=+Jt06w+_qH88*sD!|hju|+EN!zUWF|J?FGh2g+hRf~y$0wma z)4d?bjA?WFg9-NKC#n=*Y9!$7$pzJG*cV|?1KNeXURCW2&*C`8Z3lS|6Qi_F?M%q@ z?&Pv5BuYnuAh_aKCQVC0LRr@mykfP_CvU^`_)%4SNo{~kvLPZrAa!s~h{!ER#IyQ{6RSuM33U`0HL6v&$D zjNzckp0Z2F42J3vFro{isWD#j*UOF268jF{zmZH8jCqC}oY{h^K39sl^Svkk$-zgQ zXkZOC{}41Zs8IMBqL_gsA^1=4zzLr_z%j;Q%$h{_zC#h8zdqa>t|zUQ?XTwx0Ja0i z7@gWmN-Pj%+0gRHS8H$|Z%%LofVn_xK-*c=)yFm~LW>a9BzuIZtq?h1^Agb<&a#TSQ&I z)9#3wlK^#NAaD2jqjyV|LPY^?JChx58*DF-NIwVeHu}9Bx@Q>wOEaSwPw+cEuRMTy zxXiMHszP;ow@NaNOI$b2NKRcpE!Ej!4u!@NWlEcBiWML@$)C8|Y0zN#y_d2Mu9w@! zc5f~2Tp_M<-PB86GB^l?a?8@l6KjKe?-OIXtc?e|e);+sZUhf885vs~2}II~bvb*3 zw@PZ7{ks+4i?M&}VFHZ?kM46?HDqA-}bK9BaWcH*>#OD{B z%X;ea+&Q{!ReIJE2;X5OXW0%J?_5Wm<|Rg0+@wjXclm3^xEkAbHEy11P9iu84Nj! zU%gHRh~w3TcL%7HI#4Ip#V~i@GsJTZLqFlH=H`2nadr&X!ktr z|NMLE;)jsi*%^TEk%@Vy1x`sQ)h6d{9GT2U=qI+r^@xbqb(uA5U(;)A!N|e?CDzX& zm`GOy4N@N4>`H0B$=E|rtO!U)M48A2TYyS|a&o%dH?S=V@KQw#o!GrLGvqK%Q_a;* z8uiqw83lmxMrCjl&@;xTv?M@B_a%KJvssysx?&$L>E z8gYs)Ctvf-kMQf!@V|t$y^qC*cVlzyg1=i~wBuFelE6X_bk81%%F1d9Dzh^o0E6#` zEkLP+eyPa5<3(9JWt#Tvo*JKx)J8{O)q-&iNr&z~evz23;JpKU7Wp}@+ZA3Xd{H(0 zi!ru_!SMt*w;2E}b?f3vQrN&i!i2X+A%r%APS~ud`CJ-yp2jOeGcGZS)Nkkb-Zhsk zg<9`kS(Oh%+_6}$?cUKceM`kGA{JkyA@G_+C_rE?4ZO!b zAKtEeqUct>sy&qA49~l(WBR4(fc7$HmQ$3ZyU4M>0vB1_?kxT-wm}fBA^#R9Oh~9Q zG;;ThWmpk+LS90b*0VHis#O0l0QcT<=z{ zXt}R>mSSpBU-3L%6R#j)UAXX3Gd-Z>{rpu35jxDAHXIpW8+lv$Kb97r!b}o7H&TVu z5@Lp=L(GSq#?rW?nqn3}4|W)oYl(VylN&4MI5J1N|R4G1LQqZLxfCVprXibAs|Qrf>*_zAFe7tjg$ zgcg!Y6VVx7%q&j+txQrUX~UwjNSHRVy|Xt)_J~uCZo6sMv_!&080fdn4cwR z-+j$==|3UvQdl*!?B`Ckxoq9aYQ#Unv(>@cyzab80&_C-oK%pEAu)8mlCnU+Q`yxj zt>O1g)JTrTdfVAAp3(^9NihdL`&{q)^9K;^IzNL&5jrv%ucpRq{oV(qPC)hwi zdxKkt%W3X5VGXBY+o3^1wTc{cC;E8B?Psn*5nxwVZZDDF=O=sLt1AMOaFlG!8c}`# zYms(Bb2+lo*P)~QFnP9WDZ*qW{S*g@s)kmzxF()!#>>TT8CLx?R)g%y#%>)=hY59k zwBc;Dlq$pMYhP|!en{hZH1)N=!nV6=9?>EQW=LENF_ zK^dMC;2yHG>@}``xy?d;aBeeK&)b^AzZfH+X=sz>D_3xNq>bohA&f2x2d44>$rm26mXWt8N+FNt~wwDn3^QF1Bj9!_<^-& z-duB5)JnvDs`IU+*6k>cD3?#otD|UYS~lCF@w)`T41=WUtA&UvYwOBQYP98w6UfkX zt)-;@$1(!^;y5wB}ehqSFN!c0!f0nKyRa+;U@Y7#Pxj zm%B}lY0z4^EilL0PT~{R=9t&z8G87&%tM#zmz`RdPru9rsFlxQqYVztJqbmLko{ml}rAaM6BKyE7!1^a+1A<#ZWal6v`GuCI;0F2kTOd zY*)64I#wh*wF;iA(ilc!%yH=_a>6(d<@!V6o%4@lnkn%^o(EU9Ji!=^imyo5iv#W~ zT+)7E-4|>(a0MvPzJsc=OBK2(A3=qudK0Tt7e*4+Ri(gkYPoaIc-^{Q9TBmcwm?W+ zbiHLBjI=y2)wAh9@s$^P2#nY^QR#}0C0SB6>7axJ^#YXs$${jm* zd{nMWy#-O9NLVHTjoS@Ai!XD3qzTCl1mV)ygW6e5Q>|}Ah%@DN#hZxE_v8(zQb+wc zwJjT(Sltpzv$#Wy>AW*(7R!DK{2io;e+tO><#%3scO)+<>P+n=Xtc=J$&-3! z;bMBXM3NEm2g$naZhn>m#AO6;R!?~r*ALCA-Sk;0`JVS>oDFdW>plRfpwg=2Bgbjv z!L{2hYGut=ln`#R|Hug#ipHS7y~FAed@qvRwP-8N@NZ7|>oxzE$B0xK3)poM9M|Th zywC!bMX@G=(Age{H;n7ctJiIxT+T(ul|n9(^(n1JiaKp(35%Sp{aRq|2i$M0{X3H> zwE1rpJgQ|T)fp+v5}Ou{eGj{*UWycfjV8$j((xE<46RZ|FP9`F==_=&zpmPTyyosC zRCPqepY*}{yT=rN4zG3s9k}Yb?l-cTjvksHx~*aIx>gRV>g>^vf9qr$oFGg;D$~yM&_$p#1;Hoe&ju*NJE}Kah^$_0i z)?k`h-|GwTrnE+f4xi}i5eMxEQ{7Kiebv=Hw`SvHpf*>!?8ihwYMtuvEEKvH!EM>_ zju*yFsFJ_nuWyvZj3cd9yMXX9_Nv!;>l>R<=BovB#NQg^C>nOK0xq}=T=(2g#1e#$ zeb;Z(r!1@EJ|+6Ql$2MyRUH1R&K9y6Nqx40wWxWi znfbnCarJ2_M~Tv|uU}R-rsWd1c#!QV*DAd^k;w)Ytl(7E+N;Z@KgrV3vm23+(-ELX zU`(+oUm2EJ$KOmhH|h*v4!4`X!1V?87BPi5Eyi2(SovLiiBmRnvis(qA@D-AgEW0T zQ<2GNaZX$Md|H=bp&-Lx#);T1_RgvCGY7IWzFz08+SjLT{%ZT>3d7_W(w}$OF-cA z+mk3N2A`xMpWogTcbs97Xu+Q&q`l@t+Z}mlzI?#C0A;;sJ zN#+i8@#^yrs~2trkeqw$) zck;mh&)IuS*XNDbw|~ZlQJRo27b>ut|G1q05}!qSvjcLifIQ}g?|52m8zz^mH!3It z=t1@2wrwEx}#2as1H^433cquCcesoE`j}5iCUajImQ{VNgG+UbjXl?9@Y#~ zMy5Y@yaEX6)IIAY0xN7@Vx-`hFt~t8y)&zJDTU*qlq!VW^y$|Crb}+^5smz4RwcUC z4mAjowa!RL1{tjsiuxg_qP=kw z?Q5I2nm^fMbkefm(lgj7IF$fAIvm4z~?c*s@>&0|bc>Gb!I<#dp0X1ftme%Mo+bYafBNiVBr{?SJ2ls**BJq!A|!u?34Y_s&jx&!i_~K& z(}48WEugh&3xPZdFBCzeX05Cw0P2)aAXHjA9y@B?sXyftD9@r!SA9EJzbDORyMKFD zT8$sys{hbhSR5;WQxmyDX!}02K^vDe&ibC_R7x$L<|e8=>Th``d$TwK;tbRDM(hiX z4bQNj-#v|;w@)|ErrrNu*{l2bc4cSA9Q*$#>i?57gbGT&(QlvEN6P^F3E1moPA@1i zBTBi{JP}h=l6{Lal=7T9&6dH98)7bK*_4X@RMg5c0|JD0H~>4B6bXmbTE;>hhrw?N zuKo<9Rdhfk{3&;Hfw@@Pbop;WIu*rk0-7WQ`$<-O>+FFf6W-gWX3(U!V%4NSZEBK~ zN+UZrl{zF&XB}DvmgKn9^8}M!CYczzQU(>*56i_k$tk@`gB?e30j!jZNk$zREEg>| zuHawrN=^MW`Bh}Sfw_Hqme^~%LFq$!G2ch?zGXpr$so@d1cpguiku>?5O+beaJfqQ zxBar12#BgG?jtWr!Uz#4S(l;&5NwI^+PM5ZSY+L782NezD__-^*dpt&=g8>{D#$a3 z+K2U%+WsUlLgx2aTjFh|8cn`eLn?z@4*sYdOkl-s#+Vs7LBVJXEE;CZkZx-?*?#b3 zJfTk*ri9G6SwVrN8VY@o6-+H{ukC{`12hxPOEt;5fjtLdvlN%J7TGPI4b3zO$;DGL z*Y17yuD{>xUhl|UP8jqI@5~AWaPW8FvY(G~nIA;0yJ@FMu?_$Den4?x1S;R<_Hii@YUpyT6ZK~Rt#UZh~>rR$lC)+uc! zjj%+Sj2lilS3>s~%^P55@YI4O1fo={(PJMn{LU10dJrwECuquVrBL*t-YEzF5P@Ap6w}$5o7J1b&$<>_9o2I3m(rtECG)pbaot~114E~ z8hJ3h^}U25u6Y>zC$U=lmfvMVG*KTZDbX$r?naJ)wJ9bun`|UMQwOmz$h=3KjENu2 z2^fQM)xH+JF2f$|bBL_{U$nhtP@8|ZE*#ujTmrO6LUDI!i(7DlyA!OCA_WRXgIjRd z;O_1oio3LEix()=s2uv7|DOHMoH=`DpZD3{GP&j3y6*K`>k?I64U)jGm7ol8-#Twv z+tzU7bx}>4=dEWW?nN)!!8+8biUc(8I>ao58!}B!AhKAvr^D(<9yjw{NFsG=?>bm8 ziC-K#Hq1(ejU44$ks+nLOs^5AKIpg_W7>lNKTsxe%9ke<(hgx@PV=;2jODU&><@)S zjfI{A2r-y?b#@;!isu$1&m?H0IKlu%G0Du73miIs!sZ59fNCFMqow8;HYBWs*u1Y~ zJndfDryVwa>k|W$?l&AblGXD%K2XTEhEvCb_7>}M9+nuzk+s3;fH}>b-#gsuhlypQ zq>w81O}K*BpakkDOD17=(wEi*xK`lRQ73x6=gS+mI;_mg%0f61yo94RkSy zJzDB;#{ft&IK!!N((!P62A17aDWXR)L&Uu_MBCPCs_%dgAIg!&>6L$U_S1a#`zCpR zO!YKtDUkzZEhV^BHCA9)Ng9E&^^g1W($-d|$3vcU9PjMw>4Z&VyO&k=<>#375Se%5 zMxHw4PiQ@+gk@zCQudMW{{s`+^_TQ$huvG8C2Rj`ps0-XY8*+L6FFh`^MrQXPsd^m zFlwluHKOr`+b&^|J*gam9D#IMrs~UZb|NfT@e`bdvtB;)j6kS3(l+HcVL)$*$kca0~TtC!Cu%qt94CU;tu#^xE zXH%KgbOo=iD#ko_jEAhYlzA8v8)#>^7984F>p=0KLjV>Ph#06nyEXhRIct{XibB+_ zOczd977r%gkvVd1By%5bxXfPbKhfh&MhA2yP)d&!I&K(3g0>7S3i%Px##~sI!scf~ z{G~3S63<0w5x<`Go9;hv{(sc_|I4Gl)!?cioSjz zGKwJdn|QD$0r}C_)HHmZg^{}TeJZ0Na~0iavtq+1MOus~O(|W3CkqAG%gZ#>Cw{(* zeMs{j=&7!72MekcX>Q$=Hz_o{f` z>7bR8bAxH=#`qQ~3?l6O_%co)z2H!tx0$l~vj!283B%%8{3;kaWzWeQ3uVX=BX0#! zVa)cQC{2}3Pv#1^%(~gX?RUaJ!{I~ZQzRB#8T|I;$cDz#a)P6 z^qub}HnVUR0sEkmC2kBZvn&MhuH;9G+6aeZtCGnf6GzeRYU?#DlQ2 z%sv9rw9~6MCH~h5T-c)f<2;=Y_b0=92Yk8YQF4JvNoQ|7>7KZ-9zDjxRlA+Gg z5-W3KH#(Uoi-Pz&J#}m%osl#Ku;ymt}}#^f%h}Uf(XOE8mQ*HYR=ZU!!}R`2w94%6PBU- zVExY6gpHWnydnMtKx%%VP?o4CKEM`)F-VztbAUsjjocLHy2I_uKm$x)NgRe`cFC2< z=PwzlBl9j_EYP-lOw*LfsvqoX;?g?JT z9eO{0+@v#{IbuPePLDNSL|$&aTu(Bp|07XKcqYKC9#eg!AS&f^oJ5fPBj1XB!L`L- zvKHx~*|&UrueJzY7O!xrfQtWLs{6lwW+%<9$zi)qxNwORPlI*@9J)r^K1?0880GPx zn+iHN^%Uzw+}3tM@!qID>vK!P1g0I%uuWZ!ab>ZynXqW_yc6pSz5GJOT}DBt%97*m z?V0m(8@j+6g`#MZ=#%ce$RZlr@R66Yud}Ps>u|7cT)rek9BkO z%+XirDVyx)Y02GkxtRLUx=D*ipJl;&1rFoMyqj8`!=Q$)y4zFBeDm;^FI%pT-H&c- zUvkk|_xjA#U!0p&I~88bJQ|K*exlfmokH)vw*AE@J!J2ZYx+Za0f5OBJC!KRDyiP8 zkR??6Bu-clhw^|I+xUlW;eIwIxSK_0c!+|<`B1=6FNpxqI>petuS@9}1gR4yypU3t zx3Ntzxb<)2+YIk{7)XS|=_aFdC3+DvC3ncE-9VTb?Y>;vg}^DUI^<%tNLEWzSR(Bl z^?2cuRIXSp;2Og{|4ZI328?-Sbe6VY&f!0lImJl^5?7DTXH)fC+~SRtU5-&oWt@b$ zpuWUD?-(alir|Mzmc>nz>X5GQ;c?jU{DA3H+(p9{eX=;cq&Wx8-&MRf8&YQX$f_nw z*3dY7I0iJs922Pg^o}p6Sg&oful-Gp4qzcTcxisLh%g>Q9;_`QEsp6jP@~frs-=xN zBXc3dqN{l9aE?vNr>Nw%;_Ne8jL(8c%t=n)cgf*p6h$deWIX@H!(&*3$#wMY?o$h# zRQX#j8Tcd)u`L7LhNZUmARH=NTDJgH~w^6r>F30?xqZ4o2ONCv9IkTq?3& zT8=tJf)Q+rQh-UFebnwS^C)lvBJDxE)3PI4JFNrK7xMt1xu*M5&A{!uL&P z!<=q8>2$v#_KvArQpz4W@SI`K9Zu*;Lg`4WTJhROx%V5ndnQ%_Zhi{N9dIkjG%iai zS;xulyrJ(&7ayPv`$X@-BH{TycJO@7OwH$uVo}@7z;eWn`@`715&KzJ^Ud7-5ViEH zuW2!oWDaecE!{jee#s5Ky|u$HN?C+Y7G+8Ik4aYJmy5&bu`UnflcN=0khrwJfWOnf zp8fj1I*&Qj6hOU|KF*yWoLUwfDYX##eO`G%QDl=!S34Wo#M|nIsnIImto23Hq6oMe zI;U=Qa-p>>37xtrSE;bi0ffRnc=)SC&}l&<1I=0u^KJ#TLFAq6oR;JHVEPBfoki^U z&LmHSI1~&?@QjH}qM7J~?JHBO*CFEO=Mf!|Jh@q-I(F}rs!66;>0F&B_^F*jq4_qG z&+aM~!C(%U3T~p>ZoZ60a%_jtUNd7AQs%(sUiXd3d zMh&}Lf5iIBYg96j1NiIJ{N5vtiB03e$dc<NCM@8 zH}kUz7`m!H$W2?t{0x~f2tIF<X1_w)t3`@AsF9~dgk@1ZH$bd^*1FD*j(n? zXWV^*Lg6(9`y_KsrIX>t6S5RlO-*GAVIf{MOw<^!G<6wcbLvo=*=Idqajw)0KTD*l zFT`1doM1oV<5|!qBe6Ee#jS0VFI!R11Fo6jFTLx~{$!Ol9h@dCPd3L3>em+0wJS#P zoHU#{E?U$BeSx!P`YTB%|7;QBd5A#zp-;?tbY>(Q z-+n~Zm%DhsYz=!Y=lU)1AIJaja$nV7{JV=>CG7fFpFm)&@z+x&qw4rJ&b|#$wcpkW zN)VQF62A0}9s=QCqPA@=kUgII7zciug&$8GLXa3=2Anns8)laKGIl0GV&N0NWDYVieM^PZ;uJlX1$!_$ z0)6gC;MjFvX(s0z`FyM^Bnd5q6ZFj^LVxyim+9{HkOWyTI*CzQpl=^MPxjrw?-%@O zXeo})`6#QjaH=M-Iq@b-975GbQL$T`jA&y?rNiK&S3IDv2QSiyY zWqsMf6-xRA?#ab3A?>bJmMxI(>v`jF{+9*Sb9TMUrfa;$U94YK{&)~ zIz`UGEw{gBunSF2(zzgp8>#-QD~a6`_;Q~qS^NrLgJO{SWl7`HDUd;V&Bb|_KXFO- z51^4lZ+7293TmxIB{6KiFpu_HjTFy)~Kq7muTVQ1=`q6r9Mbzvt0hy?kL?#Bd7dv(7K zQmTYH(Lm#R$nHX%2~QP)b}Edvfr&6qVp;L(au}1`?N#!9cBnKNuJ8xB=|^(wow3Zt ztc$5>5d1!+?2tONMazehT+qWb0p9Bldpa%lTGqm^&B;6MPs;7J`;amprHLcR46~aR ztTxFAj-$B=mnk^d3t}!>bkOc(h6xn|c7hJlpPmQ@@y;m0L-buAsi2#ViF0E^YgCzUczAW#VM+Wqt!@cOB9p z*p9)OK+V3&Vh*9rWG&SYm2O+YZVx^y^iO1mKPxeL*+D!hopl+_K9KSP+MA}jqsuR^ z89xAvRT3s#P-;0U{dOf7E?8^>k|hB2<`H~^&D++BjzM@9q)?#TKd2;p!~pkkOTpZLJHF)Sn^wX{(6w)D80|4enU)N3=&U;0$6 znSPd-wap%$BX)T8Dzz@3LdxPaC7y?-k4iAaZDG+m!BlF|_!l>(1YWa6jV`35l8tCvJ2e1yL#C2d@I` zpw%{J@e=X|`^2}j;5c`kf?o}e+|ybzLEumb4KI$k z4Vv3(twYEAVkQ^>oX~D^jRMdx2DLhzWW>8az$D1m=iep72HF3V@5xhm_YJmV(=r!% zC!y$PS7%CJmbNf!KIBhSnkL_LoK>kFvVlSw-^Fs+v5IDiuptUn6-0lDjJq@Xo^GPD z%T4qeB1oM7ZFumXP%)^x?epclB~A@rlM|nkFq}dCz(z%lLsC#gctN6fPUXw=s-B5< zBd9{_Wrow0iAP+bp2$J6S@&Knl%%+`;zVAh11YdeKq07uRifVKcabhc+35$AM(xWI zG4x%oQmc}CJTP~TJ4yzCWbz30aIkdvdjMK@_>MT2@I^KdU%No~vnp1R58zTkB zd3;hBzD|pY*rjeFx51mHe6@Hbdg&$X;N}TDb1l)MzQ>49gk#Olz8shvBO}Dmmv6^6{{YH z_8OW;SN7&%DQhlKH&qa7PFlJIb13NYMrOG5+t8tpm@Q^7xhHGMl?Fxk*OO% z5Az9*C3jWbqUt)`Lbplf!|5r}3FBg$W^eKWvoop=OGn}re|$#`ox7lX;|#gQFRS>b zrVE1vHyh>Lc*5;(-0zI0J7#RAC$y1C7(;j}T}S2QG8LNy!XdTz>EEMPa zp_orhQb>#HC8h11++?^@zpJy@E7T6k5k8-;fl-Dr1(`A^k$!*gz7GpgqMnM?Erv}U zaM24!QVVxxplv93UfeNWpllb9=&aR-bW1$c}4P)f+75nMDPzLAf zkWadRmY9P?{G>={MXKC(bwF7oJKMHGS&!d zZYV_&3pWQvrdjOf=mdQTCGjPND&3NopgV`Vqpcic(di4Pjd@m2ATmLVfNFH7 z?Yy|SX%3r9-pv(Dw|zpq&cInsb(%LAJDM4#S==KnIscreY5k zd=f~*38aK4qw14T6gh;CX6h3V-1S^E?l3Htm&v;2o;IGo7zXc|xi#Ikm)-6Xo%gV> zEYwG_M(XJ8d+@swNe*25+Do+y`hyezrS9f#8xso`TrE^3G^D{_tAQ@W*c(fj4_&Kr zQNj};HW<;;kfN`$$rv-3oTV%d>8`CcwHZam&I)BL+`%a_LX;lEdIbEz%%Nj2=I~ll zb~sIwdyRm3s|=T=7lV^<3a3d+V`P3MVTtqXR7_F>h75U#r95e}ED4&Y?en>Fd2Km?tA%6t?7)tMJ z3r1jU(!^*jVfsRRHv>!UHy$BLlMA6qcrihM=TAVf;f` zNqrQxwk9TlP;fD<%9-rd8oyOVq&dUAy~Bt#$r3Iu`viLBg3st@M6Ue8nw-WMO%*&r zKc=F2ob1i2{D+@C$yQylDjn|>W>O1n#*Fd9sW>yAES|(DktgBDsJ?EdbLB@7EcK-w z4QwB=2>w!AqN?Kqzpy_5MEVzad=B4h#1F!HldoR%(MRyQZwAuz8B+OOfJEJ44-HOe zkz?rlx!==}amrBzi}||KscwPHk!(rlvpnRtsmk{T4Ws1ht`==4`2rKw_B6?HLbC7m1xM(|kJ@xpIanLn zGHXPr(Nyas$(t2MG*xL@f9Rso^R_p8cMC7_^t3k2pSb=?+{da~-D>*E7VOn)uu=hh4m0$55^WMI zNCumGELpkfq~>a}r=Y z;MCZ$I53Ef9;q=lq0J`$njXFj2XI3y4mA`&-YGz;lzTz{WyRcgo-ApH6|AOCZWPj? zBfE=%nqrf>TnxWZe4n{w5XuGg-I%se+3$#d1Po-YS5)@Z!EsHQ^(Q&Vif zR4=o_MguG(!y_?W(^h%}R>!f1o%-c++x75vee~aP^mZAXGAjtfj>E)x`bz+yf07qd zPomf@`s*Wg#UGi}88e;F>ALIw&Qq*{5U&oSRv%NI^f`qxnEkrr_PZaeXM6u(!mj?J z%fDuf_ugywoTbLU8YrITFwT3QrNcG=`668OEWVjXf;6sUr=zv23`;Hx>A{&2Ma7Z< z3O2d2866WiS1;Cces9+i?bTA+gnwcx8!hEImlHo;R^r6FNPC)oC@`=aO@<2=FV37!8!|^mq+RH=_)^3#7>~`wWfSbBhO+CDr zzwBLTDwcYlykdJQ#+wS;tg3O}mvwlnCKcQ40KrsvQmR{}265}wO-Hx<51~S6Zqs&c z*E+BXli4%PoC?|y1yP|I9ORpW=|w!axWY%BmK=Ism{}%PKRF|z>SH`QRpyZQEjsWJA+;oP<+%O z{%G*43>*Ot@W~ZF&?UrGpX1g`0Jw%IKS=i(AvD@f8~CZroPfD1=Oa6$PLJ?QwWM7y zZ0DRkJ&dSqJvpvC$tT8z*^Ji;xw!PaQP#?>H)=oy)O_?v;MMQ)b$X!+76G@Z)*{*B zlsa&jFpFiV@of}85#vZ+YJGh@KvtW{je{#4Z;-TW85~9~46$RQ6x}@l?d+2hu%;uoJzsADlr=H5Z`_947YU;l?BYHv7UCnb-v#`0Sp5ab%b>Y+wV(hBZ`M3Ub}yKmgHAT4#SW5@ z)>zfFt522C`l20w6EBc-xiPtb*2!}M@-%fK*GNUBg8fK%AD|kMS?IE!-SV}I6m9Or zwpq`=3U2C@3u&@N*4XYiy;a(&RO*ebIbHIl*&c|76QDC_CSj2$ar*b-$x=rK7* z!stFLWtpb1xkk>6IYl`slSjkyL|6Am`z7KfN6H{s7p;xP!u74gbM%n*ZxdBKyHGXL<_Jt7{B&szj*0FDuAJ zHQr2=P0JN_nNvo6({#2FI?8DA!;m&Lc5%Xsmh@o7yeFV&=m;+ycg)49fyLw9d3H}Q zpllP# zQDw_ZqCflEb$Uo>3hI5hq%@uiY$bc#2@Q zp=^vY_~y8-_pwtDQ)I=>>N2|4t>P!Fe+FIP;Ez*?R5?qAO2tq&a+RM z5*5-PZPlF7<|1+uFu}r`Jk0aqBQ}OrhC9sfaR;+z`9_MO=3)PH3AHBch+@m~86v|Y ztmFypS5TK{(A)<{49>3ah039R3m?BwH-@JLURmjOE~h8xQV;d|?+svuyS!XT`~HA~ zZrQ0AQJ~_Cgw<4r$Mk+=$&O&PKl|Vbe}3(n6H%Xv-YD$?x|dW0;vmHN>(3wGKmB*_ z|Dd1OFW3F~)%*PaUcdjHD?6p7?nawBEtLaoDr7o5(ASY^hU(rznW8S<8z$IMj}(Ge zRNEfdGinWu<5du|O=Dyp? zHQc|@f0|LNYW&(V$K$L>jCu1GO3t3bcT$+Hl0$Ls4js^!6S0tbU=XU8HV$ivxM^oA zw1uwE^`F?Yq)9Q&%fE#M@(m%BmVb(=Z5K?MbAPtU2*h>>%Wh3k?G!EXFL1j4EgO z#N>~nT;iXhZ<47K1;nbsLvf!afrgD)Ss<-$Z}M@y7SlXmn;(8*A%LxsBUmqGC{FA} zZ5(kMP2GZUaB%tjR_~?FnpPpr6gAiohE%5WD&tZLN??MuT!cD;i@3p?JWT~Fp2}xL zS-<&mn4YJ)2b~Sss>lw;gUr8@jlw%XTkH7Hw*wlOx|Wt3Geq!0oGtaZ8{dS;t*qbR zac=1rP(2DgU&;=z!ClI6S?1BE;n{+}PyLRBTYs;m&d@2Z)=L%48bforXav3IIQw|l zTR&49|I)xw0NreO`DYm-G(Qzu+NDvNybeqa$97_sGoYk4V*)dId|#0E3-JKO2#Q?G zRaT6YF^-)Ta0Iob#J^yZcdbyvZcPK3}^_9SsCoFm+nj>*Odi8v59WKuC=?-<@B zso2$KS9NyHisH1ZkD(jMC7Jxm*H@8K728N_-^4U`w#<2jPb5kqNhIVOq{(cO)o}EC zn@-73upF^9Dt!`b9C_IC#no_VTXm~(`IP$i_OTB|?uT>3{@Mi%iWij+W&Xj0VCzMQ z_&q;CExY#l{obK6@n6|+ZJf@x_j(76puXd3{mf5W_=Vu5JBSaiSw~E&1ea$qgnYfI z^yCy$suGYryj3nZisQ*-_r{5%KqVMO=a?+k$zu^b!~|z~j+*4jjt~Tm%aWT}=#uZa zOVGy1E4IZ-l353{Khw{k>ZIp?bapz>;P#6~RdcZVgtJ7gALp0Sn#t*TIA+SRZG0Q` zM%Tr6KyK@Y2BzF9v&X4W1ZY0B2R2`6^RUSv5sQm*J&T*_v9Ve)4pr9^$o6CF+jCge zaOd=(MCVnwMwqG44L?3`3&z7LL@xU3R^xq=JZ6luUMw)=D8vc&ChPO}KYx1n|MYwc z`eXR}!O4FI0shZ>!JVyLXx0Sb4gfAcmbAh^UpIx&2q=8oAsHp!Ed-aTYq}*ND-ytc zwpCmiOIu+00{LfS!TANY*b@tWT@}tlX6y>ihn47&&ShhNn_eCq<1|ncG;wJTyHkq# zv3|}ouPaRUcL}02eJ?BgdmD$0N)opnBA$JlGXE4++I?7k_66?W{cKE(k(s#cENiDZ z08dT}ZuD(e^nJBykU4e2hzaBoB+?Jrb#wK6X+P|N?mpw@%hG1woI>=2agNKMbuPTl z#N%GxoL1EBUw!~U6Db9M0=1{kpg7#Gg2SH6H~{DIne@~zSH4}L^9+lDISP2Gq3bqk zee93=sGrImwUsduHSk~}J#wKRP4E-aT+(pUSv}9YH<0N2xLaecNid z4=OvRi}_CylvmSgZ{Km_wMy)8+0UnJ+aE<^}PZTH2q@KYDMPqXRyE zx|sQTw`uw3OM!F1&}NS5{hZjf)rwXg<%SHROcY|mlVXI;gBwXgFff@$274X^VNs3Bq-e<3 z(_+!+eDoS?9F+N9Lvb-ZRRv}^coy80JV|x!P=UpgDj*UE`qH-s;=(*@G9*_J@;Cfp zk>v(c&%;}<5Uupm(Q#X55tN@YrwitU+35;?78S{?Emnmw56gE6mQz?T+ICHteT@O8 zK|Zdgnmkvy)HM#euxz9#iaRBQm^DgBIgE8v^loupCrQ`L_%DqMu>S`?p-z9S>kB>AV6g@Xhs#{ld;IlX!|Pp?Nh%I1F|aSdnGH!Cqvg?P-Z9QCaCa&o|U<$KKeXWpYo3Omf+@*SXGC>{` zmWm}r%w~E)Kk)DI`6Le{w@DXERfE4s`)#d6{RGy8%2}8p^&enOB(%jrDru|BZ$!KPk83 zCmG_~!oBg(3Vjr2J<~heN16?$&d=A4ZX^Yl|r|DuC@hbT=O z=f}8ZAku#NDo@qz@0|J0Fj4W+ERRp+*wN`q#?QNBv$YuVR5GZ2)avYn=X9R!YHuji zAFt6pL4E)tG7f>}S{9?<-R!$k%TFsE#1BUERB)^|T5OUUDn-W3@WcKsGNh0zVfa3c>Kv4{m7|r&bVZNSyus+u#jqN z7c-(D0Z1z3YadFc@6u5=7|!W^U1g?k{B8?|4 zOV-fYMUR|h*@h0FzN|(!)%Ck_heG3xvcggOw{}yUaBBrr z)C0J6HGw-?jSiIb==E{n3QqCnBZ8>Jp3tfL@h4md{M-l$;}pqfh!uyhb&?dr&-o2` zoIwrD1f&A|8tii9U)+xmt(bhjJjJg1Dp4R3oq40cE3_*zGE=5g^vH3I;#17Mic*>% zgA=wWV`3$Yg=iYFW6>a9k^qzrN1^$;2xIY$$lTcIWZu4Q2NYn@oMM4*FYRbOM| zxlEPrNBBhiBa|e9>WYi{`ZQOh(^kC1!%wKG-qgM-dCQidY@|1PFOou}5x5AV4N5Pm zEXK_xHA z6ayuQ9y8h{rejNB$_IDvDM_giAjYHB)3~F$=YG)PK442RHz?(YC72!^y&X*0fQ;Y~ zX7tU|;9leQvpufmc6l^Nwl*UJ%*T~P0@5JvMTLe#r-R5OIx58Nz!0DZTwIqV|^hbHxwW+&v zhGD=gng}?XCSv$}tTeZ$7~}bPUSrq$&Ex;`v;Xh<=UNZG>6MOVg$|;>ZX5{E%@>cvX(Feh8I-^PlToR=}Ha`}?=pShd72d4+Ck$iA zvQp(pCaNo9s|nT=?x4VL4Hwj7E=exm@vFGf-F!ue;i$KP70>D*Di+lt!&d0d<(Ay( zb#qZA8nUDbZR@$H_UL2w$hB`#4>rrL#qeNvr47FfZ@>NSN~u3R7ynajhrMLOC)KGH z6k2;na?w<>l`oRk_1;qIDcbOMTC6|>e$m=Kfs$ESLwN4r7&9!4RASCoHa?u%>7KZGEAWe(~n4wkmI#MR0pI^G0}PPTJi-x zrZHB=49SSc#RgDs|MI7vdRO2ul{XQ%@fx-hHR!(Ib}lQr*>XlCrtdHKeG0~~T=i`` z#@Tu6xj*u8y2dQD@~rHq3p9Uu4uU-OX*+duW?_P3ZSiZH7Aw-IIkjuR@OSJZ^r~VH z3T3f{PC0R*gJX@!EXE3zNKF*V>rtl0s@qT~PKXK7EzCxeNLbD#CcLaCiSyl}$vM`K ze9oEj%?~Cm>*TPfsh;3XrFqOOwwP6|kdJ&zUp~oKK%tfyoYx~WpB2G?%Kwrl{o&)*ZgyKxVgs!x_h6c6K4%u zKDUBsQ>|JTlJ3l^BFHtmrWt=M%NWk=XGtc%%x2|-4KPP zy6itobHn(MqZ`vj_xd}l0)B=7b<1b(imnEVG{Lcg()jtfq~lEP$-%%h<8~)f9y9)A zw0YKK)#B4i2ydc%(%a6d8~E{aR|Ur>S6g+XF48ML*Zf%&A{B(3Ejibd|1JRd&-Yvs z>WUt?Pi*kZz7ow6r=gqR1Z_^ChNk(>K1lB^m7 zED3Wne1n`iKx{H6-^9ydK{;}#+z~)ljK4iogrMfjl)dQGuviR!0cb_VxjPB^MNzF) zZf7{{C;{?ymRNN1OwX}>m%W}Yef#W(85ky|ni4W2|7)*>d~0uMCsU?Z-EY7CP7tOU z++Z82lr*uwM+7jkuaU%s8nb7SmcC}eG=-+%gMLH-ikQ%lM+TH~zZ3BT)3XnGk_LPM zgrrQW0c5bM3B%Ai*%1y+1O?dmOK3=1C2`1zT5yh`K`V9>K+eaL`Llprb$_jFdSGycY>5kfWS}g9fP~wZX zgA`X4erE+kKQTWIZB;)6^HkAyHM`TtFVFNDJEo=>ip?~zzF9IqQsr{%LZ3HAy&{w; zqS2*6KAQBXE?uA{(0`=AW}TzWWQ|Ejm&it~X_7;*HM_>(xclZ_L& zIGBZV$-RXuNRtKd3cgP-7ekxCy+W8K2V~O6Q)rR{BdM~VlsIJ&N!XJ|2*QNHV^Nq)wBmZt<*_$9~o4T70`} z(d`)cDjTj zb8G^6fGk7tOgpxMkACfgx-{Uje6^l%8Cw*~GNH_CH>T`Ou;SNVi43RXX}Woz(J(Dl zxH17XdZp_w<|%|BJj0FRbaUBM7qcqM3qMxzqd~ZxxoRYb72$NH0jsEwp`Q6G^b^>A)6C294m2h0i{E8 z*hBj*y^q}tf0?HS_5Wuj|66_mP~6C|v-rX_?OgTLEf`ZF1g&<&EC(%Kn^>dFgSYs0 ztQQ?)r$ns979$9#n|AsDqh@uI) z3M3z1b|78k3$jKerByeU_i#mz2IglRm-l)sTH-s5*Y&NjDr?b@oNKLWnp{2qY&)Yb zE{Gqne^b}m8jZ<2vB{s&w0ixNF0TL9s{I`OhOOX>+LRk5v@uHhYC!m9cIW{b0`U9 zG{i{pL+DF#l^UHAvtpd!WZ{w263pnmmAMGH6akLMqjwmYUW4H?Mobl+Ry)I0I-ovvw)Jz9n>y|9-$p zr5~9@xx&~EN>Wyru|dg=Nhgh7d6H=e8U=7_SDoC}Dp)9c9+$XW$56-ol;$fzaVN?P zp6YVV$C$uKj!#?blqy;`R-Rl=#e`+>7>yI(zTI~$tQ!9ht+eGmCfx5L$4+AYa+}|K z8vo@nc1Qg6Uc2uTQn%d4&Ju)&;sPmqnFu$d08{fHT>CX?=hR{mAiIBM0M z%TLYXT6SUyh2(Qg^A+bl9!#?cVoTPUhmk&w&<^LQuU~2PvAFKn&e*4)?ZKhSPanSB z*dJP9pxGcvQ}(FMm>>Ie@s7q9@zs`nyd@{|vqEV}MB3`V1qc4?m;9!1hk%hTwl+=p zYtDLg!up7zVK4m~Q6x3(xk@R2EApv(l#4mldDAliNoA357B#Ig>XL~Hfe&}+B2hJx zoC775J~`2%k?d03ha!6r{IG?IR%7|6h`f(qvY!M!%8Z)?Ydrm_)drP*^19IJOj=Rx zgulJ|o)^1ZzsmYxKkgc7WMdZ~1?fFM`%O_95Ux4XvDWBm zR}JT1ao2etj$e*jI-9C;^g=Wo+687-z29U((fY?OPNV?61Rd*yN7aAVzPVh zM0S~|z9nJUFgK8DVXl_6t9)MJO6F3Dn%-^6NQ}xaj04_aS^EvE;daID4BJI5Sl*C^ z$yv-34WvCLml>EE<&Sx^Mn|HcBWhT$9V4`N0< zb5PM(Hy=WIGyIGSh0=@oU(|HAu+=D^hzK$3v>K@R5GN3&jcbzOk6;O;wt{7|+t{wd zg;7>zB^oc838%s%Rp0W$0YDQ28fD4^t71~A=(kukQeiR1kR(@dyiHC^2}NMEa6_hV7t$mJfPa@FKFnEpU&UB+5mFS^Ah&D zk93PDjtZ^f#!30b#PfrTBO3QTRMCQ`?rG--T8{o~H+*{pt=Ek~2$})sbzk#*pETTzpRsd%5spMqgvbqFL29?Rx;>>ocGBLsg&Em5(ma zSt2O_Q)D%{uFp8V&$x~INElvA4-#7;_6vZZ5<$3%mXT3K+Hm6X%*YHdp_^zM>SEb1 zTQjL4JbDsV+Wdm<@-Au~IQZXeH{>;Pd>JWoRBxymu)Hk-n|ofF`Ifpoq>tcDDOW=?tiIvYRAm$ZlC^-I|#Q zT}a{Bx_5J7a2YbSw$Ua6IWdmI-;X*AZo$0`(}nZTVK@?Y^eGi{n7HWheVjbb4lS%; z3p^|gw7)%QMvmp9vk8ZWwLYuHirFh;2uzw)IgIF*HsvqrGQLRVedpuf&Sgy;Nc$d#<+u|GU6u1app1)f0Sy<*9NkTwd_pyBq zikjf8;mx6o!8xvFO-U4}sC+jJxGUvx@|?+{5Vi`6a@Zp=lS%&277W8>sakrDYo9vZTSHltVc6al?67&D@ z85S(mO`JEr(9{69;^qqAyH_uAN0c$$QbOt&PjHbjrn|g`m>b*YEsAfL)EN6Q_EJhi z|88fksYk;qgQ!Mk_k<3n?TC1wzuY@h(>Zg_^)jvFoErGe-8iD_)g%#JR*3anfJJTtPVXgUXCN_`itLg&JZ&64Te;%45&ag~ z75*dN#5Nh*y29S7Rwn;Iz*@=Ds<#>{6O?K=&l1cMN{o)N?wH{sjg(iVIMAlol+cH~ z0ylc`?m*_IHNv1Nz}V;)lGdKYkfunS+V~Ud#^_lqoTy>KIyQiotUbFD=Cq`Xm=2UB zJwVFEPs3egKN>S?(e5TUW}#m)LUhM=i+{&UBF8S6Jq1`wU8WqkdF9AXfXjvp3Om%y2ST!ic1p z%7*(YuZBIyh-ckw``}@J7F8nCo;5rYscX?~+;8-E&L3;NQK;AX{iQ;&Z88rRq~2DDEwc2_9I}x(^6mT(p+??0`T6j^v0p?%s#TgJ(v2 zUXKM=k)}!oC{9{VOGWa2A5JJX12hhzrbXjKGOJz=gyb7l`m0bmn$Rd)E-HM~b=r%P zM@p}*a@||ST|4fj3Qv_g$&w)@Zq|=)U^{kBM=>B{1srKhKCN}HieS3aC--O2WSyG4 z3%~Pp!;gPg9nxr99E1`^&i`y^4$aTFUpyudiA_rNH~+A_xcT2T#Pk<3e(QOewYHvA zTlYQ!1NiPFo-mYCx=~UYp>Mt5*$9=5Pl_ffzl#H=m_>1?QiTl-aP@5~e9TG?$mxkm z^7>MUrfqq&y4dx?mZ=O(I}aRq?J3g5`C)=L`btdEQIqipamtP-#vg19&DB}O4W5jV zox=Kb%4rwQVU+6%>J-H)Jb@?iYFyOE_y#H&T__XomzisgEz}0e0rPeU-~=CHgr?oA zxnjk`OzMf~W;muMNZ!C}7C4xF>M|NT!tw))|XM((@Q7x%tey}%*9)rrEO z7)BKVf@EOhM1IPI`1*2CAh-n(TpSAaFl3bFk1YXxE`BHHhco8PIs3WBE=o&PnLRn6 zM^A{4&Zkz@xFz=RcSZJ1F3<7-^-=;XSvn!`X`{+$?*v{OjmNYJ@Tny415#c#;Vfz*8s-8&2Hk@#mTqF6wf9(ODjf5g-?lH+<0B7l4rCBTAB~3-l!%eu4OP zfTFHt(#MPr+(BPbj{SxmM)c4sG$!|OP2=>?hfQEUj&4B1T0d}0o%=ZyVMp8|AfimN|Z_ zp|7B`_YBk@0o2k_vhs)RtJ3D=){*JjuP&Cc&HnIdw`h8Sefn)iXX*FF8<&%S&3`cA z@YV;4ZosAUxYPC42P!|!`PK)DtjXoyccq~me79U=dr^AJVK16SA`h+ zct5W&9v^r;-?->Vomc17sKiy`D7J?#=L>$pVwYgy->FnMX3R>pzLZ^L++u&s%X{5V z5Rg;tX#FVq$x-7CCk5FgSbLsF{`xTHh=Vq!@eUEDy<*g?^@gsfvSK%(D9_fF3nMPc z?$!RsYGjT?dk+B44$5znb1nqj(6Uuqdy)Nj)I?W{10#r z|8f5Ce_aKLvi&vLVlC9B3B%Ei+}dUPg7H}3D|;{adKtO&;@ z!AiZ;39@?Z%V%1+ZNY%yFC*Svrv~4r9*o373v>;}VKopv9Rqa@IGgM6T(m6%0YQDC2yEO6F_Npl>1r)iGypW%0f+C;)(p&(z1_RMD5V|QACW}`3zmB*Bg^S zwev}k}sdOSY zQq#zamra(ES}fm@EC@JSBa#%)4{~G(?6EJ-c3J&ZcgUlL3tlvq zsuWk+;Hl-?-sFUoV!zh^`DHx(zQ6HZrqM*qbO_vUg*8DeO8{U;&=~2!<)X)v5o1mv z<4_IAsY&;x(kqfkMKGDFU}mE+g0Vad+&vHMjPuU8v^Ow=HAn99&-3x)ZeGts7{9}~ zRP-4k*#y4!i3Q{1QV7A3O{n#lHRG_#7F_*~84_@|R_vB^2y zd1I@xQrmkk7+++olaY0;%$F=U@o!JeE$`<{+FNwWy6eA$p!xq8{Zl)Xq2_jPd51ZT zTVLthIY#xTI)8PW)BbwHZMWiJc3qbiT*|L}UWWV##aBAutf;CA{QP2#jUYO> z1_<6HxxXlHS+_VLRznK5E@s_n-0BN5RA9zX(w(HR1#hWv>jO&TA|?K8M#PCCf(r`! zqPV-9K6Y)mpH5l9l*E;cnl-KOpr)(oQ*<+;)}5yU6AV7Q>rO5!C8k(eccYcPPg43x zeGjq}-E;QCg_m3PylaNjyN6v%m2jCRMDv?J6zx|SMYO80{FL1hw&9BG5v#eX6;w&m z8Dt_(6{GHbnb%JA>J9uF5W0B3nft+8jj^s5dGvm5TB6;5zJ3*EuMKmmUU>O`h3mia z+(IY>x|(z5=`Q7P_oO5xGwnFf$F&C(UDgdJB+6tpi3`D z-VGUduhMW9`4DI(ck-?`xg7s9N#CFw(jXjXV&5c{o#`@RUHzr*AS6I;0h=(WW+Z4m z(t`%}#AvXo1MVC%V?Al98=rjGoOqBzJ_GdOxV(#)LW_8%yv7XMB!L1JDe;3J<}&=1mYlSUX#32Gg>g zUp(wiBM&re+-99O>Tr@q-B&9#mbcC@pkR|K=DAr~9w=(j4{X(&q*DTCFLy?Uu(D=^ zpyg&#giV+-9nrIn#MhlSF8g+2YubbGHik0&nirCHOIU|55G80yyYa!I%O-%oAPP9| z(<0bC-f#C897Zr7fOEI^AC1s?1TQFh2ShhTAAGP(;&|g)on_)DO;7k^bl6kM!#g?|zvc=V&?Q zy!#EK5nEh=CJmZVB)$?o(q4#ss@D?C#-Px|7cBvrDcYj`_B@o)MzfudlKwtg*tXfh zxh9&5pk8_N@sbs=XtDxk-gG!IMx8*$4KQK8{8^i*&B~2?kKvr!EDj@{4@88SgBY0IRQ=4^gh*J*a*^(n^ zQltp0H|Q{ic~S|yPP{({cI#t}+R?fc7L1?O_p z+0)f}H?5Cwr_wz?xmifqpD@}Fe>1W15AQ(Kep@@-?mJA!WcA$oKyA_3KfcvjYRcz! z620v)LOEQM$5^3!l?raG0H=$OiZVM^fs<^1j8ZC_z+lMWhJOw7Bi4EbFRqPIu zX6BPnBl_gf*HgjjocEt;?VCL)_08;>TxPa^4_DsTDuX)kJ;YCydAHm==s2~mN|p($O)|Sd2QtZF<tNv%+UM_HB}o&)U^;%l!F{f8)GT#`8miQUTa=rS8aQ0ccOTis9Wq< zGH9;K{F2DNQ}h>Io#8z#v7sbGS^KPn1+=Wi^sa%Jt(vk@eLf*5Zs!8*;iP}|fl9L+ot*jY@4niupXn=J< z78$cuWH2z6)j4a2&{jpS+QqzGu@%qzv^4&{DeF!ApLgUR@??LySet;Kt;z0WamN+c48?Ph0KU+3YGttl#|gEDuh#r(ZKa}ofpoT46SXc8 zm#&4qEENN4$X}d|w~8@SR--rk#9To0&d^zyz>(lYOe>m8H&4sAjYL*ox7kSz^aRK& zk3*+96bFlL-cu_@WiKPC-rp#STN14q2E6=Z@ls*u`IL1eu`y>@*!7z0?T)K)voJ#d zD2PRnlpz#X9_IMUGHKkNV0;>rd7@RIj*B(-U|G@q46UtPtW4!v13OnPW){nH3&Ub? ztJGuYlHCdA0iw1WeZX@lX_PGvtl-JwL1;quB+sqz_}8l0#Q0>yi@I{qWv@os@UYr9 z_5HWYz2Fb@8?LEGqwOJTOiH#pDyxmKA{rSxue%gt^zjPDkiEBO{`w1;vCM8tGREGK zEuY!js%g3EXJ{c^kYz;eckGYe(+1l6bOv~Lx+^6$Q!oa4E3c{A@WSY`5=39A7hU& zxP!-T$XB_z?UtYtqRO^4e@sq)Vn-nxV3Y<m26j^*ykyHc*?iNSzE`sPUkJ{y|MSDBb($d6lvrL464tjlrpb z_msY7mkLHvYxQs!cXwtli7vjCSfBf@X{;ujYB=5QBzW3Gp3u#&JD=%tIq{vm`e+4L zeDn_Nx~3M%|0U{1ONFA5&!(=ZYPgicmN{X3)etub+n^=joo5smlK}TS%c`&PNxh;z z7?PpT+|)8^@){A?wrBdN_mxp*;JeLzzg1ifzZKs$bvAdDDn1usx?YkzFXU5*^0QcJ za-LLc{#A5)CQF+1gQ3-+B+B?nAa%R#u#|ZtLbDG?4bUMVwP`*f!t%rqhnY>`>F5l; zqBX0y2s-QRHTysWpsF|g;$pS33a`LH2^KS=uxG9KRimgH?TogH7?E}GAyS%k(NHL8 zoZGx^c`yK(wf+L_m)@*YxJtVJgu!#t1Cz~Tu7XzHwC6HLm0(-6)E)CbT?&!6QE(;% zv!#&`@Z-(l3^R;@?AaT%&moUm76&eBS;@Gm2&6wt_sK198b3j&4%E*_-;`G2ISJ)F zq{9r`H+*^&IPvAvdb!@1adh5g?I3P$s}_1zEX*Ye>&QYH7ew4%Nym$Hn#Cb8QqX{s z5cICE7!&DvPPyu*ktcn2n3dN}b5c*w0d8^ie$u%Ww#4tv_1!0u3-!R1D{%k7*_))n zK5sj~s({O%2xqtA>Vr-EJD2N8uE|LXiu$)p1X1+df$33_ZO8YSxzvTz?ZSv@STWmJ zZ<3rrP68)_s4$j|C#p1A7+u&_l5JDFqym7en09&B)W_1W)m&eWk1Z!cZ(F&*E^Ccn z-9`E%+%9GtYR`lp{@d#kB(TRXfZ zQa`hybIEhO)mgf&9iq4o|LQTamGPyZTyEzR?kGKwG{=pND+WSe3lP@@rgd0kD5WP~ z8iW(%+h4!9!=PNz&DWF8FQc^Fdr%l@O~}DalnP#n)z(y|C3_n&Q6&H%*Q{{J&)9CI zkEHp)kB2W^Cu*9Rn>CtMgw=iYK}y>5LYOfa=WS`C?ao}b{F(<|1U2L1`Vo@Fi^TAU zIy>`Xku(%9b{jwq>)$ZNu^WrKLl44@BC_*I!}JdKAJ4GD$c-rkL==-BfhI*(wlkH{ zYUGSVppZQMug-)Fw3o_~0#%${PF^7}xtoP5Uh zQaQa9_~eZS#auVQOY1cn2DQtSAGi21mGy-y*ba?xJ4$JvOkPEbRy=xaa#MW@=1loX zjU!HPB4a^>7}&h!X2Q`tzL(KLm|8j9oml}y+<1d7fD;15S++t z8aCd-FeGmJp@RgQVAl}|F1x~YxdxRqND;S$Up$l^2>B>>?t8 z!RL$(lG))OLZp0;4eO)z*l>_vA4pw!b4>bYKz>RJ>--20`DRXOH2po83$3WCUSA({ zbYrB)?Xz1SVhO$|chAnhBM$^hpK(Q%o2d>XK%h9bolw3{Q0%L{^^5ckJUHi_~PRjrYb&f>!^8& zfAWNP|HehSEFWk6-GO@Fa66xHtG$+6c_f;1n z9y0eJjr%qEdD`3Vc{wevZ}aFamqjN|b%Z81j6_=`Sfeti{Dl+CrUw70Zis2I^BAA` zvR;v<*n-V@hQjkbCmpo0rA3}AO1#7H= zmWAe5Avv-9Z}3&Hvl%&#TQJ3=aHb#d6#0}_T}k9>?2e8_I&c~mwJGT?b!q(B&EhxV zFK;kqLTw5D`~Ljf8wPbooXL5sYp%0!DhUFIe8NlKP;&!pF@R82Z8iis$*W$!CFzr^ zrq+9z1nfif*N4jb?_4&tv_m*)l2V0I3Up|Rl^~0_w3D;s4^)!~xr6(a)Cw`U8}@Fy zhvdk4T3}M!!{TIcGE9TuYB`XkwX*+%?P1t9$E6Ja#sCRUoNW51j1;&Y8-Sb8A zhe8a_Y8TI3qErZR3<0`f8LAi1D6r6=poPvIw0BuRqTr0HpBXm*Q<2R?`V-XT}nRVv2okvph@B_+{$ zW#QP3k3LU~J~G|NZW~zmr>1{ynmk>qN&E*B_6?BE31La`(gbc&lz56viKr=|UQz z+wOwNJzVY!vfz*E^)NxUq0>&ak6q#MYrnmjz0;E02>nQV^*R*Qw(Az?jJ|=k(NMooRefOMGcfav_Si z&mEB zq9SwWDg1j%1QVkseqI3-^?uBlQw%_5XfpAw;erdIE(Jv%O|CKaowHLEm@?a8NhHQjFRWcI5oXbi)qc$IJa zKl%y8O!|6%Ylo#uu8rFs<9jQ;$A9-7A}T*z4T+#>!4WDF zB71S5IVsV%nuC&}ZS}aGin=TQfY7WSe(U1{yPM({_+QAgg7(1eRzdrb;7VcVQLi#~ z8QeKz2}>$vPD(4lcL81WBb^G);>~a72X_DriRv~04>?a_bkCy|uRp!~sxw25GnlJS zmV{sUtaZg#heRNTFPJS|-cMA2mYhK;ZL~YRXnqt%rZus&BarighN?|;^T(6L?!Y^l zy0y*->?X?n|B34VmJ1gEoeAFL)+j>M-L%gL$Hp#QJ3h^Ln{|p93nnG#^1DPc01IR> zl;j18gz|}9q|wC%rx7PE(Xa|R=Pn^qkHsKy2ModUpj`J<)qMtHWf9S^C+5267>tMx zb+=R*C#|M8a93{tz>++_X)Mm8@F|kML$@jX}hQ zNM)9~vN?9L?n76A_b0C<@3PVEifI;;*0Y*Ubp}h7msmndoRA%s%~Xn>ZF?j$u!+c8 z;rmOQF<_%SUMd$^bR>a`_}(7ZA{!AYtUm?^KP1J92^X~7dkQQnZlFSa!3bbdjd}pq7!ZKA7T+D;_QKfDX6$0iwI=RbS3SUc7@N$BQss%r$jcw z%Q<$r+~EUUR&tyNrN0NnR{`Gd=~rMEJJrW3o1qqBzql(G3yYAA%NZ;@I1}sN(*}!O zpfQ~DNne8aXBF!Qf0Ykq6Tg2~HE9&_N#pRxz1UvQj>?%hHZga-)zS{pwN`IpY02k2 z)bP~SD@xnelbTkQsMkbFv3&}$<}msoV!7fis>>gGsFrrHixPefklY4rMU)FD6>d&~ z$CM>QS<9(G@}L5bQ`V3^%{Yn_LZ&biu)PQFiDHarq0>)1MubjuLrd#iVFPHUfX{di zE?DUMTGotn0nkT;qW}bycmSc76r$5q&lH%l4d>oziX@U^WufEWGgRIqjY5D14q7$% zxN%U^dG=_w&84HV+KVr}0T0uNBOK{@suvMMdtJntFLuu}*HV*jQlD;|p3WyaSvbw> zzL)s0(q*=>*=A$%?z}GTzQ5D&x_^A2h)ZuVfo{TOaqNc7hTio(z)-9DR?A(d%;E12 z6iylclDcT4sD>0}5Wf|LGO^~9;%ib}A#u*+ip=_u`AUR_XKFQuHA=)m15s~9ooI%g z)LE>iN4DG#lzBxR&ZqMj@m44-9UE238-&B@6k^d*U$>-^U9ZX)FdHR(J5MRPjdQ%H zeUp}l*2`dvOt$AU`&I_;oE>tbXE#q%`TnmD@J z23^}h1=A*_`8DQHU3msub~>`f`PT|9*7bNtoNc6YJ$$Rx3(-Ci_^AV*e@^l71#~FJ zlD!f7RK@)V*vQj+p}YF62f-Ruy1p>VH$MfRUy zhbT_|Ip)f@UffkblZfvNCaHjRZ_te9uSku*E9%=UX6nEi(>Ius*4xL5C_@Jq#+d-p zWC!P|WVb9M=KE<0|BVsmJiC2p%L)=(vlPJ)IV7*UA2(S^+`_6c3Y36szWKuW*m2hW z*JaR>2R$+{Y9TLRy+z7M1vHpb#-K`Prd=(3$zSnwj#8i2OiLUvrz6ryY(y;dphj6& zT$O!agsSHx@a#UtC}9#Un<5H_EDCNV4EH)~X;YgajzjEe8C>=#CR4CLi~zcL)CO{2 zUh%0K%G=HP0u#4gXL>ca7VfdKn(~68L%`!9uo!-3^> z7Lrf$ht0!<4cHcs(Rx-%dWIJqz)^ILIUy5$68^CDlj~PBXfnRG1DU$fkkdt`QO+ew zu3Tn!#CbetmYJ$OA^u+LYXCYSznDJ3dyG;8F7IPH&TNcabAK7?{lx@d z9!}9XbR%bnTi01j>yVt3L_OHrcE1x&fN24GmBj?gH(K_|52dToDfS1q88n_N?&4}j z*#?l%7g#2G46PF*S!i8`8VeleE5LWgPG%Dn67sMpG)Wr|!+M}WMPnWMY!O!&`47!25zR*M~BiKv?Z7*IcH3uQX49)V!86;} zv?@iJ9D3VTXxx^#g}iTd)qJfKM^9P0*hwMW)iGNo+zj#R>rxV=yakm7hPD0Vr99f4 zOq^_sX_ZFzhevSwIPK<#@DqWKvtH#(dv-i>UaS}3J`H!qJlldIX+A%ERHfZd$-+T( z3O8O*T=lb{aivn%JjD2th}r(L(5N5Hz!uafK?oRsF39*Ou;5COPIq~-=O*g)1xjXU zkshu}@MZ?ZG$m^IF<`{2YJ$~kWA1&GI;b>N&GN-q-BA5o{YUWk3%86%4ZQsi*KP|t zpELKe(ffuD^M?2_x7D??R`y2W2G()SR!9S#zKVo3F$#V*7R;$ZxI;O!4fsgM6?JGa z*Pe8h$`J)t2IabY(j%ypusVgr()Wm!m%JFrhZ4eB&5L zWS_|*Q#CpU%^(y0$z&t$i?D};!BT3oy)l}7_~||mlsL*YmaV>o;4mxT=40oeL$2g; z7b7e_`a!F)9UO}OP7DIo1v8bVW+Z}##&sZZBs?x?dqOJtc^TCG1k5{ zR;02x|IqR~c+(-SB22FO_n;8P2(gKvZKpWjxV53dLU$#?&)N88WwLJat%mcrQ2Ehz^eMN)(>g!a6BbZ@xWWxH%zb?qw(v=e{wf!)c>KXodXLSp(c@?PMDqm@lo z%$2uMmaCvL@(w{4l><`9mPX!k1y*pk9A)Qbyy!PU94w~3f_(nVZ!=-p@5gYPakA#M zl>Q1@1Twc|Etz&&GLzY{*|OG*+4uEM)0?kw!~STZasGmQgK5JU*<-#e1`ZtmVLEX< zEBN<*0*T7s`Gi}YrOH6Q+uC70{8qR578A6%4}EWW2O;&EwYAr)L%0*5&O1e}v zSel@NO(bSIh?j?=&+%)f8`CAOG6fl`GIXn9sRLx3co93<^s`)_*gb20x-h?5EN_ar zCyDh7P~=>Oo+OD@Q^jqa&E=HSyG0E zN2(Z|4QD&WiKd0Z5kq1YFS-4rsNRyjEEQIuVgmG)qAKN@$#Sanks!#PuFanM64vyS^bD71H^X)lL{tZ z$k$I2XOQ^Gb!>j1uv*{3a22D`y_Suu9Ioo@RpkYa>pO3}cJeK*$=k8i3q0M>(A>-`9Vn$QmoJEA!^_ z=1bFqH<_RAE`3V7nX)b8>w50KX4DLkq&<$NYpf7!h4!nmkEhAZur!3pmwG10Nd`2B zFY>W;8he3D##I}*9l9kHy-xVhjE-vB-qt4c#BOU}OLdK&Md)1Rw7Ukc226P43{1|5 z>*mQmaWvNPNb(rrr{+|f>$^H=#t@Vg&b+{@vE)hHB2~Y?0u)A4Bt=d6QDf6vRI-#} zsFqAzqAD({m_Ju+Vdpk55>Q;OWJd|>)khkHtYm7i#I@4{!49b(wI9)VVzil#(={{+ zUMB&3{srG6^c^ZV8eWZEyitt!8ol) zwr+=2yj1Zn4@W&@wRbmb_eJj2T~x9gEBDQZ1Is$7$3QkQgQIs8E6l1EJ$B06x2}o=cPDvi*ca2eTwV!oe?+G$fUq)0p-cM+5O$w7aL}AO~ z``PGw>s{=G{J3}vG0SO_Y9$$H!>o~xKQFsCcdI+9GLV}eQ~%*29~$WXtsNGKGpBBM zptiY`Z+nbdUA(M^qB&@97ARkvI$z^M!9w64OtG=tU81Dk&}h3oumQV+V<*om(vF=) zQj$aSek!byEMmvKJr1ZI=UdesRw3|3)SZ@zw~efpCveY!H4*o$aTlZI8_>SA zqn}xz?AJ8rR=D-mgRj_}92PQvHtUQOKg&?Q9L?=q3-2TrN2~XNBr~s^oQ^G zOcvNp7Qb{gM5V6Z{-|W7oGmR?2k3>R(@zJn#^FS3+M4)rPj=?Tjgn-X1{1qBc7uh) z>z1!i@g4fP?J(99lq|e-PuR9z+r~2R2jc=$c^#?t$00QM9&=MM^%@+TM+t4xQbVdqxO8%)MWnrSb$81>~kK z^kSYTX8El+zOR}2g~EU1TWocofp=$NEJ0zc)~XMx_{}R$t}k`grM~u$p1sSd$#f+b zm=b4P^P7L`av{F?oslQiv zHkMIe?s|tR!-zoDfbB%Fj@pjDn>lN?yjmR4;l|4ykxg#gBdP^=`fX5Wdb*=FKhy?^s}7~}5bbEb368#8=dbGz^GxbmncuBR1;hHZ{*HyKh2Nsh}6 zD<_nu3W(_~{{Av~{{uyGfPpdP;tgft!r*QZ*VE)~`?I3-GGtE?t?#p%eZ#m2dR63n zD`G<(@3C|BG5TkjFjGb|#Yd-5d*hne@&2xczWJZzF%B z?LXgNFuar9_J@#QHoDVYZInuJugh`Bf%oq^s|{AF3Dex6#aBq(L-*s9Hkw=;6F^XW zx*!grcz|(=x*&eSCtgCN+NvlHbVNIbNVmFJ-}d#FYKHm(Q9S`)H{v3Vxw7pA*XLnu zO4lKICuCGXPzR|oMxC8Pwk`-SV}C=a0&@`ui=-omB~2vbov6e@K66Cbb1>GG!Yf+e z2;|Vs4e6hkl8WJH_?o5fa8lHnf6zy$?;y%;Y!g;w-KWZNHQX1eRdL;5y)Mdrs=*`n z<%1p1U70qkBj~l}Z-e3LH*b`6Lt?x$OiT9Jsr3Rk5=?re*o8D@_$eA#o>cks0+Uc1 zJ7Rwv>8ya~$f$3L9{rvr)^24dqN<7VnFtdF7NwNO&7?YXiXNIl6ocZgRDz>xe|PiX z6qYPzB*$eD!{~@jA~hDUW7kpEy%^hW_;*KqOEVXu6%SZYIcS_zSPil}KTfUFM<2wH zs7f}At9Dl$;7ZUjte(mXw8V#yx`mA$?kvm22Jaa~bCs&3MSg#%53XD+;}yXczNfA# z{=QvI14gP=T@E~~{Yc4-@9I}q>{J_N99b`)(D!j_XVFjEjlThPW~B28&)xj()Xw1l z)=x+>&^rmZtsOKfU;fozOEQGl*lRVZRv!I|lWk?G;Jd9I7DWJba$;@sOx)Zc1)LUQ zyj!Q=`Ro8SSh8k6Ortn0;=Ib!XR(AjBnE^m7i8NZ4wq}2_+5uz!H;Vgg2#=I7fSI0 zD+DQbrFVu=wA+R)`IfZTc7B9sK_4hk;|lwRWh+GflFDcZ+}kJZw5J0GTil6|NqHMi zRsxn;rnJDHdcH$U9#c6Ocd8^P)=DmWt}52wU`!S9t3jFCz%Yv_TvoHy%m7&veLTk` z;T!m{O#Pd`>oCg??H0eQo76Y++8fI?KLmeLu%wN#2`Zm{iAU_>`J3%^k=*2VnMl)M zI~QX8yxvGWP^vGWy||l1{GW#Nzi>k^S-#5mf8)AR`(XtT=M$F11L?m1*wcR}YUpGi|F8!^{%P&x|CCz9Ho~ zh1{1|0MclizVnfts()RZFA2t%EF|k_-hX)4bJSt{e5z+B@xWAEQr^&YtdTJex{#jc zO7DX9F9+37NxXN|{Pr8;)&dUHWh|}=C&#PvvhaF8#f>nu%m(no&7euSHg^( z5}fjugEl;P&X7C6f{#H+0K%m9ra0p>&@u=`^aO{6k=~CSgo|Sp)~9&OigQ=uemrE4b+^1&t) z6QKD`OrIbEPs zC~CM&tr5P2b`Zy{&Eq=Mexi8rr#W6+2ZtipjZ-7Dcvqf(&XzXkxh!F`awO34lTn-s z3+wotfZEltOgx{?V&rDac-&AO?BI~i=XC%a8RzIQyi9x&rl>RGD@w!Y=c2WU=qp0Q zQiV~(-i`{UX=OVtne9+K-`nKm4qQmsTbQzht%JNV@lb$=P~AY5pl^(#`2+2!tAC0| z3fVgo3;9Q;)8CAPTZjFWyX6&1UNR4{(ogwDs3w^dSYf1kyb7I)+d*I1StZ;^U*_0T zzjCz?%yXjcwwdl1xjy`7-+_cff9^-XU$6O2_gl}G+%wcICh#>$a{pVN0HPGrw~zH- z9ge_BFeasY#CiWA1>WdL_%uItXcEC7LWvyLs$`(U0}&)HB;4O0Y%aPFsSi^6K|pS1 z&5}z2373!KDGM)2$Y{4(QXfToN;slSaRjxA*+)@p#ta1Hu34j|F;YrOb(Z8+FOEa| zo6|hXai?BV_`a5xmN$A^wHYR@DZA8QD@9%;RyX>r!{w>v^J+0~9NaCe4V z)aSfcGko+hQ4U(*A*BnyX3bcnUCG1m!!3Me>4qUy$ke`za>zrrSPdIrM_+h+H=3d8 z+xrpSEJs&ym!+<&gZ)aIa3!#?Zn@`EVCT%g5bpoQqrY)grccjr|5|bCWOpd|jS(%u zr7CP@%3uI)cq~!-Y!zXe$ahTIsl#arbMd4~dA){9#RnSJ`nw2&y46LyDQd3?UnW#? z21mAkDi8L?+#vucSx)KARpjzr@GTn@S3y-W(OwQO`t+3@6(y1l`Yo|O-D@yTN<}$5 zER|TTAHj z#TAidL2UQaz3uFXH;^V)yKLRpjj7bNsEe$~?0A=Eq|)Fo@b z_^5H)DE7l*A5$<}sijMdgB*!C!Pcslmb4a_CqH}G2F>*vt?=)9?pLq$bAGpu@ zJz9Mke#C6Xy;KdHF?$brbek>2;x=ovYFiA!WF#D;l9eO`CmQ80Gzw-E$H?=JjCvG$ zYYWba^f2s-FpKgeTIU*|$6}qn*Gz%3j6r9&GI~xGK-;I)KiGU{<}8k#q3!|XzG(#5 zA^%pSF)@L%`ArR}DBa_PVTGs%ALh6{!his|RbCHP8Gdw8e78XAFG#pC_h+Bm0sYj^ zSx1Gi;SO=e|MVDX^!1!2CtQXr{|yuJjtn24I`cKj{yQe%+;d$!dp#?hT4q&*KlmCz zTz@wZ##+p2g9j@qU+j;3WIF+K{|svTO%C5Bo%ZEShy)kjt5@bTHzf!sGaQW1Wu@Z~ z&6+Prnl=c11K^zCRutW<-WfG7oX{>w!tB)Xih|=O{AV0;S0=(^>y=Y_`%k~>kyca$SPyz- z)5yGqONYGm%nF)J9-zGh+qlos*xGggLDB@EDl7+kE~+&b|PU8!qzSm z{T4Rx!Rg==`SoVr`HD#7f>)KzwE_SDC+nPBTVTHaJs2QD(-5lodH0OnI0kuP)bl_M z^^C(9S(UK;UOjP|TH>m--(%SL*%GsftT@zLr!ng$Ck}i9Y&Xj5u#kC$3$%TdXffup2qN zPYdnIEHoq%ejKKxLUPEHQ=f^9(jZ}6kk`S5^14xv=cr%JAI-XGuIMUKij^f}D8E%J z!)`PHd&E{gQM#J>s+Xq_?f?#LA8e`MCFO~DXb24nGOdaa3&&b&$Ju30%g6NvOWcE! z;KMHt4|4LA=w zrj?)0^`r>;%5;NUYc;Fx;lRiB6`u4Z*W6ZCcSo+5S^}<1P7UW4n{~}M7OfV_wBjdU zy3t?xKtBKfEBOD*92_ZA|LCNnnI0}>Rh4dPaVEtX+yKGYsI0frav43suwXs8gMCFj zke*{OX$Uw~02?6HcWVa?SEu((yr9p|q{!y57dCsQX=Trh(58g!<$86WR)K6BQwdXN zl7UPRy`6ioXUkM-qf?EOywH#qdpXqeJHNKlOeizW-VF=oLvJodtk6yu%UgT;5N!w6QzNjd!!R^Pa8*#;h+MzfM!+92cpm_j@R>XtfOf zVtkPR2LrvFl}dgZNA(-<6DHAA^#qQR#MG?N1R4Ub(k+|r@Ze01vWj^!xkYMYXDh-# z+Ryc<@_#XS)!Ju0x2=tiA|iJ;!{kF#UNpcM1t;et6;9)AlvGGy{o|-xnJ`$g_zigM~K{oaGVb-J};5Nn#7GMe$c#W@cx&$M6qp3%q}y zl-EzLR$nE5W-@l>$batL&^GPc>WVi3;&IYH#tCGi#$!PiPu)9Wv-N=E33Ini^){SD zYPSSkzDKn-fp8%vn1rJlHKX($QEEOmV(rU*q}{QcmNZ&?JzA_LvIR;cgnu+U>v>S$ z&5*c*%N4m8-CjDF&rGFE65VYHr#f>`mwd~~vLNo`CIbPkYx^qWA|(tDM?919gOnnu zXGDp!u*^D|8kVV|VjS~+%7W{wK|2V-<&~M5^Q;`!N}IZX<(J$IdsX~SOP2QzcA>1( z_*D$Om73YqeCaA(7+k#;lefj_ua{?|M)})+oC#84R9M)GRdSBBlo85enz+ zvzyd4CP&|Fpa}^Md(q#*xjFd;jHl{g3?d8z|v6;BW`i!%=#Q z9C!l;{>DM!RMDy}$}*-Z6&f7Q?Zm1AsIe01Nqp~c6-IJY{pUO0@Qk&GY|FA>nm$YB zS=`QH>#Gc;`pDSB06o!7NL&m#gc zoH$FADDS~PaxOq1mKgAfqeRNn;s81x;+*eo|(as2}R&H1B zm`^FxvPfIw*d*x8GSwV?M}nVUvM8%a4{O>`^-|hg4-N^(D=iNpahW*Sl9Zu2rLU+C6(&}E;a0yW)MOsO(g zj`oRaj{H~@Meb0zoI^3gIuC3b<=Y@i$IA8L78*r zZ*Z2eX@7m>TQ*o5v5`KRU`GkN`ZXx6+T(UGot$ai*OEHKYk8~JEWO}e(LJ<&5$#hLO&aMVCHS60H;cV~s5&Rv<~CW;?K*^v|5&XgnGOJU`))V9O2Q zIZJnWO?5V$fN=sacZdScJK)F|`zJKbNSk4}y{*htJ3rPIV^z?Kx|8@gR8h-MnVj|Y z$Ao>N3ypC+sXS0rhCHOr0z*Zt>iEmC7w{C>EY{;$%i^IK``|v>|`YUgtU z$EQ`k!{E_%TpHtLE1P8Q*FlhH?s9B8Hk6f?sPZZhel<_>jp10xUoED}t*l zR3(iSY`qT>X*ZbO>J9U>e$`_s~p>3AB;wLSrb(QBS*_Ri_N2+*9PGqfalH zx;|!}l|hAePN92Boj}}fIJZXY$0#=adr_^}t!&;H^ zjjguhPRw}ZsC%x>CI!o;1ws;Z6a;eGloe-|t=*irQ`UcUbD2ZELuQo2Vn0LL=1$gX3_&@f)Dk!e{?yF#}}($8pt zY{6+`;^oc`5m>sTA=Ud&CP_Ptp5W{53AzP3gKLC+l5$;;7pyTI>%y|y{wg`@rmHd9 zYox7_Egsrh54`-@SbCJCJFb7<~nfCp=zj zIX>@@Q)#60;T?I)4sb5=I496ZJNhRb9rWa(WJ8}#^@roCl`^9_U!PTttdZk!U8PFy zl@9L>ZZQ4JnW#mx;T;C00_uYtYOX;%+{EIzBygukg)2pGf<1Mat3jWRNp zW?!BQS!vLSu!$CaZgGG7SxyfkyFy7z#Tkb*f%LLtNbGt?Oqbz9!y1D7oG5rES9Rrw z^6y53*19Y6A$gB{B9faK!KlC$O2JM!!5vyETb+(lsi@S6=7eV4Y_=p#$p-$1XP?tj z>UYcPQDwDXLahx?K0E9CLRc!pE2lMKAOM}&W?7l#ctK`exvJ7trF`hdzZ0DQiT_4t z-|vF-9&K>bjF84qQY&)pi<;F@&wZx><12eNz?D)mxzA#Z*kHT#eLRtGr?M;$=N!aQ zZEIIt2M1n9z0zmU>`#XRH@^fvUVRx95P+QxEg18@S&?zivcb*8Q>mc%bK7uR4r!R{ zT_-yWephTgdl^GAHGgYaZZrfv8oTFvkINOKJaoSSs=vS4Dz6 zpOW(VH~4ql;` z(3#~P5bJ0@8FJ%0VJuUItdX8q0t}Vkkd@HQi#*Dhu@JQS1l(=6?zbxb+)`+g{lUQ% z^%=LdmOE_QrHZ}FpSB(h7bmEz~%x+^w2`WW>NIy={Sj}bTThTNA8RE^;c~QO_U_njD*ME#@_@ zV!M;bd;4IZOGUTD>=6wFK>$6M`=LudW{mpFJ#CR3ejGbZNrkIr;rn$ImE@I`G3Y8M zP3aEL*!OdYw)Ax(&2I-}s;~H6yP;sWch9m2=WaZR-CWzut< znKrV#KEds(OcElz;1`l=UBF+w14}$(jopTKF_Vel=%wxT=x(aS5+5}^>!ho!q>g;> zs(c2jW^9sOd&{ckeN2_fh6#an1IN46qIJKep{S?sVT6VLtH2ZIY$*p{7}*p>+Di_N zXUvyh^mOeGWZvnA;N@<9f9%uS;BHeo&|_L#bI~)Y$>-~nKv6G5k>`KM%N_nZ(#RpM zYYi>!ac2xXULdzjzdr4jFz$Xn!(ao&1lV`WG^ z2M_2D6tIrc6=X&*ti>o9$7 zv?yd!@UAftIbYvB@z&*KdRSKOu9Tx9D8+j5cvcZzc1a)vM{?P_tw}vD;^||pR>HXV zC9%!U=L(mT%SDOuKoPEvH)v>d|t5?ZVM*4vnE1s9+s3^r9y9{M6EGHNF z_o%_|yhr5~@9|3ie%tH^OJZT1do#Fh-lP2mn1%O&tUfF8Y%Fkj4bF#r25ZS|#f|6o z@%qZ!f|=6HUKj}Kero}3q7=CI@=ogPKmzb-3?3H2A<@RE2wR`z+Fr@ z+naVx2htSL0!=~iPK{dgr4g;A!SO2=5NVCu)Edj{O4I0yG;mt_x*@}vF)4ITqhRz- zgtVF0vKiL6S=4)iv@hOk=Ji_*uLT`pjQ=3NY$Eu%_xqW>Cc`Zh#jS?QyFHDv5B8p1 zdeW2xf+?wMAbmM>>|Bv3bO7k@1?G(8P)6FJ-I1uj{|Hh-AdmqZhF*3K|L`M7LjJrw zhxkd6l=#=m`9D{bBsf6S)cR07d^!*zh!~LP1ToLS1rfL)5DpOtL({M_obwy)O~_>)TPShmV?a(JG8SL$qktLyS8 zmuKo__iLX{agF=MKJLnKB;t^uqEm4$03G`mmi7*m^{yt zq}p8{uQa(3C6zUw1y>0aXN)(*kipNCwN^lHNi+4{*}A8h@ofEIi!Z1`An(m>@Tfxz zz7Jab&8pDKe2%F=ViIPCBiO=N?VY5)N|*M#*9DdY=hGfGcGz{$f5iFLNxkH9CG!HV z#u>;}YA_w@QPe}CYIpNyqA#o7-&Umb=1G>pt1C0lDijSEri-q*)Z@~@7Ql=1R{4|> zG9k9ECGBmP{>R<>?&oD%lqHs3!26O}gr(zLHK~N5hW>=XB(-cu%C34Tcf?;SvfXtm zyO9ivEKatJ(f7Za>RvoG8d)3zTys_0`KCLf4(Fvz*rJP5d^P)Q?}7f=E)eb$GIw7j z4|tk$&pYbXyA=_A_A_^EbLx2iILP=`RJ2%TR(dnkI#F(BA$s)^p{W|{VT2`w2C0Ox z6dw|KE0pT}M`bXAIw3GSPPL~?_6|?e93<8VrO3D`81QNf~#ofwE?Xee!C81o6Bp>*8*m;_cI#(G1kbB+Jw$c z1C1$S;8mrOijR%DPH(b`iM#|3g<^{6C(dsQ=z?9Q@@MG0Sa%7ZxEwko9OAXK#v{Q059Ux&{&HszHNw_gd!}z3`-vcvo?{_aeCek@VcC2`^xHLOXIY1 zbk9p(ySi99c*a;yRo3#}TB!oRD#WG1Jtu;fi6a%d{oS%i!Bn*Ux;GmxbF?bg%f1ZT zRPw@i>1qw3x;^H;(>#|S4Qpy6&5Xn*;;is0@2~N1fJ!n2R*8kbLwJaaO=w4-M4rd1 zFC11TqUT0Rsm(toE{Myca9GKylaj;yF&;?AzbBgAKfU)Io9E;r=IG82@X#S$F~p`C+Mb6)}`LVTZdq> zR`7+=)ilAE(vUOp?0T>2^qBkS2BQ5}`LD1HKOx)>sg79Gvbg9lO%!mKxLRh3h5CcA zWoe5%Pun~>Iwf`St5woK-u}eyT4Zz!!mr<~pI)d3)9}V=VyyXf|2LNEi|$*zHI7I> zgWCOysy~tloESEv)fL-9w6+?+PcK(dPV}qL9|1o5!<%t%ML>V`GmcDGgZF{oct}AY z+P_-w&oxUGFHbbm6Kx9%@J6DnkNsO8`YXWs4|D+@09XV%EATG>Nqi8<))(oH5)(cC zJS}K%#a-VH2*?7$9KQ%U0)3vhH(Ctk?&5$%iTz-le33ZT;NPS<03GT-(s~_w3ql(_XNEBdfJG6_J=Wk)ZH`@oY->D|^Tbe%x zF8d8oO$sp5uZ^%q{yUn~Z)q~ErC3@ZF%nXO02lkY@KF7rovz|(r|9#5gkAwWDIM^g zGd}}BE`WFb)Ql(Ya8j?+h}laz*YyBI?cWfy0P!^9#0O3zEP6T+SilM2sGQcqn(^<5 z6MLLS$nx45T>i(3C;)aot;LD&oJIuSi_p{p5D)%VkxH(=BTho&G$L_PQDF=)V;j)K z!oQePkoPBqjvWe(^!+8$er)=EyiJUxZa)Ty$I<&MS)K1EvbJ4-mmm5U@{diwCm((M zY^MNFy@9H8{-U!E|38pV9N{PCX%nDR4EW{&pS;O-8rLWA6YgIoIKll?`O`SCz-Q?; zKr0?VLqMIM%3Ht;$L;FiYv=Fc>GZEUU-fW@{MaHP3atNQ^4fnvBELic0$H(~j+<4H zA5gS2(gP`W^7)Sqoww-GCJPwM5ulv?NhuKMQ0kOgrxf~8K_CyE6i(0se>KW4K(rn& z48M@P&VxYzcN&6r_VYk^+PS!kx_Tp>PD4yKuk3lyfI!E~uZbXA{f9apNCy`?F&7Uz zC#0mPx98sooMt?cdO(vMFMthxD)JUT2uKeE(!t@x(mF2ozFsIVN3^Xf#vbWzYwG3e zhH^$C(V`f46y|gjHdr#r%>lMT1KriH%8gx@IPWI{?~|H XA4&jpyvO|;2j~;fYMP%YfN%c?xug#@ literal 0 HcmV?d00001 diff --git a/features/steps/test_files/act-props.pptx b/features/steps/test_files/act-props.pptx deleted file mode 100644 index 4ef5fbc50fa3b5e3918f7834720460fcfda96b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36160 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 X|M#yxzjl8iAtba14D1KOu{i$^c;&fH diff --git a/pptx/action.py b/pptx/action.py index 256b3e1e4..cc55e52e1 100644 --- a/pptx/action.py +++ b/pptx/action.py @@ -11,8 +11,8 @@ class ActionSetting(Subshape): """Properties specifying how a shape or run reacts to mouse actions.""" - # Subshape superclass provides access to the Slide Part, which is needed - # to access relationships. + # --- The Subshape superclass provides access to the Slide Part, which is needed + # --- to access relationships. def __init__(self, xPr, parent, hover=False): super(ActionSetting, self).__init__(parent) # xPr is either a cNvPr or rPr element @@ -22,11 +22,14 @@ def __init__(self, xPr, parent, hover=False): @property def action(self): - """ - A member of the :ref:`PpActionType` enumeration, such as - `PP_ACTION.HYPERLINK`, indicating the type of action that will result - when the specified shape or text is clicked or the mouse pointer is - positioned over the shape during a slide show. + """Member of :ref:`PpActionType` enumeration, such as `PP_ACTION.HYPERLINK`. + + The returned member indicates the type of action that will result when the + specified shape or text is clicked or the mouse pointer is positioned over the + shape during a slide show. + + If there is no click-action or the click-action value is not recognized (is not + one of the official `MsoPpAction` values) then `PP_ACTION.NONE` is returned. """ hlink = self._hlink @@ -55,7 +58,7 @@ def action(self): "ole": PP_ACTION.OLE_VERB, "macro": PP_ACTION.RUN_MACRO, "program": PP_ACTION.RUN_PROGRAM, - }[action_verb] + }.get(action_verb, PP_ACTION.NONE) @lazyproperty def hyperlink(self): diff --git a/tests/test_action.py b/tests/test_action.py index 8a3383c4e..33877eeae 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -129,6 +129,7 @@ def it_clears_the_click_action_to_help(self, clear_fixture): "ppaction://hlinkshowjump?jump=lastslideviewed", PP_ACTION.LAST_SLIDE_VIEWED, ), + ("p:cNvPr/a:hlinkClick", "ppaction://media", PP_ACTION.NONE), ] ) def action_fixture(self, request): From 127d73ab29a5729cc026bc4426e7fb7abaf5d647 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 13 Sep 2021 17:48:01 -0700 Subject: [PATCH 34/69] fix: #223 Escape p:pic filename parameter https://github.com/scanny/python-pptx/issues/223 --- pptx/oxml/shapes/picture.py | 31 +++--- tests/oxml/shapes/test_picture.py | 169 ++++++++++++++++-------------- 2 files changed, 106 insertions(+), 94 deletions(-) diff --git a/pptx/oxml/shapes/picture.py b/pptx/oxml/shapes/picture.py index e4bbe6d92..39904385d 100644 --- a/pptx/oxml/shapes/picture.py +++ b/pptx/oxml/shapes/picture.py @@ -2,18 +2,20 @@ """lxml custom element classes for picture-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import division -from .. import parse_xml -from ..ns import nsdecls -from .shared import BaseShapeElement -from ..xmlchemy import BaseOxmlElement, OneAndOnlyOne +from xml.sax.saxutils import escape + +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls +from pptx.oxml.shapes.shared import BaseShapeElement +from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne class CT_Picture(BaseShapeElement): - """ - ```` element, which represents a picture shape (an image placement - on a slide). + """`p:pic` element. + + Represents a picture shape (an image placement on a slide). """ nvPicPr = OneAndOnlyOne("p:nvPicPr") @@ -61,14 +63,11 @@ def new_ph_pic(cls, id_, name, desc, rId): return parse_xml(cls._pic_ph_tmpl() % (id_, name, desc, rId)) @classmethod - def new_pic(cls, id_, name, desc, rId, left, top, width, height): - """ - Return a new ```` element tree configured with the supplied - parameters. - """ - xml = cls._pic_tmpl() % (id_, name, desc, rId, left, top, width, height) - pic = parse_xml(xml) - return pic + def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy): + """Return new `` element tree configured with supplied parameters.""" + return parse_xml( + cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy) + ) @classmethod def new_video_pic( diff --git a/tests/oxml/shapes/test_picture.py b/tests/oxml/shapes/test_picture.py index cddd0d9d5..9f16599ba 100644 --- a/tests/oxml/shapes/test_picture.py +++ b/tests/oxml/shapes/test_picture.py @@ -1,10 +1,6 @@ # encoding: utf-8 -""" -Test suite for pptx.oxml.shapes.picture module. -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Unit-test suite for `pptx.oxml.shapes.picture` module.""" import pytest @@ -13,82 +9,99 @@ class DescribeCT_Picture(object): - def it_can_create_a_new_pic_element(self, pic_fixture): - shape_id, name, desc, rId, x, y, cx, cy, expected_xml = pic_fixture - pic = CT_Picture.new_pic(shape_id, name, desc, rId, x, y, cx, cy) - assert pic.xml == expected_xml - - def it_can_create_a_new_video_pic_element(self, video_pic_fixture): - shape_id, shape_name, video_rId, media_rId = video_pic_fixture[:4] - poster_frame_rId, x, y, cx, cy, expected_xml = video_pic_fixture[4:] - pic = CT_Picture.new_video_pic( - shape_id, shape_name, video_rId, media_rId, poster_frame_rId, x, y, cx, cy - ) - print(pic.xml) - assert pic.xml == expected_xml + """Unit-test suite for `pptx.oxml.shapes.picture.CT_Picture` objects.""" - # fixtures ------------------------------------------------------- + @pytest.mark.parametrize( + "desc, xml_desc", + ( + ("kittens.jpg", "kittens.jpg"), + ("bits&bobs.png", "bits&bobs.png"), + ("img&.png", "img&.png"), + ("ime.png", "im<ag>e.png"), + ), + ) + def it_can_create_a_new_pic_element(self, desc, xml_desc): + """`desc` attr (often filename) is XML-escaped to handle special characters. - @pytest.fixture - def pic_fixture(self): - shape_id, name, desc, rId = 9, "Diam. > 1 mm", "desc", "rId1" - x, y, cx, cy = 1, 2, 3, 4 - expected_xml = ( - '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" - % ( - nsdecls("a", "p", "r"), - shape_id, - "Diam. > 1 mm", - desc, - rId, - x, - y, - cx, - cy, - ) + In particular, ampersand ('&'), less/greater-than ('') etc. + """ + pic = CT_Picture.new_pic( + shape_id=9, name="Picture 8", desc=desc, rId="rId42", x=1, y=2, cx=3, cy=4 ) - return shape_id, name, desc, rId, x, y, cx, cy, expected_xml - @pytest.fixture - def video_pic_fixture(self): - shape_id, shape_name = 42, "media.mp4" - video_rId, media_rId, poster_frame_rId = "rId1", "rId2", "rId3" - x, y, cx, cy = 1, 2, 3, 4 - expected_xml = ( - '\n \n \n \n \n \n \n \n \n ' - ' \n \n \n \n \n \n \n \n \n \n \n \n ' - "\n \n \n \n " - '\n \n \n \n \n \n \n\n" + assert pic.xml == ( + "\n" + " \n" + ' \n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + ' \n' + ' \n' + " \n" + ' \n' + " \n" + " \n" + " \n" + "\n" % (nsdecls("a", "p", "r"), xml_desc) ) - return ( - shape_id, - shape_name, - video_rId, - media_rId, - poster_frame_rId, - x, - y, - cx, - cy, - expected_xml, + + def it_can_create_a_new_video_pic_element(self): + pic = CT_Picture.new_video_pic( + shape_id=42, + shape_name="Media 41", + video_rId="rId1", + media_rId="rId2", + poster_frame_rId="rId3", + x=1, + y=2, + cx=3, + cy=4, ) + + assert pic.xml == ( + "\n" + " \n" + ' \n' + ' \n' + " \n" + " \n" + ' \n' + " \n" + " \n" + ' \n' + " \n" + ' \n' + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + ' \n' + ' \n' + " \n" + ' \n' + " \n" + " \n" + " \n" + "\n" + ) % nsdecls("a", "p", "r") From 0b127a95f1caf7e0e88a1ed8e08e417af49488a0 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 13 Sep 2021 22:14:08 -0700 Subject: [PATCH 35/69] fix: Python 2.7 and 3.6 test errors These resulted mostly from pulling `from __future__ ...` imports out wholesale. A few were actually still needed. Also, setting autospec=True by default on `method_mock()` encountered a but that was present in 3.6 but not 2.7 or 3.8, so we need to set some method-mocks to autospec=False even though that works properly now in 3.8. --- pptx/opc/package.py | 3 ++- tests/dml/test_fill.py | 14 +++++++---- tests/opc/test_package.py | 6 ++--- tests/oxml/test_simpletypes.py | 44 ++++++++++++++++++++-------------- tests/parts/test_chart.py | 5 +++- tests/parts/test_image.py | 23 +++++++----------- tests/parts/test_slide.py | 35 +++++++++++++++++++++++---- tests/shapes/test_freeform.py | 6 ++--- tests/shapes/test_shapetree.py | 6 ++--- tests/test_media.py | 29 ++++++++++------------ tests/test_slide.py | 43 ++++++++++----------------------- tests/text/test_fonts.py | 35 +++++++++++++++++---------- tests/text/test_text.py | 2 ++ tests/unitutil/mock.py | 2 ++ 14 files changed, 140 insertions(+), 113 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 5714efdf7..03c39588a 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -508,7 +508,8 @@ def __getitem__(self, rId): def __iter__(self): """Implement iteration of relationships.""" - return iter(list(self._rels.values())) + rels = self._rels + return (rels[rId] for rId in sorted(rels.keys())) def __len__(self): """Return count of relationships in collection.""" diff --git a/tests/dml/test_fill.py b/tests/dml/test_fill.py index 2e01355a5..2c2af4e03 100644 --- a/tests/dml/test_fill.py +++ b/tests/dml/test_fill.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Test suite for pptx.dml.fill module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit-test suite for `pptx.dml.fill` module.""" import pytest @@ -492,6 +490,8 @@ def fill_type_fixture(self): class Describe_PattFill(object): + """Unit-test suite for `pptx.dml.fill._PattFill` objects.""" + def it_knows_its_fill_type(self, fill_type_fixture): patt_fill, expected_value = fill_type_fixture fill_type = patt_fill.type @@ -618,7 +618,9 @@ def pattern_set_fixture(self, request): @pytest.fixture def ColorFormat_from_colorchoice_parent_(self, request): - return method_mock(request, ColorFormat, "from_colorchoice_parent") + return method_mock( + request, ColorFormat, "from_colorchoice_parent", autospec=False + ) @pytest.fixture def color_(self, request): @@ -660,7 +662,9 @@ def fore_color_fixture(self, ColorFormat_from_colorchoice_parent_, color_): @pytest.fixture def ColorFormat_from_colorchoice_parent_(self, request): - return method_mock(request, ColorFormat, "from_colorchoice_parent") + return method_mock( + request, ColorFormat, "from_colorchoice_parent", autospec=False + ) @pytest.fixture def color_(self, request): diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index ccff47a53..0262fc12d 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -882,10 +882,10 @@ def it_can_get_a_matching_relationship_to_help( _Relationship, rId=rId, target_part=target_part, - target_ref=target_ref, - is_external=is_external, + target_ref=ref, + is_external=external, ) - for rId, target_part, target_ref, is_external in ( + for rId, target_part, ref, external in ( ("rId1", None, "http://url", True), ("rId2", part_1, "/ppt/foo.bar", False), ("rId3", None, "http://foo", True), diff --git a/tests/oxml/test_simpletypes.py b/tests/oxml/test_simpletypes.py index fabc86c93..e1a98b2ef 100644 --- a/tests/oxml/test_simpletypes.py +++ b/tests/oxml/test_simpletypes.py @@ -1,13 +1,13 @@ # encoding: utf-8 -""" -Test suite for pptx.oxml.simpletypes module, which contains simple type class -definitions. A simple type in this context corresponds to an -```` definition in the XML schema and provides data -validation and type conversion services for use by xmlchemy. -""" +"""Unit-test suite for `pptx.oxml.simpletypes` module. -from __future__ import absolute_import, print_function +`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). +""" import pytest @@ -23,10 +23,14 @@ class DescribeBaseSimpleType(object): - def it_can_convert_attr_value_to_python_type(self, from_xml_fixture): - SimpleType, str_value_, py_value_ = from_xml_fixture - py_value = SimpleType.from_xml(str_value_) - SimpleType.convert_from_xml.assert_called_once_with(str_value_) + """Unit-test suite for `pptx.oxml.simpletypes.BaseSimpleType` objects.""" + + def it_can_convert_attr_value_to_python_type( + self, str_value_, py_value_, convert_from_xml_ + ): + py_value = ST_SimpleType.from_xml(str_value_) + + ST_SimpleType.convert_from_xml.assert_called_once_with(str_value_) assert py_value is py_value_ def it_can_convert_python_value_to_string(self, to_xml_fixture): @@ -54,10 +58,6 @@ def it_can_validate_a_value_as_a_python_string(self, valid_str_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def from_xml_fixture(self, request, str_value_, py_value_, convert_from_xml_): - return ST_SimpleType, str_value_, py_value_ - @pytest.fixture def to_xml_fixture( self, request, py_value_, str_value_, convert_to_xml_, validate_ @@ -98,13 +98,21 @@ def valid_str_fixture(self, request): @pytest.fixture def convert_from_xml_(self, request, py_value_): return method_mock( - request, ST_SimpleType, "convert_from_xml", return_value=py_value_ + request, + ST_SimpleType, + "convert_from_xml", + autospec=False, + return_value=py_value_, ) @pytest.fixture def convert_to_xml_(self, request, str_value_): return method_mock( - request, ST_SimpleType, "convert_to_xml", return_value=str_value_ + request, + ST_SimpleType, + "convert_to_xml", + autospec=False, + return_value=str_value_, ) @pytest.fixture @@ -117,7 +125,7 @@ def str_value_(self, request): @pytest.fixture def validate_(self, request): - return method_mock(request, ST_SimpleType, "validate") + return method_mock(request, ST_SimpleType, "validate", autospec=False) class DescribeBaseIntType(object): diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index cb12254d5..ca7fe7771 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -27,7 +27,10 @@ def it_can_construct_from_chart_type_and_data(self, request): package_ = instance_mock(request, OpcPackage) package_.next_partname.return_value = PackURI("/ppt/charts/chart42.xml") chart_part_ = instance_mock(request, ChartPart) - load_ = method_mock(request, ChartPart, "load", return_value=chart_part_) + # --- load() must have autospec turned off to work in Python 2.7 mock --- + load_ = method_mock( + request, ChartPart, "load", autospec=False, return_value=chart_part_ + ) chart_part = ChartPart.new(XCT.RADAR, chart_data_, package_) diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 6240bf5e9..8e1f68274 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -93,10 +93,14 @@ def image_(self, request): class DescribeImage(object): """Unit-test suite for `pptx.parts.image.Image` objects.""" - def it_can_construct_from_a_path(self, from_path_fixture): - image_file, blob, filename, image_ = from_path_fixture - image = Image.from_file(image_file) - Image.from_blob.assert_called_once_with(blob, filename) + def it_can_construct_from_a_path(self, from_blob_, image_): + with open(test_image_path, "rb") as f: + blob = f.read() + from_blob_.return_value = image_ + + image = Image.from_file(test_image_path) + + Image.from_blob.assert_called_once_with(blob, "python-icon.jpeg") assert image is image_ def it_can_construct_from_a_stream(self, from_stream_fixture): @@ -203,15 +207,6 @@ def filename_fixture(self, request): image = Image(None, filename) return image, filename - @pytest.fixture - def from_path_fixture(self, from_blob_, image_): - image_file = test_image_path - with open(test_image_path, "rb") as f: - blob = f.read() - filename = "python-icon.jpeg" - from_blob_.return_value = image_ - return image_file, blob, filename, image_ - @pytest.fixture def from_stream_fixture(self, from_blob_, image_): with open(test_image_path, "rb") as f: @@ -238,7 +233,7 @@ def _format_(self, request): @pytest.fixture def from_blob_(self, request): - return method_mock(request, Image, "from_blob") + return method_mock(request, Image, "from_blob", autospec=False) @pytest.fixture def image_(self, request): diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index 3f18a8176..58929d124 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -90,9 +90,19 @@ class DescribeNotesMasterPart(object): def it_can_create_a_notes_master_part( self, request, package_, notes_master_part_, theme_part_ ): - method_mock(request, NotesMasterPart, "_new", return_value=notes_master_part_) method_mock( - request, NotesMasterPart, "_new_theme_part", return_value=theme_part_ + request, + NotesMasterPart, + "_new", + autospec=False, + return_value=notes_master_part_, + ) + method_mock( + request, + NotesMasterPart, + "_new_theme_part", + autospec=False, + return_value=theme_part_, ) notes_master_part = NotesMasterPart.create_default(package_) @@ -121,7 +131,13 @@ def it_creates_a_new_notes_master_part_to_help( request, "pptx.parts.slide.NotesMasterPart", return_value=notes_master_part_ ) notesMaster = element("p:notesMaster") - method_mock(request, CT_NotesMaster, "new_default", return_value=notesMaster) + method_mock( + request, + CT_NotesMaster, + "new_default", + autospec=False, + return_value=notesMaster, + ) notes_master_part = NotesMasterPart._new(package_) @@ -139,7 +155,13 @@ def it_creates_a_new_theme_part_to_help(self, request, package_, theme_part_): request, "pptx.parts.slide.XmlPart", return_value=theme_part_ ) theme_elm = element("p:theme") - method_mock(request, CT_OfficeStyleSheet, "new_default", return_value=theme_elm) + method_mock( + request, + CT_OfficeStyleSheet, + "new_default", + autospec=False, + return_value=theme_elm, + ) pn_tmpl = "/ppt/theme/theme%d.xml" partname = PackURI("/ppt/theme/theme2.xml") package_.next_partname.return_value = partname @@ -186,6 +208,7 @@ def it_can_create_a_notes_slide_part( request, NotesSlidePart, "_add_notes_slide_part", + autospec=False, return_value=notes_slide_part_, ) notes_slide_part_.notes_slide = notes_slide_ @@ -232,7 +255,9 @@ 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", return_value=notes) + new_ = method_mock( + request, CT_NotesSlide, "new", autospec=False, return_value=notes + ) package_.next_partname.return_value = PackURI( "/ppt/notesSlides/notesSlide42.xml" ) diff --git a/tests/shapes/test_freeform.py b/tests/shapes/test_freeform.py index 0f23c755c..26ded32e2 100644 --- a/tests/shapes/test_freeform.py +++ b/tests/shapes/test_freeform.py @@ -376,7 +376,7 @@ def close_(self, request): @pytest.fixture def _Close_new_(self, request): - return method_mock(request, _Close, "new") + return method_mock(request, _Close, "new", autospec=False) @pytest.fixture def _dx_prop_(self, request): @@ -404,7 +404,7 @@ def line_segment_(self, request): @pytest.fixture def _LineSegment_new_(self, request): - return method_mock(request, _LineSegment, "new") + return method_mock(request, _LineSegment, "new", autospec=False) @pytest.fixture def move_to_(self, request): @@ -412,7 +412,7 @@ def move_to_(self, request): @pytest.fixture def _MoveTo_new_(self, request): - return method_mock(request, _MoveTo, "new") + return method_mock(request, _MoveTo, "new", autospec=False) @pytest.fixture def shape_(self, request): diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index b2851ae87..1934fea59 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -810,7 +810,7 @@ def CT_GroupShape_add_grpSp_(self, request): @pytest.fixture def FreeformBuilder_new_(self, request): - return method_mock(request, FreeformBuilder, "new") + return method_mock(request, FreeformBuilder, "new", autospec=False) @pytest.fixture def graphic_frame_(self, request): @@ -2007,7 +2007,7 @@ def BytesIO_(self, request): @pytest.fixture def from_path_or_file_like_(self, request): - return method_mock(request, Video, "from_path_or_file_like") + return method_mock(request, Video, "from_path_or_file_like", autospec=False) @pytest.fixture def _media_rId_prop_(self, request): @@ -2019,7 +2019,7 @@ def _MoviePicElementCreator_init_(self, request): @pytest.fixture def new_video_pic_(self, request): - return method_mock(request, CT_Picture, "new_video_pic") + return method_mock(request, CT_Picture, "new_video_pic", autospec=False) @pytest.fixture def pic_(self): diff --git a/tests/test_media.py b/tests/test_media.py index 8b9601f41..9e42db9e5 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -1,8 +1,6 @@ # encoding: utf-8 -"""Unit test suite for pptx.media module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for `pptx.media` module.""" import pytest @@ -17,10 +15,16 @@ class DescribeVideo(object): - def it_can_construct_from_a_path(self, from_path_fixture): - movie_path, mime_type, blob, filename, video_ = from_path_fixture - video = Video.from_path_or_file_like(movie_path, mime_type) - Video.from_blob.assert_called_once_with(blob, mime_type, filename) + """Unit-test suite for `pptx.media.Video` objects.""" + + def it_can_construct_from_a_path(self, video_, from_blob_): + with open(TEST_VIDEO_PATH, "rb") as f: + blob = f.read() + from_blob_.return_value = video_ + + video = Video.from_path_or_file_like(TEST_VIDEO_PATH, "video/mp4") + + Video.from_blob.assert_called_once_with(blob, "video/mp4", "dummy.mp4") assert video is video_ def it_can_construct_from_a_stream(self, from_stream_fixture): @@ -97,15 +101,6 @@ def from_blob_fixture(self, Video_init_): blob, mime_type, filename = "01234", "video/mp4", "movie.mp4" return blob, mime_type, filename, Video_init_ - @pytest.fixture - def from_path_fixture(self, video_, from_blob_): - movie_path, mime_type = TEST_VIDEO_PATH, "video/mp4" - with open(movie_path, "rb") as f: - blob = f.read() - filename = "dummy.mp4" - from_blob_.return_value = video_ - return movie_path, mime_type, blob, filename, video_ - @pytest.fixture def from_stream_fixture(self, video_, from_blob_): with open(TEST_VIDEO_PATH, "rb") as f: @@ -130,7 +125,7 @@ def ext_prop_(self, request): @pytest.fixture def from_blob_(self, request): - return method_mock(request, Video, "from_blob") + return method_mock(request, Video, "from_blob", autospec=False) @pytest.fixture def video_(self, request): diff --git a/tests/test_slide.py b/tests/test_slide.py index 7e139068b..d4a1bdeef 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -1056,44 +1056,27 @@ def slide_master_(self, request): class Describe_Background(object): """Unit-test suite for `pptx.slide._Background` objects.""" - def it_provides_access_to_its_fill(self, fill_fixture): - background, cSld, expected_xml = fill_fixture[:3] - from_fill_parent_, fill_ = fill_fixture[3:] - - fill = background.fill - - assert cSld.xml == expected_xml - from_fill_parent_.assert_called_once_with(cSld.xpath("p:bg/p:bgPr")[0]) - assert fill is fill_ - - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + "cSld_xml, expected_cxml", + ( ("p:cSld{a:b=c}", "p:cSld{a:b=c}/p:bg/p:bgPr/(a:noFill,a:effectLst)"), ( "p:cSld{a:b=c}/p:bg/p:bgRef", "p:cSld{a:b=c}/p:bg/p:bgPr/(a:noFill,a:effectLst)", ), ("p:cSld/p:bg/p:bgPr/a:solidFill", "p:cSld/p:bg/p:bgPr/a:solidFill"), - ] + ), ) - def fill_fixture(self, request, from_fill_parent_, fill_): - cSld_xml, expected_cxml = request.param + def it_provides_access_to_its_fill(self, request, cSld_xml, expected_cxml): + fill_ = instance_mock(request, FillFormat) + from_fill_parent_ = method_mock( + request, FillFormat, "from_fill_parent", autospec=False, return_value=fill_ + ) cSld = element(cSld_xml) background = _Background(cSld) - from_fill_parent_.return_value = fill_ - - expected_xml = xml(expected_cxml) - return background, cSld, expected_xml, from_fill_parent_, fill_ - - # fixture components --------------------------------------------- - - @pytest.fixture - def fill_(self, request): - return instance_mock(request, FillFormat) + fill = background.fill - @pytest.fixture - def from_fill_parent_(self, request): - return method_mock(request, FillFormat, "from_fill_parent") + assert cSld.xml == xml(expected_cxml) + from_fill_parent_.assert_called_once_with(cSld.xpath("p:bg/p:bgPr")[0]) + assert fill is fill_ diff --git a/tests/text/test_fonts.py b/tests/text/test_fonts.py index 4aba411c0..275052235 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -2,6 +2,8 @@ """Unit-test suite for `pptx.text.fonts` module.""" +from __future__ import unicode_literals + import io import pytest @@ -144,29 +146,35 @@ def _Font_(self, request): @pytest.fixture def _font_directories_(self, request): - return method_mock(request, FontFiles, "_font_directories") + return method_mock(request, FontFiles, "_font_directories", autospec=False) @pytest.fixture def _installed_fonts_(self, request): - _installed_fonts_ = method_mock(request, FontFiles, "_installed_fonts") - _installed_fonts_.return_value = { - ("Foobar", False, False): "foobar.ttf", - ("Foobar", True, False): "foobarb.ttf", - ("Barfoo", False, True): "barfooi.ttf", - } - return _installed_fonts_ + return method_mock( + request, + FontFiles, + "_installed_fonts", + autospec=False, + return_value={ + ("Foobar", False, False): "foobar.ttf", + ("Foobar", True, False): "foobarb.ttf", + ("Barfoo", False, True): "barfooi.ttf", + }, + ) @pytest.fixture def _iter_font_files_in_(self, request): - return method_mock(request, FontFiles, "_iter_font_files_in") + return method_mock(request, FontFiles, "_iter_font_files_in", autospec=False) @pytest.fixture def _os_x_font_directories_(self, request): - return method_mock(request, FontFiles, "_os_x_font_directories") + return method_mock(request, FontFiles, "_os_x_font_directories", autospec=False) @pytest.fixture def _windows_font_directories_(self, request): - return method_mock(request, FontFiles, "_windows_font_directories") + return method_mock( + request, FontFiles, "_windows_font_directories", autospec=False + ) class Describe_Font(object): @@ -537,6 +545,7 @@ def it_reads_a_name_to_help_read_names(self, request): request, _NameTable, "_name_header", + autospec=False, return_value=( platform_id, encoding_id, @@ -691,7 +700,7 @@ def raw_fixture(self): @pytest.fixture def _decode_name_(self, request): - return method_mock(request, _NameTable, "_decode_name") + return method_mock(request, _NameTable, "_decode_name", autospec=False) @pytest.fixture def _names_prop_(self, request): @@ -699,7 +708,7 @@ def _names_prop_(self, request): @pytest.fixture def _raw_name_string_(self, request): - return method_mock(request, _NameTable, "_raw_name_string") + return method_mock(request, _NameTable, "_raw_name_string", autospec=False) @pytest.fixture def stream_(self, request): diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 1857814ea..28f0e65a6 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -2,6 +2,8 @@ """Unit-test suite for `pptx.text.text` module.""" +from __future__ import unicode_literals + import pytest from pptx.compat import is_unicode diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index e10a8d54c..849a927cb 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -2,6 +2,8 @@ """Utility functions wrapping the excellent `mock` library.""" +from __future__ import absolute_import + import sys if sys.version_info >= (3, 3): From 62f7f6211c29150245fd8b383c5bde69534f48dd Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 14 Sep 2021 13:24:44 -0700 Subject: [PATCH 36/69] release: prepare v0.6.20 release --- HISTORY.rst | 9 +++++++++ pptx/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index f160e7394..c5d07c4c1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,15 @@ Release History --------------- +0.6.20 (2021-09-14) ++++++++++++++++++++ + +- Fix #206 accommodate NULL target-references in relationships. +- Fix #223 escape image filename that appears as literal in XML. +- Fix #517 option to display chart categories/values in reverse order. +- Major refactoring of ancient package loading code. + + 0.6.19 (2021-05-17) +++++++++++++++++++ diff --git a/pptx/__init__.py b/pptx/__init__.py index e843ba8e2..41c5fa159 100644 --- a/pptx/__init__.py +++ b/pptx/__init__.py @@ -2,7 +2,7 @@ """Initialization module for python-pptx package.""" -__version__ = "0.6.19" +__version__ = "0.6.20" import pptx.exc as exceptions From 4d7458c320f8bc898631e17185fc39185b3e5ade Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 20 Sep 2021 15:06:29 -0700 Subject: [PATCH 37/69] xfail: add acceptance test for _DirPackageReader There was no acceptance test for loading from a PPTX package extracted to a directory. --- features/prs-open-save.feature | 7 + features/steps/presentation.py | 7 +- .../extracted-pptx/[Content_Types].xml | 27 + .../test_files/extracted-pptx/_rels/.rels | 7 + .../extracted-pptx/docProps/app.xml | 42 ++ .../extracted-pptx/docProps/core.xml | 13 + .../extracted-pptx/docProps/thumbnail.jpeg | Bin 0 -> 8147 bytes .../ppt/_rels/presentation.xml.rels | 10 + .../extracted-pptx/ppt/presProps.xml | 11 + .../extracted-pptx/ppt/presentation.xml | 106 ++++ .../ppt/printerSettings/printerSettings1.bin | Bin 0 -> 9395 bytes .../slideLayouts/_rels/slideLayout1.xml.rels | 4 + .../slideLayouts/_rels/slideLayout10.xml.rels | 4 + .../slideLayouts/_rels/slideLayout11.xml.rels | 4 + .../slideLayouts/_rels/slideLayout2.xml.rels | 4 + .../slideLayouts/_rels/slideLayout3.xml.rels | 4 + .../slideLayouts/_rels/slideLayout4.xml.rels | 4 + .../slideLayouts/_rels/slideLayout5.xml.rels | 4 + .../slideLayouts/_rels/slideLayout6.xml.rels | 4 + .../slideLayouts/_rels/slideLayout7.xml.rels | 4 + .../slideLayouts/_rels/slideLayout8.xml.rels | 4 + .../slideLayouts/_rels/slideLayout9.xml.rels | 4 + .../ppt/slideLayouts/slideLayout1.xml | 237 ++++++++ .../ppt/slideLayouts/slideLayout10.xml | 165 ++++++ .../ppt/slideLayouts/slideLayout11.xml | 175 ++++++ .../ppt/slideLayouts/slideLayout2.xml | 165 ++++++ .../ppt/slideLayouts/slideLayout3.xml | 241 +++++++++ .../ppt/slideLayouts/slideLayout4.xml | 283 ++++++++++ .../ppt/slideLayouts/slideLayout5.xml | 417 +++++++++++++++ .../ppt/slideLayouts/slideLayout6.xml | 113 ++++ .../ppt/slideLayouts/slideLayout7.xml | 90 ++++ .../ppt/slideLayouts/slideLayout8.xml | 272 ++++++++++ .../ppt/slideLayouts/slideLayout9.xml | 248 +++++++++ .../slideMasters/_rels/slideMaster1.xml.rels | 15 + .../ppt/slideMasters/slideMaster1.xml | 505 ++++++++++++++++++ .../ppt/slides/_rels/slide1.xml.rels | 4 + .../extracted-pptx/ppt/slides/slide1.xml | 74 +++ .../extracted-pptx/ppt/tableStyles.xml | 2 + .../extracted-pptx/ppt/theme/theme1.xml | 281 ++++++++++ .../extracted-pptx/ppt/viewProps.xml | 32 ++ 40 files changed, 3592 insertions(+), 1 deletion(-) create mode 100644 features/steps/test_files/extracted-pptx/[Content_Types].xml create mode 100644 features/steps/test_files/extracted-pptx/_rels/.rels create mode 100644 features/steps/test_files/extracted-pptx/docProps/app.xml create mode 100644 features/steps/test_files/extracted-pptx/docProps/core.xml create mode 100644 features/steps/test_files/extracted-pptx/docProps/thumbnail.jpeg create mode 100644 features/steps/test_files/extracted-pptx/ppt/_rels/presentation.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/presProps.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/presentation.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/printerSettings/printerSettings1.bin create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels create mode 100644 features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/tableStyles.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml create mode 100644 features/steps/test_files/extracted-pptx/ppt/viewProps.xml diff --git a/features/prs-open-save.feature b/features/prs-open-save.feature index ca0110526..daa5a675e 100644 --- a/features/prs-open-save.feature +++ b/features/prs-open-save.feature @@ -15,6 +15,13 @@ Feature: Round-trip a presentation And I save the presentation Then I see the pptx file in the working directory + @wip + Scenario: Start presentation from package extracted into directory + Given a clean working directory + When I open a presentation extracted into a directory + And I save the presentation + Then I see the pptx file in the working directory + Scenario: Save presentation to package stream Given a clean working directory When I open a basic PowerPoint presentation diff --git a/features/steps/presentation.py b/features/steps/presentation.py index 91dd57c0b..2309c7462 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -12,7 +12,7 @@ from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.util import Inches -from helpers import saved_pptx_path, test_pptx +from helpers import saved_pptx_path, test_file, test_pptx # given =================================================== @@ -69,6 +69,11 @@ def when_open_basic_pptx(context): context.prs = Presentation(test_pptx("test")) +@when("I open a presentation extracted into a directory") +def when_I_open_a_presentation_extracted_into_a_directory(context): + context.prs = Presentation(test_file("extracted-pptx")) + + @when("I open a presentation contained in a stream") def when_open_presentation_stream(context): with open(test_pptx("test"), "rb") as f: diff --git a/features/steps/test_files/extracted-pptx/[Content_Types].xml b/features/steps/test_files/extracted-pptx/[Content_Types].xml new file mode 100644 index 000000000..4af2894e0 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/[Content_Types].xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/_rels/.rels b/features/steps/test_files/extracted-pptx/_rels/.rels new file mode 100644 index 000000000..cbaca35bd --- /dev/null +++ b/features/steps/test_files/extracted-pptx/_rels/.rels @@ -0,0 +1,7 @@ + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/docProps/app.xml b/features/steps/test_files/extracted-pptx/docProps/app.xml new file mode 100644 index 000000000..d040c0468 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/docProps/app.xml @@ -0,0 +1,42 @@ + + + 2 + 5 + Microsoft Macintosh PowerPoint + On-screen Show (4:3) + 2 + 1 + 0 + 0 + 0 + false + + + + Theme + + + 1 + + + Slide Titles + + + 1 + + + + + + Office Theme + Presentation Title Text + + + + neopraxis.org + false + false + + false + 14.0000 + diff --git a/features/steps/test_files/extracted-pptx/docProps/core.xml b/features/steps/test_files/extracted-pptx/docProps/core.xml new file mode 100644 index 000000000..7803bae14 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/docProps/core.xml @@ -0,0 +1,13 @@ + + + Presentation + + python-pptx + + + Steve Canny + 4 + 2012-11-17T11:07:40Z + 2012-12-17T06:54:44Z + + diff --git a/features/steps/test_files/extracted-pptx/docProps/thumbnail.jpeg b/features/steps/test_files/extracted-pptx/docProps/thumbnail.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1f16c0ad43177f3b24bc598c95f48a595ea16d18 GIT binary patch literal 8147 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 + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/presProps.xml b/features/steps/test_files/extracted-pptx/ppt/presProps.xml new file mode 100644 index 000000000..3889a5131 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/presProps.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/presentation.xml b/features/steps/test_files/extracted-pptx/ppt/presentation.xml new file mode 100644 index 000000000..bb01bf331 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/presentation.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/printerSettings/printerSettings1.bin b/features/steps/test_files/extracted-pptx/ppt/printerSettings/printerSettings1.bin new file mode 100644 index 0000000000000000000000000000000000000000..38b1fba1a8263051d4ac2892fd3c55487d3415e1 GIT binary patch literal 9395 zcmd^E&2ri>5T?1o8}#OK2a$w^wBwMW5T?wK#1o)1J!KSIp-znDu|-q*NPWNFyE2w7 z{1@6oYCDI_I6_+O|F_ybsMTt3;P2PlT1`Fwv9gr!#Pv*SpEVELou)!;of@XSJZmoI zdLIcOK2tuRzNYSQ~*<1k{2whw?mF73b zoR0`SJw3HC-__RXif{Cg=2C~aY%>L7EjZ9Nn9+o5w$yjnfmOrQS-(+NAIYW;UyVUcZK2e@-ubKpVVg9x*?v5_aaiky1740cEnifde`?QmCv zMKQ%p#}>G9b<7YGC=3M9f8pZb3uHMH3{a_jlp<7UghFN`!EnrT%Ab)k1PiTEkdW}! zck?aIIv8;O*0k^Gtw7T>l-4daZKUzNLu!yKydolfNyzy(II=IG=O*nWqSsY{ja=md zG5I%_Dn|;wB`71NgfRk@M^sga^YL0Iz{oThk5DLvcjrPYYjxr=Sj@-chn zS3kZ1O0E1oM3OAAm?_0D#@wV!TypJa$RMfP#x1J=z8{aWoziJj3Mww0)x-cje*?PWa33dnY@~Qi6K<9Jr^N0Fx^22yqFMp5Pm= zb$GgCyuhXz%e`K@2YlFFE|>>^Dke~?Kx?7Y**NLid4iQ8MMwNEY+T74y_bZ^(0p}C zI$ck&TAeJtpNL6ccy(#oag6Gw>MY@;bX|nkOB`#0N9#bFK(m5R8KM$8c=2{R?IWlu zqxWy#!=7y%sTUBOiQnmiH3#l(*FmBkzJ~MeNb)M)>Ty={sK}g(^GQa<^diowU{po^ zOr+m9fQilTMg=A|BdV`gFdW!Q4DmX1D7CgT9N`;bD3 zj&UzJDc_Vf>k2|Ct^bqtI1_FJ6vO6ExR!c}#>Z+ic$dS>zys?HsbiLXn3gLN*S4sd z3N*q=^1K;R3)*vG`YUCSq_|gHE^ZL!t_kudr3DIwvFUn@cY;ctrs5VzNL7x2nBofB zA}*9{ilf7BuX}oY((M&z&KyfpewP&66$_ArRNN|ZYcvx+B4EijUI@#a>VHH1ptN|w L%2#%sXN%uI(4(*k literal 0 HcmV?d00001 diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels new file mode 100644 index 000000000..60e223fa5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml new file mode 100644 index 000000000..401fcb64c --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout1.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master subtitle style + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml new file mode 100644 index 000000000..05b32f9b5 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout10.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml new file mode 100644 index 000000000..7de50b1e1 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout11.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml new file mode 100644 index 000000000..c08a4aeae --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout2.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml new file mode 100644 index 000000000..9a5a9b143 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout3.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml new file mode 100644 index 000000000..9ca055ff2 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout4.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml new file mode 100644 index 000000000..9f58fd925 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout5.xml @@ -0,0 +1,417 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml new file mode 100644 index 000000000..220940510 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout6.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml new file mode 100644 index 000000000..432903279 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout7.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml new file mode 100644 index 000000000..e3d1f6bfe --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout8.xml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml new file mode 100644 index 000000000..bc9867476 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideLayouts/slideLayout9.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels new file mode 100644 index 000000000..bf3e45829 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml b/features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml new file mode 100644 index 000000000..98b610fa9 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slideMasters/slideMaster1.xml @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Click to edit Master text styles + + + + + + + Second level + + + + + + + Third level + + + + + + + Fourth level + + + + + + + Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12/16/12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ‹#› + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels b/features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels new file mode 100644 index 000000000..a1233b58c --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slides/_rels/slide1.xml.rels @@ -0,0 +1,4 @@ + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml b/features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml new file mode 100644 index 000000000..2b2508b61 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/slides/slide1.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Presentation Title Text + + + + + + + + + + + + + + + + + + + + + + + Subtitle Text + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/tableStyles.xml b/features/steps/test_files/extracted-pptx/ppt/tableStyles.xml new file mode 100644 index 000000000..179ee8a62 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/tableStyles.xml @@ -0,0 +1,2 @@ + + diff --git a/features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml b/features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml new file mode 100644 index 000000000..8eef13149 --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/theme/theme1.xml @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/steps/test_files/extracted-pptx/ppt/viewProps.xml b/features/steps/test_files/extracted-pptx/ppt/viewProps.xml new file mode 100644 index 000000000..7f36cea5a --- /dev/null +++ b/features/steps/test_files/extracted-pptx/ppt/viewProps.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 928248f67531c1e2e567f6cb34d63549b5a912a4 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 20 Sep 2021 15:13:19 -0700 Subject: [PATCH 38/69] opc: add _DirPkgReader.__contains__() --- features/prs-open-save.feature | 1 - pptx/opc/serialized.py | 5 +++++ tests/opc/test_serialized.py | 26 +++++++++++++++++++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/features/prs-open-save.feature b/features/prs-open-save.feature index daa5a675e..4176adfd3 100644 --- a/features/prs-open-save.feature +++ b/features/prs-open-save.feature @@ -15,7 +15,6 @@ Feature: Round-trip a presentation And I save the presentation Then I see the pptx file in the working directory - @wip Scenario: Start presentation from package extracted into directory Given a clean working directory When I open a presentation extracted into a directory diff --git a/pptx/opc/serialized.py b/pptx/opc/serialized.py index efe14a044..9e6ad51e0 100644 --- a/pptx/opc/serialized.py +++ b/pptx/opc/serialized.py @@ -3,6 +3,7 @@ """API for reading/writing serialized Open Packaging Convention (OPC) package.""" import os +import posixpath import zipfile from pptx.compat import Container, is_string @@ -143,6 +144,10 @@ class _DirPkgReader(_PhysPkgReader): def __init__(self, path): self._path = os.path.abspath(path) + def __contains__(self, pack_uri): + """Return True when part identified by `pack_uri` is present in zip archive.""" + return os.path.exists(posixpath.join(self._path, pack_uri.membername)) + def __getitem__(self, pack_uri): """Return bytes of file corresponding to `pack_uri` in package directory.""" path = os.path.join(self._path, pack_uri.membername) diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 867d987b8..31c965904 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -234,17 +234,29 @@ def _ZipPkgReader_(self, request): class Describe_DirPkgReader(object): """Unit-test suite for `pptx.opc.serialized._DirPkgReader` objects.""" - def it_can_retrieve_the_blob_for_a_pack_uri(self): - blob = _DirPkgReader(dir_pkg_path)[PackURI("/ppt/presentation.xml")] - - sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" + def it_knows_whether_it_contains_a_partname(self, dir_pkg_reader): + 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): + blob = dir_pkg_reader[PackURI("/ppt/presentation.xml")] + assert ( + hashlib.sha1(blob).hexdigest() == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" + ) - def but_it_raises_KeyError_when_requested_member_is_not_present(self): + def but_it_raises_KeyError_when_requested_member_is_not_present( + self, dir_pkg_reader + ): with pytest.raises(KeyError) as e: - _DirPkgReader(dir_pkg_path)[PackURI("/ppt/foobar.xml")] + dir_pkg_reader[PackURI("/ppt/foobar.xml")] assert str(e.value) == "\"no member '/ppt/foobar.xml' in package\"" + # --- fixture components ------------------------------- + + @pytest.fixture(scope="class") + def dir_pkg_reader(self, request): + return _DirPkgReader(dir_pkg_path) + class Describe_ZipPkgReader(object): """Unit-test suite for `pptx.opc.serialized._ZipPkgReader` objects.""" From 71d1ca0b2b3b9178d64cdab565e8503a25a54e0b Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 20 Sep 2021 16:48:56 -0700 Subject: [PATCH 39/69] release: prepare v0.6.21 release --- HISTORY.rst | 6 ++++++ pptx/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index c5d07c4c1..322985773 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +0.6.21 (2021-09-20) ++++++++++++++++++++ + +- Fix #741 _DirPkgReader must implement .__contains__() + + 0.6.20 (2021-09-14) +++++++++++++++++++ diff --git a/pptx/__init__.py b/pptx/__init__.py index 41c5fa159..679601d42 100644 --- a/pptx/__init__.py +++ b/pptx/__init__.py @@ -2,7 +2,7 @@ """Initialization module for python-pptx package.""" -__version__ = "0.6.20" +__version__ = "0.6.21" import pptx.exc as exceptions From 368373c5670bf95715802f942358fc3dee3f5dff Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 20:53:06 -0700 Subject: [PATCH 40/69] fix: get lint/tests running again Some version-related changes caused lint and acceptance-test failures since the last release. Fix those up. --- .ruff.toml | 11 +++++++++++ pptx/package.py | 6 ++++-- requirements.txt | 2 +- setup.py | 4 ++-- tox.ini | 25 +++++++++++++++++++++++-- 5 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 000000000..45ae2ecbc --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,11 @@ +# -- don't check these locations -- +exclude = [ + # -- docs/ - documentation Python code is incidental -- + "docs", + # -- lab/ has some experimental code that is not disciplined -- + "lab", + # -- ref/ is not source-code -- + "ref", + # -- spec/ has some ad-hoc discovery code that is not disciplined -- + "spec", +] diff --git a/pptx/package.py b/pptx/package.py index 1855ec4a1..1d5e73cd6 100644 --- a/pptx/package.py +++ b/pptx/package.py @@ -55,8 +55,10 @@ def first_available_image_idx(): [ part.partname.idx for part in self.iter_parts() - if part.partname.startswith("/ppt/media/image") - and part.partname.idx is not None + if ( + part.partname.startswith("/ppt/media/image") + and part.partname.idx is not None + ) ] ) for i, image_idx in enumerate(image_idxs): diff --git a/requirements.txt b/requirements.txt index edbb0e25c..9658a5095 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ behave>=1.2.5 flake8>=2.0 lxml>=3.1.0 mock>=1.0.1 -Pillow>=3.3.2 +Pillow>=3.3.2,<=9.5.0 pyparsing>=2.0.1 pytest>=2.5 XlsxWriter>=0.5.7 diff --git a/setup.py b/setup.py index b8dc5272c..2e16cc8e1 100755 --- a/setup.py +++ b/setup.py @@ -37,12 +37,12 @@ def ascii_bytes_from(path, *paths): KEYWORDS = "powerpoint ppt pptx office open xml" AUTHOR = "Steve Canny" AUTHOR_EMAIL = "python-pptx@googlegroups.com" -URL = "http://github.com/scanny/python-pptx" +URL = "https://github.com/scanny/python-pptx" LICENSE = license PACKAGES = find_packages(exclude=["tests", "tests.*"]) PACKAGE_DATA = {"pptx": ["templates/*"]} -INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2", "XlsxWriter>=0.5.7"] +INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2,<=9.5.0", "XlsxWriter>=0.5.7"] TEST_SUITE = "tests" TESTS_REQUIRE = ["behave", "mock", "pyparsing>=2.0.1", "pytest"] diff --git a/tox.ini b/tox.ini index d7fd79f73..17c5507e5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,21 +3,42 @@ [flake8] exclude = dist,docs,*.egg-info,.git,lab,ref,_scratch,spec,.tox +ignore = + # -- E203 - whitespace before ':'. Black disagrees for slice expressions. + E203, + + # -- W503 - line break before binary operator. Black has a different opinion about + # -- this, that binary operators should appear at the beginning of new-line + # -- expression segments. I agree because right is ragged and left lines up. + W503 max-line-length = 88 [pytest] +filterwarnings = + # -- exit on any warning not explicitly ignored here -- + error + + # -- pytest-xdist plugin may warn about `looponfailroots` deprecation -- + ignore::DeprecationWarning:xdist + + # -- pytest complains when pytest-xdist is not installed -- + ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning + +looponfailroots = pptx tests norecursedirs = docs *.egg-info features .git pptx spec .tox python_classes = Test Describe python_functions = test_ it_ they_ but_ and_it_ [tox] -envlist = py27, py38 +envlist = py27, py38, py311 +requires = virtualenv<20.22.0 +skip_missing_interpreters = false [testenv] deps = behave==1.2.5 lxml>=3.1.0 - Pillow>=3.3.2 + Pillow>=3.3.2,<=9.5.0 pyparsing>=2.0.1 pytest XlsxWriter>=0.5.7 From 620bf70f5c5e56e15b498d59ed76a1240fb71096 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 21:06:05 -0700 Subject: [PATCH 41/69] ole: add icon_width/height to `.add_ole_object()` The caller will need to specify the icon height and width when providing their own custom icon image. Add optional parameters to `.add_ole_object()` suitable for that purpose and update the documentation. --- pptx/shapes/shapetree.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index e4eaf55af..6c46de43c 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -279,7 +279,16 @@ def add_group_shape(self, shapes=[]): return self._shape_factory(grpSp) def add_ole_object( - self, object_file, prog_id, left, top, width=None, height=None, icon_file=None + self, + object_file, + prog_id, + left, + top, + width=None, + height=None, + icon_file=None, + icon_width=None, + icon_height=None, ): """Return newly-created GraphicFrame shape embedding `object_file`. @@ -289,8 +298,9 @@ def add_ole_object( which case the default icon size is used. This is advised for best appearance where applicable because it avoids an icon with a "stretched" appearance. - `object_file` may either be a str path to the file or a file-like - object (such as `io.BytesIO`) containing the bytes of the object file. + `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 @@ -302,6 +312,13 @@ def add_ole_object( 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, From 407627d9ec212e4c50836bc6fe879d759f9adfa5 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 21:36:12 -0700 Subject: [PATCH 42/69] ole: add _OleObjectElementCreator icon_width/height --- pptx/shapes/shapetree.py | 42 +++++++++++++++-- tests/shapes/test_shapetree.py | 83 +++++++++++++++++++++++++++------- 2 files changed, 106 insertions(+), 19 deletions(-) diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index 6c46de43c..709485b55 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -330,6 +330,8 @@ def add_ole_object( width, height, icon_file, + icon_width, + icon_height, ) self._spTree.append(graphicFrame) self._recalculate_extents() @@ -1017,7 +1019,18 @@ class _OleObjectElementCreator(object): """ def __init__( - self, shapes, shape_id, ole_object_file, prog_id, x, y, cx, cy, icon_file + self, + shapes, + shape_id, + ole_object_file, + prog_id, + x, + y, + cx, + cy, + icon_file, + icon_width, + icon_height, ): self._shapes = shapes self._shape_id = shape_id @@ -1028,14 +1041,37 @@ def __init__( self._cx_arg = cx self._cy_arg = cy self._icon_file_arg = icon_file + self._icon_width_arg = icon_width + self._icon_height_arg = icon_height @classmethod def graphicFrame( - cls, shapes, shape_id, ole_object_file, prog_id, x, y, cx, cy, icon_file + cls, + shapes, + shape_id, + ole_object_file, + prog_id, + x, + y, + cx, + cy, + icon_file, + icon_width, + icon_height, ): """Return new `p:graphicFrame` element containing embedded `ole_object_file`.""" return cls( - shapes, shape_id, ole_object_file, prog_id, x, y, cx, cy, icon_file + shapes, + shape_id, + ole_object_file, + prog_id, + x, + y, + cx, + cy, + icon_file, + icon_width, + icon_height, )._graphicFrame @lazyproperty diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 1934fea59..bcab9cc66 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -52,7 +52,7 @@ ) from pptx.slide import SlideLayout, SlideMaster from pptx.table import Table -from pptx.util import Emu +from pptx.util import Emu, Inches from ..oxml.unitdata.shape import a_ph, a_pic, an_nvPr, an_nvSpPr, an_sp from ..unitutil.cxml import element, xml @@ -375,10 +375,30 @@ def it_can_add_an_ole_object( x, y, cx, cy = 1, 2, 3, 4 shapes = _BaseGroupShapes(element("p:spTree"), None) - shape = shapes.add_ole_object("worksheet.xlsx", PROG_ID.XLSX, x, y, cx, cy) + shape = shapes.add_ole_object( + "worksheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "test.xlsx", + Inches(0.5), + Inches(0.75), + ) _OleObjectElementCreator_.graphicFrame.assert_called_once_with( - shapes, 42, "worksheet.xlsx", PROG_ID.XLSX, x, y, cx, cy, None + shapes, + 42, + "worksheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "test.xlsx", + Inches(0.5), + Inches(0.75), ) assert shapes._spTree[-1] is graphicFrame _recalculate_extents_.assert_called_once_with(shapes) @@ -2093,11 +2113,32 @@ def it_provides_a_graphicFrame_interface_method(self, request, shapes_): ) graphicFrame = _OleObjectElementCreator.graphicFrame( - shapes_, shape_id, "sheet.xlsx", PROG_ID.XLSX, x, y, cx, cy, "icon.png" + shapes_, + shape_id, + "sheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "icon.png", + Inches(0.5), + Inches(0.75), ) _init_.assert_called_once_with( - ANY, shapes_, shape_id, "sheet.xlsx", PROG_ID.XLSX, x, y, cx, cy, "icon.png" + ANY, + shapes_, + shape_id, + "sheet.xlsx", + PROG_ID.XLSX, + x, + y, + cx, + cy, + "icon.png", + Inches(0.5), + Inches(0.75), ) _graphicFrame_prop_.assert_called_once_with() assert graphicFrame is graphicFrame_ @@ -2119,7 +2160,7 @@ def it_creates_the_graphicFrame_element(self, request): property_mock(request, _OleObjectElementCreator, "_cx", return_value=cx) property_mock(request, _OleObjectElementCreator, "_cy", return_value=cy) element_creator = _OleObjectElementCreator( - None, shape_id, None, None, x, y, cx, cy, None + None, shape_id, None, None, x, y, cx, cy, None, Inches(0.5), Inches(0.75) ) assert element_creator._graphicFrame.xml == ( @@ -2183,9 +2224,9 @@ def it_creates_the_graphicFrame_element(self, request): (None, "Foo.Bar.6", Emu(965200)), ), ) - def it_determines_the_icon_width_to_help(self, cx_arg, prog_id, expected_value): + def it_determines_the_shape_width_to_help(self, cx_arg, prog_id, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id, None, None, cx_arg, None, None + None, None, None, prog_id, None, None, cx_arg, None, None, None, None ) assert element_creator._cx == expected_value @@ -2199,9 +2240,9 @@ def it_determines_the_icon_width_to_help(self, cx_arg, prog_id, expected_value): (None, "Foo.Bar.6", Emu(609600)), ), ) - def it_determines_the_icon_height_to_help(self, cy_arg, prog_id, expected_value): + def it_determines_the_shape_height_to_help(self, cy_arg, prog_id, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id, None, None, None, cy_arg, None + None, None, None, prog_id, None, None, None, cy_arg, None, None, None ) assert element_creator._cy == expected_value @@ -2219,7 +2260,7 @@ def it_resolves_the_icon_image_file_to_help( self, icon_file_arg, prog_id, expected_value ): element_creator = _OleObjectElementCreator( - None, None, None, prog_id, None, None, None, None, icon_file_arg + None, None, None, prog_id, None, None, None, None, icon_file_arg, None, None ) assert element_creator._icon_image_file.endswith(expected_value) @@ -2235,7 +2276,7 @@ def it_adds_and_relates_the_icon_image_part_to_help( slide_part_.get_or_add_image_part.return_value = None, "rId16" _slide_part_prop_.return_value = slide_part_ element_creator = _OleObjectElementCreator( - None, None, None, None, None, None, None, None, None + None, None, None, None, None, None, None, None, None, None, None ) rId = element_creator._icon_rId @@ -2250,7 +2291,17 @@ def it_adds_and_relates_the_ole_object_part_to_help( slide_part_.add_embedded_ole_object_part.return_value = "rId14" _slide_part_prop_.return_value = slide_part_ element_creator = _OleObjectElementCreator( - None, None, ole_object_file, PROG_ID.DOCX, None, None, None, None, None + None, + None, + ole_object_file, + PROG_ID.DOCX, + None, + None, + None, + None, + None, + None, + None, ) rId = element_creator._ole_object_rId @@ -2271,21 +2322,21 @@ def it_adds_and_relates_the_ole_object_part_to_help( ) def it_resolves_the_progId_str_to_help(self, prog_id_arg, expected_value): element_creator = _OleObjectElementCreator( - None, None, None, prog_id_arg, None, None, None, None, None + None, None, None, prog_id_arg, None, None, None, None, None, None, None ) assert element_creator._progId == expected_value def it_computes_the_shape_name_to_help(self): shape_id = 42 element_creator = _OleObjectElementCreator( - None, shape_id, None, None, None, None, None, None, None + None, shape_id, None, None, None, None, None, None, None, None, None ) assert element_creator._shape_name == "Object 41" def it_provides_access_to_the_slide_part_to_help(self, shapes_, slide_part_): shapes_.part = slide_part_ element_creator = _OleObjectElementCreator( - shapes_, None, None, None, None, None, None, None, None + shapes_, None, None, None, None, None, None, None, None, None, None ) assert element_creator._slide_part is slide_part_ From 46151d5e9317592fdbab2cc665ffc559fa09fa6c Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 21:52:20 -0700 Subject: [PATCH 43/69] ole: add _OleObjectElementCreator._icon_width --- pptx/oxml/shapes/graphfrm.py | 2 +- pptx/shapes/shapetree.py | 11 +++++++++++ tests/shapes/test_shapetree.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pptx/oxml/shapes/graphfrm.py b/pptx/oxml/shapes/graphfrm.py index 4c4215207..4c78c2acd 100644 --- a/pptx/oxml/shapes/graphfrm.py +++ b/pptx/oxml/shapes/graphfrm.py @@ -193,7 +193,7 @@ def new_graphicFrame(cls, id_, name, x, y, cx, cy): @classmethod def new_ole_object_graphicFrame( - cls, id_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy + cls, id_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy, imgW ): """Return newly-created `` for embedded OLE-object. diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index 709485b55..f711b883e 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -1087,6 +1087,7 @@ def _graphicFrame(self): self._y, self._cx, self._cy, + self._icon_width, ) @lazyproperty @@ -1146,6 +1147,16 @@ def _icon_rId(self): _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file) return rId + @lazyproperty + def _icon_width(self): + """Width of enclosed EMF icon within the OLE graphic-frame. + + This must be specified when a custom icon is used, to avoid stretching of the + image and possible undesired resizing by PowerPoint when the OLE shape is + double-clicked to open it. + """ + return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200) + @lazyproperty def _ole_object_rId(self): """str rId like "rId6" of relationship to embedded ole_object part. diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index bcab9cc66..95c6dd226 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -2284,6 +2284,16 @@ def it_adds_and_relates_the_icon_image_part_to_help( slide_part_.get_or_add_image_part.assert_called_once_with("obj-icon.emf") assert rId == "rId16" + @pytest.mark.parametrize( + "icon_width_arg, expected_value", + ((Emu(666666), Emu(666666)), (None, Emu(965200))), + ) + def it_determines_the_icon_width_to_help(self, icon_width_arg, expected_value): + element_creator = _OleObjectElementCreator( + None, None, None, None, None, None, None, None, None, icon_width_arg, None + ) + assert element_creator._icon_width == expected_value + def it_adds_and_relates_the_ole_object_part_to_help( self, request, _slide_part_prop_, slide_part_ ): From a284deee0de46654dbe8374621a26bb1fd74ab8a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 22:01:34 -0700 Subject: [PATCH 44/69] ole: add _OleObjectElementCreator._icon_height --- pptx/oxml/shapes/graphfrm.py | 2 +- pptx/shapes/shapetree.py | 16 ++++++++++++++++ tests/shapes/test_shapetree.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pptx/oxml/shapes/graphfrm.py b/pptx/oxml/shapes/graphfrm.py index 4c78c2acd..a7175bff1 100644 --- a/pptx/oxml/shapes/graphfrm.py +++ b/pptx/oxml/shapes/graphfrm.py @@ -193,7 +193,7 @@ def new_graphicFrame(cls, id_, name, x, y, cx, cy): @classmethod def new_ole_object_graphicFrame( - cls, id_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy, imgW + cls, id_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy, imgW, imgH ): """Return newly-created `` for embedded OLE-object. diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index f711b883e..65369d51e 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -1088,6 +1088,7 @@ def _graphicFrame(self): self._cx, self._cy, self._icon_width, + self._icon_height, ) @lazyproperty @@ -1120,6 +1121,21 @@ def _cy(self): else Emu(609600) ) + @lazyproperty + def _icon_height(self): + """Vertical size of enclosed EMF icon within the OLE graphic-frame. + + This must be specified when a custom icon is used, to avoid stretching of the + image and possible undesired resizing by PowerPoint when the OLE shape is + double-clicked to open it. + + The correct size can be determined by creating an example PPTX using PowerPoint + and then inspecting the XML of the OLE graphics-frame (p:oleObj.imgH). + """ + return ( + self._icon_height_arg if self._icon_height_arg is not None else Emu(609600) + ) + @lazyproperty def _icon_image_file(self): """Reference to image file containing icon to show in lieu of this object. diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 95c6dd226..0a215f049 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -2246,6 +2246,16 @@ def it_determines_the_shape_height_to_help(self, cy_arg, prog_id, expected_value ) assert element_creator._cy == expected_value + @pytest.mark.parametrize( + "icon_height_arg, expected_value", + ((Emu(666666), Emu(666666)), (None, Emu(609600)),), + ) + def it_determines_the_icon_height_to_help(self, icon_height_arg, expected_value): + element_creator = _OleObjectElementCreator( + None, None, None, None, None, None, None, None, None, None, icon_height_arg + ) + assert element_creator._icon_height == expected_value + @pytest.mark.parametrize( "icon_file_arg, prog_id, expected_value", ( From 9d26e2b2f7fce319f976fa6e4ea91d5ecfe7a0e7 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 22:08:21 -0700 Subject: [PATCH 45/69] ole: add p:oleObj.imgW and imgH --- pptx/oxml/shapes/graphfrm.py | 10 ++++++---- tests/shapes/test_shapetree.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pptx/oxml/shapes/graphfrm.py b/pptx/oxml/shapes/graphfrm.py index a7175bff1..6f65da7f4 100644 --- a/pptx/oxml/shapes/graphfrm.py +++ b/pptx/oxml/shapes/graphfrm.py @@ -208,7 +208,7 @@ def new_ole_object_graphicFrame( """ return parse_xml( cls._graphicFrame_xml_for_ole_object( - id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId + id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId, imgW, imgH ) ) @@ -247,7 +247,7 @@ def _graphicFrame_tmpl(cls): @classmethod def _graphicFrame_xml_for_ole_object( - cls, id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId + 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 ( @@ -268,8 +268,8 @@ def _graphicFrame_xml_for_ole_object( ' uri="http://schemas.openxmlformats.org/presentationml/2006/ole">\n' ' \n' " \n" " \n" @@ -309,6 +309,8 @@ def _graphicFrame_xml_for_ole_object( ole_object_rId=ole_object_rId, progId=progId, icon_rId=icon_rId, + imgW=imgW, + imgH=imgH, ) diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 0a215f049..63c1ee290 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -2183,7 +2183,7 @@ def it_creates_the_graphicFrame_element(self, request): " \n" " \n' - ' \n' " \n" " \n" From 9760472c61133c0d8d5bc7c7fb37f2dac34f71da Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 19 Aug 2023 20:49:06 -0700 Subject: [PATCH 46/69] fix: collections.abc Windows 3.10 breakage --- pptx/compat/__init__.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/pptx/compat/__init__.py b/pptx/compat/__init__.py index 61cd06c3d..198dc6a0e 100644 --- a/pptx/compat/__init__.py +++ b/pptx/compat/__init__.py @@ -4,32 +4,38 @@ import sys -import collections - try: - Container = collections.abc.Container - Mapping = collections.abc.Mapping - Sequence = collections.abc.Sequence -except AttributeError: - Container = collections.Container - Mapping = collections.Mapping - Sequence = collections.Sequence + 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, - Unicode, ) else: from .python2 import ( # noqa BytesIO, + Unicode, is_integer, is_string, is_unicode, to_unicode, - Unicode, ) + +__all__ = [ + "BytesIO", + "Container", + "Mapping", + "Sequence", + "Unicode", + "is_integer", + "is_string", + "is_unicode", + "to_unicode", +] From 81aae15c623e0b39294047e176522b577720ae42 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 20 Aug 2023 14:50:55 -0700 Subject: [PATCH 47/69] fix: #748 setup's `license` should be short string https://github.com/scanny/python-pptx/issues/748 --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2e16cc8e1..6fefd5260 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ def ascii_bytes_from(path, *paths): init_py = ascii_bytes_from(thisdir, "pptx", "__init__.py") readme = ascii_bytes_from(thisdir, "README.rst") history = ascii_bytes_from(thisdir, "HISTORY.rst") -license = ascii_bytes_from(thisdir, "LICENSE") # Read the version from pptx.__version__ without importing the package # (and thus attempting to import packages it depends on that may not be @@ -38,7 +37,7 @@ def ascii_bytes_from(path, *paths): AUTHOR = "Steve Canny" AUTHOR_EMAIL = "python-pptx@googlegroups.com" URL = "https://github.com/scanny/python-pptx" -LICENSE = license +LICENSE = "MIT" PACKAGES = find_packages(exclude=["tests", "tests.*"]) PACKAGE_DATA = {"pptx": ["templates/*"]} From df5e968a91624ef5f75cad921cf2b7e2b447ec4d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 20 Aug 2023 14:57:57 -0700 Subject: [PATCH 48/69] fix: #746 update Python 3.x support in docs --- docs/user/install.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/install.rst b/docs/user/install.rst index f402bcc68..b376d58e8 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -13,13 +13,13 @@ the Python Imaging Library (``PIL``). The charting features depend on satisfying these dependencies for you, but if you use the ``setup.py`` installation method you will need to install the dependencies yourself. -Currently |pp| requires Python 2.7, 3.3, 3.4, or 3.6. The tests are run against 2.7 and -3.6 on Travis CI. +Currently |pp| requires Python 2.7 or 3.3 or later. The tests are run against 2.7 and +3.8 on Travis CI. Dependencies ------------ -* Python 2.6, 2.7, 3.3, 3.4, or 3.6 +* Python 2.6, 2.7, 3.3 or later * lxml * Pillow * XlsxWriter (to use charting features) From a5f05dfaaf8577e3b0cb649acdb1d7a415d8e3b2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 20 Aug 2023 15:29:16 -0700 Subject: [PATCH 49/69] fix: #758 quote in autoshape name must be escaped At least one autoshape name ('"No" Symbol') contains the double-quote character which raises an XML exception when it prematurely terminates the `name="..."` attribute of the autoshape. Escape autoshape basename to avoid this problem, specifically targetting the double-quote character. --- pptx/enum/shapes.py | 8 ++------ pptx/shapes/autoshape.py | 12 ++++++++---- pptx/spec.py | 2 +- tests/shapes/test_autoshape.py | 8 ++++++++ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/pptx/enum/shapes.py b/pptx/enum/shapes.py index 0fd1e0100..e4758cf02 100644 --- a/pptx/enum/shapes.py +++ b/pptx/enum/shapes.py @@ -501,7 +501,7 @@ class MSO_AUTO_SHAPE_TYPE(XmlEnumeration): "notchedRightArrow", "Notched block arrow that points right", ), - XmlMappedEnumMember("NO_SYMBOL", 19, "noSmoking", '"No" symbol'), + XmlMappedEnumMember("NO_SYMBOL", 19, "noSmoking", '"No" Symbol'), XmlMappedEnumMember("OCTAGON", 6, "octagon", "Octagon"), XmlMappedEnumMember("OVAL", 9, "ellipse", "Oval"), XmlMappedEnumMember( @@ -858,11 +858,7 @@ def width(self): return self._width def __contains__(self, item): - return item in ( - self.DOCX, - self.PPTX, - self.XLSX, - ) + return item in (self.DOCX, self.PPTX, self.XLSX,) def __repr__(self): return "%s.PROG_ID" % __name__ diff --git a/pptx/shapes/autoshape.py b/pptx/shapes/autoshape.py index 9c176c2bd..ead5fecb5 100644 --- a/pptx/shapes/autoshape.py +++ b/pptx/shapes/autoshape.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals from numbers import Number +import xml.sax.saxutils as saxutils from pptx.dml.fill import FillFormat from pptx.dml.line import LineFormat @@ -234,11 +235,14 @@ def autoshape_type_id(self): @property def basename(self): + """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'). """ - Base of shape name (less the distinguishing integer) for this auto - shape type - """ - return self._basename + return saxutils.escape(self._basename, {'"': """}) @classmethod def default_adjustment_values(cls, prst): diff --git a/pptx/spec.py b/pptx/spec.py index 6c5607b00..835fde6d0 100644 --- a/pptx/spec.py +++ b/pptx/spec.py @@ -468,7 +468,7 @@ "basename": "Notched Right Arrow", "avLst": (("adj1", 50000), ("adj2", 50000)), }, - MSO_SHAPE.NO_SYMBOL: {"basename": '"No" symbol', "avLst": (("adj", 18750),)}, + MSO_SHAPE.NO_SYMBOL: {"basename": '"No" Symbol', "avLst": (("adj", 18750),)}, MSO_SHAPE.OCTAGON: {"basename": "Octagon", "avLst": (("adj", 29289),)}, MSO_SHAPE.OVAL: {"basename": "Oval", "avLst": ()}, MSO_SHAPE.OVAL_CALLOUT: { diff --git a/tests/shapes/test_autoshape.py b/tests/shapes/test_autoshape.py index ce901d11c..1c4787c85 100644 --- a/tests/shapes/test_autoshape.py +++ b/tests/shapes/test_autoshape.py @@ -260,12 +260,20 @@ def indexed_assignment_fixture_(self, request): class DescribeAutoShapeType(object): + """Unit-test suite for `pptx.shapes.autoshape.AutoShapeType`""" + def it_knows_the_details_of_the_auto_shape_type_it_represents(self): autoshape_type = AutoShapeType(MSO_SHAPE.ROUNDED_RECTANGLE) assert autoshape_type.autoshape_type_id == MSO_SHAPE.ROUNDED_RECTANGLE assert autoshape_type.prst == "roundRect" assert autoshape_type.basename == "Rounded Rectangle" + def it_xml_escapes_the_basename_when_the_name_contains_special_characters(self): + autoshape_type = AutoShapeType(MSO_SHAPE.NO_SYMBOL) + assert autoshape_type.autoshape_type_id == MSO_SHAPE.NO_SYMBOL + assert autoshape_type.prst == "noSmoking" + assert autoshape_type.basename == ""No" Symbol" + def it_knows_the_default_adj_vals_for_its_autoshape_type( self, default_adj_vals_fixture_ ): From c3ed33b27fa436d520b681b4f28e839cca73c45d Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 20 Aug 2023 16:21:27 -0700 Subject: [PATCH 50/69] fix: #754 _Relationships.items() raises --- pptx/opc/package.py | 35 +++++++++---- tests/opc/test_package.py | 56 ++++++++++----------- tests/test_files/snippets/relationships.txt | 2 +- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/pptx/opc/package.py b/pptx/opc/package.py index 03c39588a..0427cf347 100644 --- a/pptx/opc/package.py +++ b/pptx/opc/package.py @@ -96,7 +96,7 @@ def iter_rels(self): visited = set() def walk_rels(rels): - for rel in rels: + for rel in rels.values(): yield rel # --- external items can have no relationships --- if rel.is_external: @@ -482,14 +482,14 @@ def from_xml(cls, content_types_xml): class _Relationships(Mapping): - """Collection of |_Relationship| instances, largely having dict semantics. + """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. `rels` is a dict of |Relationship| objects keyed by - their rId. + by their relationship type. |Relationship| objects are keyed by their rId. - Note that iterating this collection generates |Relationship| references (values), - not rIds (keys) as it would 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): @@ -507,9 +507,8 @@ def __getitem__(self, rId): raise KeyError("no relationship with key '%s'" % rId) def __iter__(self): - """Implement iteration of relationships.""" - rels = self._rels - return (rels[rId] for rId in sorted(rels.keys())) + """Implement iteration of rIds (iterating a mapping produces its keys).""" + return iter(self._rels) def __len__(self): """Return count of relationships in collection.""" @@ -593,8 +592,22 @@ def xml(self): a ` elements deterministically (in numerical order) to + # -- simplify testing and manual inspection. + def iter_rels_in_numerical_order(): + sorted_num_rId_pairs = sorted( + ( + int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0, + rId, + ) + for rId in self.keys() + ) + return (self[rId] for _, rId in sorted_num_rId_pairs) + + for rel in iter_rels_in_numerical_order(): rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external) + return rels_elm.xml def _add_relationship(self, reltype, target, is_external=False): @@ -648,7 +661,7 @@ def _rels(self): def _rels_by_reltype(self): """defaultdict {reltype: [rels]} for all relationships in collection.""" D = collections.defaultdict(list) - for rel in self: + for rel in self.values(): D[rel.reltype].append(rel) return D diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 0262fc12d..d8bf20703 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -190,7 +190,7 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): part_0_, part_1_ = [ instance_mock(request, Part, name="part_%d" % i) for i in range(2) ] - rels = tuple( + all_rels = tuple( instance_mock( request, _Relationship, @@ -208,18 +208,15 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): ) ) ) - _rels_prop_.return_value = rels[:3] - part_0_.rels = rels[3:4] - part_1_.rels = rels[4:] + _rels_prop_.return_value = {r.rId: r for r in all_rels[:3]} + part_0_.rels = {r.rId: r for r in all_rels[3:4]} + part_1_.rels = {r.rId: r for r in all_rels[4:]} package = OpcPackage(None) - assert tuple(package.iter_rels()) == ( - rels[0], - rels[3], - rels[4], - rels[1], - rels[2], - ) + rels = set(package.iter_rels()) + + # -- sequence is not guaranteed, but count (len) and uniqueness are -- + assert rels == set(all_rels) def it_provides_access_to_the_main_document_part(self, request): presentation_part_ = instance_mock(request, PresentationPart) @@ -234,8 +231,7 @@ def it_provides_access_to_the_main_document_part(self, request): assert presentation_part is presentation_part_ @pytest.mark.parametrize( - "ns, expected_n", - (((), 1), ((1,), 2), ((1, 2), 3), ((2, 4), 3), ((1, 4), 3)), + "ns, expected_n", (((), 1), ((1,), 2), ((1, 2), 3), ((2, 4), 3), ((1, 4), 3)) ) def it_can_find_the_next_available_partname(self, request, ns, expected_n): tmpl = "/x%d.xml" @@ -659,13 +655,15 @@ 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_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) - for r in relationships: - rels_.remove(r) + for rId in relationships: + rels_.remove(relationships[rId]) assert len(rels_) == 0 @@ -795,10 +793,10 @@ def it_can_pop_a_relationship_to_remove_it_from_the_collection( def it_can_serialize_itself_to_XML(self, request, _rels_prop_): _rels_prop_.return_value = { - "rId1": instance_mock( + "rId11": instance_mock( request, _Relationship, - rId="rId1", + rId="rId11", reltype=RT.SLIDE, target_ref="../slides/slide1.xml", is_external=False, @@ -811,6 +809,14 @@ def it_can_serialize_itself_to_XML(self, request, _rels_prop_): target_ref="http://url", is_external=True, ), + "foo7W": instance_mock( + request, + _Relationship, + rId="foo7W", + reltype=RT.IMAGE, + target_ref="../media/image1.png", + is_external=False, + ), } relationships = _Relationships(None) @@ -867,12 +873,7 @@ def and_it_can_add_an_external_relationship_to_help( ), ) def it_can_get_a_matching_relationship_to_help( - self, - request, - _rels_by_reltype_prop_, - target_ref, - is_external, - expected_value, + self, request, _rels_by_reltype_prop_, target_ref, is_external, expected_value ): part_1, part_2 = (instance_mock(request, Part) for _ in range(2)) _rels_by_reltype_prop_.return_value = { @@ -996,12 +997,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_, + relationship, "/ppt", "rId42", RT.SLIDE, RTM.INTERNAL, part_ ) assert isinstance(relationship, _Relationship) diff --git a/tests/test_files/snippets/relationships.txt b/tests/test_files/snippets/relationships.txt index efdbee450..db1df33f8 100644 --- a/tests/test_files/snippets/relationships.txt +++ b/tests/test_files/snippets/relationships.txt @@ -1,2 +1,2 @@ - + From 9e3a9ccd31233c3c3f4ef8cdf7d00476e5581deb Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 25 Aug 2023 10:18:07 -0700 Subject: [PATCH 51/69] docs: improve docs --- README.rst | 23 ++++++++++++----------- docs/index.rst | 28 +++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 3573a8d36..24d657b37 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,17 @@ -.. image:: https://travis-ci.org/scanny/python-pptx.svg?branch=master - :target: https://travis-ci.org/scanny/python-pptx - -*python-pptx* is a Python library for creating and updating PowerPoint (.pptx) +*python-pptx* is a Python library for creating, reading, and updating PowerPoint (.pptx) files. -A typical use would be generating a customized PowerPoint presentation from -database content, downloadable by clicking a link in a web application. -Several developers have used it to automate production of presentation-ready -engineering status reports based on information held in their work management -system. It could also be used for making bulk updates to a library of -presentations or simply to automate the production of a slide or two that -would be tedious to get right by hand. +A typical use would be generating a PowerPoint presentation from dynamic content such as +a database query, analytics output, or a JSON payload, perhaps in response to an HTTP +request and downloading the generated PPTX file in response. It runs on any Python +capable platform, including macOS and Linux, and does not require the PowerPoint +application to be installed or licensed. + +It can also be used to analyze PowerPoint files from a corpus, perhaps to extract search +indexing text and images. + +In can also be used to simply automate the production of a slide or two that would be +tedious to get right by hand, which is how this all got started. More information is available in the `python-pptx documentation`_. diff --git a/docs/index.rst b/docs/index.rst index cadc8a7ad..5bb4e0e26 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,10 +7,21 @@ Release v\ |version| (:ref:`Installation `) .. include:: ../README.rst +Philosophy +---------- + +|pp| aims to broadly support the PowerPoint format (PPTX, PowerPoint 2007 and later), +but its primary commitment is to be _industrial-grade_, that is, suitable for use in a +commercial setting. Maintaining this robustness requires a high engineering standard +which includes a comprehensive two-level (e2e + unit) testing regimen. This discipline +comes at a cost in development effort/time, but we consider reliability to be an +essential requirement. + + Feature Support --------------- -|pp| has the following capabilities, with many more on the roadmap: +|pp| has the following capabilities: * Round-trip any Open XML presentation (.pptx file) including all its elements * Add slides @@ -21,11 +32,18 @@ Feature Support * Add auto shapes (e.g. polygons, flowchart shapes, etc.) to a slide * Add and manipulate column, bar, line, and pie charts * Access and change core document properties such as title and subject +* And many others ... + +Even with all |pp| does, the PowerPoint document format is very rich and there are still +features |pp| does not support. + + +New features/releases +--------------------- -Additional capabilities are actively being developed and added on a release -cadence of roughly once per month. If you find a feature you need that |pp| -doesn't yet have, reach out via the mailing list or issue tracker and we'll see -if we can jump the queue for you to pop it in there :) +New features are generally added via sponsorship. If there's a new feature you need for +your use case, feel free to reach out at the email address on the github.com/scanny +profile page. Many of the most used features such as charts were added this way. User Guide From fa2349503a741a9be0cd77c58c558ffe5d0cf397 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 28 Aug 2023 20:18:11 -0700 Subject: [PATCH 52/69] dist: modernize distribution for twine upload The `make upload` operation doesn't work anymore, so we need to use twine instead. This calls for a few modernizations to the distribution configuration and procedure. --- .gitignore | 1 + Makefile | 11 ++++------- setup.py | 1 + tests/test_api.py | 5 ++--- tests/test_package.py | 8 +++++++- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 49f59feb9..8f0219e06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/build/ .cache .coverage /.tox/ diff --git a/Makefile b/Makefile index 1ea294b40..7f90204b4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MAKE = make PYTHON = python SETUP = $(PYTHON) ./setup.py -.PHONY: accept clean cleandocs coverage docs readme sdist upload +.PHONY: accept build clean cleandocs coverage docs opendocs help: @echo "Please use \`make ' where is one or more of" @@ -20,6 +20,9 @@ help: accept: $(BEHAVE) --stop +build: + $(SETUP) bdist_wheel sdist + clean: find . -type f -name \*.pyc -exec rm {} \; find . -type f -name .DS_Store -exec rm {} \; @@ -36,9 +39,3 @@ docs: opendocs: open docs/.build/html/index.html - -sdist: - $(SETUP) sdist - -upload: - $(SETUP) sdist upload diff --git a/setup.py b/setup.py index 6fefd5260..b1f071bf6 100755 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ def ascii_bytes_from(path, *paths): "description": DESCRIPTION, "keywords": KEYWORDS, "long_description": LONG_DESCRIPTION, + "long_description_content_type": "text/x-rst", "author": AUTHOR, "author_email": AUTHOR_EMAIL, "url": URL, diff --git a/tests/test_api.py b/tests/test_api.py index d86d728e6..b44573031 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -10,6 +10,7 @@ import pytest +import pptx from pptx.api import Presentation from pptx.opc.constants import CONTENT_TYPE as CT from pptx.parts.presentation import PresentationPart @@ -29,9 +30,7 @@ def it_opens_default_template_on_no_path_provided(self, call_fixture): @pytest.fixture def call_fixture(self, Package_, prs_, prs_part_): path = os.path.abspath( - os.path.join( - os.path.split(__file__)[0], "../pptx/templates", "default.pptx" - ) + os.path.join(os.path.split(pptx.__file__)[0], "templates", "default.pptx") ) Package_.open.return_value.main_document_part = prs_part_ prs_part_.content_type = CT.PML_PRESENTATION_MAIN diff --git a/tests/test_package.py b/tests/test_package.py index c8d739f63..5e32e74ce 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -2,8 +2,11 @@ """Unit-test suite for `pptx.package` module.""" +import os + import pytest +import pptx from pptx.media import Video from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import Part, _Relationship @@ -21,7 +24,10 @@ class DescribePackage(object): """Unit-test suite for `pptx.package.Package` objects.""" def it_provides_access_to_its_core_properties_part(self): - pkg = Package.open("pptx/templates/default.pptx") + default_pptx = os.path.abspath( + os.path.join(os.path.split(pptx.__file__)[0], "templates", "default.pptx") + ) + pkg = Package.open(default_pptx) assert isinstance(pkg.core_properties, CorePropertiesPart) def it_can_get_or_add_an_image_part(self, image_part_fixture): From 0e684ff57c5dc6c5dae9f4e2904951c2b90dc82a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 28 Aug 2023 15:49:14 -0700 Subject: [PATCH 53/69] release: prepare v0.6.22 release --- HISTORY.rst | 12 ++++++++++++ pptx/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 322985773..c6974ef4a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,18 @@ Release History --------------- +0.6.22 (2023-08-28) ++++++++++++++++++++ + +- Add #909 Add imgW, imgH params to `shapes.add_ole_object()` +- fix: #754 _Relationships.items() raises +- fix: #758 quote in autoshape name must be escaped +- fix: #746 update Python 3.x support in docs +- fix: #748 setup's `license` should be short string +- fix: #762 AttributeError: module 'collections' has no attribute 'abc' + (Windows Python 3.10+) + + 0.6.21 (2021-09-20) +++++++++++++++++++ diff --git a/pptx/__init__.py b/pptx/__init__.py index 679601d42..0b45bb7b5 100644 --- a/pptx/__init__.py +++ b/pptx/__init__.py @@ -2,7 +2,7 @@ """Initialization module for python-pptx package.""" -__version__ = "0.6.21" +__version__ = "0.6.22" import pptx.exc as exceptions From 791ba4a2d79e9bb35be7f0c3c194d552bb73b31e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 13:21:21 -0700 Subject: [PATCH 54/69] fix: Support Pillow 10+ Remove <=9.5.0 constraint which was where `font.getsize()` was removed. That version has one or more security vulnerabilities not fixed until 10+. --- Makefile | 1 + features/steps/text_frame.py | 5 +++-- features/txt-fit-text.feature | 2 +- pptx/text/layout.py | 6 +++++- requirements.txt | 2 +- setup.py | 2 +- tox.ini | 4 ++-- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 7f90204b4..f6c39374f 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ accept: $(BEHAVE) --stop build: + rm -rf dist $(SETUP) bdist_wheel sdist clean: diff --git a/features/steps/text_frame.py b/features/steps/text_frame.py index fe2096e3a..48401620a 100644 --- a/features/steps/text_frame.py +++ b/features/steps/text_frame.py @@ -126,9 +126,10 @@ def then_text_frame_word_wrap_is_value(context, value): assert text_frame.word_wrap is expected_value -@then("the size of the text is 10pt") +@then("the size of the text is 10pt or 11pt") def then_the_size_of_the_text_is_10pt(context): + """Size depends on Pillow version, probably algorithm isn't quite right either.""" text_frame = context.text_frame for paragraph in text_frame.paragraphs: for run in paragraph.runs: - assert run.font.size == Pt(10.0), "got %s" % run.font.size.pt + assert run.font.size in (Pt(10.0), Pt(11.0)), "got %s" % run.font.size.pt diff --git a/features/txt-fit-text.feature b/features/txt-fit-text.feature index 5cee76c7c..dd5313b56 100644 --- a/features/txt-fit-text.feature +++ b/features/txt-fit-text.feature @@ -8,4 +8,4 @@ Feature: Resize text to fit shape When I call TextFrame.fit_text() Then text_frame.auto_size is MSO_AUTO_SIZE.NONE And text_frame.word_wrap is True - And the size of the text is 10pt + And the size of the text is 10pt or 11pt diff --git a/pptx/text/layout.py b/pptx/text/layout.py index 69aa6f678..c230a0ec6 100644 --- a/pptx/text/layout.py +++ b/pptx/text/layout.py @@ -310,7 +310,11 @@ def _rendered_size(text, point_size, font_file): px_per_inch = 72.0 font = _Fonts.font(font_file, point_size) - px_width, px_height = font.getsize(text) + try: + px_width, px_height = font.getsize(text) + except AttributeError: + left, top, right, bottom = font.getbbox(text) + px_width, px_height = right - left, bottom - top emu_width = int(px_width / px_per_inch * emu_per_inch) emu_height = int(px_height / px_per_inch * emu_per_inch) diff --git a/requirements.txt b/requirements.txt index 9658a5095..edbb0e25c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ behave>=1.2.5 flake8>=2.0 lxml>=3.1.0 mock>=1.0.1 -Pillow>=3.3.2,<=9.5.0 +Pillow>=3.3.2 pyparsing>=2.0.1 pytest>=2.5 XlsxWriter>=0.5.7 diff --git a/setup.py b/setup.py index b1f071bf6..582610e5b 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def ascii_bytes_from(path, *paths): PACKAGES = find_packages(exclude=["tests", "tests.*"]) PACKAGE_DATA = {"pptx": ["templates/*"]} -INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2,<=9.5.0", "XlsxWriter>=0.5.7"] +INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2", "XlsxWriter>=0.5.7"] TEST_SUITE = "tests" TESTS_REQUIRE = ["behave", "mock", "pyparsing>=2.0.1", "pytest"] diff --git a/tox.ini b/tox.ini index 17c5507e5..4df2d13b1 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ skip_missing_interpreters = false deps = behave==1.2.5 lxml>=3.1.0 - Pillow>=3.3.2,<=9.5.0 + Pillow>=3.3.2 pyparsing>=2.0.1 pytest XlsxWriter>=0.5.7 @@ -52,7 +52,7 @@ deps = behave==1.2.5 lxml>=3.1.0 mock - Pillow>=3.3.2,<4.0 + Pillow>=3.3.2 pyparsing>=2.0.1 pytest XlsxWriter>=0.5.7 From d043334b984736a7a2ade3fb6f9adcdd97b3e8f5 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Thu, 2 Nov 2023 14:38:39 -0700 Subject: [PATCH 55/69] release: prepare v0.6.23 release --- HISTORY.rst | 6 ++++++ Makefile | 7 +++++++ pptx/__init__.py | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index c6974ef4a..1f5d4e58a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +0.6.23 (2023-11-02) ++++++++++++++++++++ + +- fix: #912 Pillow<=9.5 constraint entails security vulnerability + + 0.6.22 (2023-08-28) +++++++++++++++++++ diff --git a/Makefile b/Makefile index f6c39374f..94891a89b 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ BEHAVE = behave MAKE = make PYTHON = python SETUP = $(PYTHON) ./setup.py +TWINE = $(PYTHON) -m twine .PHONY: accept build clean cleandocs coverage docs opendocs @@ -40,3 +41,9 @@ docs: opendocs: open docs/.build/html/index.html + +test-upload: build + $(TWINE) upload --repository testpypi dist/* + +upload: clean build + $(TWINE) upload dist/* diff --git a/pptx/__init__.py b/pptx/__init__.py index 0b45bb7b5..7952f87bd 100644 --- a/pptx/__init__.py +++ b/pptx/__init__.py @@ -2,7 +2,7 @@ """Initialization module for python-pptx package.""" -__version__ = "0.6.22" +__version__ = "0.6.23" import pptx.exc as exceptions From 86df53458d2faaca781a98b37954806eadd57f3a Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 22:14:21 -0700 Subject: [PATCH 56/69] build: move pptx package under src/ This improves packaging reliability because it prevents tests from running against the current directory instead of the installed version of the package. --- .gitignore | 2 +- MANIFEST.in | 2 +- setup.py | 5 +++-- {pptx => src/pptx}/__init__.py | 0 {pptx => src/pptx}/action.py | 0 {pptx => src/pptx}/api.py | 0 {pptx => src/pptx}/chart/__init__.py | 0 {pptx => src/pptx}/chart/axis.py | 0 {pptx => src/pptx}/chart/category.py | 0 {pptx => src/pptx}/chart/chart.py | 0 {pptx => src/pptx}/chart/data.py | 0 {pptx => src/pptx}/chart/datalabel.py | 0 {pptx => src/pptx}/chart/legend.py | 0 {pptx => src/pptx}/chart/marker.py | 0 {pptx => src/pptx}/chart/plot.py | 0 {pptx => src/pptx}/chart/point.py | 0 {pptx => src/pptx}/chart/series.py | 0 {pptx => src/pptx}/chart/xlsx.py | 0 {pptx => src/pptx}/chart/xmlwriter.py | 0 {pptx => src/pptx}/compat/__init__.py | 0 {pptx => src/pptx}/compat/python2.py | 0 {pptx => src/pptx}/compat/python3.py | 0 {pptx => src/pptx}/dml/__init__.py | 0 {pptx => src/pptx}/dml/chtfmt.py | 0 {pptx => src/pptx}/dml/color.py | 0 {pptx => src/pptx}/dml/effect.py | 0 {pptx => src/pptx}/dml/fill.py | 0 {pptx => src/pptx}/dml/line.py | 0 {pptx => src/pptx}/enum/__init__.py | 0 {pptx => src/pptx}/enum/action.py | 0 {pptx => src/pptx}/enum/base.py | 0 {pptx => src/pptx}/enum/chart.py | 0 {pptx => src/pptx}/enum/dml.py | 0 {pptx => src/pptx}/enum/lang.py | 0 {pptx => src/pptx}/enum/shapes.py | 0 {pptx => src/pptx}/enum/text.py | 0 {pptx => src/pptx}/exc.py | 0 {pptx => src/pptx}/media.py | 0 {pptx => src/pptx}/opc/__init__.py | 0 {pptx => src/pptx}/opc/constants.py | 0 {pptx => src/pptx}/opc/oxml.py | 0 {pptx => src/pptx}/opc/package.py | 0 {pptx => src/pptx}/opc/packuri.py | 0 {pptx => src/pptx}/opc/serialized.py | 0 {pptx => src/pptx}/opc/shared.py | 0 {pptx => src/pptx}/opc/spec.py | 0 {pptx => src/pptx}/oxml/__init__.py | 0 {pptx => src/pptx}/oxml/action.py | 0 {pptx => src/pptx}/oxml/chart/__init__.py | 0 {pptx => src/pptx}/oxml/chart/axis.py | 0 {pptx => src/pptx}/oxml/chart/chart.py | 0 {pptx => src/pptx}/oxml/chart/datalabel.py | 0 {pptx => src/pptx}/oxml/chart/legend.py | 0 {pptx => src/pptx}/oxml/chart/marker.py | 0 {pptx => src/pptx}/oxml/chart/plot.py | 0 {pptx => src/pptx}/oxml/chart/series.py | 0 {pptx => src/pptx}/oxml/chart/shared.py | 0 {pptx => src/pptx}/oxml/coreprops.py | 0 {pptx => src/pptx}/oxml/dml/__init__.py | 0 {pptx => src/pptx}/oxml/dml/color.py | 0 {pptx => src/pptx}/oxml/dml/fill.py | 0 {pptx => src/pptx}/oxml/dml/line.py | 0 {pptx => src/pptx}/oxml/ns.py | 0 {pptx => src/pptx}/oxml/presentation.py | 0 {pptx => src/pptx}/oxml/shapes/__init__.py | 0 {pptx => src/pptx}/oxml/shapes/autoshape.py | 0 {pptx => src/pptx}/oxml/shapes/connector.py | 0 {pptx => src/pptx}/oxml/shapes/graphfrm.py | 0 {pptx => src/pptx}/oxml/shapes/groupshape.py | 0 {pptx => src/pptx}/oxml/shapes/picture.py | 0 {pptx => src/pptx}/oxml/shapes/shared.py | 0 {pptx => src/pptx}/oxml/simpletypes.py | 0 {pptx => src/pptx}/oxml/slide.py | 0 {pptx => src/pptx}/oxml/table.py | 0 {pptx => src/pptx}/oxml/text.py | 0 {pptx => src/pptx}/oxml/theme.py | 0 {pptx => src/pptx}/oxml/xmlchemy.py | 0 {pptx => src/pptx}/package.py | 0 {pptx => src/pptx}/parts/__init__.py | 0 {pptx => src/pptx}/parts/chart.py | 0 {pptx => src/pptx}/parts/coreprops.py | 0 {pptx => src/pptx}/parts/embeddedpackage.py | 0 {pptx => src/pptx}/parts/image.py | 0 {pptx => src/pptx}/parts/media.py | 0 {pptx => src/pptx}/parts/presentation.py | 0 {pptx => src/pptx}/parts/slide.py | 0 {pptx => src/pptx}/presentation.py | 0 {pptx => src/pptx}/shapes/__init__.py | 0 {pptx => src/pptx}/shapes/autoshape.py | 0 {pptx => src/pptx}/shapes/base.py | 0 {pptx => src/pptx}/shapes/connector.py | 0 {pptx => src/pptx}/shapes/freeform.py | 0 {pptx => src/pptx}/shapes/graphfrm.py | 0 {pptx => src/pptx}/shapes/group.py | 0 {pptx => src/pptx}/shapes/picture.py | 0 {pptx => src/pptx}/shapes/placeholder.py | 0 {pptx => src/pptx}/shapes/shapetree.py | 0 {pptx => src/pptx}/shared.py | 0 {pptx => src/pptx}/slide.py | 0 {pptx => src/pptx}/spec.py | 0 {pptx => src/pptx}/table.py | 0 {pptx => src/pptx}/templates/default.pptx | Bin {pptx => src/pptx}/templates/docx-icon.emf | Bin {pptx => src/pptx}/templates/generic-icon.emf | Bin {pptx => src/pptx}/templates/notes.xml | 0 {pptx => src/pptx}/templates/notesMaster.xml | 0 {pptx => src/pptx}/templates/pptx-icon.emf | Bin {pptx => src/pptx}/templates/theme.xml | 0 {pptx => src/pptx}/templates/xlsx-icon.emf | Bin {pptx => src/pptx}/text/__init__.py | 0 {pptx => src/pptx}/text/fonts.py | 0 {pptx => src/pptx}/text/layout.py | 0 {pptx => src/pptx}/text/text.py | 0 {pptx => src/pptx}/util.py | 0 tox.ini | 4 ++-- 115 files changed, 7 insertions(+), 6 deletions(-) rename {pptx => src/pptx}/__init__.py (100%) rename {pptx => src/pptx}/action.py (100%) rename {pptx => src/pptx}/api.py (100%) rename {pptx => src/pptx}/chart/__init__.py (100%) rename {pptx => src/pptx}/chart/axis.py (100%) rename {pptx => src/pptx}/chart/category.py (100%) rename {pptx => src/pptx}/chart/chart.py (100%) rename {pptx => src/pptx}/chart/data.py (100%) rename {pptx => src/pptx}/chart/datalabel.py (100%) rename {pptx => src/pptx}/chart/legend.py (100%) rename {pptx => src/pptx}/chart/marker.py (100%) rename {pptx => src/pptx}/chart/plot.py (100%) rename {pptx => src/pptx}/chart/point.py (100%) rename {pptx => src/pptx}/chart/series.py (100%) rename {pptx => src/pptx}/chart/xlsx.py (100%) rename {pptx => src/pptx}/chart/xmlwriter.py (100%) rename {pptx => src/pptx}/compat/__init__.py (100%) rename {pptx => src/pptx}/compat/python2.py (100%) rename {pptx => src/pptx}/compat/python3.py (100%) rename {pptx => src/pptx}/dml/__init__.py (100%) rename {pptx => src/pptx}/dml/chtfmt.py (100%) rename {pptx => src/pptx}/dml/color.py (100%) rename {pptx => src/pptx}/dml/effect.py (100%) rename {pptx => src/pptx}/dml/fill.py (100%) rename {pptx => src/pptx}/dml/line.py (100%) rename {pptx => src/pptx}/enum/__init__.py (100%) rename {pptx => src/pptx}/enum/action.py (100%) rename {pptx => src/pptx}/enum/base.py (100%) rename {pptx => src/pptx}/enum/chart.py (100%) rename {pptx => src/pptx}/enum/dml.py (100%) rename {pptx => src/pptx}/enum/lang.py (100%) rename {pptx => src/pptx}/enum/shapes.py (100%) rename {pptx => src/pptx}/enum/text.py (100%) rename {pptx => src/pptx}/exc.py (100%) rename {pptx => src/pptx}/media.py (100%) rename {pptx => src/pptx}/opc/__init__.py (100%) rename {pptx => src/pptx}/opc/constants.py (100%) rename {pptx => src/pptx}/opc/oxml.py (100%) rename {pptx => src/pptx}/opc/package.py (100%) rename {pptx => src/pptx}/opc/packuri.py (100%) rename {pptx => src/pptx}/opc/serialized.py (100%) rename {pptx => src/pptx}/opc/shared.py (100%) rename {pptx => src/pptx}/opc/spec.py (100%) rename {pptx => src/pptx}/oxml/__init__.py (100%) rename {pptx => src/pptx}/oxml/action.py (100%) rename {pptx => src/pptx}/oxml/chart/__init__.py (100%) rename {pptx => src/pptx}/oxml/chart/axis.py (100%) rename {pptx => src/pptx}/oxml/chart/chart.py (100%) rename {pptx => src/pptx}/oxml/chart/datalabel.py (100%) rename {pptx => src/pptx}/oxml/chart/legend.py (100%) rename {pptx => src/pptx}/oxml/chart/marker.py (100%) rename {pptx => src/pptx}/oxml/chart/plot.py (100%) rename {pptx => src/pptx}/oxml/chart/series.py (100%) rename {pptx => src/pptx}/oxml/chart/shared.py (100%) rename {pptx => src/pptx}/oxml/coreprops.py (100%) rename {pptx => src/pptx}/oxml/dml/__init__.py (100%) rename {pptx => src/pptx}/oxml/dml/color.py (100%) rename {pptx => src/pptx}/oxml/dml/fill.py (100%) rename {pptx => src/pptx}/oxml/dml/line.py (100%) rename {pptx => src/pptx}/oxml/ns.py (100%) rename {pptx => src/pptx}/oxml/presentation.py (100%) rename {pptx => src/pptx}/oxml/shapes/__init__.py (100%) rename {pptx => src/pptx}/oxml/shapes/autoshape.py (100%) rename {pptx => src/pptx}/oxml/shapes/connector.py (100%) rename {pptx => src/pptx}/oxml/shapes/graphfrm.py (100%) rename {pptx => src/pptx}/oxml/shapes/groupshape.py (100%) rename {pptx => src/pptx}/oxml/shapes/picture.py (100%) rename {pptx => src/pptx}/oxml/shapes/shared.py (100%) rename {pptx => src/pptx}/oxml/simpletypes.py (100%) rename {pptx => src/pptx}/oxml/slide.py (100%) rename {pptx => src/pptx}/oxml/table.py (100%) rename {pptx => src/pptx}/oxml/text.py (100%) rename {pptx => src/pptx}/oxml/theme.py (100%) rename {pptx => src/pptx}/oxml/xmlchemy.py (100%) rename {pptx => src/pptx}/package.py (100%) rename {pptx => src/pptx}/parts/__init__.py (100%) rename {pptx => src/pptx}/parts/chart.py (100%) rename {pptx => src/pptx}/parts/coreprops.py (100%) rename {pptx => src/pptx}/parts/embeddedpackage.py (100%) rename {pptx => src/pptx}/parts/image.py (100%) rename {pptx => src/pptx}/parts/media.py (100%) rename {pptx => src/pptx}/parts/presentation.py (100%) rename {pptx => src/pptx}/parts/slide.py (100%) rename {pptx => src/pptx}/presentation.py (100%) rename {pptx => src/pptx}/shapes/__init__.py (100%) rename {pptx => src/pptx}/shapes/autoshape.py (100%) rename {pptx => src/pptx}/shapes/base.py (100%) rename {pptx => src/pptx}/shapes/connector.py (100%) rename {pptx => src/pptx}/shapes/freeform.py (100%) rename {pptx => src/pptx}/shapes/graphfrm.py (100%) rename {pptx => src/pptx}/shapes/group.py (100%) rename {pptx => src/pptx}/shapes/picture.py (100%) rename {pptx => src/pptx}/shapes/placeholder.py (100%) rename {pptx => src/pptx}/shapes/shapetree.py (100%) rename {pptx => src/pptx}/shared.py (100%) rename {pptx => src/pptx}/slide.py (100%) rename {pptx => src/pptx}/spec.py (100%) rename {pptx => src/pptx}/table.py (100%) rename {pptx => src/pptx}/templates/default.pptx (100%) rename {pptx => src/pptx}/templates/docx-icon.emf (100%) rename {pptx => src/pptx}/templates/generic-icon.emf (100%) rename {pptx => src/pptx}/templates/notes.xml (100%) rename {pptx => src/pptx}/templates/notesMaster.xml (100%) rename {pptx => src/pptx}/templates/pptx-icon.emf (100%) rename {pptx => src/pptx}/templates/theme.xml (100%) rename {pptx => src/pptx}/templates/xlsx-icon.emf (100%) rename {pptx => src/pptx}/text/__init__.py (100%) rename {pptx => src/pptx}/text/fonts.py (100%) rename {pptx => src/pptx}/text/layout.py (100%) rename {pptx => src/pptx}/text/text.py (100%) rename {pptx => src/pptx}/util.py (100%) diff --git a/.gitignore b/.gitignore index 8f0219e06..c043a21c1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .cache .coverage /.tox/ -*.egg-info +/src/*.egg-info *.pyc /dist/ /docs/.build diff --git a/MANIFEST.in b/MANIFEST.in index 3872c0e4e..14688f1e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include HISTORY.rst LICENSE README.rst tox.ini recursive-include features * -recursive-include pptx/templates * +recursive-include src/pptx/templates * recursive-include tests *.py recursive-include tests/test_files * diff --git a/setup.py b/setup.py index 582610e5b..6d9a0d1a5 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def ascii_bytes_from(path, *paths): # read required text from files thisdir = os.path.dirname(__file__) -init_py = ascii_bytes_from(thisdir, "pptx", "__init__.py") +init_py = ascii_bytes_from(thisdir, "src", "pptx", "__init__.py") readme = ascii_bytes_from(thisdir, "README.rst") history = ascii_bytes_from(thisdir, "HISTORY.rst") @@ -38,7 +38,7 @@ def ascii_bytes_from(path, *paths): AUTHOR_EMAIL = "python-pptx@googlegroups.com" URL = "https://github.com/scanny/python-pptx" LICENSE = "MIT" -PACKAGES = find_packages(exclude=["tests", "tests.*"]) +PACKAGES = find_packages(where="src") PACKAGE_DATA = {"pptx": ["templates/*"]} INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2", "XlsxWriter>=0.5.7"] @@ -78,6 +78,7 @@ def ascii_bytes_from(path, *paths): "license": LICENSE, "packages": PACKAGES, "package_data": PACKAGE_DATA, + "package_dir": {"": "src"}, "install_requires": INSTALL_REQUIRES, "tests_require": TESTS_REQUIRE, "test_suite": TEST_SUITE, diff --git a/pptx/__init__.py b/src/pptx/__init__.py similarity index 100% rename from pptx/__init__.py rename to src/pptx/__init__.py diff --git a/pptx/action.py b/src/pptx/action.py similarity index 100% rename from pptx/action.py rename to src/pptx/action.py diff --git a/pptx/api.py b/src/pptx/api.py similarity index 100% rename from pptx/api.py rename to src/pptx/api.py diff --git a/pptx/chart/__init__.py b/src/pptx/chart/__init__.py similarity index 100% rename from pptx/chart/__init__.py rename to src/pptx/chart/__init__.py diff --git a/pptx/chart/axis.py b/src/pptx/chart/axis.py similarity index 100% rename from pptx/chart/axis.py rename to src/pptx/chart/axis.py diff --git a/pptx/chart/category.py b/src/pptx/chart/category.py similarity index 100% rename from pptx/chart/category.py rename to src/pptx/chart/category.py diff --git a/pptx/chart/chart.py b/src/pptx/chart/chart.py similarity index 100% rename from pptx/chart/chart.py rename to src/pptx/chart/chart.py diff --git a/pptx/chart/data.py b/src/pptx/chart/data.py similarity index 100% rename from pptx/chart/data.py rename to src/pptx/chart/data.py diff --git a/pptx/chart/datalabel.py b/src/pptx/chart/datalabel.py similarity index 100% rename from pptx/chart/datalabel.py rename to src/pptx/chart/datalabel.py diff --git a/pptx/chart/legend.py b/src/pptx/chart/legend.py similarity index 100% rename from pptx/chart/legend.py rename to src/pptx/chart/legend.py diff --git a/pptx/chart/marker.py b/src/pptx/chart/marker.py similarity index 100% rename from pptx/chart/marker.py rename to src/pptx/chart/marker.py diff --git a/pptx/chart/plot.py b/src/pptx/chart/plot.py similarity index 100% rename from pptx/chart/plot.py rename to src/pptx/chart/plot.py diff --git a/pptx/chart/point.py b/src/pptx/chart/point.py similarity index 100% rename from pptx/chart/point.py rename to src/pptx/chart/point.py diff --git a/pptx/chart/series.py b/src/pptx/chart/series.py similarity index 100% rename from pptx/chart/series.py rename to src/pptx/chart/series.py diff --git a/pptx/chart/xlsx.py b/src/pptx/chart/xlsx.py similarity index 100% rename from pptx/chart/xlsx.py rename to src/pptx/chart/xlsx.py diff --git a/pptx/chart/xmlwriter.py b/src/pptx/chart/xmlwriter.py similarity index 100% rename from pptx/chart/xmlwriter.py rename to src/pptx/chart/xmlwriter.py diff --git a/pptx/compat/__init__.py b/src/pptx/compat/__init__.py similarity index 100% rename from pptx/compat/__init__.py rename to src/pptx/compat/__init__.py diff --git a/pptx/compat/python2.py b/src/pptx/compat/python2.py similarity index 100% rename from pptx/compat/python2.py rename to src/pptx/compat/python2.py diff --git a/pptx/compat/python3.py b/src/pptx/compat/python3.py similarity index 100% rename from pptx/compat/python3.py rename to src/pptx/compat/python3.py diff --git a/pptx/dml/__init__.py b/src/pptx/dml/__init__.py similarity index 100% rename from pptx/dml/__init__.py rename to src/pptx/dml/__init__.py diff --git a/pptx/dml/chtfmt.py b/src/pptx/dml/chtfmt.py similarity index 100% rename from pptx/dml/chtfmt.py rename to src/pptx/dml/chtfmt.py diff --git a/pptx/dml/color.py b/src/pptx/dml/color.py similarity index 100% rename from pptx/dml/color.py rename to src/pptx/dml/color.py diff --git a/pptx/dml/effect.py b/src/pptx/dml/effect.py similarity index 100% rename from pptx/dml/effect.py rename to src/pptx/dml/effect.py diff --git a/pptx/dml/fill.py b/src/pptx/dml/fill.py similarity index 100% rename from pptx/dml/fill.py rename to src/pptx/dml/fill.py diff --git a/pptx/dml/line.py b/src/pptx/dml/line.py similarity index 100% rename from pptx/dml/line.py rename to src/pptx/dml/line.py diff --git a/pptx/enum/__init__.py b/src/pptx/enum/__init__.py similarity index 100% rename from pptx/enum/__init__.py rename to src/pptx/enum/__init__.py diff --git a/pptx/enum/action.py b/src/pptx/enum/action.py similarity index 100% rename from pptx/enum/action.py rename to src/pptx/enum/action.py diff --git a/pptx/enum/base.py b/src/pptx/enum/base.py similarity index 100% rename from pptx/enum/base.py rename to src/pptx/enum/base.py diff --git a/pptx/enum/chart.py b/src/pptx/enum/chart.py similarity index 100% rename from pptx/enum/chart.py rename to src/pptx/enum/chart.py diff --git a/pptx/enum/dml.py b/src/pptx/enum/dml.py similarity index 100% rename from pptx/enum/dml.py rename to src/pptx/enum/dml.py diff --git a/pptx/enum/lang.py b/src/pptx/enum/lang.py similarity index 100% rename from pptx/enum/lang.py rename to src/pptx/enum/lang.py diff --git a/pptx/enum/shapes.py b/src/pptx/enum/shapes.py similarity index 100% rename from pptx/enum/shapes.py rename to src/pptx/enum/shapes.py diff --git a/pptx/enum/text.py b/src/pptx/enum/text.py similarity index 100% rename from pptx/enum/text.py rename to src/pptx/enum/text.py diff --git a/pptx/exc.py b/src/pptx/exc.py similarity index 100% rename from pptx/exc.py rename to src/pptx/exc.py diff --git a/pptx/media.py b/src/pptx/media.py similarity index 100% rename from pptx/media.py rename to src/pptx/media.py diff --git a/pptx/opc/__init__.py b/src/pptx/opc/__init__.py similarity index 100% rename from pptx/opc/__init__.py rename to src/pptx/opc/__init__.py diff --git a/pptx/opc/constants.py b/src/pptx/opc/constants.py similarity index 100% rename from pptx/opc/constants.py rename to src/pptx/opc/constants.py diff --git a/pptx/opc/oxml.py b/src/pptx/opc/oxml.py similarity index 100% rename from pptx/opc/oxml.py rename to src/pptx/opc/oxml.py diff --git a/pptx/opc/package.py b/src/pptx/opc/package.py similarity index 100% rename from pptx/opc/package.py rename to src/pptx/opc/package.py diff --git a/pptx/opc/packuri.py b/src/pptx/opc/packuri.py similarity index 100% rename from pptx/opc/packuri.py rename to src/pptx/opc/packuri.py diff --git a/pptx/opc/serialized.py b/src/pptx/opc/serialized.py similarity index 100% rename from pptx/opc/serialized.py rename to src/pptx/opc/serialized.py diff --git a/pptx/opc/shared.py b/src/pptx/opc/shared.py similarity index 100% rename from pptx/opc/shared.py rename to src/pptx/opc/shared.py diff --git a/pptx/opc/spec.py b/src/pptx/opc/spec.py similarity index 100% rename from pptx/opc/spec.py rename to src/pptx/opc/spec.py diff --git a/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py similarity index 100% rename from pptx/oxml/__init__.py rename to src/pptx/oxml/__init__.py diff --git a/pptx/oxml/action.py b/src/pptx/oxml/action.py similarity index 100% rename from pptx/oxml/action.py rename to src/pptx/oxml/action.py diff --git a/pptx/oxml/chart/__init__.py b/src/pptx/oxml/chart/__init__.py similarity index 100% rename from pptx/oxml/chart/__init__.py rename to src/pptx/oxml/chart/__init__.py diff --git a/pptx/oxml/chart/axis.py b/src/pptx/oxml/chart/axis.py similarity index 100% rename from pptx/oxml/chart/axis.py rename to src/pptx/oxml/chart/axis.py diff --git a/pptx/oxml/chart/chart.py b/src/pptx/oxml/chart/chart.py similarity index 100% rename from pptx/oxml/chart/chart.py rename to src/pptx/oxml/chart/chart.py diff --git a/pptx/oxml/chart/datalabel.py b/src/pptx/oxml/chart/datalabel.py similarity index 100% rename from pptx/oxml/chart/datalabel.py rename to src/pptx/oxml/chart/datalabel.py diff --git a/pptx/oxml/chart/legend.py b/src/pptx/oxml/chart/legend.py similarity index 100% rename from pptx/oxml/chart/legend.py rename to src/pptx/oxml/chart/legend.py diff --git a/pptx/oxml/chart/marker.py b/src/pptx/oxml/chart/marker.py similarity index 100% rename from pptx/oxml/chart/marker.py rename to src/pptx/oxml/chart/marker.py diff --git a/pptx/oxml/chart/plot.py b/src/pptx/oxml/chart/plot.py similarity index 100% rename from pptx/oxml/chart/plot.py rename to src/pptx/oxml/chart/plot.py diff --git a/pptx/oxml/chart/series.py b/src/pptx/oxml/chart/series.py similarity index 100% rename from pptx/oxml/chart/series.py rename to src/pptx/oxml/chart/series.py diff --git a/pptx/oxml/chart/shared.py b/src/pptx/oxml/chart/shared.py similarity index 100% rename from pptx/oxml/chart/shared.py rename to src/pptx/oxml/chart/shared.py diff --git a/pptx/oxml/coreprops.py b/src/pptx/oxml/coreprops.py similarity index 100% rename from pptx/oxml/coreprops.py rename to src/pptx/oxml/coreprops.py diff --git a/pptx/oxml/dml/__init__.py b/src/pptx/oxml/dml/__init__.py similarity index 100% rename from pptx/oxml/dml/__init__.py rename to src/pptx/oxml/dml/__init__.py diff --git a/pptx/oxml/dml/color.py b/src/pptx/oxml/dml/color.py similarity index 100% rename from pptx/oxml/dml/color.py rename to src/pptx/oxml/dml/color.py diff --git a/pptx/oxml/dml/fill.py b/src/pptx/oxml/dml/fill.py similarity index 100% rename from pptx/oxml/dml/fill.py rename to src/pptx/oxml/dml/fill.py diff --git a/pptx/oxml/dml/line.py b/src/pptx/oxml/dml/line.py similarity index 100% rename from pptx/oxml/dml/line.py rename to src/pptx/oxml/dml/line.py diff --git a/pptx/oxml/ns.py b/src/pptx/oxml/ns.py similarity index 100% rename from pptx/oxml/ns.py rename to src/pptx/oxml/ns.py diff --git a/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py similarity index 100% rename from pptx/oxml/presentation.py rename to src/pptx/oxml/presentation.py diff --git a/pptx/oxml/shapes/__init__.py b/src/pptx/oxml/shapes/__init__.py similarity index 100% rename from pptx/oxml/shapes/__init__.py rename to src/pptx/oxml/shapes/__init__.py diff --git a/pptx/oxml/shapes/autoshape.py b/src/pptx/oxml/shapes/autoshape.py similarity index 100% rename from pptx/oxml/shapes/autoshape.py rename to src/pptx/oxml/shapes/autoshape.py diff --git a/pptx/oxml/shapes/connector.py b/src/pptx/oxml/shapes/connector.py similarity index 100% rename from pptx/oxml/shapes/connector.py rename to src/pptx/oxml/shapes/connector.py diff --git a/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py similarity index 100% rename from pptx/oxml/shapes/graphfrm.py rename to src/pptx/oxml/shapes/graphfrm.py diff --git a/pptx/oxml/shapes/groupshape.py b/src/pptx/oxml/shapes/groupshape.py similarity index 100% rename from pptx/oxml/shapes/groupshape.py rename to src/pptx/oxml/shapes/groupshape.py diff --git a/pptx/oxml/shapes/picture.py b/src/pptx/oxml/shapes/picture.py similarity index 100% rename from pptx/oxml/shapes/picture.py rename to src/pptx/oxml/shapes/picture.py diff --git a/pptx/oxml/shapes/shared.py b/src/pptx/oxml/shapes/shared.py similarity index 100% rename from pptx/oxml/shapes/shared.py rename to src/pptx/oxml/shapes/shared.py diff --git a/pptx/oxml/simpletypes.py b/src/pptx/oxml/simpletypes.py similarity index 100% rename from pptx/oxml/simpletypes.py rename to src/pptx/oxml/simpletypes.py diff --git a/pptx/oxml/slide.py b/src/pptx/oxml/slide.py similarity index 100% rename from pptx/oxml/slide.py rename to src/pptx/oxml/slide.py diff --git a/pptx/oxml/table.py b/src/pptx/oxml/table.py similarity index 100% rename from pptx/oxml/table.py rename to src/pptx/oxml/table.py diff --git a/pptx/oxml/text.py b/src/pptx/oxml/text.py similarity index 100% rename from pptx/oxml/text.py rename to src/pptx/oxml/text.py diff --git a/pptx/oxml/theme.py b/src/pptx/oxml/theme.py similarity index 100% rename from pptx/oxml/theme.py rename to src/pptx/oxml/theme.py diff --git a/pptx/oxml/xmlchemy.py b/src/pptx/oxml/xmlchemy.py similarity index 100% rename from pptx/oxml/xmlchemy.py rename to src/pptx/oxml/xmlchemy.py diff --git a/pptx/package.py b/src/pptx/package.py similarity index 100% rename from pptx/package.py rename to src/pptx/package.py diff --git a/pptx/parts/__init__.py b/src/pptx/parts/__init__.py similarity index 100% rename from pptx/parts/__init__.py rename to src/pptx/parts/__init__.py diff --git a/pptx/parts/chart.py b/src/pptx/parts/chart.py similarity index 100% rename from pptx/parts/chart.py rename to src/pptx/parts/chart.py diff --git a/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py similarity index 100% rename from pptx/parts/coreprops.py rename to src/pptx/parts/coreprops.py diff --git a/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py similarity index 100% rename from pptx/parts/embeddedpackage.py rename to src/pptx/parts/embeddedpackage.py diff --git a/pptx/parts/image.py b/src/pptx/parts/image.py similarity index 100% rename from pptx/parts/image.py rename to src/pptx/parts/image.py diff --git a/pptx/parts/media.py b/src/pptx/parts/media.py similarity index 100% rename from pptx/parts/media.py rename to src/pptx/parts/media.py diff --git a/pptx/parts/presentation.py b/src/pptx/parts/presentation.py similarity index 100% rename from pptx/parts/presentation.py rename to src/pptx/parts/presentation.py diff --git a/pptx/parts/slide.py b/src/pptx/parts/slide.py similarity index 100% rename from pptx/parts/slide.py rename to src/pptx/parts/slide.py diff --git a/pptx/presentation.py b/src/pptx/presentation.py similarity index 100% rename from pptx/presentation.py rename to src/pptx/presentation.py diff --git a/pptx/shapes/__init__.py b/src/pptx/shapes/__init__.py similarity index 100% rename from pptx/shapes/__init__.py rename to src/pptx/shapes/__init__.py diff --git a/pptx/shapes/autoshape.py b/src/pptx/shapes/autoshape.py similarity index 100% rename from pptx/shapes/autoshape.py rename to src/pptx/shapes/autoshape.py diff --git a/pptx/shapes/base.py b/src/pptx/shapes/base.py similarity index 100% rename from pptx/shapes/base.py rename to src/pptx/shapes/base.py diff --git a/pptx/shapes/connector.py b/src/pptx/shapes/connector.py similarity index 100% rename from pptx/shapes/connector.py rename to src/pptx/shapes/connector.py diff --git a/pptx/shapes/freeform.py b/src/pptx/shapes/freeform.py similarity index 100% rename from pptx/shapes/freeform.py rename to src/pptx/shapes/freeform.py diff --git a/pptx/shapes/graphfrm.py b/src/pptx/shapes/graphfrm.py similarity index 100% rename from pptx/shapes/graphfrm.py rename to src/pptx/shapes/graphfrm.py diff --git a/pptx/shapes/group.py b/src/pptx/shapes/group.py similarity index 100% rename from pptx/shapes/group.py rename to src/pptx/shapes/group.py diff --git a/pptx/shapes/picture.py b/src/pptx/shapes/picture.py similarity index 100% rename from pptx/shapes/picture.py rename to src/pptx/shapes/picture.py diff --git a/pptx/shapes/placeholder.py b/src/pptx/shapes/placeholder.py similarity index 100% rename from pptx/shapes/placeholder.py rename to src/pptx/shapes/placeholder.py diff --git a/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py similarity index 100% rename from pptx/shapes/shapetree.py rename to src/pptx/shapes/shapetree.py diff --git a/pptx/shared.py b/src/pptx/shared.py similarity index 100% rename from pptx/shared.py rename to src/pptx/shared.py diff --git a/pptx/slide.py b/src/pptx/slide.py similarity index 100% rename from pptx/slide.py rename to src/pptx/slide.py diff --git a/pptx/spec.py b/src/pptx/spec.py similarity index 100% rename from pptx/spec.py rename to src/pptx/spec.py diff --git a/pptx/table.py b/src/pptx/table.py similarity index 100% rename from pptx/table.py rename to src/pptx/table.py diff --git a/pptx/templates/default.pptx b/src/pptx/templates/default.pptx similarity index 100% rename from pptx/templates/default.pptx rename to src/pptx/templates/default.pptx diff --git a/pptx/templates/docx-icon.emf b/src/pptx/templates/docx-icon.emf similarity index 100% rename from pptx/templates/docx-icon.emf rename to src/pptx/templates/docx-icon.emf diff --git a/pptx/templates/generic-icon.emf b/src/pptx/templates/generic-icon.emf similarity index 100% rename from pptx/templates/generic-icon.emf rename to src/pptx/templates/generic-icon.emf diff --git a/pptx/templates/notes.xml b/src/pptx/templates/notes.xml similarity index 100% rename from pptx/templates/notes.xml rename to src/pptx/templates/notes.xml diff --git a/pptx/templates/notesMaster.xml b/src/pptx/templates/notesMaster.xml similarity index 100% rename from pptx/templates/notesMaster.xml rename to src/pptx/templates/notesMaster.xml diff --git a/pptx/templates/pptx-icon.emf b/src/pptx/templates/pptx-icon.emf similarity index 100% rename from pptx/templates/pptx-icon.emf rename to src/pptx/templates/pptx-icon.emf diff --git a/pptx/templates/theme.xml b/src/pptx/templates/theme.xml similarity index 100% rename from pptx/templates/theme.xml rename to src/pptx/templates/theme.xml diff --git a/pptx/templates/xlsx-icon.emf b/src/pptx/templates/xlsx-icon.emf similarity index 100% rename from pptx/templates/xlsx-icon.emf rename to src/pptx/templates/xlsx-icon.emf diff --git a/pptx/text/__init__.py b/src/pptx/text/__init__.py similarity index 100% rename from pptx/text/__init__.py rename to src/pptx/text/__init__.py diff --git a/pptx/text/fonts.py b/src/pptx/text/fonts.py similarity index 100% rename from pptx/text/fonts.py rename to src/pptx/text/fonts.py diff --git a/pptx/text/layout.py b/src/pptx/text/layout.py similarity index 100% rename from pptx/text/layout.py rename to src/pptx/text/layout.py diff --git a/pptx/text/text.py b/src/pptx/text/text.py similarity index 100% rename from pptx/text/text.py rename to src/pptx/text/text.py diff --git a/pptx/util.py b/src/pptx/util.py similarity index 100% rename from pptx/util.py rename to src/pptx/util.py diff --git a/tox.ini b/tox.ini index 4df2d13b1..b7b3b8434 100644 --- a/tox.ini +++ b/tox.ini @@ -24,8 +24,8 @@ filterwarnings = # -- pytest complains when pytest-xdist is not installed -- ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning -looponfailroots = pptx tests -norecursedirs = docs *.egg-info features .git pptx spec .tox +looponfailroots = src tests +norecursedirs = docs *.egg-info features .git src spec .tox python_classes = Test Describe python_functions = test_ it_ they_ but_ and_it_ From ce10c20b621aaed8f94a26fd2a5f8391287a6f74 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 21:49:05 -0700 Subject: [PATCH 57/69] dev: modernize dev environment - Add `pyproject.toml` and gather configuration there. Move `.ruff.toml` contents into `pyproject.toml`. - Add `requirements-test.txt`. --- .github/workflows/ci.yml | 36 ++++++++++++ .ruff.toml | 11 ---- pyproject.toml | 100 ++++++++++++++++++++++++++++++++++ requirements-test.txt | 7 +++ src/pptx/parts/coreprops.py | 8 +-- tests/parts/test_coreprops.py | 28 +++++----- tox.ini | 16 ------ 7 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .ruff.toml create mode 100644 pyproject.toml create mode 100644 requirements-test.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..6b14f6cd5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: ci + +on: + pull_request: + branches: [ master ] + push: + branches: + - master + - develop + +permissions: + contents: write + +jobs: + + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: Install test dependencies + run: | + pip install . + pip install -r requirements-test.txt + - name: Test with pytest + run: pytest --cov=pptx --cov-report term-missing tests + - name: Acceptance tests with behave + run: behave --stop diff --git a/.ruff.toml b/.ruff.toml deleted file mode 100644 index 45ae2ecbc..000000000 --- a/.ruff.toml +++ /dev/null @@ -1,11 +0,0 @@ -# -- don't check these locations -- -exclude = [ - # -- docs/ - documentation Python code is incidental -- - "docs", - # -- lab/ has some experimental code that is not disciplined -- - "lab", - # -- ref/ is not source-code -- - "ref", - # -- spec/ has some ad-hoc discovery code that is not disciplined -- - "spec", -] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..9868f5fc3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[tool.black] +line-length = 100 + +[tool.pyright] +exclude = [ + "**/__pycache__", + "**/.*" +] +include = [ + "src/pptx", + "tests/" +] +ignore = [] +pythonPlatform = "All" +pythonVersion = "3.9" +reportImportCycles = false +reportUnnecessaryCast = true +reportUnnecessaryTypeIgnoreComment = true +stubPath = "./typings" +typeCheckingMode = "strict" +verboseOutput = true + +[tool.pytest.ini_options] +filterwarnings = [ + # -- exit on any warning not explicitly ignored here -- + "error", + # -- pytest-xdist plugin may warn about `looponfailroots` deprecation -- + "ignore::DeprecationWarning:xdist", + # -- pytest complains when pytest-xdist is not installed -- + "ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning", +] + +looponfailroots = [ + "src", + "tests", +] +norecursedirs = [ + "docs", + "*.egg-info", + "features", + ".git", + "src", + "spec", + ".tox", +] +python_classes = [ + "Test", + "Describe", +] +python_functions = [ + "test_", + "it_", + "they_", + "but_", + "and_", +] + +[tool.ruff] +line-length = 100 + +# -- don't check these locations -- +exclude = [ + # -- docs/ - documentation Python code is incidental -- + "docs", + # -- lab/ has some experimental code that is not disciplined -- + "lab", + # -- ref/ is not source-code -- + "ref", + # -- spec/ has some ad-hoc discovery code that is not disciplined -- + "spec", +] + +[tool.ruff.lint] +select = [ + "C4", # -- flake8-comprehensions -- + "COM", # -- flake8-commas -- + "E", # -- pycodestyle errors -- + "F", # -- pyflakes -- + "I", # -- isort (imports) -- + "PLR0402", # -- Name compared with itself like `foo == foo` -- + "PT", # -- flake8-pytest-style -- + "SIM", # -- flake8-simplify -- + "TCH001", # -- detect typing-only imports not under `if TYPE_CHECKING` -- + "UP015", # -- redundant `open()` mode parameter (like "r" is default) -- + "UP018", # -- Unnecessary {literal_type} call like `str("abc")`. (rewrite as a literal) -- + "UP032", # -- Use f-string instead of `.format()` call -- + "UP034", # -- Avoid extraneous parentheses -- + "W", # -- Warnings, including invalid escape-sequence -- +] +ignore = [ + "COM812", # -- over aggressively insists on trailing commas where not desireable -- + "PT001", # -- wants empty parens on @pytest.fixture where not used (essentially always) -- + "PT005", # -- flags mock fixtures with names intentionally matching private method name -- + "PT011", # -- pytest.raises({exc}) too broad, use match param or more specific exception -- + "PT012", # -- pytest.raises() block should contain a single simple statement -- + "SIM117", # -- merge `with` statements for context managers that have same scope -- +] + +[tool.ruff.lint.isort] +known-first-party = ["pptx"] diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 000000000..b542c1af7 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,7 @@ +-r requirements.txt +behave>=1.2.3 +pyparsing>=2.0.1 +pytest>=2.5 +pytest-coverage +pytest-xdist +ruff diff --git a/src/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py index 14fe583d6..e39b154d0 100644 --- a/src/pptx/parts/coreprops.py +++ b/src/pptx/parts/coreprops.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" -from datetime import datetime +from __future__ import annotations + +import datetime as dt from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import XmlPart @@ -27,7 +27,7 @@ def default(cls, package): core_props.title = "PowerPoint Presentation" core_props.last_modified_by = "python-pptx" core_props.revision = 1 - core_props.modified = datetime.utcnow() + core_props.modified = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) return core_props @property diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 0f1b37917..3f20ca933 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -1,10 +1,10 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.coreprops` module.""" -import pytest +from __future__ import annotations -from datetime import datetime, timedelta +import datetime as dt + +import pytest from pptx.opc.constants import CONTENT_TYPE as CT from pptx.oxml.coreprops import CT_CoreProperties @@ -55,16 +55,18 @@ def it_can_construct_a_default_core_props(self): assert core_props.revision == 1 # core_props.modified only stores time with seconds resolution, so # comparison needs to be a little loose (within two seconds) - modified_timedelta = datetime.utcnow() - core_props.modified - max_expected_timedelta = timedelta(seconds=2) + modified_timedelta = ( + dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) - core_props.modified + ) + max_expected_timedelta = dt.timedelta(seconds=2) assert modified_timedelta < max_expected_timedelta # fixtures ------------------------------------------------------- @pytest.fixture( params=[ - ("created", datetime(2012, 11, 17, 16, 37, 40)), - ("last_printed", datetime(2014, 6, 4, 4, 28)), + ("created", dt.datetime(2012, 11, 17, 16, 37, 40)), + ("last_printed", dt.datetime(2014, 6, 4, 4, 28)), ("modified", None), ] ) @@ -77,21 +79,21 @@ def date_prop_get_fixture(self, request, core_properties): ( "created", "dcterms:created", - datetime(2001, 2, 3, 4, 5), + dt.datetime(2001, 2, 3, 4, 5), "2001-02-03T04:05:00Z", ' xsi:type="dcterms:W3CDTF"', ), ( "last_printed", "cp:lastPrinted", - datetime(2014, 6, 4, 4), + dt.datetime(2014, 6, 4, 4), "2014-06-04T04:00:00Z", "", ), ( "modified", "dcterms:modified", - datetime(2005, 4, 3, 2, 1), + dt.datetime(2005, 4, 3, 2, 1), "2005-04-03T02:01:00Z", ' xsi:type="dcterms:W3CDTF"', ), @@ -145,9 +147,7 @@ def str_prop_set_fixture(self, request): expected_xml = self.coreProperties(tagname, value) return core_properties, prop_name, value, expected_xml - @pytest.fixture( - params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)] - ) + @pytest.fixture(params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)]) def revision_get_fixture(self, request): str_val, expected_revision = request.param tagname = "" if str_val is None else "cp:revision" diff --git a/tox.ini b/tox.ini index b7b3b8434..223cc0ffe 100644 --- a/tox.ini +++ b/tox.ini @@ -13,22 +13,6 @@ ignore = W503 max-line-length = 88 -[pytest] -filterwarnings = - # -- exit on any warning not explicitly ignored here -- - error - - # -- pytest-xdist plugin may warn about `looponfailroots` deprecation -- - ignore::DeprecationWarning:xdist - - # -- pytest complains when pytest-xdist is not installed -- - ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning - -looponfailroots = src tests -norecursedirs = docs *.egg-info features .git src spec .tox -python_classes = Test Describe -python_functions = test_ it_ they_ but_ and_it_ - [tox] envlist = py27, py38, py311 requires = virtualenv<20.22.0 From 7efa08d2a6a33024dd639af9d4fddc1b57624b55 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 22:00:20 -0700 Subject: [PATCH 58/69] type: add lxml type-stubs --- typings/lxml/_types.pyi | 38 ++++++++++++ typings/lxml/etree/__init__.pyi | 18 ++++++ typings/lxml/etree/_classlookup.pyi | 75 ++++++++++++++++++++++ typings/lxml/etree/_cleanup.pyi | 21 +++++++ typings/lxml/etree/_element.pyi | 96 +++++++++++++++++++++++++++++ typings/lxml/etree/_module_func.pyi | 19 ++++++ typings/lxml/etree/_module_misc.pyi | 5 ++ typings/lxml/etree/_nsclasses.pyi | 31 ++++++++++ typings/lxml/etree/_parser.pyi | 81 ++++++++++++++++++++++++ 9 files changed, 384 insertions(+) create mode 100644 typings/lxml/_types.pyi create mode 100644 typings/lxml/etree/__init__.pyi create mode 100644 typings/lxml/etree/_classlookup.pyi create mode 100644 typings/lxml/etree/_cleanup.pyi create mode 100644 typings/lxml/etree/_element.pyi create mode 100644 typings/lxml/etree/_module_func.pyi create mode 100644 typings/lxml/etree/_module_misc.pyi create mode 100644 typings/lxml/etree/_nsclasses.pyi create mode 100644 typings/lxml/etree/_parser.pyi diff --git a/typings/lxml/_types.pyi b/typings/lxml/_types.pyi new file mode 100644 index 000000000..a16fec3dd --- /dev/null +++ b/typings/lxml/_types.pyi @@ -0,0 +1,38 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Any, Callable, Collection, Mapping, Protocol, TypeVar + +from typing_extensions import TypeAlias + +from .etree import QName, _Element, _ElementTree + +_ET = TypeVar("_ET", bound=_Element, default=_Element) +_ET_co = TypeVar("_ET_co", bound=_Element, default=_Element, covariant=True) +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) + +_AttrName: TypeAlias = str + +_AttrVal: TypeAlias = _TextArg + +_ElemPathArg: TypeAlias = str | QName + +_ElementOrTree: TypeAlias = _ET | _ElementTree[_ET] + +_NSMapArg = Mapping[None, str] | Mapping[str, str] | Mapping[str | None, str] + +_NonDefaultNSMapArg = Mapping[str, str] + +_TagName: TypeAlias = str + +_TagSelector: TypeAlias = _TagName | Callable[..., _Element] + +# String argument also support QName in various places +_TextArg: TypeAlias = str | bytes | QName + +_XPathObject = Any + +class SupportsLaxedItems(Protocol[_KT_co, _VT_co]): + def items(self) -> Collection[tuple[_KT_co, _VT_co]]: ... diff --git a/typings/lxml/etree/__init__.pyi b/typings/lxml/etree/__init__.pyi new file mode 100644 index 000000000..e649ce0b2 --- /dev/null +++ b/typings/lxml/etree/__init__.pyi @@ -0,0 +1,18 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from ._classlookup import ElementBase as ElementBase +from ._classlookup import ElementDefaultClassLookup as ElementDefaultClassLookup +from ._cleanup import strip_elements as strip_elements +from ._element import _Element as _Element +from ._element import _ElementTree as _ElementTree +from ._module_func import fromstring as fromstring +from ._module_func import tostring as tostring +from ._module_misc import QName as QName +from ._nsclasses import ElementNamespaceClassLookup as ElementNamespaceClassLookup +from ._parser import HTMLParser as HTMLParser +from ._parser import XMLParser as XMLParser + +class CDATA: + def __init__(self, data: str) -> None: ... diff --git a/typings/lxml/etree/_classlookup.pyi b/typings/lxml/etree/_classlookup.pyi new file mode 100644 index 000000000..03313c3c4 --- /dev/null +++ b/typings/lxml/etree/_classlookup.pyi @@ -0,0 +1,75 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from ._element import _Element + +class ElementBase(_Element): + """The public Element class + + Original Docstring + ------------------ + All custom Element classes must inherit from this one. + To create an Element, use the `Element()` factory. + + BIG FAT WARNING: Subclasses *must not* override `__init__` or + `__new__` as it is absolutely undefined when these objects will be + created or destroyed. All persistent state of Elements must be + stored in the underlying XML. If you really need to initialize + the object after creation, you can implement an ``_init(self)`` + method that will be called directly after object creation. + + Subclasses of this class can be instantiated to create a new + Element. By default, the tag name will be the class name and the + namespace will be empty. You can modify this with the following + class attributes: + + * TAG - the tag name, possibly containing a namespace in Clark + notation + + * NAMESPACE - the default namespace URI, unless provided as part + of the TAG attribute. + + * HTML - flag if the class is an HTML tag, as opposed to an XML + tag. This only applies to un-namespaced tags and defaults to + false (i.e. XML). + + * PARSER - the parser that provides the configuration for the + newly created document. Providing an HTML parser here will + default to creating an HTML element. + + In user code, the latter three are commonly inherited in class + hierarchies that implement a common namespace. + """ + + def __init__( + self, + *children: object, + attrib: dict[str, str] | None = None, + **_extra: str, + ) -> None: ... + def _init(self) -> None: ... + +class ElementClassLookup: + """Superclass of Element class lookups""" + +class ElementDefaultClassLookup(ElementClassLookup): + """Element class lookup scheme that always returns the default Element + class. + + The keyword arguments ``element``, ``comment``, ``pi`` and ``entity`` + accept the respective Element classes.""" + + def __init__( + self, + element: type[ElementBase] | None = None, + ) -> None: ... + +class FallbackElementClassLookup(ElementClassLookup): + """Superclass of Element class lookups with additional fallback""" + + @property + def fallback(self) -> ElementClassLookup | None: ... + def __init__(self, fallback: ElementClassLookup | None = None) -> None: ... + def set_fallback(self, lookup: ElementClassLookup) -> None: + """Sets the fallback scheme for this lookup method""" diff --git a/typings/lxml/etree/_cleanup.pyi b/typings/lxml/etree/_cleanup.pyi new file mode 100644 index 000000000..29e6bd861 --- /dev/null +++ b/typings/lxml/etree/_cleanup.pyi @@ -0,0 +1,21 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Collection, overload + +from .._types import _ElementOrTree, _TagSelector + +@overload +def strip_elements( + __tree_or_elem: _ElementOrTree, + *tag_names: _TagSelector, + with_tail: bool = True, +) -> None: ... +@overload +def strip_elements( + __tree_or_elem: _ElementOrTree, + __tag: Collection[_TagSelector], + /, + with_tail: bool = True, +) -> None: ... diff --git a/typings/lxml/etree/_element.pyi b/typings/lxml/etree/_element.pyi new file mode 100644 index 000000000..f25a147a9 --- /dev/null +++ b/typings/lxml/etree/_element.pyi @@ -0,0 +1,96 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Collection, Generic, Iterable, Iterator, TypeVar, overload + +from typing_extensions import Self + +from .. import _types as _t +from . import CDATA + +_T = TypeVar("_T") + +# Behaves like MutableMapping but deviates a lot in details +class _Attrib: + def __bool__(self) -> bool: ... + def __contains__(self, __o: object) -> bool: ... + def __delitem__(self, __k: _t._AttrName) -> None: ... + def __getitem__(self, __k: _t._AttrName) -> str: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __setitem__(self, __k: _t._AttrName, __v: _t._AttrVal) -> None: ... + @property + def _element(self) -> _Element: ... + def get(self, key: _t._AttrName, default: _T) -> str | _T: ... + def has_key(self, key: _t._AttrName) -> bool: ... + def items(self) -> list[tuple[str, str]]: ... + def iteritems(self) -> Iterator[tuple[str, str]]: ... + def iterkeys(self) -> Iterator[str]: ... + def itervalues(self) -> Iterator[str]: ... + def keys(self) -> list[str]: ... + def values(self) -> list[str]: ... + +class _Element: + @overload + def __getitem__(self, __x: int) -> _Element: ... + @overload + def __getitem__(self, __x: slice) -> list[_Element]: ... + def __contains__(self, __o: object) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_Element]: ... + def addprevious(self, element: _Element) -> None: ... + def append(self, element: _Element) -> None: ... + @property + def attrib(self) -> _Attrib: ... + def find(self, path: _t._ElemPathArg) -> Self | None: ... + def findall( + self, path: _t._ElemPathArg, namespaces: _t._NSMapArg | None = None + ) -> list[_Element]: ... + @overload + def get(self, key: _t._AttrName) -> str | None: ... + @overload + def get(self, key: _t._AttrName, default: _T) -> str | _T: ... + def getparent(self) -> _Element | None: ... + def index(self, child: _Element, start: int | None = None, end: int | None = None) -> int: ... + def iterancestors( + self, *, tag: _t._TagSelector | Collection[_t._TagSelector] | None = None + ) -> Iterator[Self]: ... + @overload + def iterchildren( + self, *tags: _t._TagSelector, reversed: bool = False + ) -> Iterator[_Element]: ... + @overload + def iterchildren( + self, + *, + tag: _t._TagSelector | Iterable[_t._TagSelector] | None = None, + reversed: bool = False, + ) -> Iterator[_Element]: ... + @overload + def itertext(self, *tags: _t._TagSelector, with_tail: bool = True) -> Iterator[str]: ... + @overload + def itertext( + self, + *, + tag: _t._TagSelector | Collection[_t._TagSelector] | None = None, + with_tail: bool = True, + ) -> Iterator[str]: ... + def remove(self, element: _Element) -> None: ... + def set(self, key: _t._AttrName, value: _t._AttrVal) -> None: ... + @property + def tag(self) -> str: ... + @property + def tail(self) -> str | None: ... + @property + def text(self) -> str | None: ... + @text.setter + def text(self, value: str | CDATA | None) -> None: ... + def xpath( + self, + _path: str, + /, + namespaces: _t._NonDefaultNSMapArg | None = None, + ) -> _t._XPathObject: ... + +class _ElementTree(Generic[_t._ET_co]): ... diff --git a/typings/lxml/etree/_module_func.pyi b/typings/lxml/etree/_module_func.pyi new file mode 100644 index 000000000..067b25ce9 --- /dev/null +++ b/typings/lxml/etree/_module_func.pyi @@ -0,0 +1,19 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from .._types import _ElementOrTree +from ..etree import HTMLParser, XMLParser +from ._element import _Element + +def fromstring(text: str | bytes, parser: XMLParser | HTMLParser) -> _Element: ... + +# Under XML Canonicalization (C14N) mode, most arguments are ignored, +# some arguments would even raise exception outright if specified. +def tostring( + element_or_tree: _ElementOrTree, + *, + encoding: str | type[str] | None = None, + pretty_print: bool = False, + with_tail: bool = True, +) -> str: ... diff --git a/typings/lxml/etree/_module_misc.pyi b/typings/lxml/etree/_module_misc.pyi new file mode 100644 index 000000000..9da021f0c --- /dev/null +++ b/typings/lxml/etree/_module_misc.pyi @@ -0,0 +1,5 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +class QName: ... diff --git a/typings/lxml/etree/_nsclasses.pyi b/typings/lxml/etree/_nsclasses.pyi new file mode 100644 index 000000000..5118f7a80 --- /dev/null +++ b/typings/lxml/etree/_nsclasses.pyi @@ -0,0 +1,31 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Iterable, Iterator, MutableMapping, TypeVar + +from .._types import SupportsLaxedItems +from ._classlookup import ElementBase, ElementClassLookup, FallbackElementClassLookup + +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") + +class _NamespaceRegistry(MutableMapping[_KT, _VT]): + def __delitem__(self, __key: _KT) -> None: ... + def __getitem__(self, __key: _KT) -> _VT: ... + def __setitem__(self, __key: _KT, __value: _VT) -> None: ... + def __iter__(self) -> Iterator[_KT]: ... + def __len__(self) -> int: ... + def update( # type: ignore[override] + self, + class_dict_iterable: SupportsLaxedItems[_KT, _VT] | Iterable[tuple[_KT, _VT]], + ) -> None: ... + def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override] + def iteritems(self) -> Iterator[tuple[_KT, _VT]]: ... + def clear(self) -> None: ... + +class _ClassNamespaceRegistry(_NamespaceRegistry[str | None, type[ElementBase]]): ... + +class ElementNamespaceClassLookup(FallbackElementClassLookup): + def __init__(self, fallback: ElementClassLookup | None = None) -> None: ... + def get_namespace(self, ns_uri: str | None) -> _ClassNamespaceRegistry: ... diff --git a/typings/lxml/etree/_parser.pyi b/typings/lxml/etree/_parser.pyi new file mode 100644 index 000000000..2fdd144fa --- /dev/null +++ b/typings/lxml/etree/_parser.pyi @@ -0,0 +1,81 @@ +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from typing import Literal + +from ._classlookup import ElementClassLookup +from .._types import _ET_co, _NSMapArg, _TagName, SupportsLaxedItems + +class HTMLParser: + def __init__( + self, + *, + encoding: str | None = None, + remove_blank_text: bool = False, + remove_comments: bool = False, + remove_pis: bool = False, + strip_cdata: bool = True, + no_network: bool = True, + recover: bool = True, + compact: bool = True, + default_doctype: bool = True, + collect_ids: bool = True, + huge_tree: bool = False, + ) -> None: ... + def set_element_class_lookup(self, lookup: ElementClassLookup | None = None) -> None: ... + +class XMLParser: + def __init__( + self, + *, + attribute_defaults: bool = False, + collect_ids: bool = True, + compact: bool = True, + dtd_validation: bool = False, + encoding: str | None = None, + huge_tree: bool = False, + load_dtd: bool = False, + no_network: bool = True, + ns_clean: bool = False, + recover: bool = False, + remove_blank_text: bool = False, + remove_comments: bool = False, + remove_pis: bool = False, + resolve_entities: bool | Literal["internal"] = "internal", + strip_cdata: bool = True, + ) -> None: ... + def makeelement( + self, + _tag: _TagName, + /, + attrib: SupportsLaxedItems[str, str] | None = None, + nsmap: _NSMapArg | None = None, + **_extra: str, + ) -> _ET_co: ... + def set_element_class_lookup(self, lookup: ElementClassLookup | None = None) -> None: + """ + Notes + ----- + When calling this method, it is advised to also change typing + specialization of concerned parser too, because current python + typing system can't change it automatically. + + Example + ------- + Following code demonstrates how to create ``lxml.html.HTMLParser`` + manually from ``lxml.etree.HTMLParser``:: + + ```python + parser = etree.HTMLParser() + reveal_type(parser) # HTMLParser[_Element] + if TYPE_CHECKING: + parser = cast('etree.HTMLParser[HtmlElement]', parser) + else: + parser.set_element_class_lookup( + html.HtmlElementClassLookup()) + result = etree.fromstring(data, parser=parser) + reveal_type(result) # HtmlElement + ``` + """ + ... From 01b86e64de0f6f545f77dead7f9c3368a783a791 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 30 Jul 2024 22:41:34 -0700 Subject: [PATCH 59/69] rfctr(enum): modernize enumerations --- src/pptx/enum/action.py | 85 +- src/pptx/enum/base.py | 381 ++----- src/pptx/enum/chart.py | 666 +++++++----- src/pptx/enum/dml.py | 560 ++++++---- src/pptx/enum/lang.py | 1152 +++++++++++--------- src/pptx/enum/shapes.py | 1694 ++++++++++++++++------------- src/pptx/enum/text.py | 367 +++---- src/pptx/oxml/text.py | 22 +- src/pptx/parts/embeddedpackage.py | 2 +- src/pptx/parts/slide.py | 10 +- src/pptx/shapes/shapetree.py | 40 +- tests/chart/test_data.py | 66 +- tests/dml/test_line.py | 10 +- tests/enum/__init__.py | 0 tests/enum/test_base.py | 73 ++ tests/enum/test_shapes.py | 46 + tests/shapes/test_autoshape.py | 47 +- tests/test_enum.py | 120 -- 18 files changed, 2785 insertions(+), 2556 deletions(-) create mode 100644 tests/enum/__init__.py create mode 100644 tests/enum/test_base.py create mode 100644 tests/enum/test_shapes.py delete mode 100644 tests/test_enum.py diff --git a/src/pptx/enum/action.py b/src/pptx/enum/action.py index 8bbf9189a..bc447226f 100644 --- a/src/pptx/enum/action.py +++ b/src/pptx/enum/action.py @@ -1,16 +1,11 @@ -# encoding: utf-8 +"""Enumerations that describe click-action settings.""" -""" -Enumerations that describe click action settings -""" +from __future__ import annotations -from __future__ import absolute_import +from pptx.enum.base import BaseEnum -from .base import alias, Enumeration, EnumMember - -@alias("PP_ACTION") -class PP_ACTION_TYPE(Enumeration): +class PP_ACTION_TYPE(BaseEnum): """ Specifies the type of a mouse action (click or hover action). @@ -21,26 +16,56 @@ class PP_ACTION_TYPE(Enumeration): from pptx.enum.action import PP_ACTION assert shape.click_action.action == PP_ACTION.HYPERLINK + + MS API name: `PpActionType` + + https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx """ - __ms_name__ = "PpActionType" - - __url__ = "https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx" - - __members__ = ( - EnumMember("END_SHOW", 6, "Slide show ends."), - EnumMember("FIRST_SLIDE", 3, "Returns to the first slide."), - EnumMember("HYPERLINK", 7, "Hyperlink."), - EnumMember("LAST_SLIDE", 4, "Moves to the last slide."), - EnumMember("LAST_SLIDE_VIEWED", 5, "Moves to the last slide viewed."), - EnumMember("NAMED_SLIDE", 101, "Moves to slide specified by slide number."), - EnumMember("NAMED_SLIDE_SHOW", 10, "Runs the slideshow."), - EnumMember("NEXT_SLIDE", 1, "Moves to the next slide."), - EnumMember("NONE", 0, "No action is performed."), - EnumMember("OPEN_FILE", 102, "Opens the specified file."), - EnumMember("OLE_VERB", 11, "OLE Verb."), - EnumMember("PLAY", 12, "Begins the slideshow."), - EnumMember("PREVIOUS_SLIDE", 2, "Moves to the previous slide."), - EnumMember("RUN_MACRO", 8, "Runs a macro."), - EnumMember("RUN_PROGRAM", 9, "Runs a program."), - ) + END_SHOW = (6, "Slide show ends.") + """Slide show ends.""" + + FIRST_SLIDE = (3, "Returns to the first slide.") + """Returns to the first slide.""" + + HYPERLINK = (7, "Hyperlink.") + """Hyperlink.""" + + LAST_SLIDE = (4, "Moves to the last slide.") + """Moves to the last slide.""" + + LAST_SLIDE_VIEWED = (5, "Moves to the last slide viewed.") + """Moves to the last slide viewed.""" + + NAMED_SLIDE = (101, "Moves to slide specified by slide number.") + """Moves to slide specified by slide number.""" + + NAMED_SLIDE_SHOW = (10, "Runs the slideshow.") + """Runs the slideshow.""" + + NEXT_SLIDE = (1, "Moves to the next slide.") + """Moves to the next slide.""" + + NONE = (0, "No action is performed.") + """No action is performed.""" + + OPEN_FILE = (102, "Opens the specified file.") + """Opens the specified file.""" + + OLE_VERB = (11, "OLE Verb.") + """OLE Verb.""" + + PLAY = (12, "Begins the slideshow.") + """Begins the slideshow.""" + + PREVIOUS_SLIDE = (2, "Moves to the previous slide.") + """Moves to the previous slide.""" + + RUN_MACRO = (8, "Runs a macro.") + """Runs a macro.""" + + RUN_PROGRAM = (9, "Runs a program.") + """Runs a program.""" + + +PP_ACTION = PP_ACTION_TYPE diff --git a/src/pptx/enum/base.py b/src/pptx/enum/base.py index c57e15b33..1d49b9c19 100644 --- a/src/pptx/enum/base.py +++ b/src/pptx/enum/base.py @@ -1,40 +1,105 @@ -# encoding: utf-8 +"""Base classes and other objects used by enumerations.""" -""" -Base classes and other objects used by enumerations -""" +from __future__ import annotations -from __future__ import absolute_import, print_function - -import sys +import enum import textwrap +from typing import TYPE_CHECKING, Any, Type, TypeVar +if TYPE_CHECKING: + from typing_extensions import Self -def alias(*aliases): - """ - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to - be referenced by each of the names provided as arguments. +_T = TypeVar("_T", bound="BaseXmlEnum") + + +class BaseEnum(int, enum.Enum): + """Base class for Enums that do not map XML attr values. + + The enum's value will be an integer, corresponding to the integer assigned the + corresponding member in the MS API enum of the same name. """ - def decorator(cls): - # alias must be set in globals from caller's frame - caller = sys._getframe(1) - globals_dict = caller.f_globals - for alias in aliases: - globals_dict[alias] = cls - return cls + def __new__(cls, ms_api_value: int, docstr: str): + self = int.__new__(cls, ms_api_value) + self._value_ = ms_api_value + self.__doc__ = docstr.strip() + return self - return decorator + def __str__(self): + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" + return f"{self.name} ({self.value})" -class _DocsPageFormatter(object): - """ - Formats a RestructuredText documention page (string) for the enumeration - class parts passed to the constructor. An immutable one-shot service - object. +class BaseXmlEnum(int, enum.Enum): + """Base class for Enums that also map XML attr values. + + The enum's value will be an integer, corresponding to the integer assigned the + corresponding member in the MS API enum of the same name. """ - def __init__(self, clsname, clsdict): + xml_value: str | None + + def __new__(cls, ms_api_value: int, xml_value: str | None, docstr: str): + self = int.__new__(cls, ms_api_value) + self._value_ = ms_api_value + self.xml_value = xml_value + self.__doc__ = docstr.strip() + return self + + def __str__(self): + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" + return f"{self.name} ({self.value})" + + @classmethod + def from_xml(cls, xml_value: str) -> Self: + """Enumeration member corresponding to XML attribute value `xml_value`. + + Raises `ValueError` if `xml_value` is the empty string ("") or is not an XML attribute + value registered on the enumeration. Note that enum members that do not correspond to one + of the defined values for an XML attribute have `xml_value == ""`. These + "return-value only" members cannot be automatically mapped from an XML attribute value and + must be selected explicitly by code, based on the appropriate conditions. + + Example:: + + >>> WD_PARAGRAPH_ALIGNMENT.from_xml("center") + WD_PARAGRAPH_ALIGNMENT.CENTER + + """ + # -- the empty string never maps to a member -- + member = ( + next((member for member in cls if member.xml_value == xml_value), None) + if xml_value + else None + ) + + if member is None: + raise ValueError(f"{cls.__name__} has no XML mapping for {repr(xml_value)}") + + return member + + @classmethod + def to_xml(cls: Type[_T], value: int | _T) -> str: + """XML value of this enum member, generally an XML attribute value.""" + # -- presence of multi-arg `__new__()` method fools type-checker, but getting a + # -- member by its value using EnumCls(val) works as usual. + member = cls(value) + xml_value = member.xml_value + if not xml_value: + raise ValueError(f"{cls.__name__}.{member.name} has no XML representation") + return xml_value + + @classmethod + def validate(cls: Type[_T], value: _T): + """Raise |ValueError| if `value` is not an assignable value.""" + if value not in cls: + raise ValueError(f"{value} not a member of {cls.__name__} enumeration") + + +class DocsPageFormatter(object): + """Formats a reStructuredText documention page (string) for an enumeration.""" + + def __init__(self, clsname: str, clsdict: dict[str, Any]): self._clsname = clsname self._clsdict = clsdict @@ -69,12 +134,12 @@ def _intro_text(self): return textwrap.dedent(cls_docstring).strip() - def _member_def(self, member): - """ - Return an individual member definition formatted as an RST glossary - entry, wrapped to fit within 78 columns. + def _member_def(self, member: BaseEnum | BaseXmlEnum): + """Return an individual member definition formatted as an RST glossary entry. + + Output is wrapped to fit within 78 columns. """ - member_docstring = textwrap.dedent(member.docstring).strip() + member_docstring = textwrap.dedent(member.__doc__ or "").strip() member_docstring = textwrap.fill( member_docstring, width=78, @@ -90,9 +155,7 @@ def _member_defs(self): of the documentation page """ members = self._clsdict["__members__"] - member_defs = [ - self._member_def(member) for member in members if member.name is not None - ] + member_defs = [self._member_def(member) for member in members if member.name is not None] return "\n".join(member_defs) @property @@ -110,255 +173,3 @@ def _page_title(self): """ title_underscore = "=" * (len(self._clsname) + 4) return "``%s``\n%s" % (self._clsname, title_underscore) - - -class MetaEnumeration(type): - """ - The metaclass for Enumeration and its subclasses. Adds a name for each - named member and compiles state needed by the enumeration class to - respond to other attribute gets - """ - - def __new__(meta, clsname, bases, clsdict): - meta._add_enum_members(clsdict) - meta._collect_valid_settings(clsdict) - meta._generate_docs_page(clsname, clsdict) - return type.__new__(meta, clsname, bases, clsdict) - - @classmethod - def _add_enum_members(meta, clsdict): - """ - Dispatch ``.add_to_enum()`` call to each member so it can do its - thing to properly add itself to the enumeration class. This - delegation allows member sub-classes to add specialized behaviors. - """ - enum_members = clsdict["__members__"] - for member in enum_members: - member.add_to_enum(clsdict) - - @classmethod - def _collect_valid_settings(meta, clsdict): - """ - Return a sequence containing the enumeration values that are valid - assignment values. Return-only values are excluded. - """ - enum_members = clsdict["__members__"] - valid_settings = [] - for member in enum_members: - valid_settings.extend(member.valid_settings) - clsdict["_valid_settings"] = valid_settings - - @classmethod - def _generate_docs_page(meta, clsname, clsdict): - """ - Return the RST documentation page for the enumeration. - """ - clsdict["__docs_rst__"] = _DocsPageFormatter(clsname, clsdict).page_str - - -class EnumerationBase(object): - """ - Base class for all enumerations, used directly for enumerations requiring - only basic behavior. It's __dict__ is used below in the Python 2+3 - compatible metaclass definition. - """ - - __members__ = () - __ms_name__ = "" - - @classmethod - def validate(cls, value): - """ - Raise |ValueError| if *value* is not an assignable value. - """ - if value not in cls._valid_settings: - raise ValueError( - "%s not a member of %s enumeration" % (value, cls.__name__) - ) - - -Enumeration = MetaEnumeration("Enumeration", (object,), dict(EnumerationBase.__dict__)) - - -class XmlEnumeration(Enumeration): - """ - Provides ``to_xml()`` and ``from_xml()`` methods in addition to base - enumeration features - """ - - __members__ = () - __ms_name__ = "" - - @classmethod - def from_xml(cls, xml_val): - """ - Return the enumeration member corresponding to the XML value - *xml_val*. - """ - return cls._xml_to_member[xml_val] - - @classmethod - def to_xml(cls, enum_val): - """ - Return the XML value of the enumeration value *enum_val*. - """ - cls.validate(enum_val) - return cls._member_to_xml[enum_val] - - -class EnumMember(object): - """ - Used in the enumeration class definition to define a member value and its - mappings - """ - - def __init__(self, name, value, docstring): - self._name = name - if isinstance(value, int): - value = EnumValue(name, value, docstring) - self._value = value - self._docstring = docstring - - def add_to_enum(self, clsdict): - """ - Add a name to *clsdict* for this member. - """ - self.register_name(clsdict) - - @property - def docstring(self): - """ - The description of this member - """ - return self._docstring - - @property - def name(self): - """ - The distinguishing name of this member within the enumeration class, - e.g. 'MIDDLE' for MSO_VERTICAL_ANCHOR.MIDDLE, if this is a named - member. Otherwise the primitive value such as |None|, |True| or - |False|. - """ - return self._name - - def register_name(self, clsdict): - """ - Add a member name to the class dict *clsdict* containing the value of - this member object. Where the name of this object is None, do - nothing; this allows out-of-band values to be defined without adding - a name to the class dict. - """ - if self.name is None: - return - clsdict[self.name] = self.value - - @property - def valid_settings(self): - """ - A sequence containing the values valid for assignment for this - member. May be zero, one, or more in number. - """ - return (self._value,) - - @property - def value(self): - """ - The enumeration value for this member, often an instance of - EnumValue, but may be a primitive value such as |None|. - """ - return self._value - - -class EnumValue(int): - """ - A named enumeration value, providing __str__ and __doc__ string values - for its symbolic name and description, respectively. Subclasses int, so - behaves as a regular int unless the strings are asked for. - """ - - def __new__(cls, member_name, int_value, docstring): - return super(EnumValue, cls).__new__(cls, int_value) - - def __init__(self, member_name, int_value, docstring): - super(EnumValue, self).__init__() - self._member_name = member_name - self._docstring = docstring - - @property - def __doc__(self): - """ - The description of this enumeration member - """ - return self._docstring.strip() - - def __str__(self): - """ - The symbolic name and string value of this member, e.g. 'MIDDLE (3)' - """ - return "{0:s} ({1:d})".format(self._member_name, self) - - -class ReturnValueOnlyEnumMember(EnumMember): - """ - Used to define a member of an enumeration that is only valid as a query - result and is not valid as a setting, e.g. MSO_VERTICAL_ANCHOR.MIXED (-2) - """ - - @property - def valid_settings(self): - """ - No settings are valid for a return-only value. - """ - return () - - -class XmlMappedEnumMember(EnumMember): - """ - Used to define a member whose value maps to an XML attribute value. - """ - - def __init__(self, name, value, xml_value, docstring): - super(XmlMappedEnumMember, self).__init__(name, value, docstring) - self._xml_value = xml_value - - def add_to_enum(self, clsdict): - """ - Compile XML mappings in addition to base add behavior. - """ - super(XmlMappedEnumMember, self).add_to_enum(clsdict) - self.register_xml_mapping(clsdict) - - def register_xml_mapping(self, clsdict): - """ - Add XML mappings to the enumeration class state for this member. - """ - member_to_xml = self._get_or_add_member_to_xml(clsdict) - member_to_xml[self.value] = self.xml_value - xml_to_member = self._get_or_add_xml_to_member(clsdict) - xml_to_member[self.xml_value] = self.value - - @property - def xml_value(self): - """ - The XML attribute value that corresponds to this enumeration value - """ - return self._xml_value - - @staticmethod - def _get_or_add_member_to_xml(clsdict): - """ - Add the enum -> xml value mapping to the enumeration class state - """ - if "_member_to_xml" not in clsdict: - clsdict["_member_to_xml"] = dict() - return clsdict["_member_to_xml"] - - @staticmethod - def _get_or_add_xml_to_member(clsdict): - """ - Add the xml -> enum value mapping to the enumeration class state - """ - if "_xml_to_member" not in clsdict: - clsdict["_xml_to_member"] = dict() - return clsdict["_xml_to_member"] diff --git a/src/pptx/enum/chart.py b/src/pptx/enum/chart.py index 26cd3e5fc..5e609ebd3 100644 --- a/src/pptx/enum/chart.py +++ b/src/pptx/enum/chart.py @@ -1,60 +1,39 @@ -# encoding: utf-8 +"""Enumerations used by charts and related objects.""" -""" -Enumerations used by charts and related objects -""" +from __future__ import annotations -from __future__ import absolute_import +from pptx.enum.base import BaseEnum, BaseXmlEnum -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - -class XL_AXIS_CROSSES(XmlEnumeration): - """ - Specifies the point on the specified axis where the other axis crosses. +class XL_AXIS_CROSSES(BaseXmlEnum): + """Specifies the point on an axis where the other axis crosses. Example:: from pptx.enum.chart import XL_AXIS_CROSSES value_axis.crosses = XL_AXIS_CROSSES.MAXIMUM + + MS API Name: `XlAxisCrosses` + + https://msdn.microsoft.com/en-us/library/office/ff745402.aspx """ - __ms_name__ = "XlAxisCrosses" - - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff745402.aspx" - - __members__ = ( - XmlMappedEnumMember( - "AUTOMATIC", - -4105, - "autoZero", - "The axis crossing point is set " "automatically, often at zero.", - ), - ReturnValueOnlyEnumMember( - "CUSTOM", - -4114, - "The .crosses_at property specifies the axis cr" "ossing point.", - ), - XmlMappedEnumMember( - "MAXIMUM", 2, "max", "The axis crosses at the maximum value." - ), - XmlMappedEnumMember( - "MINIMUM", 4, "min", "The axis crosses at the minimum value." - ), - ) + AUTOMATIC = (-4105, "autoZero", "The axis crossing point is set automatically, often at zero.") + """The axis crossing point is set automatically, often at zero.""" + CUSTOM = (-4114, "", "The .crosses_at property specifies the axis crossing point.") + """The .crosses_at property specifies the axis crossing point.""" -class XL_CATEGORY_TYPE(Enumeration): - """ - Specifies the type of the category axis. + MAXIMUM = (2, "max", "The axis crosses at the maximum value.") + """The axis crosses at the maximum value.""" + + MINIMUM = (4, "min", "The axis crosses at the minimum value.") + """The axis crosses at the minimum value.""" + + +class XL_CATEGORY_TYPE(BaseEnum): + """Specifies the type of the category axis. Example:: @@ -62,131 +41,258 @@ class XL_CATEGORY_TYPE(Enumeration): date_axis = chart.category_axis assert date_axis.category_type == XL_CATEGORY_TYPE.TIME_SCALE + + MS API Name: `XlCategoryType` + + https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx """ - __ms_name__ = "XlCategoryType" - - __url__ = "https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx" - - __members__ = ( - EnumMember( - "AUTOMATIC_SCALE", -4105, "The application controls the axis " "type." - ), - EnumMember( - "CATEGORY_SCALE", 2, "Axis groups data by an arbitrary set of " "categories" - ), - EnumMember( - "TIME_SCALE", - 3, - "Axis groups data on a time scale of days, " "months, or years.", - ), - ) + AUTOMATIC_SCALE = (-4105, "The application controls the axis type.") + """The application controls the axis type.""" + CATEGORY_SCALE = (2, "Axis groups data by an arbitrary set of categories") + """Axis groups data by an arbitrary set of categories""" + + TIME_SCALE = (3, "Axis groups data on a time scale of days, months, or years.") + """Axis groups data on a time scale of days, months, or years.""" -class XL_CHART_TYPE(Enumeration): - """ - Specifies the type of a chart. + +class XL_CHART_TYPE(BaseEnum): + """Specifies the type of a chart. Example:: from pptx.enum.chart import XL_CHART_TYPE assert chart.chart_type == XL_CHART_TYPE.BAR_STACKED + + MS API Name: `XlChartType` + + http://msdn.microsoft.com/en-us/library/office/ff838409.aspx """ - __ms_name__ = "XlChartType" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff838409.aspx" - - __members__ = ( - EnumMember("THREE_D_AREA", -4098, "3D Area."), - EnumMember("THREE_D_AREA_STACKED", 78, "3D Stacked Area."), - EnumMember("THREE_D_AREA_STACKED_100", 79, "100% Stacked Area."), - EnumMember("THREE_D_BAR_CLUSTERED", 60, "3D Clustered Bar."), - EnumMember("THREE_D_BAR_STACKED", 61, "3D Stacked Bar."), - EnumMember("THREE_D_BAR_STACKED_100", 62, "3D 100% Stacked Bar."), - EnumMember("THREE_D_COLUMN", -4100, "3D Column."), - EnumMember("THREE_D_COLUMN_CLUSTERED", 54, "3D Clustered Column."), - EnumMember("THREE_D_COLUMN_STACKED", 55, "3D Stacked Column."), - EnumMember("THREE_D_COLUMN_STACKED_100", 56, "3D 100% Stacked Column."), - EnumMember("THREE_D_LINE", -4101, "3D Line."), - EnumMember("THREE_D_PIE", -4102, "3D Pie."), - EnumMember("THREE_D_PIE_EXPLODED", 70, "Exploded 3D Pie."), - EnumMember("AREA", 1, "Area"), - EnumMember("AREA_STACKED", 76, "Stacked Area."), - EnumMember("AREA_STACKED_100", 77, "100% Stacked Area."), - EnumMember("BAR_CLUSTERED", 57, "Clustered Bar."), - EnumMember("BAR_OF_PIE", 71, "Bar of Pie."), - EnumMember("BAR_STACKED", 58, "Stacked Bar."), - EnumMember("BAR_STACKED_100", 59, "100% Stacked Bar."), - EnumMember("BUBBLE", 15, "Bubble."), - EnumMember("BUBBLE_THREE_D_EFFECT", 87, "Bubble with 3D effects."), - EnumMember("COLUMN_CLUSTERED", 51, "Clustered Column."), - EnumMember("COLUMN_STACKED", 52, "Stacked Column."), - EnumMember("COLUMN_STACKED_100", 53, "100% Stacked Column."), - EnumMember("CONE_BAR_CLUSTERED", 102, "Clustered Cone Bar."), - EnumMember("CONE_BAR_STACKED", 103, "Stacked Cone Bar."), - EnumMember("CONE_BAR_STACKED_100", 104, "100% Stacked Cone Bar."), - EnumMember("CONE_COL", 105, "3D Cone Column."), - EnumMember("CONE_COL_CLUSTERED", 99, "Clustered Cone Column."), - EnumMember("CONE_COL_STACKED", 100, "Stacked Cone Column."), - EnumMember("CONE_COL_STACKED_100", 101, "100% Stacked Cone Column."), - EnumMember("CYLINDER_BAR_CLUSTERED", 95, "Clustered Cylinder Bar."), - EnumMember("CYLINDER_BAR_STACKED", 96, "Stacked Cylinder Bar."), - EnumMember("CYLINDER_BAR_STACKED_100", 97, "100% Stacked Cylinder Bar."), - EnumMember("CYLINDER_COL", 98, "3D Cylinder Column."), - EnumMember("CYLINDER_COL_CLUSTERED", 92, "Clustered Cone Column."), - EnumMember("CYLINDER_COL_STACKED", 93, "Stacked Cone Column."), - EnumMember("CYLINDER_COL_STACKED_100", 94, "100% Stacked Cylinder Column."), - EnumMember("DOUGHNUT", -4120, "Doughnut."), - EnumMember("DOUGHNUT_EXPLODED", 80, "Exploded Doughnut."), - EnumMember("LINE", 4, "Line."), - EnumMember("LINE_MARKERS", 65, "Line with Markers."), - EnumMember("LINE_MARKERS_STACKED", 66, "Stacked Line with Markers."), - EnumMember("LINE_MARKERS_STACKED_100", 67, "100% Stacked Line with Markers."), - EnumMember("LINE_STACKED", 63, "Stacked Line."), - EnumMember("LINE_STACKED_100", 64, "100% Stacked Line."), - EnumMember("PIE", 5, "Pie."), - EnumMember("PIE_EXPLODED", 69, "Exploded Pie."), - EnumMember("PIE_OF_PIE", 68, "Pie of Pie."), - EnumMember("PYRAMID_BAR_CLUSTERED", 109, "Clustered Pyramid Bar."), - EnumMember("PYRAMID_BAR_STACKED", 110, "Stacked Pyramid Bar."), - EnumMember("PYRAMID_BAR_STACKED_100", 111, "100% Stacked Pyramid Bar."), - EnumMember("PYRAMID_COL", 112, "3D Pyramid Column."), - EnumMember("PYRAMID_COL_CLUSTERED", 106, "Clustered Pyramid Column."), - EnumMember("PYRAMID_COL_STACKED", 107, "Stacked Pyramid Column."), - EnumMember("PYRAMID_COL_STACKED_100", 108, "100% Stacked Pyramid Column."), - EnumMember("RADAR", -4151, "Radar."), - EnumMember("RADAR_FILLED", 82, "Filled Radar."), - EnumMember("RADAR_MARKERS", 81, "Radar with Data Markers."), - EnumMember("STOCK_HLC", 88, "High-Low-Close."), - EnumMember("STOCK_OHLC", 89, "Open-High-Low-Close."), - EnumMember("STOCK_VHLC", 90, "Volume-High-Low-Close."), - EnumMember("STOCK_VOHLC", 91, "Volume-Open-High-Low-Close."), - EnumMember("SURFACE", 83, "3D Surface."), - EnumMember("SURFACE_TOP_VIEW", 85, "Surface (Top View)."), - EnumMember("SURFACE_TOP_VIEW_WIREFRAME", 86, "Surface (Top View wireframe)."), - EnumMember("SURFACE_WIREFRAME", 84, "3D Surface (wireframe)."), - EnumMember("XY_SCATTER", -4169, "Scatter."), - EnumMember("XY_SCATTER_LINES", 74, "Scatter with Lines."), - EnumMember( - "XY_SCATTER_LINES_NO_MARKERS", - 75, - "Scatter with Lines and No Da" "ta Markers.", - ), - EnumMember("XY_SCATTER_SMOOTH", 72, "Scatter with Smoothed Lines."), - EnumMember( - "XY_SCATTER_SMOOTH_NO_MARKERS", - 73, - "Scatter with Smoothed Lines" " and No Data Markers.", - ), - ) + THREE_D_AREA = (-4098, "3D Area.") + """3D Area.""" + THREE_D_AREA_STACKED = (78, "3D Stacked Area.") + """3D Stacked Area.""" -@alias("XL_LABEL_POSITION") -class XL_DATA_LABEL_POSITION(XmlEnumeration): - """ - Specifies where the data label is positioned. + THREE_D_AREA_STACKED_100 = (79, "100% Stacked Area.") + """100% Stacked Area.""" + + THREE_D_BAR_CLUSTERED = (60, "3D Clustered Bar.") + """3D Clustered Bar.""" + + THREE_D_BAR_STACKED = (61, "3D Stacked Bar.") + """3D Stacked Bar.""" + + THREE_D_BAR_STACKED_100 = (62, "3D 100% Stacked Bar.") + """3D 100% Stacked Bar.""" + + THREE_D_COLUMN = (-4100, "3D Column.") + """3D Column.""" + + THREE_D_COLUMN_CLUSTERED = (54, "3D Clustered Column.") + """3D Clustered Column.""" + + THREE_D_COLUMN_STACKED = (55, "3D Stacked Column.") + """3D Stacked Column.""" + + THREE_D_COLUMN_STACKED_100 = (56, "3D 100% Stacked Column.") + """3D 100% Stacked Column.""" + + THREE_D_LINE = (-4101, "3D Line.") + """3D Line.""" + + THREE_D_PIE = (-4102, "3D Pie.") + """3D Pie.""" + + THREE_D_PIE_EXPLODED = (70, "Exploded 3D Pie.") + """Exploded 3D Pie.""" + + AREA = (1, "Area") + """Area""" + + AREA_STACKED = (76, "Stacked Area.") + """Stacked Area.""" + + AREA_STACKED_100 = (77, "100% Stacked Area.") + """100% Stacked Area.""" + + BAR_CLUSTERED = (57, "Clustered Bar.") + """Clustered Bar.""" + + BAR_OF_PIE = (71, "Bar of Pie.") + """Bar of Pie.""" + + BAR_STACKED = (58, "Stacked Bar.") + """Stacked Bar.""" + + BAR_STACKED_100 = (59, "100% Stacked Bar.") + """100% Stacked Bar.""" + + BUBBLE = (15, "Bubble.") + """Bubble.""" + + BUBBLE_THREE_D_EFFECT = (87, "Bubble with 3D effects.") + """Bubble with 3D effects.""" + + COLUMN_CLUSTERED = (51, "Clustered Column.") + """Clustered Column.""" + + COLUMN_STACKED = (52, "Stacked Column.") + """Stacked Column.""" + + COLUMN_STACKED_100 = (53, "100% Stacked Column.") + """100% Stacked Column.""" + + CONE_BAR_CLUSTERED = (102, "Clustered Cone Bar.") + """Clustered Cone Bar.""" + + CONE_BAR_STACKED = (103, "Stacked Cone Bar.") + """Stacked Cone Bar.""" + + CONE_BAR_STACKED_100 = (104, "100% Stacked Cone Bar.") + """100% Stacked Cone Bar.""" + + CONE_COL = (105, "3D Cone Column.") + """3D Cone Column.""" + + CONE_COL_CLUSTERED = (99, "Clustered Cone Column.") + """Clustered Cone Column.""" + + CONE_COL_STACKED = (100, "Stacked Cone Column.") + """Stacked Cone Column.""" + + CONE_COL_STACKED_100 = (101, "100% Stacked Cone Column.") + """100% Stacked Cone Column.""" + + CYLINDER_BAR_CLUSTERED = (95, "Clustered Cylinder Bar.") + """Clustered Cylinder Bar.""" + + CYLINDER_BAR_STACKED = (96, "Stacked Cylinder Bar.") + """Stacked Cylinder Bar.""" + + CYLINDER_BAR_STACKED_100 = (97, "100% Stacked Cylinder Bar.") + """100% Stacked Cylinder Bar.""" + + CYLINDER_COL = (98, "3D Cylinder Column.") + """3D Cylinder Column.""" + + CYLINDER_COL_CLUSTERED = (92, "Clustered Cone Column.") + """Clustered Cone Column.""" + + CYLINDER_COL_STACKED = (93, "Stacked Cone Column.") + """Stacked Cone Column.""" + + CYLINDER_COL_STACKED_100 = (94, "100% Stacked Cylinder Column.") + """100% Stacked Cylinder Column.""" + + DOUGHNUT = (-4120, "Doughnut.") + """Doughnut.""" + + DOUGHNUT_EXPLODED = (80, "Exploded Doughnut.") + """Exploded Doughnut.""" + + LINE = (4, "Line.") + """Line.""" + + LINE_MARKERS = (65, "Line with Markers.") + """Line with Markers.""" + + LINE_MARKERS_STACKED = (66, "Stacked Line with Markers.") + """Stacked Line with Markers.""" + + LINE_MARKERS_STACKED_100 = (67, "100% Stacked Line with Markers.") + """100% Stacked Line with Markers.""" + + LINE_STACKED = (63, "Stacked Line.") + """Stacked Line.""" + + LINE_STACKED_100 = (64, "100% Stacked Line.") + """100% Stacked Line.""" + + PIE = (5, "Pie.") + """Pie.""" + + PIE_EXPLODED = (69, "Exploded Pie.") + """Exploded Pie.""" + + PIE_OF_PIE = (68, "Pie of Pie.") + """Pie of Pie.""" + + PYRAMID_BAR_CLUSTERED = (109, "Clustered Pyramid Bar.") + """Clustered Pyramid Bar.""" + + PYRAMID_BAR_STACKED = (110, "Stacked Pyramid Bar.") + """Stacked Pyramid Bar.""" + + PYRAMID_BAR_STACKED_100 = (111, "100% Stacked Pyramid Bar.") + """100% Stacked Pyramid Bar.""" + + PYRAMID_COL = (112, "3D Pyramid Column.") + """3D Pyramid Column.""" + + PYRAMID_COL_CLUSTERED = (106, "Clustered Pyramid Column.") + """Clustered Pyramid Column.""" + + PYRAMID_COL_STACKED = (107, "Stacked Pyramid Column.") + """Stacked Pyramid Column.""" + + PYRAMID_COL_STACKED_100 = (108, "100% Stacked Pyramid Column.") + """100% Stacked Pyramid Column.""" + + RADAR = (-4151, "Radar.") + """Radar.""" + + RADAR_FILLED = (82, "Filled Radar.") + """Filled Radar.""" + + RADAR_MARKERS = (81, "Radar with Data Markers.") + """Radar with Data Markers.""" + + STOCK_HLC = (88, "High-Low-Close.") + """High-Low-Close.""" + + STOCK_OHLC = (89, "Open-High-Low-Close.") + """Open-High-Low-Close.""" + + STOCK_VHLC = (90, "Volume-High-Low-Close.") + """Volume-High-Low-Close.""" + + STOCK_VOHLC = (91, "Volume-Open-High-Low-Close.") + """Volume-Open-High-Low-Close.""" + + SURFACE = (83, "3D Surface.") + """3D Surface.""" + + SURFACE_TOP_VIEW = (85, "Surface (Top View).") + """Surface (Top View).""" + + SURFACE_TOP_VIEW_WIREFRAME = (86, "Surface (Top View wireframe).") + """Surface (Top View wireframe).""" + + SURFACE_WIREFRAME = (84, "3D Surface (wireframe).") + """3D Surface (wireframe).""" + + XY_SCATTER = (-4169, "Scatter.") + """Scatter.""" + + XY_SCATTER_LINES = (74, "Scatter with Lines.") + """Scatter with Lines.""" + + XY_SCATTER_LINES_NO_MARKERS = (75, "Scatter with Lines and No Data Markers.") + """Scatter with Lines and No Data Markers.""" + + XY_SCATTER_SMOOTH = (72, "Scatter with Smoothed Lines.") + """Scatter with Smoothed Lines.""" + + XY_SCATTER_SMOOTH_NO_MARKERS = (73, "Scatter with Smoothed Lines and No Data Markers.") + """Scatter with Smoothed Lines and No Data Markers.""" + + +class XL_DATA_LABEL_POSITION(BaseXmlEnum): + """Specifies where the data label is positioned. Example:: @@ -194,66 +300,57 @@ class XL_DATA_LABEL_POSITION(XmlEnumeration): data_labels = chart.plots[0].data_labels data_labels.position = XL_LABEL_POSITION.OUTSIDE_END + + MS API Name: `XlDataLabelPosition` + + http://msdn.microsoft.com/en-us/library/office/ff745082.aspx """ - __ms_name__ = "XlDataLabelPosition" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff745082.aspx" - - __members__ = ( - XmlMappedEnumMember( - "ABOVE", 0, "t", "The data label is positioned above the data point." - ), - XmlMappedEnumMember( - "BELOW", 1, "b", "The data label is positioned below the data point." - ), - XmlMappedEnumMember( - "BEST_FIT", 5, "bestFit", "Word sets the position of the data label." - ), - XmlMappedEnumMember( - "CENTER", - -4108, - "ctr", - "The data label is centered on the data point or inside a bar or a pie " - "slice.", - ), - XmlMappedEnumMember( - "INSIDE_BASE", - 4, - "inBase", - "The data label is positioned inside the data point at the bottom edge.", - ), - XmlMappedEnumMember( - "INSIDE_END", - 3, - "inEnd", - "The data label is positioned inside the data point at the top edge.", - ), - XmlMappedEnumMember( - "LEFT", - -4131, - "l", - "The data label is positioned to the left of the data point.", - ), - ReturnValueOnlyEnumMember("MIXED", 6, "Data labels are in multiple positions."), - XmlMappedEnumMember( - "OUTSIDE_END", - 2, - "outEnd", - "The data label is positioned outside the data point at the top edge.", - ), - XmlMappedEnumMember( - "RIGHT", - -4152, - "r", - "The data label is positioned to the right of the data point.", - ), + ABOVE = (0, "t", "The data label is positioned above the data point.") + """The data label is positioned above the data point.""" + + BELOW = (1, "b", "The data label is positioned below the data point.") + """The data label is positioned below the data point.""" + + BEST_FIT = (5, "bestFit", "Word sets the position of the data label.") + """Word sets the position of the data label.""" + + CENTER = ( + -4108, + "ctr", + "The data label is centered on the data point or inside a bar or a pie slice.", + ) + """The data label is centered on the data point or inside a bar or a pie slice.""" + + INSIDE_BASE = ( + 4, + "inBase", + "The data label is positioned inside the data point at the bottom edge.", ) + """The data label is positioned inside the data point at the bottom edge.""" + INSIDE_END = (3, "inEnd", "The data label is positioned inside the data point at the top edge.") + """The data label is positioned inside the data point at the top edge.""" + + LEFT = (-4131, "l", "The data label is positioned to the left of the data point.") + """The data label is positioned to the left of the data point.""" + + OUTSIDE_END = ( + 2, + "outEnd", + "The data label is positioned outside the data point at the top edge.", + ) + """The data label is positioned outside the data point at the top edge.""" + + RIGHT = (-4152, "r", "The data label is positioned to the right of the data point.") + """The data label is positioned to the right of the data point.""" -class XL_LEGEND_POSITION(XmlEnumeration): - """ - Specifies the position of the legend on a chart. + +XL_LABEL_POSITION = XL_DATA_LABEL_POSITION + + +class XL_LEGEND_POSITION(BaseXmlEnum): + """Specifies the position of the legend on a chart. Example:: @@ -261,82 +358,111 @@ class XL_LEGEND_POSITION(XmlEnumeration): chart.has_legend = True chart.legend.position = XL_LEGEND_POSITION.BOTTOM + + MS API Name: `XlLegendPosition` + + http://msdn.microsoft.com/en-us/library/office/ff745840.aspx """ - __ms_name__ = "XlLegendPosition" + BOTTOM = (-4107, "b", "Below the chart.") + """Below the chart.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff745840.aspx" + CORNER = (2, "tr", "In the upper-right corner of the chart border.") + """In the upper-right corner of the chart border.""" - __members__ = ( - XmlMappedEnumMember("BOTTOM", -4107, "b", "Below the chart."), - XmlMappedEnumMember( - "CORNER", 2, "tr", "In the upper-right corner of the chart borde" "r." - ), - ReturnValueOnlyEnumMember("CUSTOM", -4161, "A custom position."), - XmlMappedEnumMember("LEFT", -4131, "l", "Left of the chart."), - XmlMappedEnumMember("RIGHT", -4152, "r", "Right of the chart."), - XmlMappedEnumMember("TOP", -4160, "t", "Above the chart."), - ) + CUSTOM = (-4161, "", "A custom position.") + """A custom position.""" + LEFT = (-4131, "l", "Left of the chart.") + """Left of the chart.""" + + RIGHT = (-4152, "r", "Right of the chart.") + """Right of the chart.""" + + TOP = (-4160, "t", "Above the chart.") + """Above the chart.""" -class XL_MARKER_STYLE(XmlEnumeration): - """ - Specifies the marker style for a point or series in a line chart, scatter - chart, or radar chart. + +class XL_MARKER_STYLE(BaseXmlEnum): + """Specifies the marker style for a point or series in a line, scatter, or radar chart. Example:: from pptx.enum.chart import XL_MARKER_STYLE series.marker.style = XL_MARKER_STYLE.CIRCLE + + MS API Name: `XlMarkerStyle` + + http://msdn.microsoft.com/en-us/library/office/ff197219.aspx """ - __ms_name__ = "XlMarkerStyle" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff197219.aspx" - - __members__ = ( - XmlMappedEnumMember("AUTOMATIC", -4105, "auto", "Automatic markers"), - XmlMappedEnumMember("CIRCLE", 8, "circle", "Circular markers"), - XmlMappedEnumMember("DASH", -4115, "dash", "Long bar markers"), - XmlMappedEnumMember("DIAMOND", 2, "diamond", "Diamond-shaped markers"), - XmlMappedEnumMember("DOT", -4118, "dot", "Short bar markers"), - XmlMappedEnumMember("NONE", -4142, "none", "No markers"), - XmlMappedEnumMember("PICTURE", -4147, "picture", "Picture markers"), - XmlMappedEnumMember("PLUS", 9, "plus", "Square markers with a plus sign"), - XmlMappedEnumMember("SQUARE", 1, "square", "Square markers"), - XmlMappedEnumMember("STAR", 5, "star", "Square markers with an asterisk"), - XmlMappedEnumMember("TRIANGLE", 3, "triangle", "Triangular markers"), - XmlMappedEnumMember("X", -4168, "x", "Square markers with an X"), - ) + AUTOMATIC = (-4105, "auto", "Automatic markers") + """Automatic markers""" + CIRCLE = (8, "circle", "Circular markers") + """Circular markers""" -class XL_TICK_MARK(XmlEnumeration): - """ - Specifies a type of axis tick for a chart. + DASH = (-4115, "dash", "Long bar markers") + """Long bar markers""" + + DIAMOND = (2, "diamond", "Diamond-shaped markers") + """Diamond-shaped markers""" + + DOT = (-4118, "dot", "Short bar markers") + """Short bar markers""" + + NONE = (-4142, "none", "No markers") + """No markers""" + + PICTURE = (-4147, "picture", "Picture markers") + """Picture markers""" + + PLUS = (9, "plus", "Square markers with a plus sign") + """Square markers with a plus sign""" + + SQUARE = (1, "square", "Square markers") + """Square markers""" + + STAR = (5, "star", "Square markers with an asterisk") + """Square markers with an asterisk""" + + TRIANGLE = (3, "triangle", "Triangular markers") + """Triangular markers""" + + X = (-4168, "x", "Square markers with an X") + """Square markers with an X""" + + +class XL_TICK_MARK(BaseXmlEnum): + """Specifies a type of axis tick for a chart. Example:: from pptx.enum.chart import XL_TICK_MARK chart.value_axis.minor_tick_mark = XL_TICK_MARK.INSIDE + + MS API Name: `XlTickMark` + + http://msdn.microsoft.com/en-us/library/office/ff193878.aspx """ - __ms_name__ = "XlTickMark" + CROSS = (4, "cross", "Tick mark crosses the axis") + """Tick mark crosses the axis""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff193878.aspx" + INSIDE = (2, "in", "Tick mark appears inside the axis") + """Tick mark appears inside the axis""" - __members__ = ( - XmlMappedEnumMember("CROSS", 4, "cross", "Tick mark crosses the axis"), - XmlMappedEnumMember("INSIDE", 2, "in", "Tick mark appears inside the axis"), - XmlMappedEnumMember("NONE", -4142, "none", "No tick mark"), - XmlMappedEnumMember("OUTSIDE", 3, "out", "Tick mark appears outside the axis"), - ) + NONE = (-4142, "none", "No tick mark") + """No tick mark""" + OUTSIDE = (3, "out", "Tick mark appears outside the axis") + """Tick mark appears outside the axis""" -class XL_TICK_LABEL_POSITION(XmlEnumeration): - """ - Specifies the position of tick-mark labels on a chart axis. + +class XL_TICK_LABEL_POSITION(BaseXmlEnum): + """Specifies the position of tick-mark labels on a chart axis. Example:: @@ -344,20 +470,20 @@ class XL_TICK_LABEL_POSITION(XmlEnumeration): category_axis = chart.category_axis category_axis.tick_label_position = XL_TICK_LABEL_POSITION.LOW + + MS API Name: `XlTickLabelPosition` + + http://msdn.microsoft.com/en-us/library/office/ff822561.aspx """ - __ms_name__ = "XlTickLabelPosition" + HIGH = (-4127, "high", "Top or right side of the chart.") + """Top or right side of the chart.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff822561.aspx" + LOW = (-4134, "low", "Bottom or left side of the chart.") + """Bottom or left side of the chart.""" - __members__ = ( - XmlMappedEnumMember("HIGH", -4127, "high", "Top or right side of the chart."), - XmlMappedEnumMember("LOW", -4134, "low", "Bottom or left side of the chart."), - XmlMappedEnumMember( - "NEXT_TO_AXIS", - 4, - "nextTo", - "Next to axis (where axis is not at" " either side of the chart).", - ), - XmlMappedEnumMember("NONE", -4142, "none", "No tick labels."), - ) + NEXT_TO_AXIS = (4, "nextTo", "Next to axis (where axis is not at either side of the chart).") + """Next to axis (where axis is not at either side of the chart).""" + + NONE = (-4142, "none", "No tick labels.") + """No tick labels.""" diff --git a/src/pptx/enum/dml.py b/src/pptx/enum/dml.py index 765fe2dea..e9a14b13f 100644 --- a/src/pptx/enum/dml.py +++ b/src/pptx/enum/dml.py @@ -1,20 +1,11 @@ -# encoding: utf-8 - """Enumerations used by DrawingML objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) +from pptx.enum.base import BaseEnum, BaseXmlEnum -class MSO_COLOR_TYPE(Enumeration): +class MSO_COLOR_TYPE(BaseEnum): """ Specifies the color specification scheme @@ -23,51 +14,35 @@ class MSO_COLOR_TYPE(Enumeration): from pptx.enum.dml import MSO_COLOR_TYPE assert shape.fill.fore_color.type == MSO_COLOR_TYPE.SCHEME + + MS API Name: "MsoColorType" + + http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15).aspx """ - __ms_name__ = "MsoColorType" + RGB = (1, "Color is specified by an |RGBColor| value.") + """Color is specified by an |RGBColor| value.""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15" ").aspx" - ) + SCHEME = (2, "Color is one of the preset theme colors") + """Color is one of the preset theme colors""" + + HSL = (101, "Color is specified using Hue, Saturation, and Luminosity values") + """Color is specified using Hue, Saturation, and Luminosity values""" + + PRESET = (102, "Color is specified using a named built-in color") + """Color is specified using a named built-in color""" + + SCRGB = (103, "Color is an scRGB color, a wide color gamut RGB color space") + """Color is an scRGB color, a wide color gamut RGB color space""" - __members__ = ( - EnumMember("RGB", 1, "Color is specified by an |RGBColor| value"), - EnumMember("SCHEME", 2, "Color is one of the preset theme colors"), - EnumMember( - "HSL", - 101, - """ - Color is specified using Hue, Saturation, and Luminosity values - """, - ), - EnumMember( - "PRESET", - 102, - """ - Color is specified using a named built-in color - """, - ), - EnumMember( - "SCRGB", - 103, - """ - Color is an scRGB color, a wide color gamut RGB color space - """, - ), - EnumMember( - "SYSTEM", - 104, - """ - Color is one specified by the operating system, such as the - window background color. - """, - ), + SYSTEM = ( + 104, + "Color is one specified by the operating system, such as the window background color.", ) + """Color is one specified by the operating system, such as the window background color.""" -@alias("MSO_FILL") -class MSO_FILL_TYPE(Enumeration): +class MSO_FILL_TYPE(BaseEnum): """ Specifies the type of bitmap used for the fill of a shape. @@ -78,38 +53,46 @@ class MSO_FILL_TYPE(Enumeration): from pptx.enum.dml import MSO_FILL assert shape.fill.type == MSO_FILL.SOLID + + MS API Name: `MsoFillType` + + http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx """ - __ms_name__ = "MsoFillType" - - __url__ = "http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx" - - __members__ = ( - EnumMember( - "BACKGROUND", - 5, - """ - The shape is transparent, such that whatever is behind the shape - shows through. Often this is the slide background, but if - a visible shape is behind, that will show through. - """, - ), - EnumMember("GRADIENT", 3, "Shape is filled with a gradient"), - EnumMember( - "GROUP", - 101, - "Shape is part of a group and should inherit the " - "fill properties of the group.", - ), - EnumMember("PATTERNED", 2, "Shape is filled with a pattern"), - EnumMember("PICTURE", 6, "Shape is filled with a bitmapped image"), - EnumMember("SOLID", 1, "Shape is filled with a solid color"), - EnumMember("TEXTURED", 4, "Shape is filled with a texture"), + BACKGROUND = ( + 5, + "The shape is transparent, such that whatever is behind the shape shows through." + " Often this is the slide background, but if a visible shape is behind, that will" + " show through.", ) + """The shape is transparent, such that whatever is behind the shape shows through. + + Often this is the slide background, but if a visible shape is behind, that will show through. + """ + + GRADIENT = (3, "Shape is filled with a gradient") + """Shape is filled with a gradient""" + + GROUP = (101, "Shape is part of a group and should inherit the fill properties of the group.") + """Shape is part of a group and should inherit the fill properties of the group.""" + + PATTERNED = (2, "Shape is filled with a pattern") + """Shape is filled with a pattern""" + PICTURE = (6, "Shape is filled with a bitmapped image") + """Shape is filled with a bitmapped image""" -@alias("MSO_LINE") -class MSO_LINE_DASH_STYLE(XmlEnumeration): + SOLID = (1, "Shape is filled with a solid color") + """Shape is filled with a solid color""" + + TEXTURED = (4, "Shape is filled with a texture") + """Shape is filled with a texture""" + + +MSO_FILL = MSO_FILL_TYPE + + +class MSO_LINE_DASH_STYLE(BaseXmlEnum): """Specifies the dash style for a line. Alias: ``MSO_LINE`` @@ -119,36 +102,44 @@ class MSO_LINE_DASH_STYLE(XmlEnumeration): from pptx.enum.dml import MSO_LINE shape.line.dash_style = MSO_LINE.DASH_DOT_DOT + + MS API name: `MsoLineDashStyle` + + https://learn.microsoft.com/en-us/office/vba/api/Office.MsoLineDashStyle """ - __ms_name__ = "MsoLineDashStyle" + DASH = (4, "dash", "Line consists of dashes only.") + """Line consists of dashes only.""" - __url__ = ( - "https://msdn.microsoft.com/en-us/vba/office-shared-vba/articles/mso" - "linedashstyle-enumeration-office" - ) + DASH_DOT = (5, "dashDot", "Line is a dash-dot pattern.") + """Line is a dash-dot pattern.""" - __members__ = ( - XmlMappedEnumMember("DASH", 4, "dash", "Line consists of dashes only."), - XmlMappedEnumMember("DASH_DOT", 5, "dashDot", "Line is a dash-dot pattern."), - XmlMappedEnumMember( - "DASH_DOT_DOT", 6, "lgDashDotDot", "Line is a dash-dot-dot patte" "rn." - ), - XmlMappedEnumMember("LONG_DASH", 7, "lgDash", "Line consists of long dashes."), - XmlMappedEnumMember( - "LONG_DASH_DOT", 8, "lgDashDot", "Line is a long dash-dot patter" "n." - ), - XmlMappedEnumMember("ROUND_DOT", 3, "dot", "Line is made up of round dots."), - XmlMappedEnumMember("SOLID", 1, "solid", "Line is solid."), - XmlMappedEnumMember( - "SQUARE_DOT", 2, "sysDash", "Line is made up of square dots." - ), - ReturnValueOnlyEnumMember("DASH_STYLE_MIXED", -2, "Not supported."), - ) + DASH_DOT_DOT = (6, "lgDashDotDot", "Line is a dash-dot-dot pattern.") + """Line is a dash-dot-dot pattern.""" + + LONG_DASH = (7, "lgDash", "Line consists of long dashes.") + """Line consists of long dashes.""" + + LONG_DASH_DOT = (8, "lgDashDot", "Line is a long dash-dot pattern.") + """Line is a long dash-dot pattern.""" + + ROUND_DOT = (3, "sysDot", "Line is made up of round dots.") + """Line is made up of round dots.""" + + SOLID = (1, "solid", "Line is solid.") + """Line is solid.""" + + SQUARE_DOT = (2, "sysDash", "Line is made up of square dots.") + """Line is made up of square dots.""" + DASH_STYLE_MIXED = (-2, "", "Not supported.") + """Return value only, indicating more than one dash style applies.""" -@alias("MSO_PATTERN") -class MSO_PATTERN_TYPE(XmlEnumeration): + +MSO_LINE = MSO_LINE_DASH_STYLE + + +class MSO_PATTERN_TYPE(BaseXmlEnum): """Specifies the fill pattern used in a shape. Alias: ``MSO_PATTERN`` @@ -160,99 +151,183 @@ class MSO_PATTERN_TYPE(XmlEnumeration): fill = shape.fill fill.patterned() fill.pattern = MSO_PATTERN.WAVE + + MS API Name: `MsoPatternType` + + https://learn.microsoft.com/en-us/office/vba/api/Office.MsoPatternType """ - __ms_name__ = "MsoPatternType" + CROSS = (51, "cross", "Cross") + """Cross""" - __url__ = ( - "https://msdn.microsoft.com/VBA/Office-Shared-VBA/articles/msopatter" - "ntype-enumeration-office" - ) + DARK_DOWNWARD_DIAGONAL = (15, "dkDnDiag", "Dark Downward Diagonal") + """Dark Downward Diagonal""" - __members__ = ( - XmlMappedEnumMember("CROSS", 51, "cross", "Cross"), - XmlMappedEnumMember( - "DARK_DOWNWARD_DIAGONAL", 15, "dkDnDiag", "Dark Downward Diagona" "l" - ), - XmlMappedEnumMember("DARK_HORIZONTAL", 13, "dkHorz", "Dark Horizontal"), - XmlMappedEnumMember( - "DARK_UPWARD_DIAGONAL", 16, "dkUpDiag", "Dark Upward Diagonal" - ), - XmlMappedEnumMember("DARK_VERTICAL", 14, "dkVert", "Dark Vertical"), - XmlMappedEnumMember( - "DASHED_DOWNWARD_DIAGONAL", 28, "dashDnDiag", "Dashed Downward D" "iagonal" - ), - XmlMappedEnumMember("DASHED_HORIZONTAL", 32, "dashHorz", "Dashed Horizontal"), - XmlMappedEnumMember( - "DASHED_UPWARD_DIAGONAL", 27, "dashUpDiag", "Dashed Upward Diago" "nal" - ), - XmlMappedEnumMember("DASHED_VERTICAL", 31, "dashVert", "Dashed Vertical"), - XmlMappedEnumMember("DIAGONAL_BRICK", 40, "diagBrick", "Diagonal Brick"), - XmlMappedEnumMember("DIAGONAL_CROSS", 54, "diagCross", "Diagonal Cross"), - XmlMappedEnumMember("DIVOT", 46, "divot", "Pattern Divot"), - XmlMappedEnumMember("DOTTED_DIAMOND", 24, "dotDmnd", "Dotted Diamond"), - XmlMappedEnumMember("DOTTED_GRID", 45, "dotGrid", "Dotted Grid"), - XmlMappedEnumMember("DOWNWARD_DIAGONAL", 52, "dnDiag", "Downward Diagonal"), - XmlMappedEnumMember("HORIZONTAL", 49, "horz", "Horizontal"), - XmlMappedEnumMember("HORIZONTAL_BRICK", 35, "horzBrick", "Horizontal Brick"), - XmlMappedEnumMember( - "LARGE_CHECKER_BOARD", 36, "lgCheck", "Large Checker Board" - ), - XmlMappedEnumMember("LARGE_CONFETTI", 33, "lgConfetti", "Large Confetti"), - XmlMappedEnumMember("LARGE_GRID", 34, "lgGrid", "Large Grid"), - XmlMappedEnumMember( - "LIGHT_DOWNWARD_DIAGONAL", 21, "ltDnDiag", "Light Downward Diago" "nal" - ), - XmlMappedEnumMember("LIGHT_HORIZONTAL", 19, "ltHorz", "Light Horizontal"), - XmlMappedEnumMember( - "LIGHT_UPWARD_DIAGONAL", 22, "ltUpDiag", "Light Upward Diagonal" - ), - XmlMappedEnumMember("LIGHT_VERTICAL", 20, "ltVert", "Light Vertical"), - XmlMappedEnumMember("NARROW_HORIZONTAL", 30, "narHorz", "Narrow Horizontal"), - XmlMappedEnumMember("NARROW_VERTICAL", 29, "narVert", "Narrow Vertical"), - XmlMappedEnumMember("OUTLINED_DIAMOND", 41, "openDmnd", "Outlined Diamond"), - XmlMappedEnumMember("PERCENT_10", 2, "pct10", "10% of the foreground color."), - XmlMappedEnumMember("PERCENT_20", 3, "pct20", "20% of the foreground color."), - XmlMappedEnumMember("PERCENT_25", 4, "pct25", "25% of the foreground color."), - XmlMappedEnumMember("PERCENT_30", 5, "pct30", "30% of the foreground color."), - XmlMappedEnumMember("PERCENT_40", 6, "pct40", "40% of the foreground color."), - XmlMappedEnumMember("PERCENT_5", 1, "pct5", "5% of the foreground color."), - XmlMappedEnumMember("PERCENT_50", 7, "pct50", "50% of the foreground color."), - XmlMappedEnumMember("PERCENT_60", 8, "pct60", "60% of the foreground color."), - XmlMappedEnumMember("PERCENT_70", 9, "pct70", "70% of the foreground color."), - XmlMappedEnumMember("PERCENT_75", 10, "pct75", "75% of the foreground color."), - XmlMappedEnumMember("PERCENT_80", 11, "pct80", "80% of the foreground color."), - XmlMappedEnumMember("PERCENT_90", 12, "pct90", "90% of the foreground color."), - XmlMappedEnumMember("PLAID", 42, "plaid", "Plaid"), - XmlMappedEnumMember("SHINGLE", 47, "shingle", "Shingle"), - XmlMappedEnumMember( - "SMALL_CHECKER_BOARD", 17, "smCheck", "Small Checker Board" - ), - XmlMappedEnumMember("SMALL_CONFETTI", 37, "smConfetti", "Small Confetti"), - XmlMappedEnumMember("SMALL_GRID", 23, "smGrid", "Small Grid"), - XmlMappedEnumMember("SOLID_DIAMOND", 39, "solidDmnd", "Solid Diamond"), - XmlMappedEnumMember("SPHERE", 43, "sphere", "Sphere"), - XmlMappedEnumMember("TRELLIS", 18, "trellis", "Trellis"), - XmlMappedEnumMember("UPWARD_DIAGONAL", 53, "upDiag", "Upward Diagonal"), - XmlMappedEnumMember("VERTICAL", 50, "vert", "Vertical"), - XmlMappedEnumMember("WAVE", 48, "wave", "Wave"), - XmlMappedEnumMember("WEAVE", 44, "weave", "Weave"), - XmlMappedEnumMember( - "WIDE_DOWNWARD_DIAGONAL", 25, "wdDnDiag", "Wide Downward Diagona" "l" - ), - XmlMappedEnumMember( - "WIDE_UPWARD_DIAGONAL", 26, "wdUpDiag", "Wide Upward Diagonal" - ), - XmlMappedEnumMember("ZIG_ZAG", 38, "zigZag", "Zig Zag"), - ReturnValueOnlyEnumMember("MIXED", -2, "Mixed pattern."), - ) + DARK_HORIZONTAL = (13, "dkHorz", "Dark Horizontal") + """Dark Horizontal""" + DARK_UPWARD_DIAGONAL = (16, "dkUpDiag", "Dark Upward Diagonal") + """Dark Upward Diagonal""" -@alias("MSO_THEME_COLOR") -class MSO_THEME_COLOR_INDEX(XmlEnumeration): - """ - Indicates the Office theme color, one of those shown in the color gallery - on the formatting ribbon. + DARK_VERTICAL = (14, "dkVert", "Dark Vertical") + """Dark Vertical""" + + DASHED_DOWNWARD_DIAGONAL = (28, "dashDnDiag", "Dashed Downward Diagonal") + """Dashed Downward Diagonal""" + + DASHED_HORIZONTAL = (32, "dashHorz", "Dashed Horizontal") + """Dashed Horizontal""" + + DASHED_UPWARD_DIAGONAL = (27, "dashUpDiag", "Dashed Upward Diagonal") + """Dashed Upward Diagonal""" + + DASHED_VERTICAL = (31, "dashVert", "Dashed Vertical") + """Dashed Vertical""" + + DIAGONAL_BRICK = (40, "diagBrick", "Diagonal Brick") + """Diagonal Brick""" + + DIAGONAL_CROSS = (54, "diagCross", "Diagonal Cross") + """Diagonal Cross""" + + DIVOT = (46, "divot", "Pattern Divot") + """Pattern Divot""" + + DOTTED_DIAMOND = (24, "dotDmnd", "Dotted Diamond") + """Dotted Diamond""" + + DOTTED_GRID = (45, "dotGrid", "Dotted Grid") + """Dotted Grid""" + + DOWNWARD_DIAGONAL = (52, "dnDiag", "Downward Diagonal") + """Downward Diagonal""" + + HORIZONTAL = (49, "horz", "Horizontal") + """Horizontal""" + + HORIZONTAL_BRICK = (35, "horzBrick", "Horizontal Brick") + """Horizontal Brick""" + + LARGE_CHECKER_BOARD = (36, "lgCheck", "Large Checker Board") + """Large Checker Board""" + + LARGE_CONFETTI = (33, "lgConfetti", "Large Confetti") + """Large Confetti""" + + LARGE_GRID = (34, "lgGrid", "Large Grid") + """Large Grid""" + + LIGHT_DOWNWARD_DIAGONAL = (21, "ltDnDiag", "Light Downward Diagonal") + """Light Downward Diagonal""" + + LIGHT_HORIZONTAL = (19, "ltHorz", "Light Horizontal") + """Light Horizontal""" + + LIGHT_UPWARD_DIAGONAL = (22, "ltUpDiag", "Light Upward Diagonal") + """Light Upward Diagonal""" + + LIGHT_VERTICAL = (20, "ltVert", "Light Vertical") + """Light Vertical""" + + NARROW_HORIZONTAL = (30, "narHorz", "Narrow Horizontal") + """Narrow Horizontal""" + + NARROW_VERTICAL = (29, "narVert", "Narrow Vertical") + """Narrow Vertical""" + + OUTLINED_DIAMOND = (41, "openDmnd", "Outlined Diamond") + """Outlined Diamond""" + + PERCENT_10 = (2, "pct10", "10% of the foreground color.") + """10% of the foreground color.""" + + PERCENT_20 = (3, "pct20", "20% of the foreground color.") + """20% of the foreground color.""" + + PERCENT_25 = (4, "pct25", "25% of the foreground color.") + """25% of the foreground color.""" + + PERCENT_30 = (5, "pct30", "30% of the foreground color.") + """30% of the foreground color.""" + + ERCENT_40 = (6, "pct40", "40% of the foreground color.") + """40% of the foreground color.""" + + PERCENT_5 = (1, "pct5", "5% of the foreground color.") + """5% of the foreground color.""" + + PERCENT_50 = (7, "pct50", "50% of the foreground color.") + """50% of the foreground color.""" + + PERCENT_60 = (8, "pct60", "60% of the foreground color.") + """60% of the foreground color.""" + + PERCENT_70 = (9, "pct70", "70% of the foreground color.") + """70% of the foreground color.""" + + PERCENT_75 = (10, "pct75", "75% of the foreground color.") + """75% of the foreground color.""" + + PERCENT_80 = (11, "pct80", "80% of the foreground color.") + """80% of the foreground color.""" + + PERCENT_90 = (12, "pct90", "90% of the foreground color.") + """90% of the foreground color.""" + + PLAID = (42, "plaid", "Plaid") + """Plaid""" + + SHINGLE = (47, "shingle", "Shingle") + """Shingle""" + + SMALL_CHECKER_BOARD = (17, "smCheck", "Small Checker Board") + """Small Checker Board""" + + SMALL_CONFETTI = (37, "smConfetti", "Small Confetti") + """Small Confetti""" + + SMALL_GRID = (23, "smGrid", "Small Grid") + """Small Grid""" + + SOLID_DIAMOND = (39, "solidDmnd", "Solid Diamond") + """Solid Diamond""" + + SPHERE = (43, "sphere", "Sphere") + """Sphere""" + + TRELLIS = (18, "trellis", "Trellis") + """Trellis""" + + UPWARD_DIAGONAL = (53, "upDiag", "Upward Diagonal") + """Upward Diagonal""" + + VERTICAL = (50, "vert", "Vertical") + """Vertical""" + + WAVE = (48, "wave", "Wave") + """Wave""" + + WEAVE = (44, "weave", "Weave") + """Weave""" + + WIDE_DOWNWARD_DIAGONAL = (25, "wdDnDiag", "Wide Downward Diagonal") + """Wide Downward Diagonal""" + + WIDE_UPWARD_DIAGONAL = (26, "wdUpDiag", "Wide Upward Diagonal") + """Wide Upward Diagonal""" + + ZIG_ZAG = (38, "zigZag", "Zig Zag") + """Zig Zag""" + + MIXED = (-2, "", "Mixed pattern") + """Mixed pattern""" + + +MSO_PATTERN = MSO_PATTERN_TYPE + + +class MSO_THEME_COLOR_INDEX(BaseXmlEnum): + """An Office theme color, one of those shown in the color gallery on the formatting ribbon. Alias: ``MSO_THEME_COLOR`` @@ -262,58 +337,65 @@ class MSO_THEME_COLOR_INDEX(XmlEnumeration): shape.fill.solid() shape.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_1 + + MS API Name: `MsoThemeColorIndex` + + http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx """ - __ms_name__ = "MsoThemeColorIndex" + NOT_THEME_COLOR = (0, "", "Indicates the color is not a theme color.") + """Indicates the color is not a theme color.""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15" ").aspx" - ) + ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.") + """Specifies the Accent 1 theme color.""" - __members__ = ( - EnumMember("NOT_THEME_COLOR", 0, "Indicates the color is not a theme color."), - XmlMappedEnumMember( - "ACCENT_1", 5, "accent1", "Specifies the Accent 1 theme color." - ), - XmlMappedEnumMember( - "ACCENT_2", 6, "accent2", "Specifies the Accent 2 theme color." - ), - XmlMappedEnumMember( - "ACCENT_3", 7, "accent3", "Specifies the Accent 3 theme color." - ), - XmlMappedEnumMember( - "ACCENT_4", 8, "accent4", "Specifies the Accent 4 theme color." - ), - XmlMappedEnumMember( - "ACCENT_5", 9, "accent5", "Specifies the Accent 5 theme color." - ), - XmlMappedEnumMember( - "ACCENT_6", 10, "accent6", "Specifies the Accent 6 theme color." - ), - XmlMappedEnumMember( - "BACKGROUND_1", 14, "bg1", "Specifies the Background 1 theme " "color." - ), - XmlMappedEnumMember( - "BACKGROUND_2", 16, "bg2", "Specifies the Background 2 theme " "color." - ), - XmlMappedEnumMember("DARK_1", 1, "dk1", "Specifies the Dark 1 theme color."), - XmlMappedEnumMember("DARK_2", 3, "dk2", "Specifies the Dark 2 theme color."), - XmlMappedEnumMember( - "FOLLOWED_HYPERLINK", - 12, - "folHlink", - "Specifies the theme color" " for a clicked hyperlink.", - ), - XmlMappedEnumMember( - "HYPERLINK", 11, "hlink", "Specifies the theme color for a hyper" "link." - ), - XmlMappedEnumMember("LIGHT_1", 2, "lt1", "Specifies the Light 1 theme color."), - XmlMappedEnumMember("LIGHT_2", 4, "lt2", "Specifies the Light 2 theme color."), - XmlMappedEnumMember("TEXT_1", 13, "tx1", "Specifies the Text 1 theme color."), - XmlMappedEnumMember("TEXT_2", 15, "tx2", "Specifies the Text 2 theme color."), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Indicates multiple theme colors are used, such as " "in a group shape.", - ), - ) + ACCENT_2 = (6, "accent2", "Specifies the Accent 2 theme color.") + """Specifies the Accent 2 theme color.""" + + ACCENT_3 = (7, "accent3", "Specifies the Accent 3 theme color.") + """Specifies the Accent 3 theme color.""" + + ACCENT_4 = (8, "accent4", "Specifies the Accent 4 theme color.") + """Specifies the Accent 4 theme color.""" + + ACCENT_5 = (9, "accent5", "Specifies the Accent 5 theme color.") + """Specifies the Accent 5 theme color.""" + + ACCENT_6 = (10, "accent6", "Specifies the Accent 6 theme color.") + """Specifies the Accent 6 theme color.""" + + BACKGROUND_1 = (14, "bg1", "Specifies the Background 1 theme color.") + """Specifies the Background 1 theme color.""" + + BACKGROUND_2 = (16, "bg2", "Specifies the Background 2 theme color.") + """Specifies the Background 2 theme color.""" + + DARK_1 = (1, "dk1", "Specifies the Dark 1 theme color.") + """Specifies the Dark 1 theme color.""" + + DARK_2 = (3, "dk2", "Specifies the Dark 2 theme color.") + """Specifies the Dark 2 theme color.""" + + FOLLOWED_HYPERLINK = (12, "folHlink", "Specifies the theme color for a clicked hyperlink.") + """Specifies the theme color for a clicked hyperlink.""" + + HYPERLINK = (11, "hlink", "Specifies the theme color for a hyperlink.") + """Specifies the theme color for a hyperlink.""" + + LIGHT_1 = (2, "lt1", "Specifies the Light 1 theme color.") + """Specifies the Light 1 theme color.""" + + LIGHT_2 = (4, "lt2", "Specifies the Light 2 theme color.") + """Specifies the Light 2 theme color.""" + + TEXT_1 = (13, "tx1", "Specifies the Text 1 theme color.") + """Specifies the Text 1 theme color.""" + + TEXT_2 = (15, "tx2", "Specifies the Text 2 theme color.") + """Specifies the Text 2 theme color.""" + + MIXED = (-2, "", "Indicates multiple theme colors are used, such as in a group shape.") + """Indicates multiple theme colors are used, such as in a group shape.""" + + +MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX diff --git a/src/pptx/enum/lang.py b/src/pptx/enum/lang.py index f466218ec..d2da5b6a5 100644 --- a/src/pptx/enum/lang.py +++ b/src/pptx/enum/lang.py @@ -1,20 +1,11 @@ -# encoding: utf-8 +"""Enumerations used for specifying language.""" -""" -Enumerations used for specifying language. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from pptx.enum.base import BaseXmlEnum -from .base import ( - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) - -class MSO_LANGUAGE_ID(XmlEnumeration): +class MSO_LANGUAGE_ID(BaseXmlEnum): """ Specifies the language identifier. @@ -23,478 +14,669 @@ class MSO_LANGUAGE_ID(XmlEnumeration): from pptx.enum.lang import MSO_LANGUAGE_ID font.language_id = MSO_LANGUAGE_ID.POLISH + + MS API Name: `MsoLanguageId` + + https://msdn.microsoft.com/en-us/library/office/ff862134.aspx """ - __ms_name__ = "MsoLanguageId" - - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff862134.aspx" - - __members__ = ( - ReturnValueOnlyEnumMember( - "MIXED", -2, "More than one language in specified range." - ), - EnumMember("NONE", 0, "No language specified."), - XmlMappedEnumMember("AFRIKAANS", 1078, "af-ZA", "The Afrikaans language."), - XmlMappedEnumMember("ALBANIAN", 1052, "sq-AL", "The Albanian language."), - XmlMappedEnumMember("AMHARIC", 1118, "am-ET", "The Amharic language."), - XmlMappedEnumMember("ARABIC", 1025, "ar-SA", "The Arabic language."), - XmlMappedEnumMember( - "ARABIC_ALGERIA", 5121, "ar-DZ", "The Arabic Algeria language." - ), - XmlMappedEnumMember( - "ARABIC_BAHRAIN", 15361, "ar-BH", "The Arabic Bahrain language." - ), - XmlMappedEnumMember( - "ARABIC_EGYPT", 3073, "ar-EG", "The Arabic Egypt language." - ), - XmlMappedEnumMember("ARABIC_IRAQ", 2049, "ar-IQ", "The Arabic Iraq language."), - XmlMappedEnumMember( - "ARABIC_JORDAN", 11265, "ar-JO", "The Arabic Jordan language." - ), - XmlMappedEnumMember( - "ARABIC_KUWAIT", 13313, "ar-KW", "The Arabic Kuwait language." - ), - XmlMappedEnumMember( - "ARABIC_LEBANON", 12289, "ar-LB", "The Arabic Lebanon language." - ), - XmlMappedEnumMember( - "ARABIC_LIBYA", 4097, "ar-LY", "The Arabic Libya language." - ), - XmlMappedEnumMember( - "ARABIC_MOROCCO", 6145, "ar-MA", "The Arabic Morocco language." - ), - XmlMappedEnumMember("ARABIC_OMAN", 8193, "ar-OM", "The Arabic Oman language."), - XmlMappedEnumMember( - "ARABIC_QATAR", 16385, "ar-QA", "The Arabic Qatar language." - ), - XmlMappedEnumMember( - "ARABIC_SYRIA", 10241, "ar-SY", "The Arabic Syria language." - ), - XmlMappedEnumMember( - "ARABIC_TUNISIA", 7169, "ar-TN", "The Arabic Tunisia language." - ), - XmlMappedEnumMember("ARABIC_UAE", 14337, "ar-AE", "The Arabic UAE language."), - XmlMappedEnumMember( - "ARABIC_YEMEN", 9217, "ar-YE", "The Arabic Yemen language." - ), - XmlMappedEnumMember("ARMENIAN", 1067, "hy-AM", "The Armenian language."), - XmlMappedEnumMember("ASSAMESE", 1101, "as-IN", "The Assamese language."), - XmlMappedEnumMember( - "AZERI_CYRILLIC", 2092, "az-AZ", "The Azeri Cyrillic language." - ), - XmlMappedEnumMember( - "AZERI_LATIN", 1068, "az-Latn-AZ", "The Azeri Latin language." - ), - XmlMappedEnumMember("BASQUE", 1069, "eu-ES", "The Basque language."), - XmlMappedEnumMember( - "BELGIAN_DUTCH", 2067, "nl-BE", "The Belgian Dutch language." - ), - XmlMappedEnumMember( - "BELGIAN_FRENCH", 2060, "fr-BE", "The Belgian French language." - ), - XmlMappedEnumMember("BENGALI", 1093, "bn-IN", "The Bengali language."), - XmlMappedEnumMember("BOSNIAN", 4122, "hr-BA", "The Bosnian language."), - XmlMappedEnumMember( - "BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC", - 8218, - "bs-BA", - "The Bosni" "an Bosnia Herzegovina Cyrillic language.", - ), - XmlMappedEnumMember( - "BOSNIAN_BOSNIA_HERZEGOVINA_LATIN", - 5146, - "bs-Latn-BA", - "The Bos" "nian Bosnia Herzegovina Latin language.", - ), - XmlMappedEnumMember( - "BRAZILIAN_PORTUGUESE", - 1046, - "pt-BR", - "The Brazilian Portuguese" " language.", - ), - XmlMappedEnumMember("BULGARIAN", 1026, "bg-BG", "The Bulgarian language."), - XmlMappedEnumMember("BURMESE", 1109, "my-MM", "The Burmese language."), - XmlMappedEnumMember( - "BYELORUSSIAN", 1059, "be-BY", "The Byelorussian language." - ), - XmlMappedEnumMember("CATALAN", 1027, "ca-ES", "The Catalan language."), - XmlMappedEnumMember("CHEROKEE", 1116, "chr-US", "The Cherokee language."), - XmlMappedEnumMember( - "CHINESE_HONG_KONG_SAR", - 3076, - "zh-HK", - "The Chinese Hong Kong S" "AR language.", - ), - XmlMappedEnumMember( - "CHINESE_MACAO_SAR", 5124, "zh-MO", "The Chinese Macao SAR langu" "age." - ), - XmlMappedEnumMember( - "CHINESE_SINGAPORE", 4100, "zh-SG", "The Chinese Singapore langu" "age." - ), - XmlMappedEnumMember("CROATIAN", 1050, "hr-HR", "The Croatian language."), - XmlMappedEnumMember("CZECH", 1029, "cs-CZ", "The Czech language."), - XmlMappedEnumMember("DANISH", 1030, "da-DK", "The Danish language."), - XmlMappedEnumMember("DIVEHI", 1125, "div-MV", "The Divehi language."), - XmlMappedEnumMember("DUTCH", 1043, "nl-NL", "The Dutch language."), - XmlMappedEnumMember("EDO", 1126, "bin-NG", "The Edo language."), - XmlMappedEnumMember("ENGLISH_AUS", 3081, "en-AU", "The English AUS language."), - XmlMappedEnumMember( - "ENGLISH_BELIZE", 10249, "en-BZ", "The English Belize language." - ), - XmlMappedEnumMember( - "ENGLISH_CANADIAN", 4105, "en-CA", "The English Canadian languag" "e." - ), - XmlMappedEnumMember( - "ENGLISH_CARIBBEAN", 9225, "en-CB", "The English Caribbean langu" "age." - ), - XmlMappedEnumMember( - "ENGLISH_INDONESIA", 14345, "en-ID", "The English Indonesia lang" "uage." - ), - XmlMappedEnumMember( - "ENGLISH_IRELAND", 6153, "en-IE", "The English Ireland language." - ), - XmlMappedEnumMember( - "ENGLISH_JAMAICA", 8201, "en-JA", "The English Jamaica language." - ), - XmlMappedEnumMember( - "ENGLISH_NEW_ZEALAND", 5129, "en-NZ", "The English NewZealand la" "nguage." - ), - XmlMappedEnumMember( - "ENGLISH_PHILIPPINES", - 13321, - "en-PH", - "The English Philippines " "language.", - ), - XmlMappedEnumMember( - "ENGLISH_SOUTH_AFRICA", - 7177, - "en-ZA", - "The English South Africa" " language.", - ), - XmlMappedEnumMember( - "ENGLISH_TRINIDAD_TOBAGO", - 11273, - "en-TT", - "The English Trinidad" " Tobago language.", - ), - XmlMappedEnumMember("ENGLISH_UK", 2057, "en-GB", "The English UK language."), - XmlMappedEnumMember("ENGLISH_US", 1033, "en-US", "The English US language."), - XmlMappedEnumMember( - "ENGLISH_ZIMBABWE", 12297, "en-ZW", "The English Zimbabwe langua" "ge." - ), - XmlMappedEnumMember("ESTONIAN", 1061, "et-EE", "The Estonian language."), - XmlMappedEnumMember("FAEROESE", 1080, "fo-FO", "The Faeroese language."), - XmlMappedEnumMember("FARSI", 1065, "fa-IR", "The Farsi language."), - XmlMappedEnumMember("FILIPINO", 1124, "fil-PH", "The Filipino language."), - XmlMappedEnumMember("FINNISH", 1035, "fi-FI", "The Finnish language."), - XmlMappedEnumMember( - "FRANCH_CONGO_DRC", 9228, "fr-CD", "The French Congo DRC languag" "e." - ), - XmlMappedEnumMember("FRENCH", 1036, "fr-FR", "The French language."), - XmlMappedEnumMember( - "FRENCH_CAMEROON", 11276, "fr-CM", "The French Cameroon language" "." - ), - XmlMappedEnumMember( - "FRENCH_CANADIAN", 3084, "fr-CA", "The French Canadian language." - ), - XmlMappedEnumMember( - "FRENCH_COTED_IVOIRE", - 12300, - "fr-CI", - "The French Coted Ivoire " "language.", - ), - XmlMappedEnumMember( - "FRENCH_HAITI", 15372, "fr-HT", "The French Haiti language." - ), - XmlMappedEnumMember( - "FRENCH_LUXEMBOURG", 5132, "fr-LU", "The French Luxembourg langu" "age." - ), - XmlMappedEnumMember("FRENCH_MALI", 13324, "fr-ML", "The French Mali language."), - XmlMappedEnumMember( - "FRENCH_MONACO", 6156, "fr-MC", "The French Monaco language." - ), - XmlMappedEnumMember( - "FRENCH_MOROCCO", 14348, "fr-MA", "The French Morocco language." - ), - XmlMappedEnumMember( - "FRENCH_REUNION", 8204, "fr-RE", "The French Reunion language." - ), - XmlMappedEnumMember( - "FRENCH_SENEGAL", 10252, "fr-SN", "The French Senegal language." - ), - XmlMappedEnumMember( - "FRENCH_WEST_INDIES", - 7180, - "fr-WINDIES", - "The French West Indie" "s language.", - ), - XmlMappedEnumMember( - "FRISIAN_NETHERLANDS", 1122, "fy-NL", "The Frisian Netherlands l" "anguage." - ), - XmlMappedEnumMember("FULFULDE", 1127, "ff-NG", "The Fulfulde language."), - XmlMappedEnumMember( - "GAELIC_IRELAND", 2108, "ga-IE", "The Gaelic Ireland language." - ), - XmlMappedEnumMember( - "GAELIC_SCOTLAND", 1084, "en-US", "The Gaelic Scotland language." - ), - XmlMappedEnumMember("GALICIAN", 1110, "gl-ES", "The Galician language."), - XmlMappedEnumMember("GEORGIAN", 1079, "ka-GE", "The Georgian language."), - XmlMappedEnumMember("GERMAN", 1031, "de-DE", "The German language."), - XmlMappedEnumMember( - "GERMAN_AUSTRIA", 3079, "de-AT", "The German Austria language." - ), - XmlMappedEnumMember( - "GERMAN_LIECHTENSTEIN", - 5127, - "de-LI", - "The German Liechtenstein" " language.", - ), - XmlMappedEnumMember( - "GERMAN_LUXEMBOURG", 4103, "de-LU", "The German Luxembourg langu" "age." - ), - XmlMappedEnumMember("GREEK", 1032, "el-GR", "The Greek language."), - XmlMappedEnumMember("GUARANI", 1140, "gn-PY", "The Guarani language."), - XmlMappedEnumMember("GUJARATI", 1095, "gu-IN", "The Gujarati language."), - XmlMappedEnumMember("HAUSA", 1128, "ha-NG", "The Hausa language."), - XmlMappedEnumMember("HAWAIIAN", 1141, "haw-US", "The Hawaiian language."), - XmlMappedEnumMember("HEBREW", 1037, "he-IL", "The Hebrew language."), - XmlMappedEnumMember("HINDI", 1081, "hi-IN", "The Hindi language."), - XmlMappedEnumMember("HUNGARIAN", 1038, "hu-HU", "The Hungarian language."), - XmlMappedEnumMember("IBIBIO", 1129, "ibb-NG", "The Ibibio language."), - XmlMappedEnumMember("ICELANDIC", 1039, "is-IS", "The Icelandic language."), - XmlMappedEnumMember("IGBO", 1136, "ig-NG", "The Igbo language."), - XmlMappedEnumMember("INDONESIAN", 1057, "id-ID", "The Indonesian language."), - XmlMappedEnumMember("INUKTITUT", 1117, "iu-Cans-CA", "The Inuktitut language."), - XmlMappedEnumMember("ITALIAN", 1040, "it-IT", "The Italian language."), - XmlMappedEnumMember("JAPANESE", 1041, "ja-JP", "The Japanese language."), - XmlMappedEnumMember("KANNADA", 1099, "kn-IN", "The Kannada language."), - XmlMappedEnumMember("KANURI", 1137, "kr-NG", "The Kanuri language."), - XmlMappedEnumMember("KASHMIRI", 1120, "ks-Arab", "The Kashmiri language."), - XmlMappedEnumMember( - "KASHMIRI_DEVANAGARI", - 2144, - "ks-Deva", - "The Kashmiri Devanagari" " language.", - ), - XmlMappedEnumMember("KAZAKH", 1087, "kk-KZ", "The Kazakh language."), - XmlMappedEnumMember("KHMER", 1107, "kh-KH", "The Khmer language."), - XmlMappedEnumMember("KIRGHIZ", 1088, "ky-KG", "The Kirghiz language."), - XmlMappedEnumMember("KONKANI", 1111, "kok-IN", "The Konkani language."), - XmlMappedEnumMember("KOREAN", 1042, "ko-KR", "The Korean language."), - XmlMappedEnumMember("KYRGYZ", 1088, "ky-KG", "The Kyrgyz language."), - XmlMappedEnumMember("LAO", 1108, "lo-LA", "The Lao language."), - XmlMappedEnumMember("LATIN", 1142, "la-Latn", "The Latin language."), - XmlMappedEnumMember("LATVIAN", 1062, "lv-LV", "The Latvian language."), - XmlMappedEnumMember("LITHUANIAN", 1063, "lt-LT", "The Lithuanian language."), - XmlMappedEnumMember( - "MACEDONINAN_FYROM", 1071, "mk-MK", "The Macedonian FYROM langua" "ge." - ), - XmlMappedEnumMember( - "MALAY_BRUNEI_DARUSSALAM", - 2110, - "ms-BN", - "The Malay Brunei Daru" "ssalam language.", - ), - XmlMappedEnumMember("MALAYALAM", 1100, "ml-IN", "The Malayalam language."), - XmlMappedEnumMember("MALAYSIAN", 1086, "ms-MY", "The Malaysian language."), - XmlMappedEnumMember("MALTESE", 1082, "mt-MT", "The Maltese language."), - XmlMappedEnumMember("MANIPURI", 1112, "mni-IN", "The Manipuri language."), - XmlMappedEnumMember("MAORI", 1153, "mi-NZ", "The Maori language."), - XmlMappedEnumMember("MARATHI", 1102, "mr-IN", "The Marathi language."), - XmlMappedEnumMember( - "MEXICAN_SPANISH", 2058, "es-MX", "The Mexican Spanish language." - ), - XmlMappedEnumMember("MONGOLIAN", 1104, "mn-MN", "The Mongolian language."), - XmlMappedEnumMember("NEPALI", 1121, "ne-NP", "The Nepali language."), - XmlMappedEnumMember("NO_PROOFING", 1024, "en-US", "No proofing."), - XmlMappedEnumMember( - "NORWEGIAN_BOKMOL", 1044, "nb-NO", "The Norwegian Bokmol languag" "e." - ), - XmlMappedEnumMember( - "NORWEGIAN_NYNORSK", 2068, "nn-NO", "The Norwegian Nynorsk langu" "age." - ), - XmlMappedEnumMember("ORIYA", 1096, "or-IN", "The Oriya language."), - XmlMappedEnumMember("OROMO", 1138, "om-Ethi-ET", "The Oromo language."), - XmlMappedEnumMember("PASHTO", 1123, "ps-AF", "The Pashto language."), - XmlMappedEnumMember("POLISH", 1045, "pl-PL", "The Polish language."), - XmlMappedEnumMember("PORTUGUESE", 2070, "pt-PT", "The Portuguese language."), - XmlMappedEnumMember("PUNJABI", 1094, "pa-IN", "The Punjabi language."), - XmlMappedEnumMember( - "QUECHUA_BOLIVIA", 1131, "quz-BO", "The Quechua Bolivia language" "." - ), - XmlMappedEnumMember( - "QUECHUA_ECUADOR", 2155, "quz-EC", "The Quechua Ecuador language" "." - ), - XmlMappedEnumMember( - "QUECHUA_PERU", 3179, "quz-PE", "The Quechua Peru language." - ), - XmlMappedEnumMember( - "RHAETO_ROMANIC", 1047, "rm-CH", "The Rhaeto Romanic language." - ), - XmlMappedEnumMember("ROMANIAN", 1048, "ro-RO", "The Romanian language."), - XmlMappedEnumMember( - "ROMANIAN_MOLDOVA", 2072, "ro-MO", "The Romanian Moldova languag" "e." - ), - XmlMappedEnumMember("RUSSIAN", 1049, "ru-RU", "The Russian language."), - XmlMappedEnumMember( - "RUSSIAN_MOLDOVA", 2073, "ru-MO", "The Russian Moldova language." - ), - XmlMappedEnumMember( - "SAMI_LAPPISH", 1083, "se-NO", "The Sami Lappish language." - ), - XmlMappedEnumMember("SANSKRIT", 1103, "sa-IN", "The Sanskrit language."), - XmlMappedEnumMember("SEPEDI", 1132, "ns-ZA", "The Sepedi language."), - XmlMappedEnumMember( - "SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC", - 7194, - "sr-BA", - "The Serbi" "an Bosnia Herzegovina Cyrillic language.", - ), - XmlMappedEnumMember( - "SERBIAN_BOSNIA_HERZEGOVINA_LATIN", - 6170, - "sr-Latn-BA", - "The Ser" "bian Bosnia Herzegovina Latin language.", - ), - XmlMappedEnumMember( - "SERBIAN_CYRILLIC", 3098, "sr-SP", "The Serbian Cyrillic languag" "e." - ), - XmlMappedEnumMember( - "SERBIAN_LATIN", 2074, "sr-Latn-CS", "The Serbian Latin language" "." - ), - XmlMappedEnumMember("SESOTHO", 1072, "st-ZA", "The Sesotho language."), - XmlMappedEnumMember( - "SIMPLIFIED_CHINESE", 2052, "zh-CN", "The Simplified Chinese lan" "guage." - ), - XmlMappedEnumMember("SINDHI", 1113, "sd-Deva-IN", "The Sindhi language."), - XmlMappedEnumMember( - "SINDHI_PAKISTAN", 2137, "sd-Arab-PK", "The Sindhi Pakistan lang" "uage." - ), - XmlMappedEnumMember("SINHALESE", 1115, "si-LK", "The Sinhalese language."), - XmlMappedEnumMember("SLOVAK", 1051, "sk-SK", "The Slovak language."), - XmlMappedEnumMember("SLOVENIAN", 1060, "sl-SI", "The Slovenian language."), - XmlMappedEnumMember("SOMALI", 1143, "so-SO", "The Somali language."), - XmlMappedEnumMember("SORBIAN", 1070, "wen-DE", "The Sorbian language."), - XmlMappedEnumMember("SPANISH", 1034, "es-ES_tradnl", "The Spanish language."), - XmlMappedEnumMember( - "SPANISH_ARGENTINA", 11274, "es-AR", "The Spanish Argentina lang" "uage." - ), - XmlMappedEnumMember( - "SPANISH_BOLIVIA", 16394, "es-BO", "The Spanish Bolivia language" "." - ), - XmlMappedEnumMember( - "SPANISH_CHILE", 13322, "es-CL", "The Spanish Chile language." - ), - XmlMappedEnumMember( - "SPANISH_COLOMBIA", 9226, "es-CO", "The Spanish Colombia languag" "e." - ), - XmlMappedEnumMember( - "SPANISH_COSTA_RICA", 5130, "es-CR", "The Spanish Costa Rica lan" "guage." - ), - XmlMappedEnumMember( - "SPANISH_DOMINICAN_REPUBLIC", - 7178, - "es-DO", - "The Spanish Domini" "can Republic language.", - ), - XmlMappedEnumMember( - "SPANISH_ECUADOR", 12298, "es-EC", "The Spanish Ecuador language" "." - ), - XmlMappedEnumMember( - "SPANISH_EL_SALVADOR", - 17418, - "es-SV", - "The Spanish El Salvador " "language.", - ), - XmlMappedEnumMember( - "SPANISH_GUATEMALA", 4106, "es-GT", "The Spanish Guatemala langu" "age." - ), - XmlMappedEnumMember( - "SPANISH_HONDURAS", 18442, "es-HN", "The Spanish Honduras langua" "ge." - ), - XmlMappedEnumMember( - "SPANISH_MODERN_SORT", 3082, "es-ES", "The Spanish Modern Sort l" "anguage." - ), - XmlMappedEnumMember( - "SPANISH_NICARAGUA", 19466, "es-NI", "The Spanish Nicaragua lang" "uage." - ), - XmlMappedEnumMember( - "SPANISH_PANAMA", 6154, "es-PA", "The Spanish Panama language." - ), - XmlMappedEnumMember( - "SPANISH_PARAGUAY", 15370, "es-PY", "The Spanish Paraguay langua" "ge." - ), - XmlMappedEnumMember( - "SPANISH_PERU", 10250, "es-PE", "The Spanish Peru language." - ), - XmlMappedEnumMember( - "SPANISH_PUERTO_RICO", - 20490, - "es-PR", - "The Spanish Puerto Rico " "language.", - ), - XmlMappedEnumMember( - "SPANISH_URUGUAY", 14346, "es-UR", "The Spanish Uruguay language" "." - ), - XmlMappedEnumMember( - "SPANISH_VENEZUELA", 8202, "es-VE", "The Spanish Venezuela langu" "age." - ), - XmlMappedEnumMember("SUTU", 1072, "st-ZA", "The Sutu language."), - XmlMappedEnumMember("SWAHILI", 1089, "sw-KE", "The Swahili language."), - XmlMappedEnumMember("SWEDISH", 1053, "sv-SE", "The Swedish language."), - XmlMappedEnumMember( - "SWEDISH_FINLAND", 2077, "sv-FI", "The Swedish Finland language." - ), - XmlMappedEnumMember( - "SWISS_FRENCH", 4108, "fr-CH", "The Swiss French language." - ), - XmlMappedEnumMember( - "SWISS_GERMAN", 2055, "de-CH", "The Swiss German language." - ), - XmlMappedEnumMember( - "SWISS_ITALIAN", 2064, "it-CH", "The Swiss Italian language." - ), - XmlMappedEnumMember("SYRIAC", 1114, "syr-SY", "The Syriac language."), - XmlMappedEnumMember("TAJIK", 1064, "tg-TJ", "The Tajik language."), - XmlMappedEnumMember( - "TAMAZIGHT", 1119, "tzm-Arab-MA", "The Tamazight language." - ), - XmlMappedEnumMember( - "TAMAZIGHT_LATIN", 2143, "tmz-DZ", "The Tamazight Latin language" "." - ), - XmlMappedEnumMember("TAMIL", 1097, "ta-IN", "The Tamil language."), - XmlMappedEnumMember("TATAR", 1092, "tt-RU", "The Tatar language."), - XmlMappedEnumMember("TELUGU", 1098, "te-IN", "The Telugu language."), - XmlMappedEnumMember("THAI", 1054, "th-TH", "The Thai language."), - XmlMappedEnumMember("TIBETAN", 1105, "bo-CN", "The Tibetan language."), - XmlMappedEnumMember( - "TIGRIGNA_ERITREA", 2163, "ti-ER", "The Tigrigna Eritrea languag" "e." - ), - XmlMappedEnumMember( - "TIGRIGNA_ETHIOPIC", 1139, "ti-ET", "The Tigrigna Ethiopic langu" "age." - ), - XmlMappedEnumMember( - "TRADITIONAL_CHINESE", 1028, "zh-TW", "The Traditional Chinese l" "anguage." - ), - XmlMappedEnumMember("TSONGA", 1073, "ts-ZA", "The Tsonga language."), - XmlMappedEnumMember("TSWANA", 1074, "tn-ZA", "The Tswana language."), - XmlMappedEnumMember("TURKISH", 1055, "tr-TR", "The Turkish language."), - XmlMappedEnumMember("TURKMEN", 1090, "tk-TM", "The Turkmen language."), - XmlMappedEnumMember("UKRAINIAN", 1058, "uk-UA", "The Ukrainian language."), - XmlMappedEnumMember("URDU", 1056, "ur-PK", "The Urdu language."), - XmlMappedEnumMember( - "UZBEK_CYRILLIC", 2115, "uz-UZ", "The Uzbek Cyrillic language." - ), - XmlMappedEnumMember( - "UZBEK_LATIN", 1091, "uz-Latn-UZ", "The Uzbek Latin language." - ), - XmlMappedEnumMember("VENDA", 1075, "ve-ZA", "The Venda language."), - XmlMappedEnumMember("VIETNAMESE", 1066, "vi-VN", "The Vietnamese language."), - XmlMappedEnumMember("WELSH", 1106, "cy-GB", "The Welsh language."), - XmlMappedEnumMember("XHOSA", 1076, "xh-ZA", "The Xhosa language."), - XmlMappedEnumMember("YI", 1144, "ii-CN", "The Yi language."), - XmlMappedEnumMember("YIDDISH", 1085, "yi-Hebr", "The Yiddish language."), - XmlMappedEnumMember("YORUBA", 1130, "yo-NG", "The Yoruba language."), - XmlMappedEnumMember("ZULU", 1077, "zu-ZA", "The Zulu language."), + NONE = (0, "", "No language specified.") + """No language specified.""" + + AFRIKAANS = (1078, "af-ZA", "The Afrikaans language.") + """The Afrikaans language.""" + + ALBANIAN = (1052, "sq-AL", "The Albanian language.") + """The Albanian language.""" + + AMHARIC = (1118, "am-ET", "The Amharic language.") + """The Amharic language.""" + + ARABIC = (1025, "ar-SA", "The Arabic language.") + """The Arabic language.""" + + ARABIC_ALGERIA = (5121, "ar-DZ", "The Arabic Algeria language.") + """The Arabic Algeria language.""" + + ARABIC_BAHRAIN = (15361, "ar-BH", "The Arabic Bahrain language.") + """The Arabic Bahrain language.""" + + ARABIC_EGYPT = (3073, "ar-EG", "The Arabic Egypt language.") + """The Arabic Egypt language.""" + + ARABIC_IRAQ = (2049, "ar-IQ", "The Arabic Iraq language.") + """The Arabic Iraq language.""" + + ARABIC_JORDAN = (11265, "ar-JO", "The Arabic Jordan language.") + """The Arabic Jordan language.""" + + ARABIC_KUWAIT = (13313, "ar-KW", "The Arabic Kuwait language.") + """The Arabic Kuwait language.""" + + ARABIC_LEBANON = (12289, "ar-LB", "The Arabic Lebanon language.") + """The Arabic Lebanon language.""" + + ARABIC_LIBYA = (4097, "ar-LY", "The Arabic Libya language.") + """The Arabic Libya language.""" + + ARABIC_MOROCCO = (6145, "ar-MA", "The Arabic Morocco language.") + """The Arabic Morocco language.""" + + ARABIC_OMAN = (8193, "ar-OM", "The Arabic Oman language.") + """The Arabic Oman language.""" + + ARABIC_QATAR = (16385, "ar-QA", "The Arabic Qatar language.") + """The Arabic Qatar language.""" + + ARABIC_SYRIA = (10241, "ar-SY", "The Arabic Syria language.") + """The Arabic Syria language.""" + + ARABIC_TUNISIA = (7169, "ar-TN", "The Arabic Tunisia language.") + """The Arabic Tunisia language.""" + + ARABIC_UAE = (14337, "ar-AE", "The Arabic UAE language.") + """The Arabic UAE language.""" + + ARABIC_YEMEN = (9217, "ar-YE", "The Arabic Yemen language.") + """The Arabic Yemen language.""" + + ARMENIAN = (1067, "hy-AM", "The Armenian language.") + """The Armenian language.""" + + ASSAMESE = (1101, "as-IN", "The Assamese language.") + """The Assamese language.""" + + AZERI_CYRILLIC = (2092, "az-AZ", "The Azeri Cyrillic language.") + """The Azeri Cyrillic language.""" + + AZERI_LATIN = (1068, "az-Latn-AZ", "The Azeri Latin language.") + """The Azeri Latin language.""" + + BASQUE = (1069, "eu-ES", "The Basque language.") + """The Basque language.""" + + BELGIAN_DUTCH = (2067, "nl-BE", "The Belgian Dutch language.") + """The Belgian Dutch language.""" + + BELGIAN_FRENCH = (2060, "fr-BE", "The Belgian French language.") + """The Belgian French language.""" + + BENGALI = (1093, "bn-IN", "The Bengali language.") + """The Bengali language.""" + + BOSNIAN = (4122, "hr-BA", "The Bosnian language.") + """The Bosnian language.""" + + BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC = ( + 8218, + "bs-BA", + "The Bosnian Bosnia Herzegovina Cyrillic language.", + ) + """The Bosnian Bosnia Herzegovina Cyrillic language.""" + + BOSNIAN_BOSNIA_HERZEGOVINA_LATIN = ( + 5146, + "bs-Latn-BA", + "The Bosnian Bosnia Herzegovina Latin language.", + ) + """The Bosnian Bosnia Herzegovina Latin language.""" + + BRAZILIAN_PORTUGUESE = (1046, "pt-BR", "The Brazilian Portuguese language.") + """The Brazilian Portuguese language.""" + + BULGARIAN = (1026, "bg-BG", "The Bulgarian language.") + """The Bulgarian language.""" + + BURMESE = (1109, "my-MM", "The Burmese language.") + """The Burmese language.""" + + BYELORUSSIAN = (1059, "be-BY", "The Byelorussian language.") + """The Byelorussian language.""" + + CATALAN = (1027, "ca-ES", "The Catalan language.") + """The Catalan language.""" + + CHEROKEE = (1116, "chr-US", "The Cherokee language.") + """The Cherokee language.""" + + CHINESE_HONG_KONG_SAR = (3076, "zh-HK", "The Chinese Hong Kong SAR language.") + """The Chinese Hong Kong SAR language.""" + + CHINESE_MACAO_SAR = (5124, "zh-MO", "The Chinese Macao SAR language.") + """The Chinese Macao SAR language.""" + + CHINESE_SINGAPORE = (4100, "zh-SG", "The Chinese Singapore language.") + """The Chinese Singapore language.""" + + CROATIAN = (1050, "hr-HR", "The Croatian language.") + """The Croatian language.""" + + CZECH = (1029, "cs-CZ", "The Czech language.") + """The Czech language.""" + + DANISH = (1030, "da-DK", "The Danish language.") + """The Danish language.""" + + DIVEHI = (1125, "div-MV", "The Divehi language.") + """The Divehi language.""" + + DUTCH = (1043, "nl-NL", "The Dutch language.") + """The Dutch language.""" + + EDO = (1126, "bin-NG", "The Edo language.") + """The Edo language.""" + + ENGLISH_AUS = (3081, "en-AU", "The English AUS language.") + """The English AUS language.""" + + ENGLISH_BELIZE = (10249, "en-BZ", "The English Belize language.") + """The English Belize language.""" + + ENGLISH_CANADIAN = (4105, "en-CA", "The English Canadian language.") + """The English Canadian language.""" + + ENGLISH_CARIBBEAN = (9225, "en-CB", "The English Caribbean language.") + """The English Caribbean language.""" + + ENGLISH_INDONESIA = (14345, "en-ID", "The English Indonesia language.") + """The English Indonesia language.""" + + ENGLISH_IRELAND = (6153, "en-IE", "The English Ireland language.") + """The English Ireland language.""" + + ENGLISH_JAMAICA = (8201, "en-JA", "The English Jamaica language.") + """The English Jamaica language.""" + + ENGLISH_NEW_ZEALAND = (5129, "en-NZ", "The English NewZealand language.") + """The English NewZealand language.""" + + ENGLISH_PHILIPPINES = (13321, "en-PH", "The English Philippines language.") + """The English Philippines language.""" + + ENGLISH_SOUTH_AFRICA = (7177, "en-ZA", "The English South Africa language.") + """The English South Africa language.""" + + ENGLISH_TRINIDAD_TOBAGO = (11273, "en-TT", "The English Trinidad Tobago language.") + """The English Trinidad Tobago language.""" + + ENGLISH_UK = (2057, "en-GB", "The English UK language.") + """The English UK language.""" + + ENGLISH_US = (1033, "en-US", "The English US language.") + """The English US language.""" + + ENGLISH_ZIMBABWE = (12297, "en-ZW", "The English Zimbabwe language.") + """The English Zimbabwe language.""" + + ESTONIAN = (1061, "et-EE", "The Estonian language.") + """The Estonian language.""" + + FAEROESE = (1080, "fo-FO", "The Faeroese language.") + """The Faeroese language.""" + + FARSI = (1065, "fa-IR", "The Farsi language.") + """The Farsi language.""" + + FILIPINO = (1124, "fil-PH", "The Filipino language.") + """The Filipino language.""" + + FINNISH = (1035, "fi-FI", "The Finnish language.") + """The Finnish language.""" + + FRANCH_CONGO_DRC = (9228, "fr-CD", "The French Congo DRC language.") + """The French Congo DRC language.""" + + FRENCH = (1036, "fr-FR", "The French language.") + """The French language.""" + + FRENCH_CAMEROON = (11276, "fr-CM", "The French Cameroon language.") + """The French Cameroon language.""" + + FRENCH_CANADIAN = (3084, "fr-CA", "The French Canadian language.") + """The French Canadian language.""" + + FRENCH_COTED_IVOIRE = (12300, "fr-CI", "The French Coted Ivoire language.") + """The French Coted Ivoire language.""" + + FRENCH_HAITI = (15372, "fr-HT", "The French Haiti language.") + """The French Haiti language.""" + + FRENCH_LUXEMBOURG = (5132, "fr-LU", "The French Luxembourg language.") + """The French Luxembourg language.""" + + FRENCH_MALI = (13324, "fr-ML", "The French Mali language.") + """The French Mali language.""" + + FRENCH_MONACO = (6156, "fr-MC", "The French Monaco language.") + """The French Monaco language.""" + + FRENCH_MOROCCO = (14348, "fr-MA", "The French Morocco language.") + """The French Morocco language.""" + + FRENCH_REUNION = (8204, "fr-RE", "The French Reunion language.") + """The French Reunion language.""" + + FRENCH_SENEGAL = (10252, "fr-SN", "The French Senegal language.") + """The French Senegal language.""" + + FRENCH_WEST_INDIES = (7180, "fr-WINDIES", "The French West Indies language.") + """The French West Indies language.""" + + FRISIAN_NETHERLANDS = (1122, "fy-NL", "The Frisian Netherlands language.") + """The Frisian Netherlands language.""" + + FULFULDE = (1127, "ff-NG", "The Fulfulde language.") + """The Fulfulde language.""" + + GAELIC_IRELAND = (2108, "ga-IE", "The Gaelic Ireland language.") + """The Gaelic Ireland language.""" + + GAELIC_SCOTLAND = (1084, "en-US", "The Gaelic Scotland language.") + """The Gaelic Scotland language.""" + + GALICIAN = (1110, "gl-ES", "The Galician language.") + """The Galician language.""" + + GEORGIAN = (1079, "ka-GE", "The Georgian language.") + """The Georgian language.""" + + GERMAN = (1031, "de-DE", "The German language.") + """The German language.""" + + GERMAN_AUSTRIA = (3079, "de-AT", "The German Austria language.") + """The German Austria language.""" + + GERMAN_LIECHTENSTEIN = (5127, "de-LI", "The German Liechtenstein language.") + """The German Liechtenstein language.""" + + GERMAN_LUXEMBOURG = (4103, "de-LU", "The German Luxembourg language.") + """The German Luxembourg language.""" + + GREEK = (1032, "el-GR", "The Greek language.") + """The Greek language.""" + + GUARANI = (1140, "gn-PY", "The Guarani language.") + """The Guarani language.""" + + GUJARATI = (1095, "gu-IN", "The Gujarati language.") + """The Gujarati language.""" + + HAUSA = (1128, "ha-NG", "The Hausa language.") + """The Hausa language.""" + + HAWAIIAN = (1141, "haw-US", "The Hawaiian language.") + """The Hawaiian language.""" + + HEBREW = (1037, "he-IL", "The Hebrew language.") + """The Hebrew language.""" + + HINDI = (1081, "hi-IN", "The Hindi language.") + """The Hindi language.""" + + HUNGARIAN = (1038, "hu-HU", "The Hungarian language.") + """The Hungarian language.""" + + IBIBIO = (1129, "ibb-NG", "The Ibibio language.") + """The Ibibio language.""" + + ICELANDIC = (1039, "is-IS", "The Icelandic language.") + """The Icelandic language.""" + + IGBO = (1136, "ig-NG", "The Igbo language.") + """The Igbo language.""" + + INDONESIAN = (1057, "id-ID", "The Indonesian language.") + """The Indonesian language.""" + + INUKTITUT = (1117, "iu-Cans-CA", "The Inuktitut language.") + """The Inuktitut language.""" + + ITALIAN = (1040, "it-IT", "The Italian language.") + """The Italian language.""" + + JAPANESE = (1041, "ja-JP", "The Japanese language.") + """The Japanese language.""" + + KANNADA = (1099, "kn-IN", "The Kannada language.") + """The Kannada language.""" + + KANURI = (1137, "kr-NG", "The Kanuri language.") + """The Kanuri language.""" + + KASHMIRI = (1120, "ks-Arab", "The Kashmiri language.") + """The Kashmiri language.""" + + KASHMIRI_DEVANAGARI = (2144, "ks-Deva", "The Kashmiri Devanagari language.") + """The Kashmiri Devanagari language.""" + + KAZAKH = (1087, "kk-KZ", "The Kazakh language.") + """The Kazakh language.""" + + KHMER = (1107, "kh-KH", "The Khmer language.") + """The Khmer language.""" + + KIRGHIZ = (1088, "ky-KG", "The Kirghiz language.") + """The Kirghiz language.""" + + KONKANI = (1111, "kok-IN", "The Konkani language.") + """The Konkani language.""" + + KOREAN = (1042, "ko-KR", "The Korean language.") + """The Korean language.""" + + KYRGYZ = (1088, "ky-KG", "The Kyrgyz language.") + """The Kyrgyz language.""" + + LAO = (1108, "lo-LA", "The Lao language.") + """The Lao language.""" + + LATIN = (1142, "la-Latn", "The Latin language.") + """The Latin language.""" + + LATVIAN = (1062, "lv-LV", "The Latvian language.") + """The Latvian language.""" + + LITHUANIAN = (1063, "lt-LT", "The Lithuanian language.") + """The Lithuanian language.""" + + MACEDONINAN_FYROM = (1071, "mk-MK", "The Macedonian FYROM language.") + """The Macedonian FYROM language.""" + + MALAY_BRUNEI_DARUSSALAM = (2110, "ms-BN", "The Malay Brunei Darussalam language.") + """The Malay Brunei Darussalam language.""" + + MALAYALAM = (1100, "ml-IN", "The Malayalam language.") + """The Malayalam language.""" + + MALAYSIAN = (1086, "ms-MY", "The Malaysian language.") + """The Malaysian language.""" + + MALTESE = (1082, "mt-MT", "The Maltese language.") + """The Maltese language.""" + + MANIPURI = (1112, "mni-IN", "The Manipuri language.") + """The Manipuri language.""" + + MAORI = (1153, "mi-NZ", "The Maori language.") + """The Maori language.""" + + MARATHI = (1102, "mr-IN", "The Marathi language.") + """The Marathi language.""" + + MEXICAN_SPANISH = (2058, "es-MX", "The Mexican Spanish language.") + """The Mexican Spanish language.""" + + MONGOLIAN = (1104, "mn-MN", "The Mongolian language.") + """The Mongolian language.""" + + NEPALI = (1121, "ne-NP", "The Nepali language.") + """The Nepali language.""" + + NO_PROOFING = (1024, "en-US", "No proofing.") + """No proofing.""" + + NORWEGIAN_BOKMOL = (1044, "nb-NO", "The Norwegian Bokmol language.") + """The Norwegian Bokmol language.""" + + NORWEGIAN_NYNORSK = (2068, "nn-NO", "The Norwegian Nynorsk language.") + """The Norwegian Nynorsk language.""" + + ORIYA = (1096, "or-IN", "The Oriya language.") + """The Oriya language.""" + + OROMO = (1138, "om-Ethi-ET", "The Oromo language.") + """The Oromo language.""" + + PASHTO = (1123, "ps-AF", "The Pashto language.") + """The Pashto language.""" + + POLISH = (1045, "pl-PL", "The Polish language.") + """The Polish language.""" + + PORTUGUESE = (2070, "pt-PT", "The Portuguese language.") + """The Portuguese language.""" + + PUNJABI = (1094, "pa-IN", "The Punjabi language.") + """The Punjabi language.""" + + QUECHUA_BOLIVIA = (1131, "quz-BO", "The Quechua Bolivia language.") + """The Quechua Bolivia language.""" + + QUECHUA_ECUADOR = (2155, "quz-EC", "The Quechua Ecuador language.") + """The Quechua Ecuador language.""" + + QUECHUA_PERU = (3179, "quz-PE", "The Quechua Peru language.") + """The Quechua Peru language.""" + + RHAETO_ROMANIC = (1047, "rm-CH", "The Rhaeto Romanic language.") + """The Rhaeto Romanic language.""" + + ROMANIAN = (1048, "ro-RO", "The Romanian language.") + """The Romanian language.""" + + ROMANIAN_MOLDOVA = (2072, "ro-MO", "The Romanian Moldova language.") + """The Romanian Moldova language.""" + + RUSSIAN = (1049, "ru-RU", "The Russian language.") + """The Russian language.""" + + RUSSIAN_MOLDOVA = (2073, "ru-MO", "The Russian Moldova language.") + """The Russian Moldova language.""" + + SAMI_LAPPISH = (1083, "se-NO", "The Sami Lappish language.") + """The Sami Lappish language.""" + + SANSKRIT = (1103, "sa-IN", "The Sanskrit language.") + """The Sanskrit language.""" + + SEPEDI = (1132, "ns-ZA", "The Sepedi language.") + """The Sepedi language.""" + + SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC = ( + 7194, + "sr-BA", + "The Serbian Bosnia Herzegovina Cyrillic language.", + ) + """The Serbian Bosnia Herzegovina Cyrillic language.""" + + SERBIAN_BOSNIA_HERZEGOVINA_LATIN = ( + 6170, + "sr-Latn-BA", + "The Serbian Bosnia Herzegovina Latin language.", ) + """The Serbian Bosnia Herzegovina Latin language.""" + + SERBIAN_CYRILLIC = (3098, "sr-SP", "The Serbian Cyrillic language.") + """The Serbian Cyrillic language.""" + + SERBIAN_LATIN = (2074, "sr-Latn-CS", "The Serbian Latin language.") + """The Serbian Latin language.""" + + SESOTHO = (1072, "st-ZA", "The Sesotho language.") + """The Sesotho language.""" + + SIMPLIFIED_CHINESE = (2052, "zh-CN", "The Simplified Chinese language.") + """The Simplified Chinese language.""" + + SINDHI = (1113, "sd-Deva-IN", "The Sindhi language.") + """The Sindhi language.""" + + SINDHI_PAKISTAN = (2137, "sd-Arab-PK", "The Sindhi Pakistan language.") + """The Sindhi Pakistan language.""" + + SINHALESE = (1115, "si-LK", "The Sinhalese language.") + """The Sinhalese language.""" + + SLOVAK = (1051, "sk-SK", "The Slovak language.") + """The Slovak language.""" + + SLOVENIAN = (1060, "sl-SI", "The Slovenian language.") + """The Slovenian language.""" + + SOMALI = (1143, "so-SO", "The Somali language.") + """The Somali language.""" + + SORBIAN = (1070, "wen-DE", "The Sorbian language.") + """The Sorbian language.""" + + SPANISH = (1034, "es-ES_tradnl", "The Spanish language.") + """The Spanish language.""" + + SPANISH_ARGENTINA = (11274, "es-AR", "The Spanish Argentina language.") + """The Spanish Argentina language.""" + + SPANISH_BOLIVIA = (16394, "es-BO", "The Spanish Bolivia language.") + """The Spanish Bolivia language.""" + + SPANISH_CHILE = (13322, "es-CL", "The Spanish Chile language.") + """The Spanish Chile language.""" + + SPANISH_COLOMBIA = (9226, "es-CO", "The Spanish Colombia language.") + """The Spanish Colombia language.""" + + SPANISH_COSTA_RICA = (5130, "es-CR", "The Spanish Costa Rica language.") + """The Spanish Costa Rica language.""" + + SPANISH_DOMINICAN_REPUBLIC = (7178, "es-DO", "The Spanish Dominican Republic language.") + """The Spanish Dominican Republic language.""" + + SPANISH_ECUADOR = (12298, "es-EC", "The Spanish Ecuador language.") + """The Spanish Ecuador language.""" + + SPANISH_EL_SALVADOR = (17418, "es-SV", "The Spanish El Salvador language.") + """The Spanish El Salvador language.""" + + SPANISH_GUATEMALA = (4106, "es-GT", "The Spanish Guatemala language.") + """The Spanish Guatemala language.""" + + SPANISH_HONDURAS = (18442, "es-HN", "The Spanish Honduras language.") + """The Spanish Honduras language.""" + + SPANISH_MODERN_SORT = (3082, "es-ES", "The Spanish Modern Sort language.") + """The Spanish Modern Sort language.""" + + SPANISH_NICARAGUA = (19466, "es-NI", "The Spanish Nicaragua language.") + """The Spanish Nicaragua language.""" + + SPANISH_PANAMA = (6154, "es-PA", "The Spanish Panama language.") + """The Spanish Panama language.""" + + SPANISH_PARAGUAY = (15370, "es-PY", "The Spanish Paraguay language.") + """The Spanish Paraguay language.""" + + SPANISH_PERU = (10250, "es-PE", "The Spanish Peru language.") + """The Spanish Peru language.""" + + SPANISH_PUERTO_RICO = (20490, "es-PR", "The Spanish Puerto Rico language.") + """The Spanish Puerto Rico language.""" + + SPANISH_URUGUAY = (14346, "es-UR", "The Spanish Uruguay language.") + """The Spanish Uruguay language.""" + + SPANISH_VENEZUELA = (8202, "es-VE", "The Spanish Venezuela language.") + """The Spanish Venezuela language.""" + + SUTU = (1072, "st-ZA", "The Sutu language.") + """The Sutu language.""" + + SWAHILI = (1089, "sw-KE", "The Swahili language.") + """The Swahili language.""" + + SWEDISH = (1053, "sv-SE", "The Swedish language.") + """The Swedish language.""" + + SWEDISH_FINLAND = (2077, "sv-FI", "The Swedish Finland language.") + """The Swedish Finland language.""" + + SWISS_FRENCH = (4108, "fr-CH", "The Swiss French language.") + """The Swiss French language.""" + + SWISS_GERMAN = (2055, "de-CH", "The Swiss German language.") + """The Swiss German language.""" + + SWISS_ITALIAN = (2064, "it-CH", "The Swiss Italian language.") + """The Swiss Italian language.""" + + SYRIAC = (1114, "syr-SY", "The Syriac language.") + """The Syriac language.""" + + TAJIK = (1064, "tg-TJ", "The Tajik language.") + """The Tajik language.""" + + TAMAZIGHT = (1119, "tzm-Arab-MA", "The Tamazight language.") + """The Tamazight language.""" + + TAMAZIGHT_LATIN = (2143, "tmz-DZ", "The Tamazight Latin language.") + """The Tamazight Latin language.""" + + TAMIL = (1097, "ta-IN", "The Tamil language.") + """The Tamil language.""" + + TATAR = (1092, "tt-RU", "The Tatar language.") + """The Tatar language.""" + + TELUGU = (1098, "te-IN", "The Telugu language.") + """The Telugu language.""" + + THAI = (1054, "th-TH", "The Thai language.") + """The Thai language.""" + + TIBETAN = (1105, "bo-CN", "The Tibetan language.") + """The Tibetan language.""" + + TIGRIGNA_ERITREA = (2163, "ti-ER", "The Tigrigna Eritrea language.") + """The Tigrigna Eritrea language.""" + + TIGRIGNA_ETHIOPIC = (1139, "ti-ET", "The Tigrigna Ethiopic language.") + """The Tigrigna Ethiopic language.""" + + TRADITIONAL_CHINESE = (1028, "zh-TW", "The Traditional Chinese language.") + """The Traditional Chinese language.""" + + TSONGA = (1073, "ts-ZA", "The Tsonga language.") + """The Tsonga language.""" + + TSWANA = (1074, "tn-ZA", "The Tswana language.") + """The Tswana language.""" + + TURKISH = (1055, "tr-TR", "The Turkish language.") + """The Turkish language.""" + + TURKMEN = (1090, "tk-TM", "The Turkmen language.") + """The Turkmen language.""" + + UKRAINIAN = (1058, "uk-UA", "The Ukrainian language.") + """The Ukrainian language.""" + + URDU = (1056, "ur-PK", "The Urdu language.") + """The Urdu language.""" + + UZBEK_CYRILLIC = (2115, "uz-UZ", "The Uzbek Cyrillic language.") + """The Uzbek Cyrillic language.""" + + UZBEK_LATIN = (1091, "uz-Latn-UZ", "The Uzbek Latin language.") + """The Uzbek Latin language.""" + + VENDA = (1075, "ve-ZA", "The Venda language.") + """The Venda language.""" + + VIETNAMESE = (1066, "vi-VN", "The Vietnamese language.") + """The Vietnamese language.""" + + WELSH = (1106, "cy-GB", "The Welsh language.") + """The Welsh language.""" + + XHOSA = (1076, "xh-ZA", "The Xhosa language.") + """The Xhosa language.""" + + YI = (1144, "ii-CN", "The Yi language.") + """The Yi language.""" + + YIDDISH = (1085, "yi-Hebr", "The Yiddish language.") + """The Yiddish language.""" + + YORUBA = (1130, "yo-NG", "The Yoruba language.") + """The Yoruba language.""" + + ZULU = (1077, "zu-ZA", "The Zulu language.") + """The Zulu language.""" diff --git a/src/pptx/enum/shapes.py b/src/pptx/enum/shapes.py index e4758cf02..b1dec8adc 100644 --- a/src/pptx/enum/shapes.py +++ b/src/pptx/enum/shapes.py @@ -1,22 +1,14 @@ -# encoding: utf-8 - """Enumerations used by shapes and related objects.""" -from pptx.enum.base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) -from pptx.util import lazyproperty +from __future__ import annotations +import enum + +from pptx.enum.base import BaseEnum, BaseXmlEnum -@alias("MSO_SHAPE") -class MSO_AUTO_SHAPE_TYPE(XmlEnumeration): - """ - Specifies a type of AutoShape, e.g. DOWN_ARROW + +class MSO_AUTO_SHAPE_TYPE(BaseXmlEnum): + """Specifies a type of AutoShape, e.g. DOWN_ARROW. Alias: ``MSO_SHAPE`` @@ -29,618 +21,703 @@ class MSO_AUTO_SHAPE_TYPE(XmlEnumeration): slide.shapes.add_shape( MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height ) + + MS API Name: `MsoAutoShapeType` + + https://learn.microsoft.com/en-us/office/vba/api/Office.MsoAutoShapeType """ - __ms_name__ = "MsoAutoShapeType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff862770(v=office.15" ").aspx" - ) - - __members__ = ( - XmlMappedEnumMember( - "ACTION_BUTTON_BACK_OR_PREVIOUS", - 129, - "actionButtonBackPrevious", - "Back or Previous button. Supports " "mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_BEGINNING", - 131, - "actionButtonBeginning", - "Beginning button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_CUSTOM", - 125, - "actionButtonBlank", - "Button with no default picture or text. Supports mouse-click an" - "d mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_DOCUMENT", - 134, - "actionButtonDocument", - "Document button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_END", - 132, - "actionButtonEnd", - "End button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_FORWARD_OR_NEXT", - 130, - "actionButtonForwardNext", - "Forward or Next button. Supports mouse-click and mouse-over act" "ions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_HELP", - 127, - "actionButtonHelp", - "Help button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_HOME", - 126, - "actionButtonHome", - "Home button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_INFORMATION", - 128, - "actionButtonInformation", - "Information button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_MOVIE", - 136, - "actionButtonMovie", - "Movie button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_RETURN", - 133, - "actionButtonReturn", - "Return button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember( - "ACTION_BUTTON_SOUND", - 135, - "actionButtonSound", - "Sound button. Supports mouse-click and mouse-over actions", - ), - XmlMappedEnumMember("ARC", 25, "arc", "Arc"), - XmlMappedEnumMember( - "BALLOON", 137, "wedgeRoundRectCallout", "Rounded Rectangular Callout" - ), - XmlMappedEnumMember( - "BENT_ARROW", - 41, - "bentArrow", - "Block arrow that follows a curved 90-degree angle", - ), - XmlMappedEnumMember( - "BENT_UP_ARROW", - 44, - "bentUpArrow", - "Block arrow that follows a sharp 90-degree angle. Points up by " "default", - ), - XmlMappedEnumMember("BEVEL", 15, "bevel", "Bevel"), - XmlMappedEnumMember("BLOCK_ARC", 20, "blockArc", "Block arc"), - XmlMappedEnumMember("CAN", 13, "can", "Can"), - XmlMappedEnumMember("CHART_PLUS", 182, "chartPlus", "Chart Plus"), - XmlMappedEnumMember("CHART_STAR", 181, "chartStar", "Chart Star"), - XmlMappedEnumMember("CHART_X", 180, "chartX", "Chart X"), - XmlMappedEnumMember("CHEVRON", 52, "chevron", "Chevron"), - XmlMappedEnumMember("CHORD", 161, "chord", "Geometric chord shape"), - XmlMappedEnumMember( - "CIRCULAR_ARROW", - 60, - "circularArrow", - "Block arrow that follows a curved 180-degree angle", - ), - XmlMappedEnumMember("CLOUD", 179, "cloud", "Cloud"), - XmlMappedEnumMember("CLOUD_CALLOUT", 108, "cloudCallout", "Cloud callout"), - XmlMappedEnumMember("CORNER", 162, "corner", "Corner"), - XmlMappedEnumMember("CORNER_TABS", 169, "cornerTabs", "Corner Tabs"), - XmlMappedEnumMember("CROSS", 11, "plus", "Cross"), - XmlMappedEnumMember("CUBE", 14, "cube", "Cube"), - XmlMappedEnumMember( - "CURVED_DOWN_ARROW", 48, "curvedDownArrow", "Block arrow that curves down" - ), - XmlMappedEnumMember( - "CURVED_DOWN_RIBBON", 100, "ellipseRibbon", "Ribbon banner that curves down" - ), - XmlMappedEnumMember( - "CURVED_LEFT_ARROW", 46, "curvedLeftArrow", "Block arrow that curves left" - ), - XmlMappedEnumMember( - "CURVED_RIGHT_ARROW", - 45, - "curvedRightArrow", - "Block arrow that curves right", - ), - XmlMappedEnumMember( - "CURVED_UP_ARROW", 47, "curvedUpArrow", "Block arrow that curves up" - ), - XmlMappedEnumMember( - "CURVED_UP_RIBBON", 99, "ellipseRibbon2", "Ribbon banner that curves up" - ), - XmlMappedEnumMember("DECAGON", 144, "decagon", "Decagon"), - XmlMappedEnumMember("DIAGONAL_STRIPE", 141, "diagStripe", "Diagonal Stripe"), - XmlMappedEnumMember("DIAMOND", 4, "diamond", "Diamond"), - XmlMappedEnumMember("DODECAGON", 146, "dodecagon", "Dodecagon"), - XmlMappedEnumMember("DONUT", 18, "donut", "Donut"), - XmlMappedEnumMember("DOUBLE_BRACE", 27, "bracePair", "Double brace"), - XmlMappedEnumMember("DOUBLE_BRACKET", 26, "bracketPair", "Double bracket"), - XmlMappedEnumMember("DOUBLE_WAVE", 104, "doubleWave", "Double wave"), - XmlMappedEnumMember( - "DOWN_ARROW", 36, "downArrow", "Block arrow that points down" - ), - XmlMappedEnumMember( - "DOWN_ARROW_CALLOUT", - 56, - "downArrowCallout", - "Callout with arrow that points down", - ), - XmlMappedEnumMember( - "DOWN_RIBBON", - 98, - "ribbon", - "Ribbon banner with center area below ribbon ends", - ), - XmlMappedEnumMember("EXPLOSION1", 89, "irregularSeal1", "Explosion"), - XmlMappedEnumMember("EXPLOSION2", 90, "irregularSeal2", "Explosion"), - XmlMappedEnumMember( - "FLOWCHART_ALTERNATE_PROCESS", - 62, - "flowChartAlternateProcess", - "Alternate process flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_CARD", 75, "flowChartPunchedCard", "Card flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_COLLATE", 79, "flowChartCollate", "Collate flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_CONNECTOR", - 73, - "flowChartConnector", - "Connector flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_DATA", 64, "flowChartInputOutput", "Data flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DECISION", 63, "flowChartDecision", "Decision flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DELAY", 84, "flowChartDelay", "Delay flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DIRECT_ACCESS_STORAGE", - 87, - "flowChartMagneticDrum", - "Direct access storage flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_DISPLAY", 88, "flowChartDisplay", "Display flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_DOCUMENT", 67, "flowChartDocument", "Document flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_EXTRACT", 81, "flowChartExtract", "Extract flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_INTERNAL_STORAGE", - 66, - "flowChartInternalStorage", - "Internal storage flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MAGNETIC_DISK", - 86, - "flowChartMagneticDisk", - "Magnetic disk flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MANUAL_INPUT", - 71, - "flowChartManualInput", - "Manual input flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MANUAL_OPERATION", - 72, - "flowChartManualOperation", - "Manual operation flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_MERGE", 82, "flowChartMerge", "Merge flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_MULTIDOCUMENT", - 68, - "flowChartMultidocument", - "Multi-document flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_OFFLINE_STORAGE", - 139, - "flowChartOfflineStorage", - "Offline Storage", - ), - XmlMappedEnumMember( - "FLOWCHART_OFFPAGE_CONNECTOR", - 74, - "flowChartOffpageConnector", - "Off-page connector flowchart symbol", - ), - XmlMappedEnumMember("FLOWCHART_OR", 78, "flowChartOr", '"Or" flowchart symbol'), - XmlMappedEnumMember( - "FLOWCHART_PREDEFINED_PROCESS", - 65, - "flowChartPredefinedProcess", - "Predefined process flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_PREPARATION", - 70, - "flowChartPreparation", - "Preparation flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_PROCESS", 61, "flowChartProcess", "Process flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_PUNCHED_TAPE", - 76, - "flowChartPunchedTape", - "Punched tape flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_SEQUENTIAL_ACCESS_STORAGE", - 85, - "flowChartMagneticTape", - "Sequential access storage flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_SORT", 80, "flowChartSort", "Sort flowchart symbol" - ), - XmlMappedEnumMember( - "FLOWCHART_STORED_DATA", - 83, - "flowChartOnlineStorage", - "Stored data flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_SUMMING_JUNCTION", - 77, - "flowChartSummingJunction", - "Summing junction flowchart symbol", - ), - XmlMappedEnumMember( - "FLOWCHART_TERMINATOR", - 69, - "flowChartTerminator", - "Terminator flowchart symbol", - ), - XmlMappedEnumMember("FOLDED_CORNER", 16, "foldedCorner", "Folded corner"), - XmlMappedEnumMember("FRAME", 158, "frame", "Frame"), - XmlMappedEnumMember("FUNNEL", 174, "funnel", "Funnel"), - XmlMappedEnumMember("GEAR_6", 172, "gear6", "Gear 6"), - XmlMappedEnumMember("GEAR_9", 173, "gear9", "Gear 9"), - XmlMappedEnumMember("HALF_FRAME", 159, "halfFrame", "Half Frame"), - XmlMappedEnumMember("HEART", 21, "heart", "Heart"), - XmlMappedEnumMember("HEPTAGON", 145, "heptagon", "Heptagon"), - XmlMappedEnumMember("HEXAGON", 10, "hexagon", "Hexagon"), - XmlMappedEnumMember( - "HORIZONTAL_SCROLL", 102, "horizontalScroll", "Horizontal scroll" - ), - XmlMappedEnumMember("ISOSCELES_TRIANGLE", 7, "triangle", "Isosceles triangle"), - XmlMappedEnumMember( - "LEFT_ARROW", 34, "leftArrow", "Block arrow that points left" - ), - XmlMappedEnumMember( - "LEFT_ARROW_CALLOUT", - 54, - "leftArrowCallout", - "Callout with arrow that points left", - ), - XmlMappedEnumMember("LEFT_BRACE", 31, "leftBrace", "Left brace"), - XmlMappedEnumMember("LEFT_BRACKET", 29, "leftBracket", "Left bracket"), - XmlMappedEnumMember( - "LEFT_CIRCULAR_ARROW", 176, "leftCircularArrow", "Left Circular Arrow" - ), - XmlMappedEnumMember( - "LEFT_RIGHT_ARROW", - 37, - "leftRightArrow", - "Block arrow with arrowheads that point both left and right", - ), - XmlMappedEnumMember( - "LEFT_RIGHT_ARROW_CALLOUT", - 57, - "leftRightArrowCallout", - "Callout with arrowheads that point both left and right", - ), - XmlMappedEnumMember( - "LEFT_RIGHT_CIRCULAR_ARROW", - 177, - "leftRightCircularArrow", - "Left Right Circular Arrow", - ), - XmlMappedEnumMember( - "LEFT_RIGHT_RIBBON", 140, "leftRightRibbon", "Left Right Ribbon" - ), - XmlMappedEnumMember( - "LEFT_RIGHT_UP_ARROW", - 40, - "leftRightUpArrow", - "Block arrow with arrowheads that point left, right, and up", - ), - XmlMappedEnumMember( - "LEFT_UP_ARROW", - 43, - "leftUpArrow", - "Block arrow with arrowheads that point left and up", - ), - XmlMappedEnumMember("LIGHTNING_BOLT", 22, "lightningBolt", "Lightning bolt"), - XmlMappedEnumMember( - "LINE_CALLOUT_1", - 109, - "borderCallout1", - "Callout with border and horizontal callout line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_1_ACCENT_BAR", - 113, - "accentCallout1", - "Callout with vertical accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR", - 121, - "accentBorderCallout1", - "Callout with border and vertical accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_1_NO_BORDER", 117, "callout1", "Callout with horizontal line" - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2", - 110, - "borderCallout2", - "Callout with diagonal straight line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2_ACCENT_BAR", - 114, - "accentCallout2", - "Callout with diagonal callout line and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR", - 122, - "accentBorderCallout2", - "Callout with border, diagonal straight line, and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_2_NO_BORDER", - 118, - "callout2", - "Callout with no border and diagonal callout line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3", 111, "borderCallout3", "Callout with angled line" - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3_ACCENT_BAR", - 115, - "accentCallout3", - "Callout with angled callout line and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR", - 123, - "accentBorderCallout3", - "Callout with border, angled callout line, and accent bar", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_3_NO_BORDER", - 119, - "callout3", - "Callout with no border and angled callout line", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4", - 112, - "borderCallout3", - "Callout with callout line segments forming a U-shape.", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4_ACCENT_BAR", - 116, - "accentCallout3", - "Callout with accent bar and callout line segments forming a U-s" "hape.", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR", - 124, - "accentBorderCallout3", - "Callout with border, accent bar, and callout line segments form" - "ing a U-shape.", - ), - XmlMappedEnumMember( - "LINE_CALLOUT_4_NO_BORDER", - 120, - "callout3", - "Callout with no border and callout line segments forming a U-sh" "ape.", - ), - XmlMappedEnumMember("LINE_INVERSE", 183, "lineInv", "Straight Connector"), - XmlMappedEnumMember("MATH_DIVIDE", 166, "mathDivide", "Division"), - XmlMappedEnumMember("MATH_EQUAL", 167, "mathEqual", "Equal"), - XmlMappedEnumMember("MATH_MINUS", 164, "mathMinus", "Minus"), - XmlMappedEnumMember("MATH_MULTIPLY", 165, "mathMultiply", "Multiply"), - XmlMappedEnumMember("MATH_NOT_EQUAL", 168, "mathNotEqual", "Not Equal"), - XmlMappedEnumMember("MATH_PLUS", 163, "mathPlus", "Plus"), - XmlMappedEnumMember("MOON", 24, "moon", "Moon"), - XmlMappedEnumMember( - "NON_ISOSCELES_TRAPEZOID", - 143, - "nonIsoscelesTrapezoid", - "Non-isosceles Trapezoid", - ), - XmlMappedEnumMember( - "NOTCHED_RIGHT_ARROW", - 50, - "notchedRightArrow", - "Notched block arrow that points right", - ), - XmlMappedEnumMember("NO_SYMBOL", 19, "noSmoking", '"No" Symbol'), - XmlMappedEnumMember("OCTAGON", 6, "octagon", "Octagon"), - XmlMappedEnumMember("OVAL", 9, "ellipse", "Oval"), - XmlMappedEnumMember( - "OVAL_CALLOUT", 107, "wedgeEllipseCallout", "Oval-shaped callout" - ), - XmlMappedEnumMember("PARALLELOGRAM", 2, "parallelogram", "Parallelogram"), - XmlMappedEnumMember("PENTAGON", 51, "homePlate", "Pentagon"), - XmlMappedEnumMember("PIE", 142, "pie", "Pie"), - XmlMappedEnumMember("PIE_WEDGE", 175, "pieWedge", "Pie"), - XmlMappedEnumMember("PLAQUE", 28, "plaque", "Plaque"), - XmlMappedEnumMember("PLAQUE_TABS", 171, "plaqueTabs", "Plaque Tabs"), - XmlMappedEnumMember( - "QUAD_ARROW", - 39, - "quadArrow", - "Block arrows that point up, down, left, and right", - ), - XmlMappedEnumMember( - "QUAD_ARROW_CALLOUT", - 59, - "quadArrowCallout", - "Callout with arrows that point up, down, left, and right", - ), - XmlMappedEnumMember("RECTANGLE", 1, "rect", "Rectangle"), - XmlMappedEnumMember( - "RECTANGULAR_CALLOUT", 105, "wedgeRectCallout", "Rectangular callout" - ), - XmlMappedEnumMember("REGULAR_PENTAGON", 12, "pentagon", "Pentagon"), - XmlMappedEnumMember( - "RIGHT_ARROW", 33, "rightArrow", "Block arrow that points right" - ), - XmlMappedEnumMember( - "RIGHT_ARROW_CALLOUT", - 53, - "rightArrowCallout", - "Callout with arrow that points right", - ), - XmlMappedEnumMember("RIGHT_BRACE", 32, "rightBrace", "Right brace"), - XmlMappedEnumMember("RIGHT_BRACKET", 30, "rightBracket", "Right bracket"), - XmlMappedEnumMember("RIGHT_TRIANGLE", 8, "rtTriangle", "Right triangle"), - XmlMappedEnumMember("ROUNDED_RECTANGLE", 5, "roundRect", "Rounded rectangle"), - XmlMappedEnumMember( - "ROUNDED_RECTANGULAR_CALLOUT", - 106, - "wedgeRoundRectCallout", - "Rounded rectangle-shaped callout", - ), - XmlMappedEnumMember( - "ROUND_1_RECTANGLE", 151, "round1Rect", "Round Single Corner Rectangle" - ), - XmlMappedEnumMember( - "ROUND_2_DIAG_RECTANGLE", - 153, - "round2DiagRect", - "Round Diagonal Corner Rectangle", - ), - XmlMappedEnumMember( - "ROUND_2_SAME_RECTANGLE", - 152, - "round2SameRect", - "Round Same Side Corner Rectangle", - ), - XmlMappedEnumMember("SMILEY_FACE", 17, "smileyFace", "Smiley face"), - XmlMappedEnumMember( - "SNIP_1_RECTANGLE", 155, "snip1Rect", "Snip Single Corner Rectangle" - ), - XmlMappedEnumMember( - "SNIP_2_DIAG_RECTANGLE", - 157, - "snip2DiagRect", - "Snip Diagonal Corner Rectangle", - ), - XmlMappedEnumMember( - "SNIP_2_SAME_RECTANGLE", - 156, - "snip2SameRect", - "Snip Same Side Corner Rectangle", - ), - XmlMappedEnumMember( - "SNIP_ROUND_RECTANGLE", - 154, - "snipRoundRect", - "Snip and Round Single Corner Rectangle", - ), - XmlMappedEnumMember("SQUARE_TABS", 170, "squareTabs", "Square Tabs"), - XmlMappedEnumMember("STAR_10_POINT", 149, "star10", "10-Point Star"), - XmlMappedEnumMember("STAR_12_POINT", 150, "star12", "12-Point Star"), - XmlMappedEnumMember("STAR_16_POINT", 94, "star16", "16-point star"), - XmlMappedEnumMember("STAR_24_POINT", 95, "star24", "24-point star"), - XmlMappedEnumMember("STAR_32_POINT", 96, "star32", "32-point star"), - XmlMappedEnumMember("STAR_4_POINT", 91, "star4", "4-point star"), - XmlMappedEnumMember("STAR_5_POINT", 92, "star5", "5-point star"), - XmlMappedEnumMember("STAR_6_POINT", 147, "star6", "6-Point Star"), - XmlMappedEnumMember("STAR_7_POINT", 148, "star7", "7-Point Star"), - XmlMappedEnumMember("STAR_8_POINT", 93, "star8", "8-point star"), - XmlMappedEnumMember( - "STRIPED_RIGHT_ARROW", - 49, - "stripedRightArrow", - "Block arrow that points right with stripes at the tail", - ), - XmlMappedEnumMember("SUN", 23, "sun", "Sun"), - XmlMappedEnumMember("SWOOSH_ARROW", 178, "swooshArrow", "Swoosh Arrow"), - XmlMappedEnumMember("TEAR", 160, "teardrop", "Teardrop"), - XmlMappedEnumMember("TRAPEZOID", 3, "trapezoid", "Trapezoid"), - XmlMappedEnumMember("UP_ARROW", 35, "upArrow", "Block arrow that points up"), - XmlMappedEnumMember( - "UP_ARROW_CALLOUT", - 55, - "upArrowCallout", - "Callout with arrow that points up", - ), - XmlMappedEnumMember( - "UP_DOWN_ARROW", 38, "upDownArrow", "Block arrow that points up and down" - ), - XmlMappedEnumMember( - "UP_DOWN_ARROW_CALLOUT", - 58, - "upDownArrowCallout", - "Callout with arrows that point up and down", - ), - XmlMappedEnumMember( - "UP_RIBBON", - 97, - "ribbon2", - "Ribbon banner with center area above ribbon ends", - ), - XmlMappedEnumMember( - "U_TURN_ARROW", 42, "uturnArrow", "Block arrow forming a U shape" - ), - XmlMappedEnumMember( - "VERTICAL_SCROLL", 101, "verticalScroll", "Vertical scroll" - ), - XmlMappedEnumMember("WAVE", 103, "wave", "Wave"), - ) - - -@alias("MSO_CONNECTOR") -class MSO_CONNECTOR_TYPE(XmlEnumeration): + ACTION_BUTTON_BACK_OR_PREVIOUS = ( + 129, + "actionButtonBackPrevious", + "Back or Previous button. Supports mouse-click and mouse-over actions", + ) + """Back or Previous button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_BEGINNING = ( + 131, + "actionButtonBeginning", + "Beginning button. Supports mouse-click and mouse-over actions", + ) + """Beginning button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_CUSTOM = ( + 125, + "actionButtonBlank", + "Button with no default picture or text. Supports mouse-click and mouse-over actions", + ) + """Button with no default picture or text. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_DOCUMENT = ( + 134, + "actionButtonDocument", + "Document button. Supports mouse-click and mouse-over actions", + ) + """Document button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_END = ( + 132, + "actionButtonEnd", + "End button. Supports mouse-click and mouse-over actions", + ) + """End button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_FORWARD_OR_NEXT = ( + 130, + "actionButtonForwardNext", + "Forward or Next button. Supports mouse-click and mouse-over actions", + ) + """Forward or Next button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_HELP = ( + 127, + "actionButtonHelp", + "Help button. Supports mouse-click and mouse-over actions", + ) + """Help button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_HOME = ( + 126, + "actionButtonHome", + "Home button. Supports mouse-click and mouse-over actions", + ) + """Home button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_INFORMATION = ( + 128, + "actionButtonInformation", + "Information button. Supports mouse-click and mouse-over actions", + ) + """Information button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_MOVIE = ( + 136, + "actionButtonMovie", + "Movie button. Supports mouse-click and mouse-over actions", + ) + """Movie button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_RETURN = ( + 133, + "actionButtonReturn", + "Return button. Supports mouse-click and mouse-over actions", + ) + """Return button. Supports mouse-click and mouse-over actions""" + + ACTION_BUTTON_SOUND = ( + 135, + "actionButtonSound", + "Sound button. Supports mouse-click and mouse-over actions", + ) + """Sound button. Supports mouse-click and mouse-over actions""" + + ARC = (25, "arc", "Arc") + """Arc""" + + BALLOON = (137, "wedgeRoundRectCallout", "Rounded Rectangular Callout") + """Rounded Rectangular Callout""" + + BENT_ARROW = (41, "bentArrow", "Block arrow that follows a curved 90-degree angle") + """Block arrow that follows a curved 90-degree angle""" + + BENT_UP_ARROW = ( + 44, + "bentUpArrow", + "Block arrow that follows a sharp 90-degree angle. Points up by default", + ) + """Block arrow that follows a sharp 90-degree angle. Points up by default""" + + BEVEL = (15, "bevel", "Bevel") + """Bevel""" + + BLOCK_ARC = (20, "blockArc", "Block arc") + """Block arc""" + + CAN = (13, "can", "Can") + """Can""" + + CHART_PLUS = (182, "chartPlus", "Chart Plus") + """Chart Plus""" + + CHART_STAR = (181, "chartStar", "Chart Star") + """Chart Star""" + + CHART_X = (180, "chartX", "Chart X") + """Chart X""" + + CHEVRON = (52, "chevron", "Chevron") + """Chevron""" + + CHORD = (161, "chord", "Geometric chord shape") + """Geometric chord shape""" + + CIRCULAR_ARROW = (60, "circularArrow", "Block arrow that follows a curved 180-degree angle") + """Block arrow that follows a curved 180-degree angle""" + + CLOUD = (179, "cloud", "Cloud") + """Cloud""" + + CLOUD_CALLOUT = (108, "cloudCallout", "Cloud callout") + """Cloud callout""" + + CORNER = (162, "corner", "Corner") + """Corner""" + + CORNER_TABS = (169, "cornerTabs", "Corner Tabs") + """Corner Tabs""" + + CROSS = (11, "plus", "Cross") + """Cross""" + + CUBE = (14, "cube", "Cube") + """Cube""" + + CURVED_DOWN_ARROW = (48, "curvedDownArrow", "Block arrow that curves down") + """Block arrow that curves down""" + + CURVED_DOWN_RIBBON = (100, "ellipseRibbon", "Ribbon banner that curves down") + """Ribbon banner that curves down""" + + CURVED_LEFT_ARROW = (46, "curvedLeftArrow", "Block arrow that curves left") + """Block arrow that curves left""" + + CURVED_RIGHT_ARROW = (45, "curvedRightArrow", "Block arrow that curves right") + """Block arrow that curves right""" + + CURVED_UP_ARROW = (47, "curvedUpArrow", "Block arrow that curves up") + """Block arrow that curves up""" + + CURVED_UP_RIBBON = (99, "ellipseRibbon2", "Ribbon banner that curves up") + """Ribbon banner that curves up""" + + DECAGON = (144, "decagon", "Decagon") + """Decagon""" + + DIAGONAL_STRIPE = (141, "diagStripe", "Diagonal Stripe") + """Diagonal Stripe""" + + DIAMOND = (4, "diamond", "Diamond") + """Diamond""" + + DODECAGON = (146, "dodecagon", "Dodecagon") + """Dodecagon""" + + DONUT = (18, "donut", "Donut") + """Donut""" + + DOUBLE_BRACE = (27, "bracePair", "Double brace") + """Double brace""" + + DOUBLE_BRACKET = (26, "bracketPair", "Double bracket") + """Double bracket""" + + DOUBLE_WAVE = (104, "doubleWave", "Double wave") + """Double wave""" + + DOWN_ARROW = (36, "downArrow", "Block arrow that points down") + """Block arrow that points down""" + + DOWN_ARROW_CALLOUT = (56, "downArrowCallout", "Callout with arrow that points down") + """Callout with arrow that points down""" + + DOWN_RIBBON = (98, "ribbon", "Ribbon banner with center area below ribbon ends") + """Ribbon banner with center area below ribbon ends""" + + EXPLOSION1 = (89, "irregularSeal1", "Explosion") + """Explosion""" + + EXPLOSION2 = (90, "irregularSeal2", "Explosion") + """Explosion""" + + FLOWCHART_ALTERNATE_PROCESS = ( + 62, + "flowChartAlternateProcess", + "Alternate process flowchart symbol", + ) + """Alternate process flowchart symbol""" + + FLOWCHART_CARD = (75, "flowChartPunchedCard", "Card flowchart symbol") + """Card flowchart symbol""" + + FLOWCHART_COLLATE = (79, "flowChartCollate", "Collate flowchart symbol") + """Collate flowchart symbol""" + + FLOWCHART_CONNECTOR = (73, "flowChartConnector", "Connector flowchart symbol") + """Connector flowchart symbol""" + + FLOWCHART_DATA = (64, "flowChartInputOutput", "Data flowchart symbol") + """Data flowchart symbol""" + + FLOWCHART_DECISION = (63, "flowChartDecision", "Decision flowchart symbol") + """Decision flowchart symbol""" + + FLOWCHART_DELAY = (84, "flowChartDelay", "Delay flowchart symbol") + """Delay flowchart symbol""" + + FLOWCHART_DIRECT_ACCESS_STORAGE = ( + 87, + "flowChartMagneticDrum", + "Direct access storage flowchart symbol", + ) + """Direct access storage flowchart symbol""" + + FLOWCHART_DISPLAY = (88, "flowChartDisplay", "Display flowchart symbol") + """Display flowchart symbol""" + + FLOWCHART_DOCUMENT = (67, "flowChartDocument", "Document flowchart symbol") + """Document flowchart symbol""" + + FLOWCHART_EXTRACT = (81, "flowChartExtract", "Extract flowchart symbol") + """Extract flowchart symbol""" + + FLOWCHART_INTERNAL_STORAGE = ( + 66, + "flowChartInternalStorage", + "Internal storage flowchart symbol", + ) + """Internal storage flowchart symbol""" + + FLOWCHART_MAGNETIC_DISK = (86, "flowChartMagneticDisk", "Magnetic disk flowchart symbol") + """Magnetic disk flowchart symbol""" + + FLOWCHART_MANUAL_INPUT = (71, "flowChartManualInput", "Manual input flowchart symbol") + """Manual input flowchart symbol""" + + FLOWCHART_MANUAL_OPERATION = ( + 72, + "flowChartManualOperation", + "Manual operation flowchart symbol", + ) + """Manual operation flowchart symbol""" + + FLOWCHART_MERGE = (82, "flowChartMerge", "Merge flowchart symbol") + """Merge flowchart symbol""" + + FLOWCHART_MULTIDOCUMENT = (68, "flowChartMultidocument", "Multi-document flowchart symbol") + """Multi-document flowchart symbol""" + + FLOWCHART_OFFLINE_STORAGE = (139, "flowChartOfflineStorage", "Offline Storage") + """Offline Storage""" + + FLOWCHART_OFFPAGE_CONNECTOR = ( + 74, + "flowChartOffpageConnector", + "Off-page connector flowchart symbol", + ) + """Off-page connector flowchart symbol""" + + FLOWCHART_OR = (78, "flowChartOr", '"Or" flowchart symbol') + """\"Or\" flowchart symbol""" + + FLOWCHART_PREDEFINED_PROCESS = ( + 65, + "flowChartPredefinedProcess", + "Predefined process flowchart symbol", + ) + """Predefined process flowchart symbol""" + + FLOWCHART_PREPARATION = (70, "flowChartPreparation", "Preparation flowchart symbol") + """Preparation flowchart symbol""" + + FLOWCHART_PROCESS = (61, "flowChartProcess", "Process flowchart symbol") + """Process flowchart symbol""" + + FLOWCHART_PUNCHED_TAPE = (76, "flowChartPunchedTape", "Punched tape flowchart symbol") + """Punched tape flowchart symbol""" + + FLOWCHART_SEQUENTIAL_ACCESS_STORAGE = ( + 85, + "flowChartMagneticTape", + "Sequential access storage flowchart symbol", + ) + """Sequential access storage flowchart symbol""" + + FLOWCHART_SORT = (80, "flowChartSort", "Sort flowchart symbol") + """Sort flowchart symbol""" + + FLOWCHART_STORED_DATA = (83, "flowChartOnlineStorage", "Stored data flowchart symbol") + """Stored data flowchart symbol""" + + FLOWCHART_SUMMING_JUNCTION = ( + 77, + "flowChartSummingJunction", + "Summing junction flowchart symbol", + ) + """Summing junction flowchart symbol""" + + FLOWCHART_TERMINATOR = (69, "flowChartTerminator", "Terminator flowchart symbol") + """Terminator flowchart symbol""" + + FOLDED_CORNER = (16, "foldedCorner", "Folded corner") + """Folded corner""" + + FRAME = (158, "frame", "Frame") + """Frame""" + + FUNNEL = (174, "funnel", "Funnel") + """Funnel""" + + GEAR_6 = (172, "gear6", "Gear 6") + """Gear 6""" + + GEAR_9 = (173, "gear9", "Gear 9") + """Gear 9""" + + HALF_FRAME = (159, "halfFrame", "Half Frame") + """Half Frame""" + + HEART = (21, "heart", "Heart") + """Heart""" + + HEPTAGON = (145, "heptagon", "Heptagon") + """Heptagon""" + + HEXAGON = (10, "hexagon", "Hexagon") + """Hexagon""" + + HORIZONTAL_SCROLL = (102, "horizontalScroll", "Horizontal scroll") + """Horizontal scroll""" + + ISOSCELES_TRIANGLE = (7, "triangle", "Isosceles triangle") + """Isosceles triangle""" + + LEFT_ARROW = (34, "leftArrow", "Block arrow that points left") + """Block arrow that points left""" + + LEFT_ARROW_CALLOUT = (54, "leftArrowCallout", "Callout with arrow that points left") + """Callout with arrow that points left""" + + LEFT_BRACE = (31, "leftBrace", "Left brace") + """Left brace""" + + LEFT_BRACKET = (29, "leftBracket", "Left bracket") + """Left bracket""" + + LEFT_CIRCULAR_ARROW = (176, "leftCircularArrow", "Left Circular Arrow") + """Left Circular Arrow""" + + LEFT_RIGHT_ARROW = ( + 37, + "leftRightArrow", + "Block arrow with arrowheads that point both left and right", + ) + """Block arrow with arrowheads that point both left and right""" + + LEFT_RIGHT_ARROW_CALLOUT = ( + 57, + "leftRightArrowCallout", + "Callout with arrowheads that point both left and right", + ) + """Callout with arrowheads that point both left and right""" + + LEFT_RIGHT_CIRCULAR_ARROW = (177, "leftRightCircularArrow", "Left Right Circular Arrow") + """Left Right Circular Arrow""" + + LEFT_RIGHT_RIBBON = (140, "leftRightRibbon", "Left Right Ribbon") + """Left Right Ribbon""" + + LEFT_RIGHT_UP_ARROW = ( + 40, + "leftRightUpArrow", + "Block arrow with arrowheads that point left, right, and up", + ) + """Block arrow with arrowheads that point left, right, and up""" + + LEFT_UP_ARROW = (43, "leftUpArrow", "Block arrow with arrowheads that point left and up") + """Block arrow with arrowheads that point left and up""" + + LIGHTNING_BOLT = (22, "lightningBolt", "Lightning bolt") + """Lightning bolt""" + + LINE_CALLOUT_1 = (109, "borderCallout1", "Callout with border and horizontal callout line") + """Callout with border and horizontal callout line""" + + LINE_CALLOUT_1_ACCENT_BAR = (113, "accentCallout1", "Callout with vertical accent bar") + """Callout with vertical accent bar""" + + LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR = ( + 121, + "accentBorderCallout1", + "Callout with border and vertical accent bar", + ) + """Callout with border and vertical accent bar""" + + LINE_CALLOUT_1_NO_BORDER = (117, "callout1", "Callout with horizontal line") + """Callout with horizontal line""" + + LINE_CALLOUT_2 = (110, "borderCallout2", "Callout with diagonal straight line") + """Callout with diagonal straight line""" + + LINE_CALLOUT_2_ACCENT_BAR = ( + 114, + "accentCallout2", + "Callout with diagonal callout line and accent bar", + ) + """Callout with diagonal callout line and accent bar""" + + LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR = ( + 122, + "accentBorderCallout2", + "Callout with border, diagonal straight line, and accent bar", + ) + """Callout with border, diagonal straight line, and accent bar""" + + LINE_CALLOUT_2_NO_BORDER = (118, "callout2", "Callout with no border and diagonal callout line") + """Callout with no border and diagonal callout line""" + + LINE_CALLOUT_3 = (111, "borderCallout3", "Callout with angled line") + """Callout with angled line""" + + LINE_CALLOUT_3_ACCENT_BAR = ( + 115, + "accentCallout3", + "Callout with angled callout line and accent bar", + ) + """Callout with angled callout line and accent bar""" + + LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR = ( + 123, + "accentBorderCallout3", + "Callout with border, angled callout line, and accent bar", + ) + """Callout with border, angled callout line, and accent bar""" + + LINE_CALLOUT_3_NO_BORDER = (119, "callout3", "Callout with no border and angled callout line") + """Callout with no border and angled callout line""" + + LINE_CALLOUT_4 = ( + 112, + "borderCallout3", + "Callout with callout line segments forming a U-shape.", + ) + """Callout with callout line segments forming a U-shape.""" + + LINE_CALLOUT_4_ACCENT_BAR = ( + 116, + "accentCallout3", + "Callout with accent bar and callout line segments forming a U-shape.", + ) + """Callout with accent bar and callout line segments forming a U-shape.""" + + LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR = ( + 124, + "accentBorderCallout3", + "Callout with border, accent bar, and callout line segments forming a U-shape.", + ) + """Callout with border, accent bar, and callout line segments forming a U-shape.""" + + LINE_CALLOUT_4_NO_BORDER = ( + 120, + "callout3", + "Callout with no border and callout line segments forming a U-shape.", + ) + """Callout with no border and callout line segments forming a U-shape.""" + + LINE_INVERSE = (183, "lineInv", "Straight Connector") + """Straight Connector""" + + MATH_DIVIDE = (166, "mathDivide", "Division") + """Division""" + + MATH_EQUAL = (167, "mathEqual", "Equal") + """Equal""" + + MATH_MINUS = (164, "mathMinus", "Minus") + """Minus""" + + MATH_MULTIPLY = (165, "mathMultiply", "Multiply") + """Multiply""" + + MATH_NOT_EQUAL = (168, "mathNotEqual", "Not Equal") + """Not Equal""" + + MATH_PLUS = (163, "mathPlus", "Plus") + """Plus""" + + MOON = (24, "moon", "Moon") + """Moon""" + + NON_ISOSCELES_TRAPEZOID = (143, "nonIsoscelesTrapezoid", "Non-isosceles Trapezoid") + """Non-isosceles Trapezoid""" + + NOTCHED_RIGHT_ARROW = (50, "notchedRightArrow", "Notched block arrow that points right") + """Notched block arrow that points right""" + + NO_SYMBOL = (19, "noSmoking", "'No' Symbol") + """'No' Symbol""" + + OCTAGON = (6, "octagon", "Octagon") + """Octagon""" + + OVAL = (9, "ellipse", "Oval") + """Oval""" + + OVAL_CALLOUT = (107, "wedgeEllipseCallout", "Oval-shaped callout") + """Oval-shaped callout""" + + PARALLELOGRAM = (2, "parallelogram", "Parallelogram") + """Parallelogram""" + + PENTAGON = (51, "homePlate", "Pentagon") + """Pentagon""" + + PIE = (142, "pie", "Pie") + """Pie""" + + PIE_WEDGE = (175, "pieWedge", "Pie") + """Pie""" + + PLAQUE = (28, "plaque", "Plaque") + """Plaque""" + + PLAQUE_TABS = (171, "plaqueTabs", "Plaque Tabs") + """Plaque Tabs""" + + QUAD_ARROW = (39, "quadArrow", "Block arrows that point up, down, left, and right") + """Block arrows that point up, down, left, and right""" + + QUAD_ARROW_CALLOUT = ( + 59, + "quadArrowCallout", + "Callout with arrows that point up, down, left, and right", + ) + """Callout with arrows that point up, down, left, and right""" + + RECTANGLE = (1, "rect", "Rectangle") + """Rectangle""" + + RECTANGULAR_CALLOUT = (105, "wedgeRectCallout", "Rectangular callout") + """Rectangular callout""" + + REGULAR_PENTAGON = (12, "pentagon", "Pentagon") + """Pentagon""" + + RIGHT_ARROW = (33, "rightArrow", "Block arrow that points right") + """Block arrow that points right""" + + RIGHT_ARROW_CALLOUT = (53, "rightArrowCallout", "Callout with arrow that points right") + """Callout with arrow that points right""" + + RIGHT_BRACE = (32, "rightBrace", "Right brace") + """Right brace""" + + RIGHT_BRACKET = (30, "rightBracket", "Right bracket") + """Right bracket""" + + RIGHT_TRIANGLE = (8, "rtTriangle", "Right triangle") + """Right triangle""" + + ROUNDED_RECTANGLE = (5, "roundRect", "Rounded rectangle") + """Rounded rectangle""" + + ROUNDED_RECTANGULAR_CALLOUT = (106, "wedgeRoundRectCallout", "Rounded rectangle-shaped callout") + """Rounded rectangle-shaped callout""" + + ROUND_1_RECTANGLE = (151, "round1Rect", "Round Single Corner Rectangle") + """Round Single Corner Rectangle""" + + ROUND_2_DIAG_RECTANGLE = (153, "round2DiagRect", "Round Diagonal Corner Rectangle") + """Round Diagonal Corner Rectangle""" + + ROUND_2_SAME_RECTANGLE = (152, "round2SameRect", "Round Same Side Corner Rectangle") + """Round Same Side Corner Rectangle""" + + SMILEY_FACE = (17, "smileyFace", "Smiley face") + """Smiley face""" + + SNIP_1_RECTANGLE = (155, "snip1Rect", "Snip Single Corner Rectangle") + """Snip Single Corner Rectangle""" + + SNIP_2_DIAG_RECTANGLE = (157, "snip2DiagRect", "Snip Diagonal Corner Rectangle") + """Snip Diagonal Corner Rectangle""" + + SNIP_2_SAME_RECTANGLE = (156, "snip2SameRect", "Snip Same Side Corner Rectangle") + """Snip Same Side Corner Rectangle""" + + SNIP_ROUND_RECTANGLE = (154, "snipRoundRect", "Snip and Round Single Corner Rectangle") + """Snip and Round Single Corner Rectangle""" + + SQUARE_TABS = (170, "squareTabs", "Square Tabs") + """Square Tabs""" + + STAR_10_POINT = (149, "star10", "10-Point Star") + """10-Point Star""" + + STAR_12_POINT = (150, "star12", "12-Point Star") + """12-Point Star""" + + STAR_16_POINT = (94, "star16", "16-point star") + """16-point star""" + + STAR_24_POINT = (95, "star24", "24-point star") + """24-point star""" + + STAR_32_POINT = (96, "star32", "32-point star") + """32-point star""" + + STAR_4_POINT = (91, "star4", "4-point star") + """4-point star""" + + STAR_5_POINT = (92, "star5", "5-point star") + """5-point star""" + + STAR_6_POINT = (147, "star6", "6-Point Star") + """6-Point Star""" + + STAR_7_POINT = (148, "star7", "7-Point Star") + """7-Point Star""" + + STAR_8_POINT = (93, "star8", "8-point star") + """8-point star""" + + STRIPED_RIGHT_ARROW = ( + 49, + "stripedRightArrow", + "Block arrow that points right with stripes at the tail", + ) + """Block arrow that points right with stripes at the tail""" + + SUN = (23, "sun", "Sun") + """Sun""" + + SWOOSH_ARROW = (178, "swooshArrow", "Swoosh Arrow") + """Swoosh Arrow""" + + TEAR = (160, "teardrop", "Teardrop") + """Teardrop""" + + TRAPEZOID = (3, "trapezoid", "Trapezoid") + """Trapezoid""" + + UP_ARROW = (35, "upArrow", "Block arrow that points up") + """Block arrow that points up""" + + UP_ARROW_CALLOUT = (55, "upArrowCallout", "Callout with arrow that points up") + """Callout with arrow that points up""" + + UP_DOWN_ARROW = (38, "upDownArrow", "Block arrow that points up and down") + """Block arrow that points up and down""" + + UP_DOWN_ARROW_CALLOUT = (58, "upDownArrowCallout", "Callout with arrows that point up and down") + """Callout with arrows that point up and down""" + + UP_RIBBON = (97, "ribbon2", "Ribbon banner with center area above ribbon ends") + """Ribbon banner with center area above ribbon ends""" + + U_TURN_ARROW = (42, "uturnArrow", "Block arrow forming a U shape") + """Block arrow forming a U shape""" + + VERTICAL_SCROLL = (101, "verticalScroll", "Vertical scroll") + """Vertical scroll""" + + WAVE = (103, "wave", "Wave") + """Wave""" + + +MSO_SHAPE = MSO_AUTO_SHAPE_TYPE + + +class MSO_CONNECTOR_TYPE(BaseXmlEnum): """ Specifies a type of connector. @@ -656,28 +733,27 @@ class MSO_CONNECTOR_TYPE(XmlEnumeration): MSO_CONNECTOR.STRAIGHT, Cm(2), Cm(2), Cm(10), Cm(10) ) assert connector.left.cm == 2 + + MS API Name: `MsoConnectorType` + + http://msdn.microsoft.com/en-us/library/office/ff860918.aspx """ - __ms_name__ = "MsoConnectorType" + CURVE = (3, "curvedConnector3", "Curved connector.") + """Curved connector.""" - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff860918.aspx" + ELBOW = (2, "bentConnector3", "Elbow connector.") + """Elbow connector.""" - __members__ = ( - XmlMappedEnumMember("CURVE", 3, "curvedConnector3", "Curved connector."), - XmlMappedEnumMember("ELBOW", 2, "bentConnector3", "Elbow connector."), - XmlMappedEnumMember("STRAIGHT", 1, "line", "Straight line connector."), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates a combination of othe" "r states.", - ), - ) + STRAIGHT = (1, "line", "Straight line connector.") + """Straight line connector.""" -@alias("MSO") -class MSO_SHAPE_TYPE(Enumeration): - """ - Specifies the type of a shape +MSO_CONNECTOR = MSO_CONNECTOR_TYPE + + +class MSO_SHAPE_TYPE(BaseEnum): + """Specifies the type of a shape, more specifically than the five base types. Alias: ``MSO`` @@ -686,47 +762,93 @@ class MSO_SHAPE_TYPE(Enumeration): from pptx.enum.shapes import MSO_SHAPE_TYPE assert shape.type == MSO_SHAPE_TYPE.PICTURE - """ - __ms_name__ = "MsoShapeType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15" ").aspx" - ) - - __members__ = ( - EnumMember("AUTO_SHAPE", 1, "AutoShape"), - EnumMember("CALLOUT", 2, "Callout shape"), - EnumMember("CANVAS", 20, "Drawing canvas"), - EnumMember("CHART", 3, "Chart, e.g. pie chart, bar chart"), - EnumMember("COMMENT", 4, "Comment"), - EnumMember("DIAGRAM", 21, "Diagram"), - EnumMember("EMBEDDED_OLE_OBJECT", 7, "Embedded OLE object"), - EnumMember("FORM_CONTROL", 8, "Form control"), - EnumMember("FREEFORM", 5, "Freeform"), - EnumMember("GROUP", 6, "Group shape"), - EnumMember("IGX_GRAPHIC", 24, "SmartArt graphic"), - EnumMember("INK", 22, "Ink"), - EnumMember("INK_COMMENT", 23, "Ink Comment"), - EnumMember("LINE", 9, "Line"), - EnumMember("LINKED_OLE_OBJECT", 10, "Linked OLE object"), - EnumMember("LINKED_PICTURE", 11, "Linked picture"), - EnumMember("MEDIA", 16, "Media"), - EnumMember("OLE_CONTROL_OBJECT", 12, "OLE control object"), - EnumMember("PICTURE", 13, "Picture"), - EnumMember("PLACEHOLDER", 14, "Placeholder"), - EnumMember("SCRIPT_ANCHOR", 18, "Script anchor"), - EnumMember("TABLE", 19, "Table"), - EnumMember("TEXT_BOX", 17, "Text box"), - EnumMember("TEXT_EFFECT", 15, "Text effect"), - EnumMember("WEB_VIDEO", 26, "Web video"), - ReturnValueOnlyEnumMember("MIXED", -2, "Mixed shape types"), - ) - - -class PP_MEDIA_TYPE(Enumeration): + MS API Name: `MsoShapeType` + + http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15).aspx """ - Indicates the OLE media type. + + AUTO_SHAPE = (1, "AutoShape") + """AutoShape""" + + CALLOUT = (2, "Callout shape") + """Callout shape""" + + CANVAS = (20, "Drawing canvas") + """Drawing canvas""" + + CHART = (3, "Chart, e.g. pie chart, bar chart") + """Chart, e.g. pie chart, bar chart""" + + COMMENT = (4, "Comment") + """Comment""" + + DIAGRAM = (21, "Diagram") + """Diagram""" + + EMBEDDED_OLE_OBJECT = (7, "Embedded OLE object") + """Embedded OLE object""" + + FORM_CONTROL = (8, "Form control") + """Form control""" + + FREEFORM = (5, "Freeform") + """Freeform""" + + GROUP = (6, "Group shape") + """Group shape""" + + IGX_GRAPHIC = (24, "SmartArt graphic") + """SmartArt graphic""" + + INK = (22, "Ink") + """Ink""" + + INK_COMMENT = (23, "Ink Comment") + """Ink Comment""" + + LINE = (9, "Line") + """Line""" + + LINKED_OLE_OBJECT = (10, "Linked OLE object") + """Linked OLE object""" + + LINKED_PICTURE = (11, "Linked picture") + """Linked picture""" + + MEDIA = (16, "Media") + """Media""" + + OLE_CONTROL_OBJECT = (12, "OLE control object") + """OLE control object""" + + PICTURE = (13, "Picture") + """Picture""" + + PLACEHOLDER = (14, "Placeholder") + """Placeholder""" + + SCRIPT_ANCHOR = (18, "Script anchor") + """Script anchor""" + + TABLE = (19, "Table") + """Table""" + + TEXT_BOX = (17, "Text box") + """Text box""" + + TEXT_EFFECT = (15, "Text effect") + """Text effect""" + + WEB_VIDEO = (26, "Web video") + """Web video""" + + +MSO = MSO_SHAPE_TYPE + + +class PP_MEDIA_TYPE(BaseEnum): + """Indicates the OLE media type. Example:: @@ -734,30 +856,24 @@ class PP_MEDIA_TYPE(Enumeration): movie = slide.shapes[0] assert movie.media_type == PP_MEDIA_TYPE.MOVIE + + MS API Name: `PpMediaType` + + https://msdn.microsoft.com/en-us/library/office/ff746008.aspx """ - __ms_name__ = "PpMediaType" + MOVIE = (3, "Video media such as MP4.") + """Video media such as MP4.""" - __url__ = "https://msdn.microsoft.com/en-us/library/office/ff746008.aspx" + OTHER = (1, "Other media types") + """Other media types""" - __members__ = ( - EnumMember("MOVIE", 3, "Video media such as MP4."), - EnumMember("OTHER", 1, "Other media types"), - EnumMember("SOUND", 1, "Audio media such as MP3."), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates multiple media types," - " typically for a collection of shapes. May not be applicable in" - " python-pptx.", - ), - ) + SOUND = (1, "Audio media such as MP3.") + """Audio media such as MP3.""" -@alias("PP_PLACEHOLDER") -class PP_PLACEHOLDER_TYPE(XmlEnumeration): - """ - Specifies one of the 18 distinct types of placeholder. +class PP_PLACEHOLDER_TYPE(BaseXmlEnum): + """Specifies one of the 18 distinct types of placeholder. Alias: ``PP_PLACEHOLDER`` @@ -767,48 +883,77 @@ class PP_PLACEHOLDER_TYPE(XmlEnumeration): placeholder = slide.placeholders[0] assert placeholder.type == PP_PLACEHOLDER.TITLE + + MS API name: `PpPlaceholderType` + + http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15 ").aspx" """ - __ms_name__ = "PpPlaceholderType" - - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15" ").aspx" - ) - - __members__ = ( - XmlMappedEnumMember("BITMAP", 9, "clipArt", "Clip art placeholder"), - XmlMappedEnumMember("BODY", 2, "body", "Body"), - XmlMappedEnumMember("CENTER_TITLE", 3, "ctrTitle", "Center Title"), - XmlMappedEnumMember("CHART", 8, "chart", "Chart"), - XmlMappedEnumMember("DATE", 16, "dt", "Date"), - XmlMappedEnumMember("FOOTER", 15, "ftr", "Footer"), - XmlMappedEnumMember("HEADER", 14, "hdr", "Header"), - XmlMappedEnumMember("MEDIA_CLIP", 10, "media", "Media Clip"), - XmlMappedEnumMember("OBJECT", 7, "obj", "Object"), - XmlMappedEnumMember( - "ORG_CHART", - 11, - "dgm", - "SmartArt placeholder. Organization char" "t is a legacy name.", - ), - XmlMappedEnumMember("PICTURE", 18, "pic", "Picture"), - XmlMappedEnumMember("SLIDE_IMAGE", 101, "sldImg", "Slide Image"), - XmlMappedEnumMember("SLIDE_NUMBER", 13, "sldNum", "Slide Number"), - XmlMappedEnumMember("SUBTITLE", 4, "subTitle", "Subtitle"), - XmlMappedEnumMember("TABLE", 12, "tbl", "Table"), - XmlMappedEnumMember("TITLE", 1, "title", "Title"), - ReturnValueOnlyEnumMember("VERTICAL_BODY", 6, "Vertical Body"), - ReturnValueOnlyEnumMember("VERTICAL_OBJECT", 17, "Vertical Object"), - ReturnValueOnlyEnumMember("VERTICAL_TITLE", 5, "Vertical Title"), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; multiple placeholders of differ" "ing types.", - ), - ) - - -class _ProgIdEnum(object): + BITMAP = (9, "clipArt", "Clip art placeholder") + """Clip art placeholder""" + + BODY = (2, "body", "Body") + """Body""" + + CENTER_TITLE = (3, "ctrTitle", "Center Title") + """Center Title""" + + CHART = (8, "chart", "Chart") + """Chart""" + + DATE = (16, "dt", "Date") + """Date""" + + FOOTER = (15, "ftr", "Footer") + """Footer""" + + HEADER = (14, "hdr", "Header") + """Header""" + + MEDIA_CLIP = (10, "media", "Media Clip") + """Media Clip""" + + OBJECT = (7, "obj", "Object") + """Object""" + + ORG_CHART = (11, "dgm", "SmartArt placeholder. Organization chart is a legacy name.") + """SmartArt placeholder. Organization chart is a legacy name.""" + + PICTURE = (18, "pic", "Picture") + """Picture""" + + SLIDE_IMAGE = (101, "sldImg", "Slide Image") + """Slide Image""" + + SLIDE_NUMBER = (13, "sldNum", "Slide Number") + """Slide Number""" + + SUBTITLE = (4, "subTitle", "Subtitle") + """Subtitle""" + + TABLE = (12, "tbl", "Table") + """Table""" + + TITLE = (1, "title", "Title") + """Title""" + + VERTICAL_BODY = (6, "", "Vertical Body") + """Vertical Body""" + + VERTICAL_OBJECT = (17, "", "Vertical Object") + """Vertical Object""" + + VERTICAL_TITLE = (5, "", "Vertical Title") + """Vertical Title""" + + MIXED = (-2, "", "Return value only; multiple placeholders of differing types.") + """Return value only; multiple placeholders of differing types.""" + + +PP_PLACEHOLDER = PP_PLACEHOLDER_TYPE + + +class PROG_ID(enum.Enum): """One-off Enum-like object for progId values. Indicates the type of an OLE object in terms of the program used to open it. @@ -828,54 +973,41 @@ class _ProgIdEnum(object): assert embedded_xlsx_shape.ole_format.prog_id == "Excel.Sheet.12" """ - class Member(object): - """A particular progID with its attributes.""" - - def __init__(self, name, progId, icon_filename, width, height): - self._name = name - self._progId = progId - self._icon_filename = icon_filename - self._width = width - self._height = height - - def __repr__(self): - return "PROG_ID.%s" % self._name + _progId: str + _icon_filename: str + _width: int + _height: int - @property - def height(self): - return self._height + def __new__(cls, value: str, progId: str, icon_filename: str, width: int, height: int): + self = object.__new__(cls) + self._value_ = value + self._progId = progId + self._icon_filename = icon_filename + self._width = width + self._height = height + return self - @property - def icon_filename(self): - return self._icon_filename + @property + def height(self): + return self._height - @property - def progId(self): - return self._progId + @property + def icon_filename(self): + return self._icon_filename - @property - def width(self): - return self._width + @property + def progId(self): + return self._progId - def __contains__(self, item): - return item in (self.DOCX, self.PPTX, self.XLSX,) - - def __repr__(self): - return "%s.PROG_ID" % __name__ - - @lazyproperty - def DOCX(self): - return self.Member("DOCX", "Word.Document.12", "docx-icon.emf", 965200, 609600) - - @lazyproperty - def PPTX(self): - return self.Member( - "PPTX", "PowerPoint.Show.12", "pptx-icon.emf", 965200, 609600 - ) + @property + def width(self): + return self._width - @lazyproperty - def XLSX(self): - return self.Member("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600) + DOCX = ("DOCX", "Word.Document.12", "docx-icon.emf", 965200, 609600) + """`progId` for an embedded Word 2007+ (.docx) document.""" + PPTX = ("PPTX", "PowerPoint.Show.12", "pptx-icon.emf", 965200, 609600) + """`progId` for an embedded PowerPoint 2007+ (.pptx) document.""" -PROG_ID = _ProgIdEnum() + XLSX = ("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600) + """`progId` for an embedded Excel 2007+ (.xlsx) document.""" diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index 54297bbd5..892385153 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -1,85 +1,64 @@ -# encoding: utf-8 +"""Enumerations used by text and related objects.""" -""" -Enumerations used by text and related objects -""" +from __future__ import annotations -from __future__ import absolute_import +from pptx.enum.base import BaseEnum, BaseXmlEnum -from .base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) +class MSO_AUTO_SIZE(BaseEnum): + """Determines the type of automatic sizing allowed. -class MSO_AUTO_SIZE(Enumeration): - """ - Determines the type of automatic sizing allowed. - - The following names can be used to specify the automatic sizing behavior - used to fit a shape's text within the shape bounding box, for example:: + The following names can be used to specify the automatic sizing behavior used to fit a shape's + text within the shape bounding box, for example:: from pptx.enum.text import MSO_AUTO_SIZE shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE - The word-wrap setting of the text frame interacts with the auto-size - setting to determine the specific auto-sizing behavior. + The word-wrap setting of the text frame interacts with the auto-size setting to determine the + specific auto-sizing behavior. + + Note that `TextFrame.auto_size` can also be set to |None|, which removes the auto size setting + altogether. This causes the setting to be inherited, either from the layout placeholder, in the + case of a placeholder shape, or from the theme. + + MS API Name: `MsoAutoSize` - Note that ``TextFrame.auto_size`` can also be set to |None|, which removes - the auto size setting altogether. This causes the setting to be inherited, - either from the layout placeholder, in the case of a placeholder shape, or - from the theme. + http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15).aspx """ - NONE = 0 - SHAPE_TO_FIT_TEXT = 1 - TEXT_TO_FIT_SHAPE = 2 + NONE = ( + 0, + "No automatic sizing of the shape or text will be done.\n\nText can freely extend beyond" + " the horizontal and vertical edges of the shape bounding box.", + ) + """No automatic sizing of the shape or text will be done. - __ms_name__ = "MsoAutoSize" + Text can freely extend beyond the horizontal and vertical edges of the shape bounding box. + """ - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15" ").aspx" + SHAPE_TO_FIT_TEXT = ( + 1, + "The shape height and possibly width are adjusted to fit the text.\n\nNote this setting" + " interacts with the TextFrame.word_wrap property setting. If word wrap is turned on," + " only the height of the shape will be adjusted; soft line breaks will be used to fit the" + " text horizontally.", ) + """The shape height and possibly width are adjusted to fit the text. - __members__ = ( - EnumMember( - "NONE", - 0, - "No automatic sizing of the shape or text will be don" - "e. Text can freely extend beyond the horizontal and vertical ed" - "ges of the shape bounding box.", - ), - EnumMember( - "SHAPE_TO_FIT_TEXT", - 1, - "The shape height and possibly width are" - " adjusted to fit the text. Note this setting interacts with the" - " TextFrame.word_wrap property setting. If word wrap is turned o" - "n, only the height of the shape will be adjusted; soft line bre" - "aks will be used to fit the text horizontally.", - ), - EnumMember( - "TEXT_TO_FIT_SHAPE", - 2, - "The font size is reduced as necessary t" - "o fit the text within the shape.", - ), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates a combination of auto" - "matic sizing schemes are used.", - ), + Note this setting interacts with the TextFrame.word_wrap property setting. If word wrap is + turned on, only the height of the shape will be adjusted; soft line breaks will be used to fit + the text horizontally. + """ + + TEXT_TO_FIT_SHAPE = ( + 2, + "The font size is reduced as necessary to fit the text within the shape.", ) + """The font size is reduced as necessary to fit the text within the shape.""" -@alias("MSO_UNDERLINE") -class MSO_TEXT_UNDERLINE_TYPE(XmlEnumeration): +class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum): """ Indicates the type of underline for text. Used with :attr:`.Font.underline` to specify the style of text underlining. @@ -91,165 +70,149 @@ class MSO_TEXT_UNDERLINE_TYPE(XmlEnumeration): from pptx.enum.text import MSO_UNDERLINE run.font.underline = MSO_UNDERLINE.DOUBLE_LINE + + MS API Name: `MsoTextUnderlineType` + + http://msdn.microsoft.com/en-us/library/aa432699.aspx """ - __ms_name__ = "MsoTextUnderlineType" - - __url__ = "http://msdn.microsoft.com/en-us/library/aa432699.aspx" - - __members__ = ( - XmlMappedEnumMember("NONE", 0, "none", "Specifies no underline."), - XmlMappedEnumMember( - "DASH_HEAVY_LINE", 8, "dashHeavy", "Specifies a dash underline." - ), - XmlMappedEnumMember("DASH_LINE", 7, "dash", "Specifies a dash line underline."), - XmlMappedEnumMember( - "DASH_LONG_HEAVY_LINE", - 10, - "dashLongHeavy", - "Specifies a long heavy line underline.", - ), - XmlMappedEnumMember( - "DASH_LONG_LINE", 9, "dashLong", "Specifies a dashed long line underline." - ), - XmlMappedEnumMember( - "DOT_DASH_HEAVY_LINE", - 12, - "dotDashHeavy", - "Specifies a dot dash heavy line underline.", - ), - XmlMappedEnumMember( - "DOT_DASH_LINE", 11, "dotDash", "Specifies a dot dash line underline." - ), - XmlMappedEnumMember( - "DOT_DOT_DASH_HEAVY_LINE", - 14, - "dotDotDashHeavy", - "Specifies a dot dot dash heavy line underline.", - ), - XmlMappedEnumMember( - "DOT_DOT_DASH_LINE", - 13, - "dotDotDash", - "Specifies a dot dot dash line underline.", - ), - XmlMappedEnumMember( - "DOTTED_HEAVY_LINE", - 6, - "dottedHeavy", - "Specifies a dotted heavy line underline.", - ), - XmlMappedEnumMember( - "DOTTED_LINE", 5, "dotted", "Specifies a dotted line underline." - ), - XmlMappedEnumMember( - "DOUBLE_LINE", 3, "dbl", "Specifies a double line underline." - ), - XmlMappedEnumMember( - "HEAVY_LINE", 4, "heavy", "Specifies a heavy line underline." - ), - XmlMappedEnumMember( - "SINGLE_LINE", 2, "sng", "Specifies a single line underline." - ), - XmlMappedEnumMember( - "WAVY_DOUBLE_LINE", 17, "wavyDbl", "Specifies a wavy double line underline." - ), - XmlMappedEnumMember( - "WAVY_HEAVY_LINE", 16, "wavyHeavy", "Specifies a wavy heavy line underline." - ), - XmlMappedEnumMember( - "WAVY_LINE", 15, "wavy", "Specifies a wavy line underline." - ), - XmlMappedEnumMember("WORDS", 1, "words", "Specifies underlining words."), - ReturnValueOnlyEnumMember("MIXED", -2, "Specifies a mixed of underline types."), - ) + NONE = (0, "none", "Specifies no underline.") + """Specifies no underline.""" + DASH_HEAVY_LINE = (8, "dashHeavy", "Specifies a dash underline.") + """Specifies a dash underline.""" -@alias("MSO_ANCHOR") -class MSO_VERTICAL_ANCHOR(XmlEnumeration): - """ - Specifies the vertical alignment of text in a text frame. Used with the - ``.vertical_anchor`` property of the |TextFrame| object. Note that the - ``vertical_anchor`` property can also have the value None, indicating - there is no directly specified vertical anchor setting and its effective - value is inherited from its placeholder if it has one or from the theme. - |None| may also be assigned to remove an explicitly specified vertical - anchor setting. - """ + DASH_LINE = (7, "dash", "Specifies a dash line underline.") + """Specifies a dash line underline.""" - __ms_name__ = "MsoVerticalAnchor" - - __url__ = "http://msdn.microsoft.com/en-us/library/office/ff865255.aspx" - - __members__ = ( - XmlMappedEnumMember( - None, - None, - None, - "Text frame has no vertical anchor specified " - "and inherits its value from its layout placeholder or theme.", - ), - XmlMappedEnumMember("TOP", 1, "t", "Aligns text to top of text frame"), - XmlMappedEnumMember("MIDDLE", 3, "ctr", "Centers text vertically"), - XmlMappedEnumMember("BOTTOM", 4, "b", "Aligns text to bottom of text frame"), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates a combination of the " "other states.", - ), + DASH_LONG_HEAVY_LINE = (10, "dashLongHeavy", "Specifies a long heavy line underline.") + """Specifies a long heavy line underline.""" + + DASH_LONG_LINE = (9, "dashLong", "Specifies a dashed long line underline.") + """Specifies a dashed long line underline.""" + + DOT_DASH_HEAVY_LINE = (12, "dotDashHeavy", "Specifies a dot dash heavy line underline.") + """Specifies a dot dash heavy line underline.""" + + DOT_DASH_LINE = (11, "dotDash", "Specifies a dot dash line underline.") + """Specifies a dot dash line underline.""" + + DOT_DOT_DASH_HEAVY_LINE = ( + 14, + "dotDotDashHeavy", + "Specifies a dot dot dash heavy line underline.", ) + """Specifies a dot dot dash heavy line underline.""" + + DOT_DOT_DASH_LINE = (13, "dotDotDash", "Specifies a dot dot dash line underline.") + """Specifies a dot dot dash line underline.""" + + DOTTED_HEAVY_LINE = (6, "dottedHeavy", "Specifies a dotted heavy line underline.") + """Specifies a dotted heavy line underline.""" + + DOTTED_LINE = (5, "dotted", "Specifies a dotted line underline.") + """Specifies a dotted line underline.""" + + DOUBLE_LINE = (3, "dbl", "Specifies a double line underline.") + """Specifies a double line underline.""" + + HEAVY_LINE = (4, "heavy", "Specifies a heavy line underline.") + """Specifies a heavy line underline.""" + + SINGLE_LINE = (2, "sng", "Specifies a single line underline.") + """Specifies a single line underline.""" + + WAVY_DOUBLE_LINE = (17, "wavyDbl", "Specifies a wavy double line underline.") + """Specifies a wavy double line underline.""" + + WAVY_HEAVY_LINE = (16, "wavyHeavy", "Specifies a wavy heavy line underline.") + """Specifies a wavy heavy line underline.""" + WAVY_LINE = (15, "wavy", "Specifies a wavy line underline.") + """Specifies a wavy line underline.""" -@alias("PP_ALIGN") -class PP_PARAGRAPH_ALIGNMENT(XmlEnumeration): + WORDS = (1, "words", "Specifies underlining words.") + """Specifies underlining words.""" + + +MSO_UNDERLINE = MSO_TEXT_UNDERLINE_TYPE + + +class MSO_VERTICAL_ANCHOR(BaseXmlEnum): + """Specifies the vertical alignment of text in a text frame. + + Used with the `.vertical_anchor` property of the |TextFrame| object. Note that the + `vertical_anchor` property can also have the value None, indicating there is no directly + specified vertical anchor setting and its effective value is inherited from its placeholder if + it has one or from the theme. |None| may also be assigned to remove an explicitly specified + vertical anchor setting. + + MS API Name: `MsoVerticalAnchor` + + http://msdn.microsoft.com/en-us/library/office/ff865255.aspx """ - Specifies the horizontal alignment for one or more paragraphs. - Alias: ``PP_ALIGN`` + TOP = (1, "t", "Aligns text to top of text frame") + """Aligns text to top of text frame""" + + MIDDLE = (3, "ctr", "Centers text vertically") + """Centers text vertically""" + + BOTTOM = (4, "b", "Aligns text to bottom of text frame") + """Aligns text to bottom of text frame""" + + +MSO_ANCHOR = MSO_VERTICAL_ANCHOR + + +class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): + """Specifies the horizontal alignment for one or more paragraphs. + + Alias: `PP_ALIGN` Example:: from pptx.enum.text import PP_ALIGN shape.paragraphs[0].alignment = PP_ALIGN.CENTER + + MS API Name: `PpParagraphAlignment` + + http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15).aspx """ - __ms_name__ = "PpParagraphAlignment" + CENTER = (2, "ctr", "Center align") + """Center align""" - __url__ = ( - "http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15" ").aspx" + DISTRIBUTE = ( + 5, + "dist", + "Evenly distributes e.g. Japanese characters from left to right within a line", ) + """Evenly distributes e.g. Japanese characters from left to right within a line""" - __members__ = ( - XmlMappedEnumMember("CENTER", 2, "ctr", "Center align"), - XmlMappedEnumMember( - "DISTRIBUTE", - 5, - "dist", - "Evenly distributes e.g. Japanese chara" - "cters from left to right within a line", - ), - XmlMappedEnumMember( - "JUSTIFY", - 4, - "just", - "Justified, i.e. each line both begins and" - " ends at the margin with spacing between words adjusted such th" - "at the line exactly fills the width of the paragraph.", - ), - XmlMappedEnumMember( - "JUSTIFY_LOW", - 7, - "justLow", - "Justify using a small amount of sp" "ace between words.", - ), - XmlMappedEnumMember("LEFT", 1, "l", "Left aligned"), - XmlMappedEnumMember("RIGHT", 3, "r", "Right aligned"), - XmlMappedEnumMember("THAI_DISTRIBUTE", 6, "thaiDist", "Thai distributed"), - ReturnValueOnlyEnumMember( - "MIXED", - -2, - "Return value only; indicates multiple paragraph al" - "ignments are present in a set of paragraphs.", - ), + JUSTIFY = ( + 4, + "just", + "Justified, i.e. each line both begins and ends at the margin.\n\nSpacing between words" + " is adjusted such that the line exactly fills the width of the paragraph.", ) + """Justified, i.e. each line both begins and ends at the margin. + + Spacing between words is adjusted such that the line exactly fills the width of the paragraph. + """ + + JUSTIFY_LOW = (7, "justLow", "Justify using a small amount of space between words.") + """Justify using a small amount of space between words.""" + + LEFT = (1, "l", "Left aligned") + """Left aligned""" + + RIGHT = (3, "r", "Right aligned") + """Right aligned""" + + THAI_DISTRIBUTE = (6, "thaiDist", "Thai distributed") + """Thai distributed""" + + +PP_ALIGN = PP_PARAGRAPH_ALIGNMENT diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index cf99dd0da..ced0f8088 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -69,9 +69,7 @@ def _escape_ctrl_chars(s): (x09) and line-feed (x0A) are not escaped. All other characters in the range x00-x1F are escaped. """ - return re.sub( - r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s - ) + return re.sub(r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s) class CT_TextBody(BaseOxmlElement): @@ -178,20 +176,12 @@ def unclear_content(self): @classmethod def _a_txBody_tmpl(cls): - return ( - "\n" - " \n" - " \n" - "\n" % (nsdecls("a")) - ) + return "\n" " \n" " \n" "\n" % (nsdecls("a")) @classmethod def _p_txBody_tmpl(cls): return ( - "\n" - " \n" - " \n" - "\n" % (nsdecls("p", "a")) + "\n" " \n" " \n" "\n" % (nsdecls("p", "a")) ) @classmethod @@ -237,7 +227,7 @@ def autofit(self): @autofit.setter def autofit(self, value): - if value is not None and value not in MSO_AUTO_SIZE._valid_settings: + if value is not None and value not in MSO_AUTO_SIZE: raise ValueError( "only None or a member of the MSO_AUTO_SIZE enumeration can " "be assigned to CT_TextBodyProperties.autofit, got %s" % value @@ -297,9 +287,7 @@ class CT_TextCharacterProperties(BaseOxmlElement): "a:extLst", ), ) - hlinkClick = ZeroOrOne( - "a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst") - ) + hlinkClick = ZeroOrOne("a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst")) lang = OptionalAttribute("lang", MSO_LANGUAGE_ID) sz = OptionalAttribute("sz", ST_TextFontSize) diff --git a/src/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py index a04cc224a..c2d434e04 100644 --- a/src/pptx/parts/embeddedpackage.py +++ b/src/pptx/parts/embeddedpackage.py @@ -25,7 +25,7 @@ def factory(cls, prog_id, object_blob, package): bytes of `object_blob` and has the content-type also determined by `prog_id`. """ # --- a generic OLE object has no subclass --- - if prog_id not in PROG_ID: + if not isinstance(prog_id, PROG_ID): return cls( package.next_partname("/ppt/embeddings/oleObject%d.bin"), CT.OFC_OLE_OBJECT, diff --git a/src/pptx/parts/slide.py b/src/pptx/parts/slide.py index dfd81b4e4..5d721bb41 100644 --- a/src/pptx/parts/slide.py +++ b/src/pptx/parts/slide.py @@ -112,9 +112,7 @@ def new(cls, package, slide_part): one is created based on the default template. """ notes_master_part = package.presentation_part.notes_master_part - notes_slide_part = cls._add_notes_slide_part( - package, slide_part, notes_master_part - ) + notes_slide_part = cls._add_notes_slide_part(package, slide_part, notes_master_part) notes_slide = notes_slide_part.notes_slide notes_slide.clone_master_placeholders(notes_master_part.notes_master) return notes_slide_part @@ -167,13 +165,11 @@ def add_chart_part(self, chart_type, chart_data): The chart depicts `chart_data` and is related to the slide contained in this part by `rId`. """ - return self.relate_to( - ChartPart.new(chart_type, chart_data, self._package), RT.CHART - ) + return self.relate_to(ChartPart.new(chart_type, chart_data, self._package), RT.CHART) def add_embedded_ole_object_part(self, prog_id, ole_object_file): """Return rId of newly-added OLE-object part formed from `ole_object_file`.""" - relationship_type = RT.PACKAGE if prog_id in PROG_ID else RT.OLE_OBJECT + relationship_type = RT.PACKAGE if isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT return self.relate_to( EmbeddedPackagePart.factory( prog_id, self._blob_from_file(ole_object_file), self._package diff --git a/src/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py index 65369d51e..cebdfc91f 100644 --- a/src/pptx/shapes/shapetree.py +++ b/src/pptx/shapes/shapetree.py @@ -437,9 +437,7 @@ def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, end_y): x, y = min(begin_x, end_x), min(begin_y, end_y) cx, cy = abs(end_x - begin_x), abs(end_y - begin_y) - return self._element.add_cxnSp( - id_, name, connector_type, x, y, cx, cy, flipH, flipV - ) + return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV) def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy): """Return a newly appended `p:pic` element as specified. @@ -564,9 +562,7 @@ def add_table(self, rows, cols, left, top, width, height): the ``.table`` property on the returned |GraphicFrame| shape must be used to access the enclosed |Table| object. """ - graphicFrame = self._add_graphicFrame_containing_table( - rows, cols, left, top, width, height - ) + graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height) graphic_frame = self._shape_factory(graphicFrame) return graphic_frame @@ -788,9 +784,7 @@ def __iter__(self): """ Generate placeholder shapes in `idx` order. """ - ph_elms = sorted( - [e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx - ) + ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx) return (SlideShapeFactory(e, self) for e in ph_elms) def __len__(self): @@ -896,9 +890,7 @@ class is not intended to be constructed or an instance of it retained by a object such that its helper methods can be organized here. """ - def __init__( - self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file, mime_type - ): + def __init__(self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file, mime_type): super(_MoviePicElementCreator, self).__init__() self._shapes = shapes self._shape_id = shape_id @@ -917,9 +909,7 @@ def new_movie_pic( *poster_frame_file* is None, the default "media loudspeaker" image is used. """ - return cls( - shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type - )._pic + return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic return @property @@ -965,9 +955,7 @@ def _poster_frame_rId(self): The poster frame is the image used to represent the video before it's played. """ - _, poster_frame_rId = self._slide_part.get_or_add_image_part( - self._poster_frame_image_file - ) + _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file) return poster_frame_rId @property @@ -1101,9 +1089,7 @@ def _cx(self): # --- the default width is specified by the PROG_ID member if prog_id is one, # --- otherwise it gets the default icon width. return ( - Emu(self._prog_id_arg.width) - if self._prog_id_arg in PROG_ID - else Emu(965200) + Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200) ) @lazyproperty @@ -1116,9 +1102,7 @@ def _cy(self): # --- the default height is specified by the PROG_ID member if prog_id is one, # --- otherwise it gets the default icon height. return ( - Emu(self._prog_id_arg.height) - if self._prog_id_arg in PROG_ID - else Emu(609600) + Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600) ) @lazyproperty @@ -1132,9 +1116,7 @@ def _icon_height(self): The correct size can be determined by creating an example PPTX using PowerPoint and then inspecting the XML of the OLE graphics-frame (p:oleObj.imgH). """ - return ( - self._icon_height_arg if self._icon_height_arg is not None else Emu(609600) - ) + return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600) @lazyproperty def _icon_image_file(self): @@ -1150,7 +1132,7 @@ def _icon_image_file(self): # --- user-specified (str) prog_id gets the default icon. icon_filename = ( self._prog_id_arg.icon_filename - if self._prog_id_arg in PROG_ID + if isinstance(self._prog_id_arg, PROG_ID) else "generic-icon.emf" ) @@ -1195,7 +1177,7 @@ def _progId(self): # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller # --- has specified it explicitly (as str) - return prog_id_arg.progId if prog_id_arg in PROG_ID else prog_id_arg + return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg @lazyproperty def _shape_name(self): diff --git a/tests/chart/test_data.py b/tests/chart/test_data.py index 537875569..10b325bf9 100644 --- a/tests/chart/test_data.py +++ b/tests/chart/test_data.py @@ -28,7 +28,7 @@ XySeriesData, ) from pptx.chart.xlsx import CategoryWorkbookWriter -from pptx.enum.base import EnumValue +from pptx.enum.chart import XL_CHART_TYPE from ..unitutil.mock import call, class_mock, instance_mock, property_mock @@ -74,8 +74,8 @@ def ChartXmlWriter_(self, request): return ChartXmlWriter_ @pytest.fixture - def chart_type_(self, request): - return instance_mock(request, EnumValue) + def chart_type_(self): + return XL_CHART_TYPE.PIE class Describe_BaseSeriesData(object): @@ -229,15 +229,11 @@ def values_ref_fixture(self, _workbook_writer_prop_, workbook_writer_, series_): @pytest.fixture def Categories_(self, request, categories_): - return class_mock( - request, "pptx.chart.data.Categories", return_value=categories_ - ) + return class_mock(request, "pptx.chart.data.Categories", return_value=categories_) @pytest.fixture def CategorySeriesData_(self, request, series_): - return class_mock( - request, "pptx.chart.data.CategorySeriesData", return_value=series_ - ) + return class_mock(request, "pptx.chart.data.CategorySeriesData", return_value=series_) @pytest.fixture def categories_(self, request): @@ -245,9 +241,7 @@ def categories_(self, request): @pytest.fixture def categories_prop_(self, request, categories_): - return property_mock( - request, CategoryChartData, "categories", return_value=categories_ - ) + return property_mock(request, CategoryChartData, "categories", return_value=categories_) @pytest.fixture def category_(self, request): @@ -357,9 +351,7 @@ def are_numeric_fixture(self, request): categories.add_category(label) return categories, expected_value - @pytest.fixture( - params=[((), 0), ((1,), 1), ((3,), 3), ((1, 1, 1), 1), ((3, 3, 3), 3)] - ) + @pytest.fixture(params=[((), 0), ((1,), 1), ((3,), 3), ((1, 1, 1), 1), ((3, 3, 3), 3)]) def depth_fixture(self, request): depths, expected_value = request.param categories = Categories() @@ -392,9 +384,7 @@ def leaf_fixture(self, request): leaf_counts, expected_value = request.param categories = Categories() for leaf_count in leaf_counts: - categories._categories.append( - instance_mock(request, Category, leaf_count=leaf_count) - ) + categories._categories.append(instance_mock(request, Category, leaf_count=leaf_count)) return categories, expected_value @pytest.fixture( @@ -525,9 +515,7 @@ def it_calculates_an_excel_date_number_to_help(self, excel_date_fixture): def add_sub_fixture(self, request, category_): category = Category(None, None) name = "foobar" - Category_ = class_mock( - request, "pptx.chart.data.Category", return_value=category_ - ) + Category_ = class_mock(request, "pptx.chart.data.Category", return_value=category_) return category, name, Category_, category_ @pytest.fixture(params=[((), 1), ((1,), 2), ((1, 1, 1), 2), ((2, 2, 2), 3)]) @@ -535,18 +523,14 @@ def depth_fixture(self, request): depths, expected_value = request.param category = Category(None, None) for depth in depths: - category._sub_categories.append( - instance_mock(request, Category, depth=depth) - ) + category._sub_categories.append(instance_mock(request, Category, depth=depth)) return category, expected_value @pytest.fixture def depth_raises_fixture(self, request): category = Category(None, None) for depth in (1, 2, 1): - category._sub_categories.append( - instance_mock(request, Category, depth=depth) - ) + category._sub_categories.append(instance_mock(request, Category, depth=depth)) return category @pytest.fixture( @@ -594,9 +578,7 @@ def leaf_fixture(self, request): leaf_counts, expected_value = request.param category = Category(None, None) for leaf_count in leaf_counts: - category._sub_categories.append( - instance_mock(request, Category, leaf_count=leaf_count) - ) + category._sub_categories.append(instance_mock(request, Category, leaf_count=leaf_count)) return category, expected_value @pytest.fixture( @@ -683,9 +665,7 @@ def values_fixture(self, request): series_data = CategorySeriesData(None, None, None) expected_values = [1, 2, 3] for value in expected_values: - series_data._data_points.append( - instance_mock(request, CategoryDataPoint, value=value) - ) + series_data._data_points.append(instance_mock(request, CategoryDataPoint, value=value)) return series_data, expected_values @pytest.fixture @@ -699,9 +679,7 @@ def values_ref_fixture(self, chart_data_): @pytest.fixture def CategoryDataPoint_(self, request, data_point_): - return class_mock( - request, "pptx.chart.data.CategoryDataPoint", return_value=data_point_ - ) + return class_mock(request, "pptx.chart.data.CategoryDataPoint", return_value=data_point_) @pytest.fixture def categories_(self, request): @@ -736,9 +714,7 @@ def add_series_fixture(self, request, BubbleSeriesData_, series_data_): @pytest.fixture def BubbleSeriesData_(self, request, series_data_): - return class_mock( - request, "pptx.chart.data.BubbleSeriesData", return_value=series_data_ - ) + return class_mock(request, "pptx.chart.data.BubbleSeriesData", return_value=series_data_) @pytest.fixture def series_data_(self, request): @@ -768,9 +744,7 @@ def add_series_fixture(self, request, XySeriesData_, series_data_): @pytest.fixture def XySeriesData_(self, request, series_data_): - return class_mock( - request, "pptx.chart.data.XySeriesData", return_value=series_data_ - ) + return class_mock(request, "pptx.chart.data.XySeriesData", return_value=series_data_) @pytest.fixture def series_data_(self, request): @@ -797,9 +771,7 @@ def add_data_point_fixture(self, request, BubbleDataPoint_, data_point_): @pytest.fixture def BubbleDataPoint_(self, request, data_point_): - return class_mock( - request, "pptx.chart.data.BubbleDataPoint", return_value=data_point_ - ) + return class_mock(request, "pptx.chart.data.BubbleDataPoint", return_value=data_point_) @pytest.fixture def data_point_(self, request): @@ -842,9 +814,7 @@ def data_point_(self, request): @pytest.fixture def XyDataPoint_(self, request, data_point_): - return class_mock( - request, "pptx.chart.data.XyDataPoint", return_value=data_point_ - ) + return class_mock(request, "pptx.chart.data.XyDataPoint", return_value=data_point_) class DescribeCategoryDataPoint(object): diff --git a/tests/dml/test_line.py b/tests/dml/test_line.py index 9d6cec30a..b33e6e094 100644 --- a/tests/dml/test_line.py +++ b/tests/dml/test_line.py @@ -53,9 +53,7 @@ def it_has_a_color(self, color_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture( - params=[(MSO_FILL.SOLID, False), (MSO_FILL.BACKGROUND, True), (None, True)] - ) + @pytest.fixture(params=[(MSO_FILL.SOLID, False), (MSO_FILL.BACKGROUND, True), (None, True)]) def color_fixture(self, request, line, fill_prop_, fill_, color_): pre_call_fill_type, solid_call_expected = request.param fill_.type = pre_call_fill_type @@ -80,7 +78,7 @@ def dash_style_get_fixture(self, request): @pytest.fixture( params=[ ("p:spPr{a:b=c}", MSO_LINE.DASH, "p:spPr{a:b=c}/a:ln/a:prstDash{val=dash}"), - ("p:spPr/a:ln", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=dot}"), + ("p:spPr/a:ln", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=sysDot}"), ( "p:spPr/a:ln/a:prstDash", MSO_LINE.SOLID, @@ -129,9 +127,7 @@ def width_get_fixture(self, request, shape_): ) def width_set_fixture(self, request, shape_): initial_width, width = request.param - shape_.ln = shape_.get_or_add_ln.return_value = self.ln_bldr( - initial_width - ).element + shape_.ln = shape_.get_or_add_ln.return_value = self.ln_bldr(initial_width).element line = LineFormat(shape_) expected_xml = self.ln_bldr(width).xml() return line, width, expected_xml diff --git a/tests/enum/__init__.py b/tests/enum/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/enum/test_base.py b/tests/enum/test_base.py new file mode 100644 index 000000000..3b4e970a7 --- /dev/null +++ b/tests/enum/test_base.py @@ -0,0 +1,73 @@ +"""Unit-test suite for `pptx.enum.base`.""" + +from __future__ import annotations + +import pytest + +from pptx.enum.action import PP_ACTION, PP_ACTION_TYPE +from pptx.enum.dml import MSO_LINE_DASH_STYLE + + +class DescribeBaseEnum: + """Unit-test suite for `pptx.enum.base.BaseEnum`.""" + + def it_produces_members_each_equivalent_to_an_integer_value(self): + assert PP_ACTION_TYPE.END_SHOW == 6 + assert PP_ACTION_TYPE.NONE == 0 + + def but_member_reprs_are_a_str_indicating_the_enum_and_member_name(self): + assert repr(PP_ACTION_TYPE.END_SHOW) == "" + assert repr(PP_ACTION_TYPE.RUN_MACRO) == "" + + def and_member_str_values_are_a_str_indicating_the_member_name(self): + assert str(PP_ACTION_TYPE.FIRST_SLIDE) == "FIRST_SLIDE (3)" + assert str(PP_ACTION_TYPE.HYPERLINK) == "HYPERLINK (7)" + + def it_provides_a_docstring_for_each_member(self): + assert PP_ACTION_TYPE.LAST_SLIDE.__doc__ == "Moves to the last slide." + assert PP_ACTION_TYPE.LAST_SLIDE_VIEWED.__doc__ == "Moves to the last slide viewed." + + def it_can_look_up_a_member_by_its_value(self): + assert PP_ACTION_TYPE(10) == PP_ACTION_TYPE.NAMED_SLIDE_SHOW + assert PP_ACTION_TYPE(101) == PP_ACTION_TYPE.NAMED_SLIDE + + def but_it_raises_when_no_member_has_that_value(self): + with pytest.raises(ValueError, match="42 is not a valid PP_ACTION_TYPE"): + PP_ACTION_TYPE(42) + + def it_knows_its_name(self): + assert PP_ACTION_TYPE.NEXT_SLIDE.name == "NEXT_SLIDE" + assert PP_ACTION_TYPE.NONE.name == "NONE" + + def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): + assert PP_ACTION_TYPE.OPEN_FILE is PP_ACTION.OPEN_FILE + + +class DescribeBaseXmlEnum: + """Unit-test suite for `pptx.enum.base.BaseXmlEnum`.""" + + def it_can_look_up_a_member_by_its_corresponding_XML_attribute_value(self): + assert MSO_LINE_DASH_STYLE.from_xml("dash") == MSO_LINE_DASH_STYLE.DASH + assert MSO_LINE_DASH_STYLE.from_xml("dashDot") == MSO_LINE_DASH_STYLE.DASH_DOT + + def but_it_raises_on_an_attribute_value_that_is_not_regitstered(self): + with pytest.raises(ValueError, match="MSO_LINE_DASH_STYLE has no XML mapping for 'wavy'"): + MSO_LINE_DASH_STYLE.from_xml("wavy") + + def and_the_empty_string_never_maps_to_a_member(self): + with pytest.raises(ValueError, match="MSO_LINE_DASH_STYLE has no XML mapping for ''"): + MSO_LINE_DASH_STYLE.from_xml("") + + def it_knows_the_XML_attribute_value_for_each_member_that_has_one(self): + assert MSO_LINE_DASH_STYLE.to_xml(MSO_LINE_DASH_STYLE.SOLID) == "solid" + + def and_it_looks_up_the_member_by_int_value_before_mapping_when_provided_that_way(self): + assert MSO_LINE_DASH_STYLE.to_xml(3) == "sysDot" + + def but_it_raises_when_no_member_has_the_provided_int_value(self): + with pytest.raises(ValueError, match="42 is not a valid MSO_LINE_DASH_STYLE"): + MSO_LINE_DASH_STYLE.to_xml(42) + + def and_it_raises_when_the_member_has_no_XML_value(self): + with pytest.raises(ValueError, match="MSO_LINE_DASH_STYLE.DASH_STYLE_MIXED has no XML r"): + MSO_LINE_DASH_STYLE.to_xml(-2) diff --git a/tests/enum/test_shapes.py b/tests/enum/test_shapes.py new file mode 100644 index 000000000..5bfac6966 --- /dev/null +++ b/tests/enum/test_shapes.py @@ -0,0 +1,46 @@ +"""Unit-test suite for `pptx.enum.shapes`.""" + +from __future__ import annotations + +import pytest + +from pptx.enum.shapes import PROG_ID + + +class DescribeProgId: + """Unit-test suite for `pptx.enum.shapes.ProgId`.""" + + def it_has_members_for_the_OLE_embeddings_known_to_work_on_Windows(self): + assert PROG_ID.DOCX + assert PROG_ID.PPTX + assert PROG_ID.XLSX + + @pytest.mark.parametrize( + ("member", "expected_value"), + [(PROG_ID.DOCX, 609600), (PROG_ID.PPTX, 609600), (PROG_ID.XLSX, 609600)], + ) + def it_knows_its_height(self, member: PROG_ID, expected_value: int): + assert member.height == expected_value + + def it_knows_its_icon_filename(self): + assert PROG_ID.DOCX.icon_filename == "docx-icon.emf" + + def it_knows_its_progId(self): + assert PROG_ID.PPTX.progId == "PowerPoint.Show.12" + + def it_knows_its_width(self): + assert PROG_ID.XLSX.width == 965200 + + @pytest.mark.parametrize( + ("value", "expected_value"), + [ + # -DELETEME--------------------------------------------------------------- + (PROG_ID.DOCX, True), + (PROG_ID.PPTX, True), + (PROG_ID.XLSX, True), + (17, False), + ("XLSX", False), + ], + ) + def it_knows_each_of_its_members_is_an_instance(self, value: object, expected_value: bool): + assert isinstance(value, PROG_ID) is expected_value diff --git a/tests/shapes/test_autoshape.py b/tests/shapes/test_autoshape.py index 1c4787c85..9e6173caf 100644 --- a/tests/shapes/test_autoshape.py +++ b/tests/shapes/test_autoshape.py @@ -72,17 +72,13 @@ def it_should_load_adj_val_actuals_from_xml(self, load_adj_actuals_fixture_): actual_actuals = dict([(a.name, a.actual) for a in adjustments]) assert actual_actuals == expected_actuals - def it_provides_normalized_effective_value_on_indexed_access( - self, indexed_access_fixture_ - ): + def it_provides_normalized_effective_value_on_indexed_access(self, indexed_access_fixture_): prstGeom, prst, expected_values = indexed_access_fixture_ adjustments = AdjustmentCollection(prstGeom) actual_values = [adjustments[idx] for idx in range(len(adjustments))] assert actual_values == expected_values - def it_should_update_actual_value_on_indexed_assignment( - self, indexed_assignment_fixture_ - ): + def it_should_update_actual_value_on_indexed_assignment(self, indexed_assignment_fixture_): """ Assignment to AdjustmentCollection[n] updates nth actual """ @@ -147,9 +143,7 @@ def adjustments_with_prstGeom_(self, request): def _adj_actuals_cases(): gd_bldr = a_gd().with_name("adj2").with_fmla("val 25000") avLst_bldr = an_avLst().with_child(gd_bldr) - mathDivide_bldr = ( - a_prstGeom().with_nsdecls().with_prst("mathDivide").with_child(avLst_bldr) - ) + mathDivide_bldr = a_prstGeom().with_nsdecls().with_prst("mathDivide").with_child(avLst_bldr) gd_bldr = a_gd().with_name("adj").with_fmla("val 25000") avLst_bldr = an_avLst().with_child(gd_bldr) @@ -158,14 +152,9 @@ def _adj_actuals_cases(): gd_bldr_1 = a_gd().with_name("adj1").with_fmla("val 111") gd_bldr_2 = a_gd().with_name("adj2").with_fmla("val 222") gd_bldr_3 = a_gd().with_name("adj3").with_fmla("val 333") - avLst_bldr = ( - an_avLst().with_child(gd_bldr_1).with_child(gd_bldr_2).with_child(gd_bldr_3) - ) + avLst_bldr = an_avLst().with_child(gd_bldr_1).with_child(gd_bldr_2).with_child(gd_bldr_3) wedgeRoundRectCallout_bldr = ( - a_prstGeom() - .with_nsdecls() - .with_prst("wedgeRoundRectCallout") - .with_child(avLst_bldr) + a_prstGeom().with_nsdecls().with_prst("wedgeRoundRectCallout").with_child(avLst_bldr) ) return [ @@ -230,9 +219,7 @@ def _prstGeom_cases(): @pytest.fixture(params=_prstGeom_cases()) def prstGeom_cases_(self, request): prst, expected_values = request.param - prstGeom = ( - a_prstGeom().with_nsdecls().with_prst(prst).with_child(an_avLst()).element - ) + prstGeom = a_prstGeom().with_nsdecls().with_prst(prst).with_child(an_avLst()).element return prstGeom, prst, expected_values def _effective_val_cases(): @@ -274,9 +261,7 @@ def it_xml_escapes_the_basename_when_the_name_contains_special_characters(self): assert autoshape_type.prst == "noSmoking" assert autoshape_type.basename == ""No" Symbol" - def it_knows_the_default_adj_vals_for_its_autoshape_type( - self, default_adj_vals_fixture_ - ): + def it_knows_the_default_adj_vals_for_its_autoshape_type(self, default_adj_vals_fixture_): prst, default_adj_vals = default_adj_vals_fixture_ _default_adj_vals = AutoShapeType.default_adjustment_values(prst) assert _default_adj_vals == default_adj_vals @@ -285,7 +270,7 @@ def it_knows_the_autoshape_type_id_for_each_prst_key(self): assert AutoShapeType.id_from_prst("rect") == MSO_SHAPE.RECTANGLE def it_raises_when_asked_for_autoshape_type_id_with_a_bad_prst(self): - with pytest.raises(KeyError): + with pytest.raises(ValueError): AutoShapeType.id_from_prst("badPrst") def it_caches_autoshape_type_lookups(self): @@ -335,9 +320,7 @@ def it_knows_its_autoshape_type(self, autoshape_type_fixture_): auto_shape_type = shape.auto_shape_type assert auto_shape_type == expected_value - def but_it_raises_when_auto_shape_type_called_on_non_autoshape( - self, non_autoshape_shape_ - ): + def but_it_raises_when_auto_shape_type_called_on_non_autoshape(self, non_autoshape_shape_): with pytest.raises(ValueError): non_autoshape_shape_.auto_shape_type @@ -354,9 +337,7 @@ def it_has_a_line(self, shape): def it_knows_its_shape_type_when_its_a_placeholder(self, placeholder_shape_): assert placeholder_shape_.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER - def and_it_knows_its_shape_type_when_its_not_a_placeholder( - self, non_placeholder_shapes_ - ): + def and_it_knows_its_shape_type_when_its_not_a_placeholder(self, non_placeholder_shapes_): autoshape_shape_, textbox_shape_, freeform_ = non_placeholder_shapes_ assert autoshape_shape_.shape_type == MSO_SHAPE_TYPE.AUTO_SHAPE assert textbox_shape_.shape_type == MSO_SHAPE_TYPE.TEXT_BOX @@ -417,9 +398,7 @@ def autoshape_type_fixture_(self, shape, prst): return shape, MSO_SHAPE.CHEVRON @pytest.fixture - def init_adjs_fixture_( - self, request, shape, sp_, adjustments_, AdjustmentCollection_ - ): + def init_adjs_fixture_(self, request, shape, sp_, adjustments_, AdjustmentCollection_): return shape, adjustments_, AdjustmentCollection_, sp_ @pytest.fixture @@ -508,9 +487,7 @@ def sp_(self, request, prst): @pytest.fixture def TextFrame_(self, request, text_frame_): - return class_mock( - request, "pptx.shapes.autoshape.TextFrame", return_value=text_frame_ - ) + return class_mock(request, "pptx.shapes.autoshape.TextFrame", return_value=text_frame_) @pytest.fixture def text_frame_(self, request): diff --git a/tests/test_enum.py b/tests/test_enum.py deleted file mode 100644 index db1ff58d0..000000000 --- a/tests/test_enum.py +++ /dev/null @@ -1,120 +0,0 @@ -# encoding: utf-8 - -"""Unit-test suite for `pptx.enum` subpackage, focused on base classes. - -Configured a little differently because of the meta-programming, the two enumeration -classes at the top constitute the entire fixture and most of the tests themselves just -make assertions on those. -""" - -import pytest - -from pptx.enum.base import ( - alias, - Enumeration, - EnumMember, - ReturnValueOnlyEnumMember, - XmlEnumeration, - XmlMappedEnumMember, -) -from pptx.enum.shapes import PROG_ID, _ProgIdEnum -from pptx.util import Emu - - -@alias("BARFOO") -class FOOBAR(Enumeration): - """ - Enumeration docstring - """ - - __ms_name__ = "MsoFoobar" - - __url__ = "http://msdn.microsoft.com/foobar.aspx" - - __members__ = ( - EnumMember("READ_WRITE", 1, "Readable and settable"), - ReturnValueOnlyEnumMember("READ_ONLY", -2, "Return value only"), - ) - - -@alias("XML-FU") -class XMLFOO(XmlEnumeration): - """ - XmlEnumeration docstring - """ - - __ms_name__ = "MsoXmlFoobar" - - __url__ = "http://msdn.microsoft.com/msoxmlfoobar.aspx" - - __members__ = ( - XmlMappedEnumMember(None, None, None, "No setting"), - XmlMappedEnumMember("XML_RW", 42, "attrVal", "Read/write setting"), - ReturnValueOnlyEnumMember("RO", -2, "Return value only;"), - ) - - -class DescribeEnumeration(object): - def it_has_the_right_metaclass(self): - assert type(FOOBAR).__name__ == "MetaEnumeration" - - def it_provides_an_EnumValue_instance_for_each_named_member(self): - for obj in (FOOBAR.READ_WRITE, FOOBAR.READ_ONLY): - assert type(obj).__name__ == "EnumValue" - - def it_provides_the_enumeration_value_for_each_named_member(self): - assert FOOBAR.READ_WRITE == 1 - assert FOOBAR.READ_ONLY == -2 - - def it_knows_if_a_setting_is_valid(self): - FOOBAR.validate(FOOBAR.READ_WRITE) - with pytest.raises(ValueError): - FOOBAR.validate("foobar") - with pytest.raises(ValueError): - FOOBAR.validate(FOOBAR.READ_ONLY) - - def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): - assert BARFOO is FOOBAR # noqa - - -class DescribeEnumValue(object): - def it_provides_its_symbolic_name_as_its_string_value(self): - assert ("%s" % FOOBAR.READ_WRITE) == "READ_WRITE (1)" - - def it_provides_its_description_as_its_docstring(self): - assert FOOBAR.READ_ONLY.__doc__ == "Return value only" - - -class DescribeXmlEnumeration(object): - def it_knows_the_XML_value_for_each_of_its_xml_members(self): - assert XMLFOO.to_xml(XMLFOO.XML_RW) == "attrVal" - assert XMLFOO.to_xml(42) == "attrVal" - with pytest.raises(ValueError): - XMLFOO.to_xml(XMLFOO.RO) - - def it_can_map_each_of_its_xml_members_from_the_XML_value(self): - assert XMLFOO.from_xml(None) is None - assert XMLFOO.from_xml("attrVal") == XMLFOO.XML_RW - assert str(XMLFOO.from_xml("attrVal")) == "XML_RW (42)" - - -class Describe_ProgIdEnum(object): - """Unit-test suite for `pptx.enum.shapes._ProgIdEnum.""" - - def it_provides_access_to_its_members(self): - assert type(PROG_ID.XLSX) == _ProgIdEnum.Member - - def it_can_test_an_item_for_membership(self): - assert PROG_ID.XLSX in PROG_ID - - def it_has_a_readable_representation_for_itself(self): - assert repr(PROG_ID) == "pptx.enum.shapes.PROG_ID" - - def it_has_a_readable_representation_for_each_of_its_members(self): - assert repr(PROG_ID.XLSX) == "PROG_ID.XLSX" - - def it_has_attributes_on_each_member(self): - assert PROG_ID.XLSX.height == Emu(609600) - assert PROG_ID.XLSX.icon_filename == "xlsx-icon.emf" - assert PROG_ID.XLSX.progId == "Excel.Sheet.12" - assert PROG_ID.XLSX.width == Emu(965200) From c38d5f5c6850ae3aefdc3a86dbf9bd0af35cf346 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 26 Jul 2024 23:20:30 -0700 Subject: [PATCH 60/69] type: general modernization Add type-annotations broadly, but prioritizing interface objects and methods. Adjust text-width to 100, remove Python 2 support, etc. --- docs/index.rst | 2 +- features/steps/action.py | 8 +- features/steps/axis.py | 28 +- features/steps/background.py | 8 +- features/steps/category.py | 8 +- features/steps/chart.py | 13 +- features/steps/chartdata.py | 5 +- features/steps/color.py | 10 +- features/steps/coreprops.py | 16 +- features/steps/datalabel.py | 8 +- features/steps/effect.py | 8 +- features/steps/fill.py | 24 +- features/steps/font.py | 12 +- features/steps/font_color.py | 16 +- features/steps/helpers.py | 40 +- features/steps/legend.py | 12 +- features/steps/line.py | 8 +- features/steps/picture.py | 19 +- features/steps/placeholder.py | 18 +- features/steps/plot.py | 12 +- features/steps/presentation.py | 71 +- features/steps/series.py | 8 +- features/steps/shape.py | 28 +- features/steps/shapes.py | 36 +- features/steps/slide.py | 20 +- features/steps/slides.py | 10 +- features/steps/table.py | 16 +- features/steps/text.py | 18 +- features/steps/text_frame.py | 12 +- src/pptx/__init__.py | 42 +- src/pptx/action.py | 100 +-- src/pptx/api.py | 36 +- src/pptx/chart/axis.py | 4 +- src/pptx/chart/category.py | 6 +- src/pptx/chart/chart.py | 14 +- src/pptx/chart/data.py | 21 +- src/pptx/chart/datalabel.py | 12 +- src/pptx/chart/legend.py | 14 +- src/pptx/chart/marker.py | 14 +- src/pptx/chart/plot.py | 28 +- src/pptx/chart/point.py | 7 +- src/pptx/chart/series.py | 11 +- src/pptx/chart/xlsx.py | 27 +- src/pptx/chart/xmlwriter.py | 59 +- src/pptx/compat/__init__.py | 41 -- src/pptx/compat/python2.py | 41 -- src/pptx/compat/python3.py | 43 -- src/pptx/dml/chtfmt.py | 18 +- src/pptx/dml/color.py | 12 +- src/pptx/dml/effect.py | 4 +- src/pptx/dml/fill.py | 46 +- src/pptx/dml/line.py | 10 +- src/pptx/exc.py | 7 +- src/pptx/media.py | 20 +- src/pptx/opc/constants.py | 450 ++++--------- src/pptx/opc/oxml.py | 171 +++-- src/pptx/opc/package.py | 461 ++++++------- src/pptx/opc/packuri.py | 95 ++- src/pptx/opc/serialized.py | 150 +++-- src/pptx/opc/shared.py | 4 +- src/pptx/opc/spec.py | 9 +- src/pptx/oxml/__init__.py | 88 ++- src/pptx/oxml/action.py | 46 +- src/pptx/oxml/chart/axis.py | 4 +- src/pptx/oxml/chart/chart.py | 20 +- src/pptx/oxml/chart/datalabel.py | 4 +- src/pptx/oxml/chart/legend.py | 14 +- src/pptx/oxml/chart/marker.py | 14 +- src/pptx/oxml/chart/plot.py | 16 +- src/pptx/oxml/chart/series.py | 14 +- src/pptx/oxml/chart/shared.py | 9 +- src/pptx/oxml/coreprops.py | 174 +++-- src/pptx/oxml/dml/color.py | 14 +- src/pptx/oxml/dml/fill.py | 16 +- src/pptx/oxml/dml/line.py | 4 +- src/pptx/oxml/ns.py | 130 ++-- src/pptx/oxml/presentation.py | 116 ++-- src/pptx/oxml/shapes/__init__.py | 19 + src/pptx/oxml/shapes/autoshape.py | 358 +++++----- src/pptx/oxml/shapes/connector.py | 136 ++-- src/pptx/oxml/shapes/graphfrm.py | 412 ++++++------ src/pptx/oxml/shapes/groupshape.py | 138 ++-- src/pptx/oxml/shapes/picture.py | 59 +- src/pptx/oxml/shapes/shared.py | 180 ++--- src/pptx/oxml/simpletypes.py | 59 +- src/pptx/oxml/slide.py | 134 ++-- src/pptx/oxml/table.py | 374 +++++------ src/pptx/oxml/text.py | 377 ++++++----- src/pptx/oxml/theme.py | 8 +- src/pptx/oxml/xmlchemy.py | 502 ++++++-------- src/pptx/package.py | 30 +- src/pptx/parts/chart.py | 22 +- src/pptx/parts/coreprops.py | 62 +- src/pptx/parts/embeddedpackage.py | 13 +- src/pptx/parts/image.py | 252 ++++--- src/pptx/parts/media.py | 4 +- src/pptx/parts/presentation.py | 57 +- src/pptx/parts/slide.py | 81 +-- src/pptx/presentation.py | 72 +- src/pptx/shapes/__init__.py | 29 +- src/pptx/shapes/autoshape.py | 334 ++++----- src/pptx/shapes/base.py | 220 +++--- src/pptx/shapes/connector.py | 4 +- src/pptx/shapes/freeform.py | 253 +++---- src/pptx/shapes/graphfrm.py | 111 +-- src/pptx/shapes/group.py | 44 +- src/pptx/shapes/picture.py | 152 ++--- src/pptx/shapes/placeholder.py | 30 +- src/pptx/shapes/shapetree.py | 965 +++++++++++++-------------- src/pptx/shared.py | 79 +-- src/pptx/slide.py | 357 +++++----- src/pptx/spec.py | 19 +- src/pptx/table.py | 383 +++++------ src/pptx/text/fonts.py | 31 +- src/pptx/text/layout.py | 21 +- src/pptx/text/text.py | 592 ++++++++-------- src/pptx/types.py | 36 + src/pptx/util.py | 236 +++---- tests/chart/test_axis.py | 45 +- tests/chart/test_category.py | 27 +- tests/chart/test_chart.py | 44 +- tests/chart/test_data.py | 40 +- tests/chart/test_datalabel.py | 8 +- tests/chart/test_legend.py | 19 +- tests/chart/test_marker.py | 12 +- tests/chart/test_plot.py | 40 +- tests/chart/test_point.py | 16 +- tests/chart/test_series.py | 54 +- tests/chart/test_xlsx.py | 42 +- tests/chart/test_xmlwriter.py | 48 +- tests/dml/test_chtfmt.py | 8 +- tests/dml/test_color.py | 22 +- tests/dml/test_effect.py | 6 +- tests/dml/test_fill.py | 18 +- tests/dml/test_line.py | 73 +- tests/opc/test_oxml.py | 197 +++--- tests/opc/test_package.py | 173 ++--- tests/opc/test_packuri.py | 56 +- tests/opc/test_serialized.py | 196 +++--- tests/opc/unitdata/__init__.py | 0 tests/opc/unitdata/rels.py | 261 -------- tests/oxml/shapes/test_autoshape.py | 17 +- tests/oxml/shapes/test_graphfrm.py | 9 +- tests/oxml/shapes/test_groupshape.py | 20 +- tests/oxml/shapes/test_picture.py | 4 +- tests/oxml/test___init__.py | 9 +- tests/oxml/test_dml.py | 8 +- tests/oxml/test_ns.py | 16 +- tests/oxml/test_presentation.py | 44 +- tests/oxml/test_simpletypes.py | 24 +- tests/oxml/test_slide.py | 4 +- tests/oxml/test_table.py | 26 +- tests/oxml/test_theme.py | 8 +- tests/oxml/test_xmlchemy.py | 71 +- tests/oxml/unitdata/dml.py | 8 +- tests/oxml/unitdata/shape.py | 4 +- tests/oxml/unitdata/text.py | 8 +- tests/parts/test_chart.py | 19 +- tests/parts/test_coreprops.py | 221 +++--- tests/parts/test_embeddedpackage.py | 32 +- tests/parts/test_image.py | 14 +- tests/parts/test_media.py | 4 +- tests/parts/test_presentation.py | 32 +- tests/parts/test_slide.py | 71 +- tests/shapes/test_autoshape.py | 171 +++-- tests/shapes/test_base.py | 110 +-- tests/shapes/test_connector.py | 4 +- tests/shapes/test_freeform.py | 321 ++++----- tests/shapes/test_graphfrm.py | 17 +- tests/shapes/test_group.py | 4 +- tests/shapes/test_picture.py | 10 +- tests/shapes/test_placeholder.py | 29 +- tests/shapes/test_shapetree.py | 183 ++--- tests/test_action.py | 15 +- tests/test_api.py | 8 +- tests/test_media.py | 14 +- tests/test_package.py | 11 +- tests/test_presentation.py | 28 +- tests/test_shared.py | 4 +- tests/test_slide.py | 98 +-- tests/test_table.py | 33 +- tests/test_util.py | 17 +- tests/text/test_fonts.py | 45 +- tests/text/test_layout.py | 34 +- tests/text/test_text.py | 233 +++---- tests/unitdata.py | 8 +- tests/unitutil/__init__.py | 6 +- tests/unitutil/cxml.py | 38 +- tests/unitutil/file.py | 23 +- tests/unitutil/mock.py | 68 +- typings/behave/__init__.pyi | 17 + typings/behave/runner.pyi | 3 + typings/lxml/_types.pyi | 4 +- typings/lxml/etree/_module_func.pyi | 29 +- 194 files changed, 6311 insertions(+), 7751 deletions(-) delete mode 100644 src/pptx/compat/__init__.py delete mode 100644 src/pptx/compat/python2.py delete mode 100644 src/pptx/compat/python3.py create mode 100644 src/pptx/types.py delete mode 100644 tests/opc/unitdata/__init__.py delete mode 100644 tests/opc/unitdata/rels.py create mode 100644 typings/behave/__init__.pyi create mode 100644 typings/behave/runner.pyi diff --git a/docs/index.rst b/docs/index.rst index 5bb4e0e26..79ad6c369 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ Philosophy ---------- |pp| aims to broadly support the PowerPoint format (PPTX, PowerPoint 2007 and later), -but its primary commitment is to be _industrial-grade_, that is, suitable for use in a +but its primary commitment is to be *industrial-grade*, that is, suitable for use in a commercial setting. Maintaining this robustness requires a high engineering standard which includes a comprehensive two-level (e2e + unit) testing regimen. This discipline comes at a cost in development effort/time, but we consider reliability to be an diff --git a/features/steps/action.py b/features/steps/action.py index c3f5de0e2..2a2da135a 100644 --- a/features/steps/action.py +++ b/features/steps/action.py @@ -1,16 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for click action-related features.""" +from __future__ import annotations + from behave import given, then, when +from helpers import test_file from pptx import Presentation from pptx.action import Hyperlink from pptx.enum.action import PP_ACTION -from helpers import test_file - - # given =================================================== diff --git a/features/steps/axis.py b/features/steps/axis.py index 9cffbb93a..59697461b 100644 --- a/features/steps/axis.py +++ b/features/steps/axis.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - """Gherkin step implementations for chart axis features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.chart import XL_AXIS_CROSSES, XL_CATEGORY_TYPE -from helpers import test_pptx - - # given =================================================== @@ -19,9 +15,7 @@ def given_a_axis_type_axis(context, axis_type): prs = Presentation(test_pptx("cht-axis-props")) chart = prs.slides[0].shapes[0].chart - context.axis = {"category": chart.category_axis, "value": chart.value_axis}[ - axis_type - ] + context.axis = {"category": chart.category_axis, "value": chart.value_axis}[axis_type] @given("a major gridlines") @@ -33,9 +27,7 @@ def given_a_major_gridlines(context): @given("a value axis having category axis crossing of {crossing}") def given_a_value_axis_having_cat_ax_crossing_of(context, crossing): - slide_idx = {"automatic": 0, "maximum": 2, "minimum": 3, "2.75": 4, "-1.5": 5}[ - crossing - ] + slide_idx = {"automatic": 0, "maximum": 2, "minimum": 3, "2.75": 4, "-1.5": 5}[crossing] prs = Presentation(test_pptx("cht-axis-props")) context.value_axis = prs.slides[slide_idx].shapes[0].chart.value_axis @@ -122,9 +114,7 @@ def when_I_assign_value_to_axis_has_title(context, value): @when("I assign {value} to axis.has_{major_or_minor}_gridlines") -def when_I_assign_value_to_axis_has_major_or_minor_gridlines( - context, value, major_or_minor -): +def when_I_assign_value_to_axis_has_major_or_minor_gridlines(context, value, major_or_minor): axis = context.axis propname = "has_%s_gridlines" % major_or_minor new_value = {"True": True, "False": False}[value] @@ -210,9 +200,7 @@ def then_axis_has_title_is_value(context, value): @then("axis.has_{major_or_minor}_gridlines is {value}") -def then_axis_has_major_or_minor_gridlines_is_expected_value( - context, major_or_minor, value -): +def then_axis_has_major_or_minor_gridlines_is_expected_value(context, major_or_minor, value): axis = context.axis actual_value = { "major": axis.has_major_gridlines, @@ -233,9 +221,7 @@ def then_axis_major_or_minor_unit_is_value(context, major_or_minor, value): axis = context.axis propname = "%s_unit" % major_or_minor actual_value = getattr(axis, propname) - expected_value = {"20.0": 20.0, "8.4": 8.4, "5.0": 5.0, "4.2": 4.2, "None": None}[ - value - ] + expected_value = {"20.0": 20.0, "8.4": 8.4, "5.0": 5.0, "4.2": 4.2, "None": None}[value] assert actual_value == expected_value, "got %s" % actual_value diff --git a/features/steps/background.py b/features/steps/background.py index 596a3a665..b629cea74 100644 --- a/features/steps/background.py +++ b/features/steps/background.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for slide background-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== diff --git a/features/steps/category.py b/features/steps/category.py index 3a119f960..2c4a10ce3 100644 --- a/features/steps/category.py +++ b/features/steps/category.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for chart category features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== diff --git a/features/steps/chart.py b/features/steps/chart.py index fd4edefc2..ced211f32 100644 --- a/features/steps/chart.py +++ b/features/steps/chart.py @@ -1,16 +1,12 @@ -# encoding: utf-8 +"""Gherkin step implementations for chart features.""" -""" -Gherkin step implementations for chart features. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import hashlib - from itertools import islice from behave import given, then, when +from helpers import count, test_pptx from pptx import Presentation from pptx.chart.chart import Legend @@ -19,9 +15,6 @@ from pptx.parts.embeddedpackage import EmbeddedXlsxPart from pptx.util import Inches -from helpers import count, test_pptx - - # given =================================================== diff --git a/features/steps/chartdata.py b/features/steps/chartdata.py index c116a0cb3..82e88ff5a 100644 --- a/features/steps/chartdata.py +++ b/features/steps/chartdata.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Gherkin step implementations for chart data features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import datetime @@ -12,7 +10,6 @@ from pptx.enum.chart import XL_CHART_TYPE from pptx.util import Inches - # given =================================================== diff --git a/features/steps/color.py b/features/steps/color.py index 590cabf79..43bb3cc08 100644 --- a/features/steps/color.py +++ b/features/steps/color.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for ColorFormat-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from behave import given, when, then +from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.dml import MSO_THEME_COLOR -from helpers import test_pptx - - # given ==================================================== diff --git a/features/steps/coreprops.py b/features/steps/coreprops.py index bda998b71..9989c2e01 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -1,20 +1,14 @@ -# encoding: utf-8 +"""Gherkin step implementations for core properties-related features.""" -""" -Gherkin step implementations for core properties-related features. -""" - -from __future__ import absolute_import +from __future__ import annotations from datetime import datetime, timedelta -from behave import given, when, then +from behave import given, then, when +from helpers import no_core_props_pptx_path, saved_pptx_path from pptx import Presentation -from helpers import saved_pptx_path, no_core_props_pptx_path - - # given =================================================== @@ -49,7 +43,7 @@ def step_when_set_core_doc_props_to_valid_values(context): ("revision", 9), ("subject", "Subject"), # --- exercise unicode-text case for Python 2.7 --- - ("title", u"åß∂Title°"), + ("title", "åß∂Title°"), ("version", "Version"), ) for name, value in context.propvals: diff --git a/features/steps/datalabel.py b/features/steps/datalabel.py index dc56de4e4..bb4f474aa 100644 --- a/features/steps/datalabel.py +++ b/features/steps/datalabel.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - """Gherkin step implementations for chart data label features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.chart import XL_DATA_LABEL_POSITION -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/effect.py b/features/steps/effect.py index f319545fc..c9e2806cc 100644 --- a/features/steps/effect.py +++ b/features/steps/effect.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for ShadowFormat-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given ==================================================== diff --git a/features/steps/fill.py b/features/steps/fill.py index fea93ec8f..cbdad36a1 100644 --- a/features/steps/fill.py +++ b/features/steps/fill.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - """Gherkin step implementations for FillFormat-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.dml import MSO_FILL, MSO_PATTERN # noqa -from helpers import test_pptx - - # given ==================================================== @@ -23,9 +19,7 @@ def given_a_FillFormat_object_as_fill(context): @given("a FillFormat object as fill having {pattern} fill") def given_a_FillFormat_object_as_fill_having_pattern(context, pattern): - shape_idx = {"no pattern": 0, "MSO_PATTERN.DIVOT": 1, "MSO_PATTERN.WAVE": 2}[ - pattern - ] + shape_idx = {"no pattern": 0, "MSO_PATTERN.DIVOT": 1, "MSO_PATTERN.WAVE": 2}[pattern] slide = Presentation(test_pptx("dml-fill")).slides[1] fill = slide.shapes[shape_idx].fill context.fill = fill @@ -102,18 +96,14 @@ def when_I_call_fill_solid(context): def then_fill_back_color_is_a_ColorFormat_object(context): actual_value = context.fill.back_color.__class__.__name__ expected_value = "ColorFormat" - assert actual_value == expected_value, ( - "fill.back_color is a %s object" % actual_value - ) + assert actual_value == expected_value, "fill.back_color is a %s object" % actual_value @then("fill.fore_color is a ColorFormat object") def then_fill_fore_color_is_a_ColorFormat_object(context): actual_value = context.fill.fore_color.__class__.__name__ expected_value = "ColorFormat" - assert actual_value == expected_value, ( - "fill.fore_color is a %s object" % actual_value - ) + assert actual_value == expected_value, "fill.fore_color is a %s object" % actual_value @then("fill.gradient_angle == {value}") @@ -127,9 +117,7 @@ def then_fill_gradient_angle_eq_value(context, value): def then_fill_gradient_stops_is_a_GradientStops_object(context): expected_value = "_GradientStops" actual_value = context.fill.gradient_stops.__class__.__name__ - assert actual_value == expected_value, ( - "fill.gradient_stops is a %s object" % actual_value - ) + assert actual_value == expected_value, "fill.gradient_stops is a %s object" % actual_value @then("fill.pattern is {value}") diff --git a/features/steps/font.py b/features/steps/font.py index 2a4c279c5..a9ea45c6b 100644 --- a/features/steps/font.py +++ b/features/steps/font.py @@ -1,20 +1,14 @@ -# encoding: utf-8 +"""Step implementations for run property (font)-related features.""" -""" -Step implementations for run property (font)-related features -""" - -from __future__ import absolute_import +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import MSO_UNDERLINE -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/font_color.py b/features/steps/font_color.py index e336978af..53872dff1 100644 --- a/features/steps/font_color.py +++ b/features/steps/font_color.py @@ -1,20 +1,14 @@ -# encoding: utf-8 +"""Gherkin step implementations for font color features.""" -""" -Gherkin step implementations for font color features -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR -from helpers import test_pptx - - font_color_pptx_path = test_pptx("font-color") @@ -31,9 +25,7 @@ def step_given_font_with_color_type(context, color_type): @given("a font with a color brightness setting of {setting}") def step_font_with_color_brightness(context, setting): - textbox_idx = {"no brightness adjustment": 2, "25% darker": 3, "40% lighter": 4}[ - setting - ] + textbox_idx = {"no brightness adjustment": 2, "25% darker": 3, "40% lighter": 4}[setting] context.prs = Presentation(font_color_pptx_path) textbox = context.prs.slides[0].shapes[textbox_idx] context.font = textbox.text_frame.paragraphs[0].runs[0].font diff --git a/features/steps/helpers.py b/features/steps/helpers.py index bd6d7a330..67a29439a 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -1,13 +1,9 @@ -# encoding: utf-8 - -""" -Helper methods and variables for acceptance tests. -""" +"""Helper methods and variables for acceptance tests.""" import os -def absjoin(*paths): +def absjoin(*paths: str) -> str: return os.path.abspath(os.path.join(*paths)) @@ -17,9 +13,7 @@ def absjoin(*paths): test_pptx_dir = absjoin(thisdir, "test_files") # legacy test pptx files --------------- -no_core_props_pptx_path = absjoin( - thisdir, "../../tests/test_files", "no-core-props.pptx" -) +no_core_props_pptx_path = absjoin(thisdir, "../../tests/test_files", "no-core-props.pptx") # scratch test pptx file --------------- saved_pptx_path = absjoin(scratch_dir, "test_out.pptx") @@ -27,41 +21,31 @@ def absjoin(*paths): test_text = "python-pptx was here!" -def cls_qname(obj): +def cls_qname(obj: object) -> str: module_name = obj.__module__ cls_name = obj.__class__.__name__ qname = "%s.%s" % (module_name, cls_name) return qname -def count(start=0, step=1): - """ - Local implementation of `itertools.count()` to allow v2.6 compatibility. - """ +def count(start: int = 0, step: int = 1): + """Local implementation of `itertools.count()` to allow v2.6 compatibility.""" n = start while True: yield n n += step -def test_file(filename): - """ - Return the absolute path to the file having *filename* in acceptance - test_files directory. - """ +def test_file(filename: str) -> str: + """Return the absolute path to the file having *filename* in acceptance test_files directory.""" return absjoin(thisdir, "test_files", filename) -def test_image(filename): - """ - Return the absolute path to image file having *filename* in test_files - directory. - """ +def test_image(filename: str): + """Return the absolute path to image file having *filename* in test_files directory.""" return absjoin(thisdir, "test_files", filename) -def test_pptx(name): - """ - Return the absolute path to test .pptx file with root name *name*. - """ +def test_pptx(name: str) -> str: + """Return the absolute path to test .pptx file with root name *name*.""" return absjoin(thisdir, "test_files", "%s.pptx" % name) diff --git a/features/steps/legend.py b/features/steps/legend.py index f8385f12a..7c35cd7f7 100644 --- a/features/steps/legend.py +++ b/features/steps/legend.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for chart legend features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.chart import XL_LEGEND_POSITION from pptx.text.text import Font -from helpers import test_pptx - - # given =================================================== @@ -31,9 +27,7 @@ def given_a_legend_having_horizontal_offset_of_value(context, value): @given("a legend positioned {location} the chart") def given_a_legend_positioned_location_the_chart(context, location): - slide_idx = {"at an unspecified location of": 0, "below": 1, "to the right of": 2}[ - location - ] + slide_idx = {"at an unspecified location of": 0, "below": 1, "to the right of": 2}[location] prs = Presentation(test_pptx("cht-legend-props")) context.legend = prs.slides[slide_idx].shapes[0].chart.legend diff --git a/features/steps/line.py b/features/steps/line.py index 5489b03ed..fb1cb1bb3 100644 --- a/features/steps/line.py +++ b/features/steps/line.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Step implementations for LineFormat-related features.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.dml import MSO_LINE from pptx.util import Length, Pt -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/picture.py b/features/steps/picture.py index ef6dbbe75..2fce7f2ca 100644 --- a/features/steps/picture.py +++ b/features/steps/picture.py @@ -1,18 +1,17 @@ -# encoding: utf-8 - """Gherkin step implementations for picture-related features.""" -from behave import given, when, then +from __future__ import annotations + +import io + +from behave import given, then, when +from helpers import saved_pptx_path, test_image, test_pptx from pptx import Presentation -from pptx.compat import BytesIO from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE from pptx.package import Package from pptx.util import Inches -from helpers import saved_pptx_path, test_image, test_pptx - - # given =================================================== @@ -42,7 +41,7 @@ def when_I_add_the_image_filename_using_shapes_add_picture(context, filename): def when_I_add_the_stream_image_filename_using_add_picture(context, filename): shapes = context.slide.shapes with open(test_image(filename), "rb") as f: - stream = BytesIO(f.read()) + stream = io.BytesIO(f.read()) shapes.add_picture(stream, Inches(1.25), Inches(1.25)) @@ -59,9 +58,7 @@ def step_then_a_ext_image_part_appears_in_the_pptx_file(context, ext): pkg = Package.open(saved_pptx_path) partnames = frozenset(p.partname for p in pkg.iter_parts()) image_partname = "/ppt/media/image1.%s" % ext - assert image_partname in partnames, "got %s" % [ - p for p in partnames if "image" in p - ] + assert image_partname in partnames, "got %s" % [p for p in partnames if "image" in p] @then("picture.auto_shape_type == MSO_AUTO_SHAPE_TYPE.{member}") diff --git a/features/steps/placeholder.py b/features/steps/placeholder.py index 2ea14f492..43638373d 100644 --- a/features/steps/placeholder.py +++ b/features/steps/placeholder.py @@ -1,14 +1,11 @@ -# encoding: utf-8 +"""Gherkin step implementations for placeholder-related features.""" -""" -Gherkin step implementations for placeholder-related features. -""" - -from __future__ import absolute_import +from __future__ import annotations import hashlib -from behave import given, when, then +from behave import given, then, when +from helpers import saved_pptx_path, test_file, test_pptx, test_text from pptx import Presentation from pptx.chart.data import CategoryChartData @@ -16,9 +13,6 @@ from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.shapes.base import _PlaceholderFormat -from helpers import saved_pptx_path, test_file, test_pptx, test_text - - # given =================================================== @@ -32,9 +26,7 @@ def given_a_bullet_body_placeholder(context): @given("a known {placeholder_type} placeholder shape") def given_a_known_placeholder_shape(context, placeholder_type): - context.execute_steps( - "given an unpopulated %s placeholder shape" % placeholder_type - ) + context.execute_steps("given an unpopulated %s placeholder shape" % placeholder_type) @given("a layout placeholder having directly set position and size") diff --git a/features/steps/plot.py b/features/steps/plot.py index 98feb88e5..0a3636717 100644 --- a/features/steps/plot.py +++ b/features/steps/plot.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for chart plot features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== @@ -95,9 +91,7 @@ def when_I_assign_value_to_plot_vary_by_categories(context, value): def then_bubble_plot_bubble_scale_is_value(context, value): expected_value = int(value) bubble_plot = context.bubble_plot - assert bubble_plot.bubble_scale == expected_value, ( - "got %s" % bubble_plot.bubble_scale - ) + assert bubble_plot.bubble_scale == expected_value, "got %s" % bubble_plot.bubble_scale @then("len(plot.categories) is {count}") diff --git a/features/steps/presentation.py b/features/steps/presentation.py index 2309c7462..0c1c6ba26 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -1,51 +1,50 @@ -# encoding: utf-8 - """Gherkin step implementations for presentation-level features.""" +from __future__ import annotations + +import io import os import zipfile -from behave import given, when, then +from behave import given, then, when +from behave.runner import Context +from helpers import saved_pptx_path, test_file, test_pptx from pptx import Presentation -from pptx.compat import BytesIO from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.util import Inches -from helpers import saved_pptx_path, test_file, test_pptx - - # given =================================================== @given("a clean working directory") -def given_clean_working_dir(context): +def given_clean_working_dir(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) @given("a presentation") -def given_a_presentation(context): +def given_a_presentation(context: Context): context.presentation = Presentation(test_pptx("prs-properties")) @given("a presentation having a notes master") -def given_a_presentation_having_a_notes_master(context): +def given_a_presentation_having_a_notes_master(context: Context): context.prs = Presentation(test_pptx("prs-notes")) @given("a presentation having no notes master") -def given_a_presentation_having_no_notes_master(context): +def given_a_presentation_having_no_notes_master(context: Context): context.prs = Presentation(test_pptx("prs-properties")) @given("a presentation with external relationships") -def given_prs_with_ext_rels(context): +def given_prs_with_ext_rels(context: Context): context.prs = Presentation(test_pptx("ext-rels")) @given("an initialized pptx environment") -def given_initialized_pptx_env(context): +def given_initialized_pptx_env(context: Context): pass @@ -53,37 +52,37 @@ def given_initialized_pptx_env(context): @when("I change the slide width and height") -def when_change_slide_width_and_height(context): +def when_change_slide_width_and_height(context: Context): presentation = context.presentation presentation.slide_width = Inches(4) presentation.slide_height = Inches(3) @when("I construct a Presentation instance with no path argument") -def when_construct_default_prs(context): +def when_construct_default_prs(context: Context): context.prs = Presentation() @when("I open a basic PowerPoint presentation") -def when_open_basic_pptx(context): +def when_open_basic_pptx(context: Context): context.prs = Presentation(test_pptx("test")) @when("I open a presentation extracted into a directory") -def when_I_open_a_presentation_extracted_into_a_directory(context): +def when_I_open_a_presentation_extracted_into_a_directory(context: Context): context.prs = Presentation(test_file("extracted-pptx")) @when("I open a presentation contained in a stream") -def when_open_presentation_stream(context): +def when_open_presentation_stream(context: Context): with open(test_pptx("test"), "rb") as f: - stream = BytesIO(f.read()) + stream = io.BytesIO(f.read()) context.prs = Presentation(stream) stream.close() @when("I save and reload the presentation") -def when_save_and_reload_prs(context): +def when_save_and_reload_prs(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) context.prs.save(saved_pptx_path) @@ -91,7 +90,7 @@ def when_save_and_reload_prs(context): @when("I save that stream to a file") -def when_save_stream_to_a_file(context): +def when_save_stream_to_a_file(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) context.stream.seek(0) @@ -100,15 +99,15 @@ def when_save_stream_to_a_file(context): @when("I save the presentation") -def when_save_presentation(context): +def when_save_presentation(context: Context): if os.path.isfile(saved_pptx_path): os.remove(saved_pptx_path) context.prs.save(saved_pptx_path) @when("I save the presentation to a stream") -def when_save_presentation_to_stream(context): - context.stream = BytesIO() +def when_save_presentation_to_stream(context: Context): + context.stream = io.BytesIO() context.prs.save(context.stream) @@ -116,7 +115,7 @@ def when_save_presentation_to_stream(context): @then("I receive a presentation based on the default template") -def then_receive_prs_based_on_def_tmpl(context): +def then_receive_prs_based_on_def_tmpl(context: Context): prs = context.prs assert prs is not None slide_masters = prs.slide_masters @@ -128,19 +127,19 @@ def then_receive_prs_based_on_def_tmpl(context): @then("its slide height matches its known value") -def then_slide_height_matches_known_value(context): +def then_slide_height_matches_known_value(context: Context): presentation = context.presentation assert presentation.slide_height == 6858000 @then("its slide width matches its known value") -def then_slide_width_matches_known_value(context): +def then_slide_width_matches_known_value(context: Context): presentation = context.presentation assert presentation.slide_width == 9144000 @then("I see the pptx file in the working directory") -def then_see_pptx_file_in_working_dir(context): +def then_see_pptx_file_in_working_dir(context: Context): assert os.path.isfile(saved_pptx_path) minimum = 30000 actual = os.path.getsize(saved_pptx_path) @@ -148,7 +147,7 @@ def then_see_pptx_file_in_working_dir(context): @then("len(notes_master.shapes) is {shape_count}") -def then_len_notes_master_shapes_is_shape_count(context, shape_count): +def then_len_notes_master_shapes_is_shape_count(context: Context, shape_count: str): notes_master = context.prs.notes_master expected = int(shape_count) actual = len(notes_master.shapes) @@ -156,25 +155,25 @@ def then_len_notes_master_shapes_is_shape_count(context, shape_count): @then("prs.notes_master is a NotesMaster object") -def then_prs_notes_master_is_a_NotesMaster_object(context): +def then_prs_notes_master_is_a_NotesMaster_object(context: Context): prs = context.prs assert type(prs.notes_master).__name__ == "NotesMaster" @then("prs.slides is a Slides object") -def then_prs_slides_is_a_Slides_object(context): +def then_prs_slides_is_a_Slides_object(context: Context): prs = context.presentation assert type(prs.slides).__name__ == "Slides" @then("prs.slide_masters is a SlideMasters object") -def then_prs_slide_masters_is_a_SlideMasters_object(context): +def then_prs_slide_masters_is_a_SlideMasters_object(context: Context): prs = context.presentation assert type(prs.slide_masters).__name__ == "SlideMasters" @then("the external relationships are still there") -def then_ext_rels_are_preserved(context): +def then_ext_rels_are_preserved(context: Context): prs = context.prs sld = prs.slides[0] rel = sld.part._rels["rId2"] @@ -184,19 +183,19 @@ def then_ext_rels_are_preserved(context): @then("the package has the expected number of .rels parts") -def then_the_package_has_the_expected_number_of_rels_parts(context): +def then_the_package_has_the_expected_number_of_rels_parts(context: Context): with zipfile.ZipFile(saved_pptx_path, "r") as z: member_count = len(z.namelist()) assert member_count == 18, "expected 18, got %d" % member_count @then("the slide height matches the new value") -def then_slide_height_matches_new_value(context): +def then_slide_height_matches_new_value(context: Context): presentation = context.presentation assert presentation.slide_height == Inches(3) @then("the slide width matches the new value") -def then_slide_width_matches_new_value(context): +def then_slide_width_matches_new_value(context: Context): presentation = context.presentation assert presentation.slide_width == Inches(4) diff --git a/features/steps/series.py b/features/steps/series.py index 3b900e746..35965fe94 100644 --- a/features/steps/series.py +++ b/features/steps/series.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - """Gherkin step implementations for chart plot features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from ast import literal_eval from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.chart import XL_MARKER_STYLE from pptx.enum.dml import MSO_FILL_TYPE, MSO_THEME_COLOR -from helpers import test_pptx - - # given =================================================== diff --git a/features/steps/shape.py b/features/steps/shape.py index c5154a45b..b10ad0659 100644 --- a/features/steps/shape.py +++ b/features/steps/shape.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - """Gherkin step implementations for shape-related features.""" -from __future__ import unicode_literals +from __future__ import annotations import hashlib -from behave import given, when, then +from behave import given, then, when +from helpers import cls_qname, test_file, test_pptx from pptx import Presentation -from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE from pptx.action import ActionSetting +from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE from pptx.util import Emu -from helpers import cls_qname, test_file, test_pptx - - # given =================================================== @@ -222,9 +218,7 @@ def given_a_shape_of_known_position_and_size(context): @when("I add a {cx} x {cy} shape at ({x}, {y})") def when_I_add_a_cx_cy_shape_at_x_y(context, cx, cy, x, y): - context.shape.shapes.add_shape( - MSO_SHAPE.ROUNDED_RECTANGLE, int(x), int(y), int(cx), int(cy) - ) + context.shape.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, int(x), int(y), int(cx), int(cy)) @when("I assign 0.15 to shape.adjustments[0]") @@ -272,9 +266,7 @@ def when_I_assign_value_to_connector_end_y(context, value): @when("I assign {value} to picture.crop_{side}") def when_I_assign_value_to_picture_crop_side(context, value, side): - new_value = ( - None if value == "None" else float(value) if "." in value else int(value) - ) + new_value = None if value == "None" else float(value) if "." in value else int(value) setattr(context.picture, "crop_%s" % side, new_value) @@ -336,9 +328,7 @@ def then_accessing_shape_click_action_raises_TypeError(context): except TypeError: return except Exception as e: - raise AssertionError( - "Accessing GroupShape.click_action raised %s" % type(e).__name__ - ) + raise AssertionError("Accessing GroupShape.click_action raised %s" % type(e).__name__) raise AssertionError("Accessing GroupShape.click_action did not raise") @@ -658,9 +648,7 @@ def then_shape_shape_id_equals(context, value_str): def then_shape_shape_type_is_MSO_SHAPE_TYPE_member(context, member_name): expected_shape_type = getattr(MSO_SHAPE_TYPE, member_name) actual_shape_type = context.shape.shape_type - assert actual_shape_type == expected_shape_type, ( - "shape.shape_type == %s" % actual_shape_type - ) + assert actual_shape_type == expected_shape_type, "shape.shape_type == %s" % actual_shape_type @then("shape.text == {value}") diff --git a/features/steps/shapes.py b/features/steps/shapes.py index 53a081ce6..57d5f2bb0 100644 --- a/features/steps/shapes.py +++ b/features/steps/shapes.py @@ -1,10 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for shape collections.""" +from __future__ import annotations + import io from behave import given, then, when +from helpers import saved_pptx_path, test_file, test_image, test_pptx from pptx import Presentation from pptx.chart.data import CategoryChartData @@ -13,9 +14,6 @@ from pptx.shapes.base import BaseShape from pptx.util import Emu, Inches -from helpers import saved_pptx_path, test_file, test_image, test_pptx - - # given =================================================== @@ -174,9 +172,7 @@ def when_I_assign_shapes_add_ole_object_to_shape(context): @when("I assign shapes.add_picture() to shape") def when_I_assign_shapes_add_picture_to_shape(context): - context.shape = context.shapes.add_picture( - test_image("sonic.gif"), Inches(1), Inches(2) - ) + context.shape = context.shapes.add_picture(test_image("sonic.gif"), Inches(1), Inches(2)) @when("I assign shapes.add_shape() to shape") @@ -188,9 +184,7 @@ def when_I_assign_shapes_add_shape_to_shape(context): @when("I assign shapes.add_textbox() to shape") def when_I_assign_shapes_add_textbox_to_shape(context): - context.shape = context.shapes.add_textbox( - Inches(1), Inches(2), Inches(3), Inches(0.5) - ) + context.shape = context.shapes.add_textbox(Inches(1), Inches(2), Inches(3), Inches(0.5)) @when("I assign shapes.build_freeform() to builder") @@ -229,9 +223,7 @@ def when_I_assign_True_to_shapes_turbo_add_enabled(context): @when("I call shapes.add_chart({type_}, chart_data)") def when_I_call_shapes_add_chart(context, type_): chart_type = getattr(XL_CHART_TYPE, type_) - context.chart = context.shapes.add_chart( - chart_type, 0, 0, 0, 0, context.chart_data - ).chart + context.chart = context.shapes.add_chart(chart_type, 0, 0, 0, 0, context.chart_data).chart @when("I call shapes.add_connector(MSO_CONNECTOR.STRAIGHT, 1, 2, 3, 4)") @@ -252,9 +244,7 @@ def when_I_call_shapes_add_movie(context): @then("iterating shapes produces {count} objects of type {class_name}") -def then_iterating_shapes_produces_count_objects_of_type_class_name( - context, count, class_name -): +def then_iterating_shapes_produces_count_objects_of_type_class_name(context, count, class_name): shapes = context.shapes expected_count, expected_class_name = int(count), class_name idx = -1 @@ -268,17 +258,13 @@ def then_iterating_shapes_produces_count_objects_of_type_class_name( @then("iterating shapes produces {count} objects that subclass BaseShape") -def then_iterating_shapes_produces_count_objects_that_subclass_BaseShape( - context, count -): +def then_iterating_shapes_produces_count_objects_that_subclass_BaseShape(context, count): shapes = context.shapes expected_count = int(count) idx = -1 for idx, shape in enumerate(shapes): class_name = shape.__class__.__name__ - assert isinstance(shape, BaseShape), ( - "%s does not subclass BaseShape" % class_name - ) + assert isinstance(shape, BaseShape), "%s does not subclass BaseShape" % class_name actual_count = idx + 1 assert actual_count == expected_count, "got %d items" % actual_count @@ -294,9 +280,7 @@ def then_len_shapes_eq_value(context, value): def then_shape_is_a_type_object(context, clsname): actual_class_name = context.shape.__class__.__name__ expected_class_name = clsname - assert actual_class_name == expected_class_name, ( - "shape is a %s object" % actual_class_name - ) + assert actual_class_name == expected_class_name, "shape is a %s object" % actual_class_name @then("shapes[-1] == shape") diff --git a/features/steps/slide.py b/features/steps/slide.py index 3d13c7d64..a7527f36d 100644 --- a/features/steps/slide.py +++ b/features/steps/slide.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for slide-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then - -from pptx import Presentation - from helpers import test_pptx +from pptx import Presentation # given =================================================== @@ -144,9 +140,7 @@ def then_slide_background_is_a_Background_object(context): def then_slide_follow_master_background_is_value(context, value): expected_value = {"True": True, "False": False}[value] actual_value = context.slide.follow_master_background - assert actual_value is expected_value, ( - "slide.follow_master_background is %s" % actual_value - ) + assert actual_value is expected_value, "slide.follow_master_background is %s" % actual_value @then("slide.has_notes_slide is {value}") @@ -173,18 +167,14 @@ def then_slide_notes_slide_is_a_NotesSlide_object(context): def then_slide_placeholders_is_a_clsname_object(context, clsname): actual_clsname = context.slide.placeholders.__class__.__name__ expected_clsname = clsname - assert actual_clsname == expected_clsname, ( - "slide.placeholders is a %s object" % actual_clsname - ) + assert actual_clsname == expected_clsname, "slide.placeholders is a %s object" % actual_clsname @then("slide.shapes is a {clsname} object") def then_slide_shapes_is_a_clsname_object(context, clsname): actual_clsname = context.slide.shapes.__class__.__name__ expected_clsname = clsname - assert actual_clsname == expected_clsname, ( - "slide.shapes is a %s object" % actual_clsname - ) + assert actual_clsname == expected_clsname, "slide.shapes is a %s object" % actual_clsname @then("slide.slide_id is 256") diff --git a/features/steps/slides.py b/features/steps/slides.py index 16283057c..42ef66885 100644 --- a/features/steps/slides.py +++ b/features/steps/slides.py @@ -1,15 +1,11 @@ -# encoding: utf-8 - """Gherkin step implementations for slide collection-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals - -from behave import given, when, then - -from pptx import Presentation +from __future__ import annotations +from behave import given, then, when from helpers import test_pptx +from pptx import Presentation # given =================================================== diff --git a/features/steps/table.py b/features/steps/table.py index 3aec6671e..8cbf43afd 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for table-related features""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from behave import given, when, then +from behave import given, then, when +from helpers import test_pptx from pptx import Presentation -from pptx.enum.text import MSO_ANCHOR # noqa +from pptx.enum.text import MSO_ANCHOR # noqa # pyright: ignore[reportUnusedImport] from pptx.util import Inches -from helpers import test_pptx - - # given =================================================== @@ -57,9 +53,7 @@ def given_a_Cell_object_with_known_margins_as_cell(context): @given("a _Cell object with {setting} vertical alignment as cell") def given_a_Cell_object_with_setting_vertical_alignment(context, setting): - cell_coordinates = {"inherited": (0, 1), "middle": (0, 2), "bottom": (0, 3)}[ - setting - ] + cell_coordinates = {"inherited": (0, 1), "middle": (0, 2), "bottom": (0, 3)}[setting] prs = Presentation(test_pptx("tbl-cell")) context.cell = prs.slides[0].shapes[0].table.cell(*cell_coordinates) diff --git a/features/steps/text.py b/features/steps/text.py index 78c515191..5c3692b5d 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -1,18 +1,14 @@ -# encoding: utf-8 - """Gherkin step implementations for text-related features.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from behave import given, when, then +from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.text import PP_ALIGN from pptx.util import Emu -from helpers import test_pptx - - # given =================================================== @@ -38,9 +34,7 @@ def given_a_paragraph_having_line_spacing_of_setting(context, setting): @given("a paragraph having space {before_after} of {setting}") -def given_a_paragraph_having_space_before_after_of_setting( - context, before_after, setting -): +def given_a_paragraph_having_space_before_after_of_setting(context, before_after, setting): slide_idx = {"before": 0, "after": 1}[before_after] paragraph_idx = {"no explicit setting": 0, "6 pt": 1}[setting] prs = Presentation(test_pptx("txt-paragraph-spacing")) @@ -126,9 +120,7 @@ def when_I_assign_value_to_paragraph_line_spacing(context, value_str): @when("I assign {value_str} to paragraph.space_{before_after}") -def when_I_assign_value_to_paragraph_space_before_after( - context, value_str, before_after -): +def when_I_assign_value_to_paragraph_space_before_after(context, value_str, before_after): value = {"76200": 76200, "38100": 38100, "None": None}[value_str] attr_name = {"before": "space_before", "after": "space_after"}[before_after] paragraph = context.paragraph diff --git a/features/steps/text_frame.py b/features/steps/text_frame.py index 48401620a..49fd44092 100644 --- a/features/steps/text_frame.py +++ b/features/steps/text_frame.py @@ -1,26 +1,20 @@ -# encoding: utf-8 - """Step implementations for text frame-related features""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from behave import given, then, when +from helpers import test_pptx from pptx import Presentation from pptx.enum.text import MSO_AUTO_SIZE from pptx.util import Inches, Pt -from helpers import test_pptx - - # given =================================================== @given("a TextFrame object as text_frame") def given_a_text_frame(context): - context.text_frame = ( - Presentation(test_pptx("txt-text")).slides[0].shapes[0].text_frame - ) + context.text_frame = Presentation(test_pptx("txt-text")).slides[0].shapes[0].text_frame @given("a TextFrame object containing {value} as text_frame") diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 7952f87bd..0c951298c 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -1,26 +1,20 @@ -# encoding: utf-8 - """Initialization module for python-pptx package.""" -__version__ = "0.6.23" - +from __future__ import annotations -import pptx.exc as exceptions import sys +from typing import TYPE_CHECKING -sys.modules["pptx.exceptions"] = exceptions -del sys - -from pptx.api import Presentation # noqa - -from pptx.opc.constants import CONTENT_TYPE as CT # noqa: E402 -from pptx.opc.package import PartFactory # noqa: E402 -from pptx.parts.chart import ChartPart # noqa: E402 -from pptx.parts.coreprops import CorePropertiesPart # noqa: E402 -from pptx.parts.image import ImagePart # noqa: E402 -from pptx.parts.media import MediaPart # noqa: E402 -from pptx.parts.presentation import PresentationPart # noqa: E402 -from pptx.parts.slide import ( # noqa: E402 +import pptx.exc as exceptions +from pptx.api import Presentation +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.package import PartFactory +from pptx.parts.chart import ChartPart +from pptx.parts.coreprops import CorePropertiesPart +from pptx.parts.image import ImagePart +from pptx.parts.media import MediaPart +from pptx.parts.presentation import PresentationPart +from pptx.parts.slide import ( NotesMasterPart, NotesSlidePart, SlideLayoutPart, @@ -28,7 +22,17 @@ SlidePart, ) -content_type_to_part_class_map = { +if TYPE_CHECKING: + from pptx.opc.package import Part + +__version__ = "0.6.23" + +sys.modules["pptx.exceptions"] = exceptions +del sys + +__all__ = ["Presentation"] + +content_type_to_part_class_map: dict[str, type[Part]] = { CT.PML_PRESENTATION_MAIN: PresentationPart, CT.PML_PRES_MACRO_MAIN: PresentationPart, CT.PML_TEMPLATE_MAIN: PresentationPart, diff --git a/src/pptx/action.py b/src/pptx/action.py index cc55e52e1..83c6ebf19 100644 --- a/src/pptx/action.py +++ b/src/pptx/action.py @@ -1,19 +1,35 @@ -# encoding: utf-8 - """Objects related to mouse click and hover actions on a shape or text.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + from pptx.enum.action import PP_ACTION from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.shapes import Subshape from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.shapes.shared import CT_NonVisualDrawingProps + from pptx.oxml.text import CT_TextCharacterProperties + from pptx.parts.slide import SlidePart + from pptx.shapes.base import BaseShape + from pptx.slide import Slide, Slides + class ActionSetting(Subshape): """Properties specifying how a shape or run reacts to mouse actions.""" - # --- The Subshape superclass provides access to the Slide Part, which is needed - # --- to access relationships. - def __init__(self, xPr, parent, hover=False): + # -- The Subshape base class provides access to the Slide Part, which is needed to access + # -- relationships, which is where hyperlinks live. + + def __init__( + self, + xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties, + parent: BaseShape, + hover: bool = False, + ): super(ActionSetting, self).__init__(parent) # xPr is either a cNvPr or rPr element self._element = xPr @@ -61,7 +77,7 @@ def action(self): }.get(action_verb, PP_ACTION.NONE) @lazyproperty - def hyperlink(self): + def hyperlink(self) -> Hyperlink: """ A |Hyperlink| object representing the hyperlink action defined on this click or hover mouse event. A |Hyperlink| object is always @@ -70,7 +86,7 @@ def hyperlink(self): return Hyperlink(self._element, self._parent, self._hover) @property - def target_slide(self): + def target_slide(self) -> Slide | None: """ A reference to the slide in this presentation that is the target of the slide jump action in this shape. Slide jump actions include @@ -116,11 +132,13 @@ def target_slide(self): raise ValueError("no previous slide") return self._slides[prev_slide_idx] elif self.action == PP_ACTION.NAMED_SLIDE: + assert self._hlink is not None rId = self._hlink.rId - return self.part.related_part(rId).slide + slide_part = cast("SlidePart", self.part.related_part(rId)) + return slide_part.slide @target_slide.setter - def target_slide(self, slide): + def target_slide(self, slide: Slide | None): self._clear_click_action() if slide is None: return @@ -139,12 +157,13 @@ def _clear_click_action(self): self._element.remove(hlink) @property - def _hlink(self): + def _hlink(self) -> CT_Hyperlink | None: """ - Reference to the `a:hlinkClick` or `h:hlinkHover` element for this + Reference to the `a:hlinkClick` or `a:hlinkHover` element for this click action. Returns |None| if the element is not present. """ if self._hover: + assert isinstance(self._element, CT_NonVisualDrawingProps) return self._element.hlinkHover return self._element.hlinkClick @@ -164,7 +183,7 @@ def _slide_index(self): return self._slides.index(self._slide) @lazyproperty - def _slides(self): + def _slides(self) -> Slides: """ Reference to the slide collection for this presentation. """ @@ -172,11 +191,14 @@ def _slides(self): class Hyperlink(Subshape): - """ - Represents a hyperlink action on a shape or text run. - """ - - def __init__(self, xPr, parent, hover=False): + """Represents a hyperlink action on a shape or text run.""" + + def __init__( + self, + xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties, + parent: BaseShape, + hover: bool = False, + ): super(Hyperlink, self).__init__(parent) # xPr is either a cNvPr or rPr element self._element = xPr @@ -184,14 +206,13 @@ def __init__(self, xPr, parent, hover=False): self._hover = hover @property - def address(self): - """ - Read/write. The URL of the hyperlink. URL can be on http, https, - mailto, or file scheme; others may work. Returns |None| if no - hyperlink is defined, including when another action such as - `RUN_MACRO` is defined on the object. Assigning |None| removes any - action defined on the object, whether it is a hyperlink action or - not. + def address(self) -> str | None: + """Read/write. The URL of the hyperlink. + + URL can be on http, https, mailto, or file scheme; others may work. Returns |None| if no + hyperlink is defined, including when another action such as `RUN_MACRO` is defined on the + object. Assigning |None| removes any action defined on the object, whether it is a hyperlink + action or not. """ hlink = self._hlink @@ -207,7 +228,7 @@ def address(self): return self.part.target_ref(rId) @address.setter - def address(self, url): + def address(self, url: str | None): # implements all three of add, change, and remove hyperlink self._remove_hlink() @@ -216,30 +237,29 @@ def address(self, url): hlink = self._get_or_add_hlink() hlink.rId = rId - def _get_or_add_hlink(self): - """ - Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink - object, depending on the value of `self._hover`. Create one if not - present. + def _get_or_add_hlink(self) -> CT_Hyperlink: + """Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink object. + + The actual element depends on the value of `self._hover`. Create the element if not present. """ if self._hover: - return self._element.get_or_add_hlinkHover() + return cast("CT_NonVisualDrawingProps", self._element).get_or_add_hlinkHover() return self._element.get_or_add_hlinkClick() @property - def _hlink(self): - """ - Reference to the `a:hlinkClick` or `h:hlinkHover` element for this - click action. Returns |None| if the element is not present. + def _hlink(self) -> CT_Hyperlink | None: + """Reference to the `a:hlinkClick` or `h:hlinkHover` element for this click action. + + Returns |None| if the element is not present. """ if self._hover: - return self._element.hlinkHover + return cast("CT_NonVisualDrawingProps", self._element).hlinkHover return self._element.hlinkClick def _remove_hlink(self): - """ - Remove the a:hlinkClick or a:hlinkHover element, including dropping - any relationship it might have. + """Remove the a:hlinkClick or a:hlinkHover element. + + Also drops any relationship it might have. """ hlink = self._hlink if hlink is None: diff --git a/src/pptx/api.py b/src/pptx/api.py index 7670c886f..892f425ab 100644 --- a/src/pptx/api.py +++ b/src/pptx/api.py @@ -1,21 +1,24 @@ -# encoding: utf-8 +"""Directly exposed API classes, Presentation for now. -""" -Directly exposed API classes, Presentation for now. Provides some syntactic -sugar for interacting with the pptx.presentation.Package graph and also -provides some insulation so not so many classes in the other modules need to -be named as internal (leading underscore). +Provides some syntactic sugar for interacting with the pptx.presentation.Package graph and also +provides some insulation so not so many classes in the other modules need to be named as internal +(leading underscore). """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import os +from typing import IO, TYPE_CHECKING + +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.package import Package -from .opc.constants import CONTENT_TYPE as CT -from .package import Package +if TYPE_CHECKING: + from pptx import presentation + from pptx.parts.presentation import PresentationPart -def Presentation(pptx=None): +def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation: """ Return a |Presentation| object loaded from *pptx*, where *pptx* can be either a path to a ``.pptx`` file (a string) or a file-like object. If @@ -34,18 +37,13 @@ def Presentation(pptx=None): return presentation_part.presentation -def _default_pptx_path(): - """ - Return the path to the built-in default .pptx package. - """ +def _default_pptx_path() -> str: + """Return the path to the built-in default .pptx package.""" _thisdir = os.path.split(__file__)[0] return os.path.join(_thisdir, "templates", "default.pptx") -def _is_pptx_package(prs_part): - """ - Return |True| if *prs_part* is a valid main document part, |False| - otherwise. - """ +def _is_pptx_package(prs_part: PresentationPart): + """Return |True| if *prs_part* is a valid main document part, |False| otherwise.""" valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN) return prs_part.content_type in valid_content_types diff --git a/src/pptx/chart/axis.py b/src/pptx/chart/axis.py index 66f325185..a9b877039 100644 --- a/src/pptx/chart/axis.py +++ b/src/pptx/chart/axis.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Axis-related chart objects.""" +from __future__ import annotations + from pptx.dml.chtfmt import ChartFormat from pptx.enum.chart import ( XL_AXIS_CROSSES, diff --git a/src/pptx/chart/category.py b/src/pptx/chart/category.py index 3d16e6f42..2c28aff5e 100644 --- a/src/pptx/chart/category.py +++ b/src/pptx/chart/category.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - """Category-related objects. The |category.Categories| object is returned by ``Plot.categories`` and contains zero or @@ -8,9 +6,9 @@ discovery of the depth of that hierarchy and providing means to navigate it. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from pptx.compat import Sequence +from collections.abc import Sequence class Categories(Sequence): diff --git a/src/pptx/chart/chart.py b/src/pptx/chart/chart.py index 14500a418..d73aa9338 100644 --- a/src/pptx/chart/chart.py +++ b/src/pptx/chart/chart.py @@ -1,13 +1,14 @@ -# encoding: utf-8 - """Chart-related objects such as Chart and ChartTitle.""" +from __future__ import annotations + +from collections.abc import Sequence + from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis from pptx.chart.legend import Legend from pptx.chart.plot import PlotFactory, PlotTypeInspector from pptx.chart.series import SeriesCollection from pptx.chart.xmlwriter import SeriesXmlRewriterFactory -from pptx.compat import Sequence from pptx.dml.chtfmt import ChartFormat from pptx.shared import ElementProxy, PartElementProxy from pptx.text.text import Font, TextFrame @@ -88,12 +89,7 @@ def chart_type(self): @lazyproperty def font(self): """Font object controlling text format defaults for this chart.""" - defRPr = ( - self._chartSpace.get_or_add_txPr() - .p_lst[0] - .get_or_add_pPr() - .get_or_add_defRPr() - ) + defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr() return Font(defRPr) @property diff --git a/src/pptx/chart/data.py b/src/pptx/chart/data.py index 35e2e6b64..ec6a61f31 100644 --- a/src/pptx/chart/data.py +++ b/src/pptx/chart/data.py @@ -1,8 +1,9 @@ -# encoding: utf-8 - """ChartData and related objects.""" +from __future__ import annotations + import datetime +from collections.abc import Sequence from numbers import Number from pptx.chart.xlsx import ( @@ -11,19 +12,17 @@ XyWorkbookWriter, ) from pptx.chart.xmlwriter import ChartXmlWriter -from pptx.compat import Sequence from pptx.util import lazyproperty class _BaseChartData(Sequence): - """ - Base class providing common members for chart data objects. A chart data - object serves as a proxy for the chart data table that will be written to - an Excel worksheet; operating as a sequence of series as well as - providing access to chart-level attributes. A chart data object is used - as a parameter in :meth:`shapes.add_chart` and - :meth:`Chart.replace_data`. The data structure varies between major chart - categories such as category charts and XY charts. + """Base class providing common members for chart data objects. + + A chart data object serves as a proxy for the chart data table that will be written to an + Excel worksheet; operating as a sequence of series as well as providing access to chart-level + attributes. A chart data object is used as a parameter in :meth:`shapes.add_chart` and + :meth:`Chart.replace_data`. The data structure varies between major chart categories such as + category charts and XY charts. """ def __init__(self, number_format="General"): diff --git a/src/pptx/chart/datalabel.py b/src/pptx/chart/datalabel.py index ec6f7cba5..af7cdf5c0 100644 --- a/src/pptx/chart/datalabel.py +++ b/src/pptx/chart/datalabel.py @@ -1,13 +1,9 @@ -# encoding: utf-8 +"""Data label-related objects.""" -""" -Data label-related objects. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals - -from ..text.text import Font, TextFrame -from ..util import lazyproperty +from pptx.text.text import Font, TextFrame +from pptx.util import lazyproperty class DataLabels(object): diff --git a/src/pptx/chart/legend.py b/src/pptx/chart/legend.py index 2926fae23..9bc64dbf8 100644 --- a/src/pptx/chart/legend.py +++ b/src/pptx/chart/legend.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""Legend of a chart.""" -""" -Legend of a chart. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ..enum.chart import XL_LEGEND_POSITION -from ..text.text import Font -from ..util import lazyproperty +from pptx.enum.chart import XL_LEGEND_POSITION +from pptx.text.text import Font +from pptx.util import lazyproperty class Legend(object): diff --git a/src/pptx/chart/marker.py b/src/pptx/chart/marker.py index 22dc0ff19..cd4b7f024 100644 --- a/src/pptx/chart/marker.py +++ b/src/pptx/chart/marker.py @@ -1,15 +1,13 @@ -# encoding: utf-8 +"""Marker-related objects. -""" -Marker-related objects. Only the line-type charts Line, XY, and Radar have -markers. +Only the line-type charts Line, XY, and Radar have markers. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from ..dml.chtfmt import ChartFormat -from ..shared import ElementProxy -from ..util import lazyproperty +from pptx.dml.chtfmt import ChartFormat +from pptx.shared import ElementProxy +from pptx.util import lazyproperty class Marker(ElementProxy): diff --git a/src/pptx/chart/plot.py b/src/pptx/chart/plot.py index ce2d1167e..6e7235855 100644 --- a/src/pptx/chart/plot.py +++ b/src/pptx/chart/plot.py @@ -1,20 +1,18 @@ -# encoding: utf-8 +"""Plot-related objects. -""" -Plot-related objects. A plot is known as a chart group in the MS API. A chart -can have more than one plot overlayed on each other, such as a line plot -layered over a bar plot. +A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on +each other, such as a line plot layered over a bar plot. """ -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations -from .category import Categories -from .datalabel import DataLabels -from ..enum.chart import XL_CHART_TYPE as XL -from ..oxml.ns import qn -from ..oxml.simpletypes import ST_BarDir, ST_Grouping -from .series import SeriesCollection -from ..util import lazyproperty +from pptx.chart.category import Categories +from pptx.chart.datalabel import DataLabels +from pptx.chart.series import SeriesCollection +from pptx.enum.chart import XL_CHART_TYPE as XL +from pptx.oxml.ns import qn +from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping +from pptx.util import lazyproperty class _BasePlot(object): @@ -58,9 +56,7 @@ def data_labels(self): """ dLbls = self._element.dLbls if dLbls is None: - raise ValueError( - "plot has no data labels, set has_data_labels = True first" - ) + raise ValueError("plot has no data labels, set has_data_labels = True first") return DataLabels(dLbls) @property diff --git a/src/pptx/chart/point.py b/src/pptx/chart/point.py index 258f6ae19..2d42436cb 100644 --- a/src/pptx/chart/point.py +++ b/src/pptx/chart/point.py @@ -1,12 +1,11 @@ -# encoding: utf-8 - """Data point-related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence from pptx.chart.datalabel import DataLabel from pptx.chart.marker import Marker -from pptx.compat import Sequence from pptx.dml.chtfmt import ChartFormat from pptx.util import lazyproperty diff --git a/src/pptx/chart/series.py b/src/pptx/chart/series.py index 4ae19fbc0..16112eabe 100644 --- a/src/pptx/chart/series.py +++ b/src/pptx/chart/series.py @@ -1,13 +1,12 @@ -# encoding: utf-8 - """Series-related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence from pptx.chart.datalabel import DataLabels from pptx.chart.marker import Marker from pptx.chart.point import BubblePoints, CategoryPoints, XyPoints -from pptx.compat import Sequence from pptx.dml.chtfmt import ChartFormat from pptx.oxml.ns import qn from pptx.util import lazyproperty @@ -254,8 +253,6 @@ def _SeriesFactory(ser): qn("c:scatterChart"): XySeries, }[xChart_tag] except KeyError: - raise NotImplementedError( - "series class for %s not yet implemented" % xChart_tag - ) + raise NotImplementedError("series class for %s not yet implemented" % xChart_tag) return SeriesCls(ser) diff --git a/src/pptx/chart/xlsx.py b/src/pptx/chart/xlsx.py index 17e1e4fc8..30b212728 100644 --- a/src/pptx/chart/xlsx.py +++ b/src/pptx/chart/xlsx.py @@ -1,13 +1,12 @@ -# encoding: utf-8 - """Chart builder and related objects.""" +from __future__ import annotations + +import io from contextlib import contextmanager from xlsxwriter import Workbook -from ..compat import BytesIO - class _BaseWorkbookWriter(object): """Base class for workbook writers, providing shared members.""" @@ -19,7 +18,7 @@ def __init__(self, chart_data): @property def xlsx_blob(self): """bytes for Excel file containing chart_data.""" - xlsx_file = BytesIO() + xlsx_file = io.BytesIO() with self._open_worksheet(xlsx_file) as (workbook, worksheet): self._populate_worksheet(workbook, worksheet) return xlsx_file.getvalue() @@ -29,7 +28,7 @@ def _open_worksheet(self, xlsx_file): """ Enable XlsxWriter Worksheet object to be opened, operated on, and then automatically closed within a `with` statement. A filename or - stream object (such as a ``BytesIO`` instance) is expected as + stream object (such as an `io.BytesIO` instance) is expected as *xlsx_file*. """ workbook = Workbook(xlsx_file, {"in_memory": True}) @@ -225,13 +224,9 @@ def _populate_worksheet(self, workbook, worksheet): table, X values in column A and Y values in column B. Place the series label in the first (heading) cell of the column. """ - chart_num_format = workbook.add_format( - {"num_format": self._chart_data.number_format} - ) + chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format}) for series in self._chart_data: - series_num_format = workbook.add_format( - {"num_format": series.number_format} - ) + series_num_format = workbook.add_format({"num_format": series.number_format}) offset = self.series_table_row_offset(series) # write X values worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format) @@ -263,13 +258,9 @@ def _populate_worksheet(self, workbook, worksheet): column C. Place the series label in the first (heading) cell of the values column. """ - chart_num_format = workbook.add_format( - {"num_format": self._chart_data.number_format} - ) + chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format}) for series in self._chart_data: - series_num_format = workbook.add_format( - {"num_format": series.number_format} - ) + series_num_format = workbook.add_format({"num_format": series.number_format}) offset = self.series_table_row_offset(series) # write X values worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format) diff --git a/src/pptx/chart/xmlwriter.py b/src/pptx/chart/xmlwriter.py index c485a4b88..703c53dd5 100644 --- a/src/pptx/chart/xmlwriter.py +++ b/src/pptx/chart/xmlwriter.py @@ -1,18 +1,13 @@ -# encoding: utf-8 +"""Composers for default chart XML for various chart types.""" -""" -Composers for default chart XML for various chart types. -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from copy import deepcopy from xml.sax.saxutils import escape -from ..compat import to_unicode -from ..enum.chart import XL_CHART_TYPE -from ..oxml import parse_xml -from ..oxml.ns import nsdecls +from pptx.enum.chart import XL_CHART_TYPE +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls def ChartXmlWriter(chart_type, chart_data): @@ -54,9 +49,7 @@ def ChartXmlWriter(chart_type, chart_data): XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XyChartXmlWriter, }[chart_type] except KeyError: - raise NotImplementedError( - "XML writer for chart type %s not yet implemented" % chart_type - ) + raise NotImplementedError("XML writer for chart type %s not yet implemented" % chart_type) return BuilderCls(chart_type, chart_data) @@ -136,9 +129,7 @@ def numRef_xml(self, wksht_ref, number_format, values): "{pt_xml}" " \n" " \n" - ).format( - **{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml} - ) + ).format(**{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml}) def pt_xml(self, values): """ @@ -149,9 +140,7 @@ def pt_xml(self, values): in the overall data point sequence of the chart and is started at *offset*. """ - xml = (' \n').format( - pt_count=len(values) - ) + xml = (' \n').format(pt_count=len(values)) pt_tmpl = ( ' \n' @@ -289,9 +278,7 @@ def _trim_ser_count_by(self, plotArea, count): for ser in extra_sers: parent = ser.getparent() parent.remove(ser) - extra_xCharts = [ - xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0 - ] + extra_xCharts = [xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0] for xChart in extra_xCharts: parent = xChart.getparent() parent.remove(xChart) @@ -529,9 +516,7 @@ def _barDir_xml(self): return ' \n' elif self._chart_type in col_types: return ' \n' - raise NotImplementedError( - "no _barDir_xml() for chart type %s" % self._chart_type - ) + raise NotImplementedError("no _barDir_xml() for chart type %s" % self._chart_type) @property def _cat_ax_pos(self): @@ -601,9 +586,7 @@ def _grouping_xml(self): return ' \n' elif self._chart_type in percentStacked_types: return ' \n' - raise NotImplementedError( - "no _grouping_xml() for chart type %s" % self._chart_type - ) + raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type) @property def _overlap_xml(self): @@ -870,9 +853,7 @@ def _grouping_xml(self): return ' \n' elif self._chart_type in percentStacked_types: return ' \n' - raise NotImplementedError( - "no _grouping_xml() for chart type %s" % self._chart_type - ) + raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type) @property def _marker_xml(self): @@ -1532,9 +1513,7 @@ def _cat_pt_xml(self): ' \n' " {cat_label}\n" " \n" - ).format( - **{"cat_idx": idx, "cat_label": escape(to_unicode(category.label))} - ) + ).format(**{"cat_idx": idx, "cat_label": escape(str(category.label))}) return xml @property @@ -1573,9 +1552,9 @@ def lvl_pt_xml(level): xml = "" for level in categories.levels: - xml += ( - " \n" "{lvl_pt_xml}" " \n" - ).format(**{"lvl_pt_xml": lvl_pt_xml(level)}) + xml += (" \n" "{lvl_pt_xml}" " \n").format( + **{"lvl_pt_xml": lvl_pt_xml(level)} + ) return xml @property @@ -1793,11 +1772,7 @@ def _bubbleSize_tmpl(self): containing the bubble size values and their spreadsheet range reference. """ - return ( - " \n" - "{numRef_xml}" - " \n" - ) + return " \n" "{numRef_xml}" " \n" class _BubbleSeriesXmlRewriter(_BaseSeriesXmlRewriter): diff --git a/src/pptx/compat/__init__.py b/src/pptx/compat/__init__.py deleted file mode 100644 index 198dc6a0e..000000000 --- a/src/pptx/compat/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# encoding: utf-8 - -"""Provides Python 2/3 compatibility objects.""" - -import sys - -try: - from collections.abc import Container, Mapping, Sequence -except ImportError: - from collections import Container, Mapping, Sequence - -if sys.version_info >= (3, 0): - from .python3 import ( # noqa - BytesIO, - Unicode, - is_integer, - is_string, - is_unicode, - to_unicode, - ) -else: - from .python2 import ( # noqa - BytesIO, - Unicode, - is_integer, - is_string, - is_unicode, - to_unicode, - ) - -__all__ = [ - "BytesIO", - "Container", - "Mapping", - "Sequence", - "Unicode", - "is_integer", - "is_string", - "is_unicode", - "to_unicode", -] diff --git a/src/pptx/compat/python2.py b/src/pptx/compat/python2.py deleted file mode 100644 index 34e2535d5..000000000 --- a/src/pptx/compat/python2.py +++ /dev/null @@ -1,41 +0,0 @@ -# encoding: utf-8 - -"""Provides Python 2 compatibility objects.""" - -from StringIO import StringIO as BytesIO # noqa - - -def is_integer(obj): - """Return True if *obj* is an integer (int, long), False otherwise.""" - return isinstance(obj, (int, long)) # noqa F821 - - -def is_string(obj): - """Return True if *obj* is a string, False otherwise.""" - return isinstance(obj, basestring) # noqa F821 - - -def is_unicode(obj): - """Return True if *obj* is a unicode string, False otherwise.""" - return isinstance(obj, unicode) # noqa F821 - - -def to_unicode(text): - """Return *text* as a unicode string. - - *text* can be a 7-bit ASCII string, a UTF-8 encoded 8-bit string, or unicode. String - values are converted to unicode assuming UTF-8 encoding. Unicode values are returned - unchanged. - """ - # both str and unicode inherit from basestring - if not isinstance(text, basestring): # noqa F821 - tmpl = "expected unicode or UTF-8 (or ASCII) encoded str, got %s value %s" - raise TypeError(tmpl % (type(text), text)) - # return unicode strings unchanged - if isinstance(text, unicode): # noqa F821 - return text - # otherwise assume UTF-8 encoding, which also works for ASCII - return unicode(text, "utf-8") # noqa F821 - - -Unicode = unicode # noqa F821 diff --git a/src/pptx/compat/python3.py b/src/pptx/compat/python3.py deleted file mode 100644 index 85fce2376..000000000 --- a/src/pptx/compat/python3.py +++ /dev/null @@ -1,43 +0,0 @@ -# encoding: utf-8 - -"""Provides Python 3 compatibility objects.""" - -from io import BytesIO # noqa - - -def is_integer(obj): - """ - Return True if *obj* is an int, False otherwise. - """ - return isinstance(obj, int) - - -def is_string(obj): - """ - Return True if *obj* is a string, False otherwise. - """ - return isinstance(obj, str) - - -def is_unicode(obj): - """ - Return True if *obj* is a unicode string, False otherwise. - """ - return isinstance(obj, str) - - -def to_unicode(text): - """Return *text* as a (unicode) str. - - *text* can be str or bytes. A bytes object is assumed to be encoded as UTF-8. - If *text* is a str object it is returned unchanged. - """ - if isinstance(text, str): - return text - try: - return text.decode("utf-8") - except AttributeError: - raise TypeError("expected unicode string, got %s value %s" % (type(text), text)) - - -Unicode = str diff --git a/src/pptx/dml/chtfmt.py b/src/pptx/dml/chtfmt.py index dcecb63ab..c37e4844d 100644 --- a/src/pptx/dml/chtfmt.py +++ b/src/pptx/dml/chtfmt.py @@ -1,17 +1,15 @@ -# encoding: utf-8 +"""|ChartFormat| and related objects. -""" -|ChartFormat| and related objects. |ChartFormat| acts as proxy for the `spPr` -element, which provides visual shape properties such as line and fill for -chart elements. +|ChartFormat| acts as proxy for the `spPr` element, which provides visual shape properties such as +line and fill for chart elements. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from .fill import FillFormat -from .line import LineFormat -from ..shared import ElementProxy -from ..util import lazyproperty +from pptx.dml.fill import FillFormat +from pptx.dml.line import LineFormat +from pptx.shared import ElementProxy +from pptx.util import lazyproperty class ChartFormat(ElementProxy): diff --git a/src/pptx/dml/color.py b/src/pptx/dml/color.py index 71e619c9b..54155823d 100644 --- a/src/pptx/dml/color.py +++ b/src/pptx/dml/color.py @@ -1,13 +1,9 @@ -# encoding: utf-8 +"""DrawingML objects related to color, ColorFormat being the most prominent.""" -""" -DrawingML objects related to color, ColorFormat being the most prominent. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ..enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR -from ..oxml.dml.color import ( +from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR +from pptx.oxml.dml.color import ( CT_HslColor, CT_PresetColor, CT_SchemeColor, diff --git a/src/pptx/dml/effect.py b/src/pptx/dml/effect.py index 65753014a..9df69ce49 100644 --- a/src/pptx/dml/effect.py +++ b/src/pptx/dml/effect.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Visual effects on a shape such as shadow, glow, and reflection.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations class ShadowFormat(object): diff --git a/src/pptx/dml/fill.py b/src/pptx/dml/fill.py index e84bea9c6..8212af9e8 100644 --- a/src/pptx/dml/fill.py +++ b/src/pptx/dml/fill.py @@ -1,10 +1,10 @@ -# encoding: utf-8 - """DrawingML objects related to fill.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING -from pptx.compat import Sequence from pptx.dml.color import ColorFormat from pptx.enum.dml import MSO_FILL from pptx.oxml.dml.fill import ( @@ -15,23 +15,28 @@ CT_PatternFillProperties, CT_SolidColorFillProperties, ) +from pptx.oxml.xmlchemy import BaseOxmlElement from pptx.shared import ElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.enum.dml import MSO_FILL_TYPE + from pptx.oxml.xmlchemy import BaseOxmlElement + class FillFormat(object): - """ - Provides access to the current fill properties object and provides - methods to change the fill type. + """Provides access to the current fill properties. + + Also provides methods to change the fill type. """ - def __init__(self, eg_fill_properties_parent, fill_obj): + def __init__(self, eg_fill_properties_parent: BaseOxmlElement, fill_obj: _Fill): super(FillFormat, self).__init__() self._xPr = eg_fill_properties_parent self._fill = fill_obj @classmethod - def from_fill_parent(cls, eg_fillProperties_parent): + def from_fill_parent(cls, eg_fillProperties_parent: BaseOxmlElement) -> FillFormat: """ Return a |FillFormat| instance initialized to the settings contained in *eg_fillProperties_parent*, which must be an element having @@ -151,11 +156,8 @@ def solid(self): self._fill = _SolidFill(solidFill) @property - def type(self): - """ - Return a value from the :ref:`MsoFillType` enumeration corresponding - to the type of this fill. - """ + def type(self) -> MSO_FILL_TYPE: + """The type of this fill, e.g. `MSO_FILL_TYPE.SOLID`.""" return self._fill.type @@ -194,10 +196,7 @@ def back_color(self): @property def fore_color(self): """Raise TypeError for types that do not override this property.""" - tmpl = ( - "fill type %s has no foreground color, call .solid() or .pattern" - "ed() first" - ) + tmpl = "fill type %s has no foreground color, call .solid() or .pattern" "ed() first" raise TypeError(tmpl % self.__class__.__name__) @property @@ -207,9 +206,10 @@ def pattern(self): raise TypeError(tmpl % self.__class__.__name__) @property - def type(self): # pragma: no cover - tmpl = ".type property must be implemented on %s" - raise NotImplementedError(tmpl % self.__class__.__name__) + def type(self) -> MSO_FILL_TYPE: # pragma: no cover + raise NotImplementedError( + f".type property must be implemented on {self.__class__.__name__}" + ) class _BlipFill(_Fill): @@ -251,9 +251,7 @@ def gradient_angle(self): # Since the UI is consistent with trigonometry conventions, we # respect that in the API. clockwise_angle = lin.ang - counter_clockwise_angle = ( - 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle) - ) + counter_clockwise_angle = 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle) return counter_clockwise_angle @gradient_angle.setter diff --git a/src/pptx/dml/line.py b/src/pptx/dml/line.py index 698c7f633..82be47a40 100644 --- a/src/pptx/dml/line.py +++ b/src/pptx/dml/line.py @@ -1,12 +1,10 @@ -# encoding: utf-8 - """DrawingML objects related to line formatting.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations -from ..enum.dml import MSO_FILL -from .fill import FillFormat -from ..util import Emu, lazyproperty +from pptx.dml.fill import FillFormat +from pptx.enum.dml import MSO_FILL +from pptx.util import Emu, lazyproperty class LineFormat(object): diff --git a/src/pptx/exc.py b/src/pptx/exc.py index 8641fe44f..0a1e03b81 100644 --- a/src/pptx/exc.py +++ b/src/pptx/exc.py @@ -1,11 +1,10 @@ -# encoding: utf-8 - -""" -Exceptions used with python-pptx. +"""Exceptions used with python-pptx. The base exception class is PythonPptxError. """ +from __future__ import annotations + class PythonPptxError(Exception): """Generic error class.""" diff --git a/src/pptx/media.py b/src/pptx/media.py index c5adf24ea..7aaf47ca1 100644 --- a/src/pptx/media.py +++ b/src/pptx/media.py @@ -1,40 +1,38 @@ -# encoding: utf-8 - """Objects related to images, audio, and video.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import base64 import hashlib import os +from typing import IO -from .compat import is_string -from .opc.constants import CONTENT_TYPE as CT -from .util import lazyproperty +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.util import lazyproperty class Video(object): """Immutable value object representing a video such as MP4.""" - def __init__(self, blob, mime_type, filename): + def __init__(self, blob: bytes, mime_type: str | None, filename: str | None): super(Video, self).__init__() self._blob = blob self._mime_type = mime_type self._filename = filename @classmethod - def from_blob(cls, blob, mime_type, filename=None): + def from_blob(cls, blob: bytes, mime_type: str | None, filename: str | None = None): """Return a new |Video| object loaded from image binary in *blob*.""" return cls(blob, mime_type, filename) @classmethod - def from_path_or_file_like(cls, movie_file, mime_type): + def from_path_or_file_like(cls, movie_file: str | IO[bytes], mime_type: str | None) -> Video: """Return a new |Video| object containing video in *movie_file*. *movie_file* can be either a path (string) or a file-like (e.g. StringIO) object. """ - if is_string(movie_file): + if isinstance(movie_file, str): # treat movie_file as a path with open(movie_file, "rb") as f: blob = f.read() @@ -79,7 +77,7 @@ def ext(self): }.get(self._mime_type, "vid") @property - def filename(self): + def filename(self) -> str: """Return a filename.ext string appropriate to this video. The base filename from the original path is used if this image was diff --git a/src/pptx/opc/constants.py b/src/pptx/opc/constants.py index 9eef0ee23..e1b08a93a 100644 --- a/src/pptx/opc/constants.py +++ b/src/pptx/opc/constants.py @@ -1,34 +1,24 @@ -# encoding: utf-8 - """Constant values related to the Open Packaging Convention. In particular, this includes content (MIME) types and relationship types. """ +from __future__ import annotations + -class CONTENT_TYPE(object): +class CONTENT_TYPE: """Content type URIs (like MIME-types) that specify a part's format.""" ASF = "video/x-ms-asf" AVI = "video/avi" BMP = "image/bmp" DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" - DML_CHARTSHAPES = ( - "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml" - ) - DML_DIAGRAM_COLORS = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml" - ) - DML_DIAGRAM_DATA = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml" - ) + DML_CHARTSHAPES = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml" + DML_DIAGRAM_COLORS = "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml" + DML_DIAGRAM_DATA = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml" DML_DIAGRAM_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml" - DML_DIAGRAM_LAYOUT = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" - ) - DML_DIAGRAM_STYLE = ( - "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" - ) + DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" + DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" GIF = "image/gif" INK = "application/inkml+xml" JPEG = "image/jpeg" @@ -40,9 +30,7 @@ class CONTENT_TYPE(object): OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml" OFC_CHART_EX = "application/vnd.ms-office.chartex+xml" OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml" - OFC_CUSTOM_PROPERTIES = ( - "application/vnd.openxmlformats-officedocument.custom-properties+xml" - ) + OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml" OFC_CUSTOM_XML_PROPERTIES = ( "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" ) @@ -53,59 +41,40 @@ class CONTENT_TYPE(object): OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml" - OFC_THEME_OVERRIDE = ( - "application/vnd.openxmlformats-officedocument.themeOverride+xml" - ) + OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml" OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing" OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml" OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( "application/vnd.openxmlformats-package.digital-signature-certificate" ) - OPC_DIGITAL_SIGNATURE_ORIGIN = ( - "application/vnd.openxmlformats-package.digital-signature-origin" - ) + OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin" OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" ) OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" - PML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" - ) + PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" PML_COMMENT_AUTHORS = ( - "application/vnd.openxmlformats-officedocument.presentationml.commen" - "tAuthors+xml" + "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml" ) PML_HANDOUT_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.handou" - "tMaster+xml" + "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml" ) PML_NOTES_MASTER = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesM" - "aster+xml" - ) - PML_NOTES_SLIDE = ( - "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" - ) - PML_PRESENTATION = ( - "application/vnd.openxmlformats-officedocument.presentationml.presentation" + "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml" ) + PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" + PML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation" PML_PRESENTATION_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.presentation.ma" - "in+xml" - ) - PML_PRES_MACRO_MAIN = ( - "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml" - ) - PML_PRES_PROPS = ( - "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" ) + PML_PRES_MACRO_MAIN = "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml" + PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" PML_PRINTER_SETTINGS = ( "application/vnd.openxmlformats-officedocument.presentationml.printerSettings" ) PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" PML_SLIDESHOW_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+" - "xml" + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" ) PML_SLIDE_LAYOUT = ( "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" @@ -114,146 +83,88 @@ class CONTENT_TYPE(object): "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" ) PML_SLIDE_UPDATE_INFO = ( - "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo" - "+xml" + "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml" ) PML_TABLE_STYLES = ( "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml" ) PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml" PML_TEMPLATE_MAIN = ( - "application/vnd.openxmlformats-officedocument.presentationml.template.main+x" - "ml" - ) - PML_VIEW_PROPS = ( - "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" + "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml" ) + PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" PNG = "image/png" - SML_CALC_CHAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" - ) - SML_CHARTSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" - ) - SML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" - ) - SML_CONNECTIONS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" - ) + SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" + SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" SML_CUSTOM_PROPERTY = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" ) - SML_DIALOGSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" - ) + SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" SML_EXTERNAL_LINK = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" ) SML_PIVOT_CACHE_DEFINITION = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefini" - "tion+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" ) SML_PIVOT_CACHE_RECORDS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecord" - "s+xml" - ) - SML_PIVOT_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml" ) + SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" SML_PRINTER_SETTINGS = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" ) - SML_QUERY_TABLE = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" - ) + SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" SML_REVISION_HEADERS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+" - "xml" - ) - SML_REVISION_LOG = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml" ) + SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" SML_SHARED_STRINGS = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" ) SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - SML_SHEET_MAIN = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - ) + SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" SML_SHEET_METADATA = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml" ) - SML_STYLES = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" - ) + SML_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" SML_TABLE_SINGLE_CELLS = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells" - "+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml" ) SML_TEMPLATE_MAIN = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" ) - SML_USER_NAMES = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml" - ) + SML_USER_NAMES = "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml" SML_VOLATILE_DEPENDENCIES = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependen" - "cies+xml" - ) - SML_WORKSHEET = ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml" ) + SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" SWF = "application/x-shockwave-flash" TIFF = "image/tiff" VIDEO = "video/unknown" - WML_COMMENTS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" - ) - WML_DOCUMENT = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ) + WML_COMMENTS = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" + WML_DOCUMENT = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" WML_DOCUMENT_GLOSSARY = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glos" - "sary+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml" ) WML_DOCUMENT_MAIN = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main" - "+xml" - ) - WML_ENDNOTES = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" - ) - WML_FONT_TABLE = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml" - ) - WML_FOOTER = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" - ) - WML_FOOTNOTES = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.foot" - "notes+xml" - ) - WML_HEADER = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" - ) - WML_NUMBERING = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml" - ) + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" + ) + WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" + WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml" + WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" + WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" + WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" + WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml" WML_PRINTER_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettin" - "gs" - ) - WML_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" - ) - WML_STYLES = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings" ) + WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" + WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" WML_WEB_SETTINGS = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+x" - "ml" + "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml" ) WMV = "video/x-ms-wmv" XML = "application/xml" @@ -264,171 +175,100 @@ class CONTENT_TYPE(object): X_WMF = "image/x-wmf" -class NAMESPACE(object): +class NAMESPACE: """Constant values for OPC XML namespaces""" DML_WORDPROCESSING_DRAWING = ( "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ) - OFC_RELATIONSHIPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - ) + OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships" OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types" WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" -class RELATIONSHIP_TARGET_MODE(object): +class RELATIONSHIP_TARGET_MODE: """Open XML relationship target modes""" EXTERNAL = "External" INTERNAL = "Internal" -class RELATIONSHIP_TYPE(object): +class RELATIONSHIP_TYPE: AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" - A_F_CHUNK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" - ) - CALC_CHAIN = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" - ) + A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" + CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" CERTIFICATE = ( "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" "re/certificate" ) CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - CHARTSHEET = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" - ) - CHART_COLOR_STYLE = ( - "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle" - ) + CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle" CHART_USER_SHAPES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUse" - "rShapes" - ) - COMMENTS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes" ) + COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" COMMENT_AUTHORS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentA" - "uthors" - ) - CONNECTIONS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connecti" - "ons" - ) - CONTROL = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors" ) + CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections" + CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" CORE_PROPERTIES = ( - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-p" - "roperties" + "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" ) CUSTOM_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-p" - "roperties" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" ) CUSTOM_PROPERTY = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - "/customProperty" - ) - CUSTOM_XML = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty" ) + CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" CUSTOM_XML_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXm" - "lProps" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps" ) DIAGRAM_COLORS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramC" - "olors" - ) - DIAGRAM_DATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramD" - "ata" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors" ) + DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" DIAGRAM_LAYOUT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramL" - "ayout" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout" ) DIAGRAM_QUICK_STYLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQ" - "uickStyle" - ) - DIALOGSHEET = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsh" - "eet" - ) - DRAWING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - ) - ENDNOTES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle" ) + DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" EXTENDED_PROPERTIES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended" - "-properties" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" ) EXTERNAL_LINK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/external" - "Link" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink" ) FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" - FONT_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" - ) - FOOTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" - ) - FOOTNOTES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" - ) + FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" + FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" + FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" GLOSSARY_DOCUMENT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossary" - "Document" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument" ) HANDOUT_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutM" - "aster" - ) - HEADER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" - ) - HYPERLINK = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlin" - "k" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster" ) + HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" + HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media" - NOTES_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMas" - "ter" - ) - NOTES_SLIDE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSli" - "de" - ) - NUMBERING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numberin" - "g" - ) + NOTES_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" + NOTES_SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" + NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" OFFICE_DOCUMENT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDo" - "cument" - ) - OLE_OBJECT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObjec" - "t" - ) - ORIGIN = ( - "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" - "re/origin" - ) - PACKAGE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" ) + OLE_OBJECT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" + ORIGIN = "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin" + PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" PIVOT_CACHE_DEFINITION = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac" "heDefinition" @@ -437,105 +277,55 @@ class RELATIONSHIP_TYPE(object): "http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh" "eetml/pivotCacheRecords" ) - PIVOT_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTab" - "le" - ) - PRES_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProp" - "s" - ) + PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" PRINTER_SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerS" - "ettings" - ) - QUERY_TABLE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTab" - "le" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings" ) + QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable" REVISION_HEADERS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revision" - "Headers" - ) - REVISION_LOG = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revision" - "Log" - ) - SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders" ) + REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog" + SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" SHARED_STRINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedSt" - "rings" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" ) SHEET_METADATA = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMet" - "adata" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata" ) SIGNATURE = ( "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" "re/signature" ) SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" - SLIDE_LAYOUT = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLay" - "out" - ) - SLIDE_MASTER = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMas" - "ter" - ) + SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" + SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" SLIDE_UPDATE_INFO = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpd" - "ateInfo" - ) - STYLES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo" ) + STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" TABLE_SINGLE_CELLS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSin" - "gleCells" - ) - TABLE_STYLES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSty" - "les" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells" ) + TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles" TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags" THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" THEME_OVERRIDE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOve" - "rride" - ) - THUMBNAIL = ( - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbn" - "ail" - ) - USERNAMES = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/username" - "s" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride" ) + THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" + USERNAMES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames" VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video" - VIEW_PROPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProp" - "s" - ) - VML_DRAWING = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawi" - "ng" - ) + VIEW_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps" + VML_DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" VOLATILE_DEPENDENCIES = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile" "Dependencies" ) - WEB_SETTINGS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSetti" - "ngs" - ) + WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" WORKSHEET_SOURCE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/workshee" - "tSource" - ) - XML_MAPS = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource" ) + XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" diff --git a/src/pptx/opc/oxml.py b/src/pptx/opc/oxml.py index da774c3c9..5dd902a55 100644 --- a/src/pptx/opc/oxml.py +++ b/src/pptx/opc/oxml.py @@ -1,25 +1,30 @@ -# encoding: utf-8 - """OPC-local oxml module to handle OPC-local concerns like relationship parsing.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast + from lxml import etree -from .constants import NAMESPACE as NS, RELATIONSHIP_TARGET_MODE as RTM -from ..oxml import parse_xml, register_element_cls -from ..oxml.simpletypes import ( +from pptx.opc.constants import NAMESPACE as NS +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.oxml import parse_xml, register_element_cls +from pptx.oxml.simpletypes import ( ST_ContentType, ST_Extension, ST_TargetMode, XsdAnyUri, XsdId, ) -from ..oxml.xmlchemy import ( +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore, ) +if TYPE_CHECKING: + from pptx.opc.packuri import PackURI nsmap = { "ct": NS.OPC_CONTENT_TYPES, @@ -28,58 +33,89 @@ } -def oxml_tostring(elm, encoding=None, pretty_print=False, standalone=None): +def oxml_to_encoded_bytes( + element: BaseOxmlElement, + encoding: str = "utf-8", + pretty_print: bool = False, + standalone: bool | None = None, +) -> bytes: return etree.tostring( - elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone + element, encoding=encoding, pretty_print=pretty_print, standalone=standalone ) -def serialize_part_xml(part_elm): - xml = etree.tostring(part_elm, encoding="UTF-8", standalone=True) - return xml +def oxml_tostring( + elm: BaseOxmlElement, + encoding: str | None = None, + pretty_print: bool = False, + standalone: bool | None = None, +): + return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone) -class CT_Default(BaseOxmlElement): +def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes: + """Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file. + + Includes XML-declaration header. """ - ```` element, specifying the default content type to be applied - to a part with the specified extension. + return etree.tostring(part_elm, encoding="UTF-8", standalone=True) + + +class CT_Default(BaseOxmlElement): + """`` element. + + Specifies the default content type to be applied to a part with the specified extension. """ - extension = RequiredAttribute("Extension", ST_Extension) - contentType = RequiredAttribute("ContentType", ST_ContentType) + extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Extension", ST_Extension + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) class CT_Override(BaseOxmlElement): - """ - ```` element, specifying the content type to be applied for a - part with the specified partname. + """`` element. + + Specifies the content type to be applied for a part with the specified partname. """ - partName = RequiredAttribute("PartName", XsdAnyUri) - contentType = RequiredAttribute("ContentType", ST_ContentType) + partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "PartName", XsdAnyUri + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) class CT_Relationship(BaseOxmlElement): - """ - ```` element, representing a single relationship from a - source to a target part. + """`` element. + + Represents a single relationship from a source to a target part. """ - rId = RequiredAttribute("Id", XsdId) - reltype = RequiredAttribute("Type", XsdAnyUri) - target_ref = RequiredAttribute("Target", XsdAnyUri) - targetMode = OptionalAttribute("TargetMode", ST_TargetMode, default=RTM.INTERNAL) + rId: str = RequiredAttribute("Id", XsdId) # pyright: ignore[reportAssignmentType] + reltype: str = RequiredAttribute("Type", XsdAnyUri) # pyright: ignore[reportAssignmentType] + target_ref: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Target", XsdAnyUri + ) + targetMode: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "TargetMode", ST_TargetMode, default=RTM.INTERNAL + ) @classmethod - def new(cls, rId, reltype, target, target_mode=RTM.INTERNAL): - """ - Return a new ```` element. + def new( + cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL + ) -> CT_Relationship: + """Return a new `` element. + + `target_ref` is either a partname or a URI. """ - xml = '' % nsmap["pr"] - relationship = parse_xml(xml) + relationship = cast(CT_Relationship, parse_xml(f'')) relationship.rId = rId relationship.reltype = reltype - relationship.target_ref = target + relationship.target_ref = target_ref relationship.targetMode = target_mode return relationship @@ -87,62 +123,61 @@ def new(cls, rId, reltype, target, target_mode=RTM.INTERNAL): class CT_Relationships(BaseOxmlElement): """`` element, the root element in a .rels file.""" + relationship_lst: list[CT_Relationship] + _insert_relationship: Callable[[CT_Relationship], CT_Relationship] + relationship = ZeroOrMore("pr:Relationship") - def add_rel(self, rId, reltype, target, is_external=False): - """ - Add a child ```` element with attributes set according - to parameter values. - """ + def add_rel( + self, rId: str, reltype: str, target: str, is_external: bool = False + ) -> CT_Relationship: + """Add a child `` element with attributes set as specified.""" target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL relationship = CT_Relationship.new(rId, reltype, target, target_mode) - self._insert_relationship(relationship) + return self._insert_relationship(relationship) @classmethod - def new(cls): - """Return a new ```` element.""" - return parse_xml('' % nsmap["pr"]) + def new(cls) -> CT_Relationships: + """Return a new `` element.""" + return cast(CT_Relationships, parse_xml(f'')) @property - def xml(self): - """ - Return XML string for this element, suitable for saving in a .rels - stream, not pretty printed and with an XML declaration at the top. + def xml_file_bytes(self) -> bytes: + """Return XML bytes, with XML-declaration, for this `` element. + + Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at + the top. """ - return oxml_tostring(self, encoding="UTF-8", standalone=True) + return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True) class CT_Types(BaseOxmlElement): + """`` element. + + The container element for Default and Override elements in [Content_Types].xml. """ - ```` element, the container element for Default and Override - elements in [Content_Types].xml. - """ + + default_lst: list[CT_Default] + override_lst: list[CT_Override] + + _add_default: Callable[..., CT_Default] + _add_override: Callable[..., CT_Override] default = ZeroOrMore("ct:Default") override = ZeroOrMore("ct:Override") - def add_default(self, ext, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ + def add_default(self, ext: str, content_type: str) -> CT_Default: + """Add a child `` element with attributes set to parameter values.""" return self._add_default(extension=ext, contentType=content_type) - def add_override(self, partname, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ + def add_override(self, partname: PackURI, content_type: str) -> CT_Override: + """Add a child `` element with attributes set to parameter values.""" return self._add_override(partName=partname, contentType=content_type) @classmethod - def new(cls): - """ - Return a new ```` element. - """ - xml = '' % nsmap["ct"] - types = parse_xml(xml) - return types + def new(cls) -> CT_Types: + """Return a new `` element.""" + return cast(CT_Types, parse_xml(f'')) register_element_cls("ct:Default", CT_Default) diff --git a/src/pptx/opc/package.py b/src/pptx/opc/package.py index 0427cf347..03ee5f43b 100644 --- a/src/pptx/opc/package.py +++ b/src/pptx/opc/package.py @@ -1,15 +1,17 @@ -# encoding: utf-8 - """Fundamental Open Packaging Convention (OPC) objects. The :mod:`pptx.packaging` module coheres around the concerns of reading and writing presentations to and from a .pptx file. """ +from __future__ import annotations + import collections +from collections.abc import Mapping +from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Set, cast -from pptx.compat import is_string, Mapping -from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationships, serialize_part_xml from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI from pptx.opc.serialized import PackageReader, PackageWriter @@ -17,41 +19,49 @@ from pptx.oxml import parse_xml from pptx.util import lazyproperty +if TYPE_CHECKING: + from typing_extensions import Self + + from pptx.opc.oxml import CT_Relationship, CT_Types + from pptx.oxml.xmlchemy import BaseOxmlElement + from pptx.package import Package + from pptx.parts.presentation import PresentationPart -class _RelatableMixin(object): + +class _RelatableMixin: """Provide relationship methods required by both the package and each part.""" - def part_related_by(self, reltype): + def part_related_by(self, reltype: str) -> Part: """Return (single) part having relationship to this package of `reltype`. - Raises |KeyError| if no such relationship is found and |ValueError| if more than - one such relationship is found. + Raises |KeyError| if no such relationship is found and |ValueError| if more than one such + relationship is found. """ return self._rels.part_with_reltype(reltype) - def relate_to(self, target, reltype, is_external=False): + def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str: """Return rId key of relationship of `reltype` to `target`. - If such a relationship already exists, its rId is returned. Otherwise the - relationship is added and its new rId returned. + If such a relationship already exists, its rId is returned. Otherwise the relationship is + added and its new rId returned. """ - return ( - self._rels.get_or_add_ext_rel(reltype, target) - if is_external - else self._rels.get_or_add(reltype, target) - ) + if isinstance(target, str): + assert is_external + return self._rels.get_or_add_ext_rel(reltype, target) + + return self._rels.get_or_add(reltype, target) - def related_part(self, rId): + def related_part(self, rId: str) -> Part: """Return related |Part| subtype identified by `rId`.""" return self._rels[rId].target_part - def target_ref(self, rId): + def target_ref(self, rId: str) -> str: """Return URL contained in target ref of relationship identified by `rId`.""" return self._rels[rId].target_ref @lazyproperty - def _rels(self): - """|Relationships| object containing relationships from this part to others.""" + def _rels(self) -> _Relationships: + """|_Relationships| object containing relationships from this part to others.""" raise NotImplementedError( # pragma: no cover "`%s` must implement `.rels`" % type(self).__name__ ) @@ -60,25 +70,25 @@ def _rels(self): class OpcPackage(_RelatableMixin): """Main API class for |python-opc|. - A new instance is constructed by calling the :meth:`open` classmethod with a path - to a package file or file-like object containing a package (.pptx file). + A new instance is constructed by calling the :meth:`open` classmethod with a path to a package + file or file-like object containing a package (.pptx file). """ - def __init__(self, pkg_file): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file @classmethod - def open(cls, pkg_file): + def open(cls, pkg_file: str | IO[bytes]) -> Self: """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" return cls(pkg_file)._load() - def drop_rel(self, rId): + def drop_rel(self, rId: str) -> None: """Remove relationship identified by `rId`.""" self._rels.pop(rId) - def iter_parts(self): + def iter_parts(self) -> Iterator[Part]: """Generate exactly one reference to each part in the package.""" - visited = set() + visited: Set[Part] = set() for rel in self.iter_rels(): if rel.is_external: continue @@ -88,114 +98,109 @@ def iter_parts(self): yield part visited.add(part) - def iter_rels(self): + def iter_rels(self) -> Iterator[_Relationship]: """Generate exactly one reference to each relationship in package. Performs a depth-first traversal of the rels graph. """ - visited = set() + visited: Set[Part] = set() - def walk_rels(rels): + def walk_rels(rels: _Relationships) -> Iterator[_Relationship]: for rel in rels.values(): yield rel # --- external items can have no relationships --- if rel.is_external: continue - # --- all relationships other than those for the package belong to a - # --- part. Once that part has been processed, processing it again - # --- would lead to the same relationships appearing more than once. + # -- all relationships other than those for the package belong to a part. Once + # -- that part has been processed, processing it again would lead to the same + # -- relationships appearing more than once. part = rel.target_part if part in visited: continue visited.add(part) # --- recurse into relationships of each unvisited target-part --- - for rel in walk_rels(part.rels): - yield rel + yield from walk_rels(part.rels) - for rel in walk_rels(self._rels): - yield rel + yield from walk_rels(self._rels) @property - def main_document_part(self): + def main_document_part(self) -> PresentationPart: """Return |Part| subtype serving as the main document part for this package. In this case it will be a |Presentation| part. """ - return self.part_related_by(RT.OFFICE_DOCUMENT) + return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT)) - def next_partname(self, tmpl): + def next_partname(self, tmpl: str) -> PackURI: """Return |PackURI| next available partname matching `tmpl`. - `tmpl` is a printf (%)-style template string containing a single replacement - item, a '%d' to be used to insert the integer portion of the partname. - Example: '/ppt/slides/slide%d.xml' + `tmpl` is a printf (%)-style template string containing a single replacement item, a '%d' + to be used to insert the integer portion of the partname. Example: + '/ppt/slides/slide%d.xml' """ # --- expected next partname is tmpl % n where n is one greater than the number # --- of existing partnames that match tmpl. Speed up finding the next one # --- (maybe) by searching from the end downward rather than from 1 upward. prefix = tmpl[: (tmpl % 42).find("42")] - partnames = set( - p.partname for p in self.iter_parts() if p.partname.startswith(prefix) - ) + partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)} for n in range(len(partnames) + 1, 0, -1): candidate_partname = tmpl % n if candidate_partname not in partnames: return PackURI(candidate_partname) - raise Exception( # pragma: no cover - "ProgrammingError: ran out of candidate_partnames" - ) + raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover - def save(self, pkg_file): + def save(self, pkg_file: str | IO[bytes]) -> None: """Save this package to `pkg_file`. `file` can be either a path to a file (a string) or a file-like object. """ PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts())) - def _load(self): + def _load(self) -> Self: """Return the package after loading all parts and relationships.""" - pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, self) + pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self)) self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts) return self @lazyproperty - def _rels(self): + def _rels(self) -> _Relationships: """|Relationships| object containing relationships of this package.""" return _Relationships(PACKAGE_URI.baseURI) -class _PackageLoader(object): +class _PackageLoader: """Function-object that loads a package from disk (or other store).""" - def __init__(self, pkg_file, package): + def __init__(self, pkg_file: str | IO[bytes], package: Package): self._pkg_file = pkg_file self._package = package @classmethod - def load(cls, pkg_file, package): + def load( + cls, pkg_file: str | IO[bytes], package: Package + ) -> tuple[CT_Relationships, dict[PackURI, Part]]: """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`. - The returned `parts` value is a {partname: part} mapping with each part in the - package included and constructed complete with its relationships to other parts - in the package. + The returned `parts` value is a {partname: part} mapping with each part in the package + included and constructed complete with its relationships to other parts in the package. - The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the - parsed package relationships. It is the caller's responsibility (the package - object) to load those relationships into its |_Relationships| object. + The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed + package relationships. It is the caller's responsibility (the package object) to load + those relationships into its |_Relationships| object. """ return cls(pkg_file, package)._load() - def _load(self): + def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]: """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file.""" parts, xml_rels = self._parts, self._xml_rels for partname, part in parts.items(): part.load_rels_from_xml(xml_rels[partname], parts) - return xml_rels["/"], parts + return xml_rels[PACKAGE_URI], parts @lazyproperty - def _content_types(self): + def _content_types(self) -> _ContentTypeMap: """|_ContentTypeMap| object providing content-types for items of this package. Provides a content-type (MIME-type) for any given partname. @@ -203,18 +208,17 @@ def _content_types(self): return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI]) @lazyproperty - def _package_reader(self): + def _package_reader(self) -> PackageReader: """|PackageReader| object providing access to package-items in pkg_file.""" return PackageReader(self._pkg_file) @lazyproperty - def _parts(self): + def _parts(self) -> dict[PackURI, Part]: """dict {partname: Part} populated with parts loading from package. - Among other duties, this collection is passed to each relationships collection - so each relationship can resolve a reference to its target part when required. - This reference can only be reliably carried out once the all parts have been - loaded. + Among other duties, this collection is passed to each relationships collection so each + relationship can resolve a reference to its target part when required. This reference can + only be reliably carried out once the all parts have been loaded. """ content_types = self._content_types package = self._package @@ -227,30 +231,30 @@ def _parts(self): package, blob=package_reader[partname], ) - for partname in (p for p in self._xml_rels.keys() if p != "/") - # --- invalid partnames can arise in some packages; ignore those rather - # --- than raise an exception. + for partname in (p for p in self._xml_rels if p != "/") + # -- invalid partnames can arise in some packages; ignore those rather than raise an + # -- exception. if partname in package_reader } @lazyproperty - def _xml_rels(self): + def _xml_rels(self) -> dict[PackURI, CT_Relationships]: """dict {partname: xml_rels} for package and all package parts. This is used as the basis for other loading operations such as loading parts and populating their relationships. """ - xml_rels = {} - visited_partnames = set() + xml_rels: dict[PackURI, CT_Relationships] = {} + visited_partnames: Set[PackURI] = set() - def load_rels(source_partname, rels): + def load_rels(source_partname: PackURI, rels: CT_Relationships): """Populate `xml_rels` dict by traversing relationships depth-first.""" xml_rels[source_partname] = rels visited_partnames.add(source_partname) base_uri = source_partname.baseURI # --- recursion stops when there are no unvisited partnames in rels --- - for rel in rels: + for rel in rels.relationship_lst: if rel.targetMode == RTM.EXTERNAL: continue target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref) @@ -261,26 +265,31 @@ def load_rels(source_partname, rels): load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI)) return xml_rels - def _xml_rels_for(self, partname): + def _xml_rels_for(self, partname: PackURI) -> CT_Relationships: """Return CT_Relationships object formed by parsing rels XML for `partname`. - A CT_Relationships object is returned in all cases. A part that has no - relationships receives an "empty" CT_Relationships object, i.e. containing no - `CT_Relationship` objects. + A CT_Relationships object is returned in all cases. A part that has no relationships + receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects. """ rels_xml = self._package_reader.rels_xml_for(partname) - return CT_Relationships.new() if rels_xml is None else parse_xml(rels_xml) + return ( + CT_Relationships.new() + if rels_xml is None + else cast(CT_Relationships, parse_xml(rels_xml)) + ) class Part(_RelatableMixin): """Base class for package parts. - Provides common properties and methods, but intended to be subclassed in client code - to implement specific part behaviors. Also serves as the default class for parts - that are not yet given specific behaviors. + Provides common properties and methods, but intended to be subclassed in client code to + implement specific part behaviors. Also serves as the default class for parts that are not yet + given specific behaviors. """ - def __init__(self, partname, content_type, package, blob=None): + def __init__( + self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None + ): # --- XmlPart subtypes, don't store a blob (the original XML) --- self._partname = partname self._content_type = content_type @@ -288,86 +297,74 @@ def __init__(self, partname, content_type, package, blob=None): self._blob = blob @classmethod - def load(cls, partname, content_type, package, blob): + def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self: """Return `cls` instance loaded from arguments. - This one is a straight pass-through, but subtypes may do some pre-processing, - see XmlPart for an example. + This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart + for an example. """ return cls(partname, content_type, package, blob) @property - def blob(self): + def blob(self) -> bytes: """Contents of this package part as a sequence of bytes. - May be text (XML generally) or binary. Intended to be overridden by subclasses. - Default behavior is to return the blob initial loaded during `Package.open()` - operation. + Intended to be overridden by subclasses. Default behavior is to return the blob initial + loaded during `Package.open()` operation. """ - return self._blob + return self._blob or b"" @blob.setter - def blob(self, bytes_): + def blob(self, blob: bytes): """Note that not all subclasses use the part blob as their blob source. - In particular, the |XmlPart| subclass uses its `self._element` to serialize a - blob on demand. This works fine for binary parts though. + In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on + demand. This works fine for binary parts though. """ - self._blob = bytes_ + self._blob = blob @lazyproperty - def content_type(self): + def content_type(self) -> str: """Content-type (MIME-type) of this part.""" return self._content_type - def drop_rel(self, rId): - """Remove relationship identified by `rId` if its reference count is under 2. - - Relationships with a reference count of 0 are implicit relationships. Note that - only XML parts can drop relationships. - """ - if self._rel_ref_count(rId) < 2: - self._rels.pop(rId) - - def load_rels_from_xml(self, xml_rels, parts): + def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None: """load _Relationships for this part from `xml_rels`. - Part references are resolved using the `parts` dict that maps each partname to - the loaded part with that partname. These relationships are loaded from a - serialized package and so already have assigned rIds. This method is only used - during package loading. + Part references are resolved using the `parts` dict that maps each partname to the loaded + part with that partname. These relationships are loaded from a serialized package and so + already have assigned rIds. This method is only used during package loading. """ self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts) @lazyproperty - def package(self): - """|OpcPackage| instance this part belongs to.""" + def package(self) -> Package: + """Package this part belongs to.""" return self._package @property - def partname(self): + def partname(self) -> PackURI: """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" return self._partname @partname.setter - def partname(self, partname): - if not isinstance(partname, PackURI): + def partname(self, partname: PackURI): + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] raise TypeError( # pragma: no cover - "partname must be instance of PackURI, got '%s'" - % type(partname).__name__ + "partname must be instance of PackURI, got '%s'" % type(partname).__name__ ) self._partname = partname @lazyproperty - def rels(self): - """|Relationships| collection of relationships from this part to other parts.""" + def rels(self) -> _Relationships: + """Collection of relationships from this part to other parts.""" # --- this must be public to allow the part graph to be traversed --- return self._rels - def _blob_from_file(self, file): + def _blob_from_file(self, file: str | IO[bytes]) -> bytes: """Return bytes of `file`, which is either a str path or a file-like object.""" # --- a str `file` is assumed to be a path --- - if is_string(file): + if isinstance(file, str): with open(file, "rb") as f: return f.read() @@ -377,13 +374,9 @@ def _blob_from_file(self, file): file.seek(0) return file.read() - def _rel_ref_count(self, rId): - """Return int count of references in this part's XML to `rId`.""" - return len([r for r in self._element.xpath("//@r:id") if r == rId]) - @lazyproperty - def _rels(self): - """|Relationships| object containing relationships from this part to others.""" + def _rels(self) -> _Relationships: + """Relationships from this part to others.""" return _Relationships(self._partname.baseURI) @@ -394,46 +387,65 @@ class XmlPart(Part): reserializing the XML payload and managing relationships to other parts. """ - def __init__(self, partname, content_type, package, element): + def __init__( + self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement + ): super(XmlPart, self).__init__(partname, content_type, package) self._element = element @classmethod - def load(cls, partname, content_type, package, blob): + def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes): """Return instance of `cls` loaded with parsed XML from `blob`.""" - return cls(partname, content_type, package, element=parse_xml(blob)) + return cls( + partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob)) + ) @property - def blob(self): + def blob(self) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride] """bytes XML serialization of this part.""" return serialize_part_xml(self._element) + # -- XmlPart cannot set its blob, which is why pyright complains -- + + def drop_rel(self, rId: str) -> None: + """Remove relationship identified by `rId` if its reference count is under 2. + + Relationships with a reference count of 0 are implicit relationships. Note that only XML + parts can drop relationships. + """ + if self._rel_ref_count(rId) < 2: + self._rels.pop(rId) + @property def part(self): """This part. - This is part of the parent protocol, "children" of the document will not know - the part that contains them so must ask their parent object. That chain of - delegation ends here for child objects. + This is part of the parent protocol, "children" of the document will not know the part + that contains them so must ask their parent object. That chain of delegation ends here for + child objects. """ return self + def _rel_ref_count(self, rId: str) -> int: + """Return int count of references in this part's XML to `rId`.""" + return len([r for r in cast(list[str], self._element.xpath("//@r:id")) if r == rId]) + -class PartFactory(object): +class PartFactory: """Constructs a registered subtype of |Part|. - Client code can register a subclass of |Part| to be used for a package blob based on - its content type. + Client code can register a subclass of |Part| to be used for a package blob based on its + content type. """ - part_type_for = {} + part_type_for: dict[str, type[Part]] = {} - def __new__(cls, partname, content_type, package, blob): + def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part: PartClass = cls._part_cls_for(content_type) return PartClass.load(partname, content_type, package, blob) @classmethod - def _part_cls_for(cls, content_type): + def _part_cls_for(cls, content_type: str) -> type[Part]: """Return the custom part class registered for `content_type`. Returns |Part| if no custom class is registered for `content_type`. @@ -443,19 +455,18 @@ def _part_cls_for(cls, content_type): return Part -class _ContentTypeMap(object): +class _ContentTypeMap: """Value type providing dict semantics for looking up content type by partname.""" - def __init__(self, overrides, defaults): + def __init__(self, overrides: dict[str, str], defaults: dict[str, str]): self._overrides = overrides self._defaults = defaults - def __getitem__(self, partname): + def __getitem__(self, partname: PackURI) -> str: """Return content-type (MIME-type) for part identified by *partname*.""" - if not isinstance(partname, PackURI): + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] raise TypeError( - "_ContentTypeMap key must be , got %s" - % type(partname).__name__ + "_ContentTypeMap key must be , got %s" % type(partname).__name__ ) if partname in self._overrides: @@ -464,14 +475,13 @@ def __getitem__(self, partname): if partname.ext in self._defaults: return self._defaults[partname.ext] - raise KeyError( - "no content-type for partname '%s' in [Content_Types].xml" % partname - ) + raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname) @classmethod - def from_xml(cls, content_types_xml): + def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap: """Return |_ContentTypeMap| instance populated from `content_types_xml`.""" - types_elm = parse_xml(content_types_xml) + types_elm = cast("CT_Types", parse_xml(content_types_xml)) + # -- note all partnames in [Content_Types].xml are absolute -- overrides = CaseInsensitiveDict( (o.partName.lower(), o.contentType) for o in types_elm.override_lst ) @@ -481,57 +491,55 @@ def from_xml(cls, content_types_xml): return cls(overrides, defaults) -class _Relationships(Mapping): +class _Relationships(Mapping[str, "_Relationship"]): """Collection of |_Relationship| instances having `dict` semantics. - Relationships are keyed by their rId, but may also be found in other ways, such as - by their relationship type. |Relationship| objects are keyed by their rId. + Relationships are keyed by their rId, but may also be found in other ways, such as by their + relationship type. |Relationship| objects are keyed by their rId. - Iterating this collection has normal mapping semantics, generating the keys (rIds) - of the mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as - they would be for a `dict`. + Iterating this collection has normal mapping semantics, generating the keys (rIds) of the + mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a + `dict`. """ - def __init__(self, base_uri): + def __init__(self, base_uri: str): self._base_uri = base_uri - def __contains__(self, rId): + def __contains__(self, rId: object) -> bool: """Implement 'in' operation, like `"rId7" in relationships`.""" return rId in self._rels - def __getitem__(self, rId): + def __getitem__(self, rId: str) -> _Relationship: """Implement relationship lookup by rId using indexed access, like rels[rId].""" try: return self._rels[rId] except KeyError: raise KeyError("no relationship with key '%s'" % rId) - def __iter__(self): + def __iter__(self) -> Iterator[str]: """Implement iteration of rIds (iterating a mapping produces its keys).""" return iter(self._rels) - def __len__(self): + def __len__(self) -> int: """Return count of relationships in collection.""" return len(self._rels) - def get_or_add(self, reltype, target_part): + def get_or_add(self, reltype: str, target_part: Part) -> str: """Return str rId of `reltype` to `target_part`. - The rId of an existing matching relationship is used if present. Otherwise, a - new relationship is added and that rId is returned. + The rId of an existing matching relationship is used if present. Otherwise, a new + relationship is added and that rId is returned. """ existing_rId = self._get_matching(reltype, target_part) return ( - self._add_relationship(reltype, target_part) - if existing_rId is None - else existing_rId + self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId ) - def get_or_add_ext_rel(self, reltype, target_ref): + def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str: """Return str rId of external relationship of `reltype` to `target_ref`. - The rId of an existing matching relationship is used if present. Otherwise, a - new relationship is added and that rId is returned. + The rId of an existing matching relationship is used if present. Otherwise, a new + relationship is added and that rId is returned. """ existing_rId = self._get_matching(reltype, target_ref, is_external=True) return ( @@ -540,7 +548,9 @@ def get_or_add_ext_rel(self, reltype, target_ref): else existing_rId ) - def load_from_xml(self, base_uri, xml_rels, parts): + def load_from_xml( + self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part] + ) -> None: """Replace any relationships in this collection with those from `xml_rels`.""" def iter_valid_rels(): @@ -559,11 +569,11 @@ def iter_valid_rels(): self._rels.clear() self._rels.update((rel.rId, rel) for rel in iter_valid_rels()) - def part_with_reltype(self, reltype): + def part_with_reltype(self, reltype: str) -> Part: """Return target part of relationship with matching `reltype`. - Raises |KeyError| if not found and |ValueError| if more than one matching - relationship is found. + Raises |KeyError| if not found and |ValueError| if more than one matching relationship is + found. """ rels_of_reltype = self._rels_by_reltype[reltype] @@ -571,14 +581,12 @@ def part_with_reltype(self, reltype): raise KeyError("no relationship of type '%s' in collection" % reltype) if len(rels_of_reltype) > 1: - raise ValueError( - "multiple relationships of type '%s' in collection" % reltype - ) + raise ValueError("multiple relationships of type '%s' in collection" % reltype) return rels_of_reltype[0].target_part - def pop(self, rId): - """Return |Relationship| identified by `rId` after removing it from collection. + def pop(self, rId: str) -> _Relationship: + """Return |_Relationship| identified by `rId` after removing it from collection. The caller is responsible for ensuring it is no longer required. """ @@ -588,8 +596,8 @@ def pop(self, rId): def xml(self): """bytes XML serialization of this relationship collection. - This value is suitable for storage as a .rels file in an OPC package. Includes - a ` str: """Return str rId of |_Relationship| newly added to spec.""" rId = self._next_rId self._rels[rId] = _Relationship( @@ -622,7 +630,9 @@ def _add_relationship(self, reltype, target, is_external=False): ) return rId - def _get_matching(self, reltype, target, is_external=False): + def _get_matching( + self, reltype: str, target: Part | str, is_external: bool = False + ) -> str | None: """Return optional str rId of rel of `reltype`, `target`, and `is_external`. Returns `None` on no matching relationship @@ -631,18 +641,17 @@ def _get_matching(self, reltype, target, is_external=False): if rel.is_external != is_external: continue rel_target = rel.target_ref if rel.is_external else rel.target_part - if rel_target != target: - continue - return rel.rId + if rel_target == target: + return rel.rId return None @property - def _next_rId(self): + def _next_rId(self) -> str: """Next str rId available in collection. - The next rId is the first unused key starting from "rId1" and making use of any - gaps in numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']. + The next rId is the first unused key starting from "rId1" and making use of any gaps in + numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']. """ # --- The common case is where all sequential numbers starting at "rId1" are # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start @@ -651,25 +660,28 @@ def _next_rId(self): rId_candidate = "rId%d" % n # like 'rId19' if rId_candidate not in self._rels: return rId_candidate + raise Exception( + "ProgrammingError: Impossible to have more distinct rIds than relationships" + ) @lazyproperty - def _rels(self): + def _rels(self) -> dict[str, _Relationship]: """dict {rId: _Relationship} containing relationships of this collection.""" - return dict() + return {} @property - def _rels_by_reltype(self): + def _rels_by_reltype(self) -> dict[str, list[_Relationship]]: """defaultdict {reltype: [rels]} for all relationships in collection.""" - D = collections.defaultdict(list) + D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list) for rel in self.values(): D[rel.reltype].append(rel) return D -class _Relationship(object): +class _Relationship: """Value object describing link from a part or package to another part.""" - def __init__(self, base_uri, rId, reltype, target_mode, target): + def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str): self._base_uri = base_uri self._rId = rId self._reltype = reltype @@ -677,7 +689,9 @@ def __init__(self, base_uri, rId, reltype, target_mode, target): self._target = target @classmethod - def from_xml(cls, base_uri, rel, parts): + def from_xml( + cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part] + ) -> _Relationship: """Return |_Relationship| object based on CT_Relationship element `rel`.""" target = ( rel.target_ref @@ -687,62 +701,63 @@ def from_xml(cls, base_uri, rel, parts): return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target) @lazyproperty - def is_external(self): + def is_external(self) -> bool: """True if target_mode is `RTM.EXTERNAL`. - An external relationship is a link to a resource outside the package, such as - a web-resource (URL). + An external relationship is a link to a resource outside the package, such as a + web-resource (URL). """ return self._target_mode == RTM.EXTERNAL @lazyproperty - def reltype(self): + def reltype(self) -> str: """Member of RELATIONSHIP_TYPE describing relationship of target to source.""" return self._reltype @lazyproperty - def rId(self): + def rId(self) -> str: """str relationship-id, like 'rId9'. - Corresponds to the `Id` attribute on the `CT_Relationship` element and - uniquely identifies this relationship within its peers for the source-part or - package. + Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies + this relationship within its peers for the source-part or package. """ return self._rId @lazyproperty - def target_part(self): + def target_part(self) -> Part: """|Part| or subtype referred to by this relationship.""" if self.is_external: raise ValueError( "`.target_part` property on _Relationship is undefined when " "target-mode is external" ) + assert isinstance(self._target, Part) return self._target @lazyproperty - def target_partname(self): + def target_partname(self) -> PackURI: """|PackURI| instance containing partname targeted by this relationship. - Raises `ValueError` on reference if target_mode is external. Use - :attr:`target_mode` to check before referencing. + Raises `ValueError` on reference if target_mode is external. Use :attr:`target_mode` to + check before referencing. """ if self.is_external: raise ValueError( "`.target_partname` property on _Relationship is undefined when " "target-mode is external" ) + assert isinstance(self._target, Part) return self._target.partname @lazyproperty - def target_ref(self): + def target_ref(self) -> str: """str reference to relationship target. - For internal relationships this is the relative partname, suitable for - serialization purposes. For an external relationship it is typically a URL. + For internal relationships this is the relative partname, suitable for serialization + purposes. For an external relationship it is typically a URL. """ - return ( - self._target - if self.is_external - else self.target_partname.relative_ref(self._base_uri) - ) + if self.is_external: + assert isinstance(self._target, str) + return self._target + + return self.target_partname.relative_ref(self._base_uri) diff --git a/src/pptx/opc/packuri.py b/src/pptx/opc/packuri.py index 65a0b44ab..74ddd333f 100644 --- a/src/pptx/opc/packuri.py +++ b/src/pptx/opc/packuri.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI.""" +from __future__ import annotations + import posixpath import re @@ -9,62 +9,59 @@ class PackURI(str): """Proxy for a pack URI (partname). - Provides utility properties the baseURI and the filename slice. Behaves as |str| - otherwise. + Provides utility properties the baseURI and the filename slice. Behaves as |str| otherwise. """ _filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?") - def __new__(cls, pack_uri_str): + def __new__(cls, pack_uri_str: str): if not pack_uri_str[0] == "/": - raise ValueError("PackURI must begin with slash, got '%s'" % pack_uri_str) + raise ValueError(f"PackURI must begin with slash, got {repr(pack_uri_str)}") return str.__new__(cls, pack_uri_str) @staticmethod - def from_rel_ref(baseURI, relative_ref): - """ - Return a |PackURI| instance containing the absolute pack URI formed by - translating *relative_ref* onto *baseURI*. - """ + def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI: + """Construct an absolute pack URI formed by translating `relative_ref` onto `baseURI`.""" joined_uri = posixpath.join(baseURI, relative_ref) abs_uri = posixpath.abspath(joined_uri) return PackURI(abs_uri) @property - def baseURI(self): - """ - The base URI of this pack URI, the directory portion, roughly - speaking. E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. - For the package pseudo-partname '/', baseURI is '/'. + def baseURI(self) -> str: + """The base URI of this pack URI; the directory portion, roughly speaking. + + E.g. `"/ppt/slides"` for `"/ppt/slides/slide1.xml"`. + + For the package pseudo-partname "/", the baseURI is "/". """ return posixpath.split(self)[0] @property - def ext(self): - """ - The extension portion of this pack URI, e.g. ``'xml'`` for - ``'/ppt/slides/slide1.xml'``. Note that the period is not included. + def ext(self) -> str: + """The extension portion of this pack URI. + + E.g. `"xml"` for `"/ppt/slides/slide1.xml"`. Note the leading period is not included. """ - # raw_ext is either empty string or starts with period, e.g. '.xml' + # -- raw_ext is either empty string or starts with period, e.g. ".xml" -- raw_ext = posixpath.splitext(self)[1] return raw_ext[1:] if raw_ext.startswith(".") else raw_ext @property - def filename(self): - """ - The "filename" portion of this pack URI, e.g. ``'slide1.xml'`` for - ``'/ppt/slides/slide1.xml'``. For the package pseudo-partname '/', - filename is ''. + def filename(self) -> str: + """The "filename" portion of this pack URI. + + E.g. `"slide1.xml"` for `"/ppt/slides/slide1.xml"`. + + For the package pseudo-partname "/", `filename` is ''. """ return posixpath.split(self)[1] @property - def idx(self): + def idx(self) -> int | None: """Optional int partname index. - Value is an integer for an "array" partname or None for singleton partname, e.g. - ``21`` for ``'/ppt/slides/slide21.xml'`` and |None| for - ``'/ppt/presentation.xml'``. + Value is an integer for an "array" partname or None for singleton partname, e.g. `21` for + `"/ppt/slides/slide21.xml"` and |None| for `"/ppt/presentation.xml"`. """ filename = self.filename if not filename: @@ -78,34 +75,30 @@ def idx(self): return None @property - def membername(self): - """ - The pack URI with the leading slash stripped off, the form used as - the Zip file membername for the package item. Returns '' for the - package pseudo-partname '/'. + def membername(self) -> str: + """The pack URI with the leading slash stripped off. + + This is the form used as the Zip file membername for the package item. Returns "" for the + package pseudo-partname "/". """ return self[1:] - def relative_ref(self, baseURI): - """ - Return string containing relative reference to package item from - *baseURI*. E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would - return '../slideLayouts/slideLayout1.xml' for baseURI '/ppt/slides'. + def relative_ref(self, baseURI: str) -> str: + """Return string containing relative reference to package item from `baseURI`. + + E.g. PackURI("/ppt/slideLayouts/slideLayout1.xml") would return + "../slideLayouts/slideLayout1.xml" for baseURI "/ppt/slides". """ # workaround for posixpath bug in 2.6, doesn't generate correct - # relative path when *start* (second) parameter is root ('/') - if baseURI == "/": - relpath = self[1:] - else: - relpath = posixpath.relpath(self, baseURI) - return relpath + # relative path when `start` (second) parameter is root ("/") + return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) @property - def rels_uri(self): - """ - The pack URI of the .rels part corresponding to the current pack URI. - Only produces sensible output if the pack URI is a partname or the - package pseudo-partname '/'. + def rels_uri(self) -> PackURI: + """The pack URI of the .rels part corresponding to the current pack URI. + + Only produces sensible output if the pack URI is a partname or the package pseudo-partname + "/". """ rels_filename = "%s.rels" % self.filename rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename) diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py index 9e6ad51e0..eba628247 100644 --- a/src/pptx/opc/serialized.py +++ b/src/pptx/opc/serialized.py @@ -1,13 +1,14 @@ -# encoding: utf-8 - """API for reading/writing serialized Open Packaging Convention (OPC) package.""" +from __future__ import annotations + import os import posixpath import zipfile +from collections.abc import Container +from typing import IO, TYPE_CHECKING, Any, Sequence -from pptx.compat import Container, is_string -from pptx.exceptions import PackageNotFoundError +from pptx.exc import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.oxml import CT_Types, serialize_part_xml from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI @@ -15,114 +16,123 @@ from pptx.opc.spec import default_content_types from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.opc.package import Part, _Relationships # pyright: ignore[reportPrivateUsage] + -class PackageReader(Container): +class PackageReader(Container[bytes]): """Provides access to package-parts of an OPC package with dict semantics. - The package may be in zip-format (a .pptx file) or expanded into a directory - structure, perhaps by unzipping a .pptx file. + The package may be in zip-format (a .pptx file) or expanded into a directory structure, + perhaps by unzipping a .pptx file. """ - def __init__(self, pkg_file): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file - def __contains__(self, pack_uri): + def __contains__(self, pack_uri: object) -> bool: """Return True when part identified by `pack_uri` is present in package.""" return pack_uri in self._blob_reader - def __getitem__(self, pack_uri): + def __getitem__(self, pack_uri: PackURI) -> bytes: """Return bytes for part corresponding to `pack_uri`.""" return self._blob_reader[pack_uri] - def rels_xml_for(self, partname): + def rels_xml_for(self, partname: PackURI) -> bytes | None: """Return optional rels item XML for `partname`. - Returns `None` if no rels item is present for `partname`. `partname` is a - |PackURI| instance. + Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI| + instance. """ blob_reader, uri = self._blob_reader, partname.rels_uri return blob_reader[uri] if uri in blob_reader else None @lazyproperty - def _blob_reader(self): + def _blob_reader(self) -> _PhysPkgReader: """|_PhysPkgReader| subtype providing read access to the package file.""" return _PhysPkgReader.factory(self._pkg_file) -class PackageWriter(object): +class PackageWriter: """Writes a zip-format OPC package to `pkg_file`. - `pkg_file` can be either a path to a zip file (a string) or a file-like object. - `pkg_rels` is the |_Relationships| object containing relationships for the package. - `parts` is a sequence of |Part| subtype instance to be written to the package. + `pkg_file` can be either a path to a zip file (a string) or a file-like object. `pkg_rels` is + the |_Relationships| object containing relationships for the package. `parts` is a sequence of + |Part| subtype instance to be written to the package. - Its single API classmethod is :meth:`write`. This class is not intended to be - instantiated. + Its single API classmethod is :meth:`write`. This class is not intended to be instantiated. """ - def __init__(self, pkg_file, pkg_rels, parts): + def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]): self._pkg_file = pkg_file self._pkg_rels = pkg_rels self._parts = parts @classmethod - def write(cls, pkg_file, pkg_rels, parts): + def write( + cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part] + ) -> None: """Write a physical package (.pptx file) to `pkg_file`. - The serialized package contains `pkg_rels` and `parts`, a content-types stream - based on the content type of each part, and a .rels file for each part that has - relationships. + The serialized package contains `pkg_rels` and `parts`, a content-types stream based on + the content type of each part, and a .rels file for each part that has relationships. """ cls(pkg_file, pkg_rels, parts)._write() - def _write(self): + def _write(self) -> None: """Write physical package (.pptx file).""" with _PhysPkgWriter.factory(self._pkg_file) as phys_writer: self._write_content_types_stream(phys_writer) self._write_pkg_rels(phys_writer) self._write_parts(phys_writer) - def _write_content_types_stream(self, phys_writer): + def _write_content_types_stream(self, phys_writer: _PhysPkgWriter) -> None: """Write `[Content_Types].xml` part to the physical package. - This part must contain an appropriate content type lookup target for each part - in the package. + This part must contain an appropriate content type lookup target for each part in the + package. """ phys_writer.write( CONTENT_TYPES_URI, serialize_part_xml(_ContentTypesItem.xml_for(self._parts)), ) - def _write_parts(self, phys_writer): + def _write_parts(self, phys_writer: _PhysPkgWriter) -> None: """Write blob of each part in `parts` to the package. A rels item for each part is also written when the part has relationships. """ for part in self._parts: phys_writer.write(part.partname, part.blob) - if part._rels: + if part._rels: # pyright: ignore[reportPrivateUsage] phys_writer.write(part.partname.rels_uri, part.rels.xml) - def _write_pkg_rels(self, phys_writer): - """Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the package.""" + def _write_pkg_rels(self, phys_writer: _PhysPkgWriter) -> None: + """Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package.""" phys_writer.write(PACKAGE_URI.rels_uri, self._pkg_rels.xml) -class _PhysPkgReader(Container): +class _PhysPkgReader(Container[PackURI]): """Base class for physical package reader objects.""" - def __contains__(self, item): + def __contains__(self, item: object) -> bool: """Must be implemented by each subclass.""" raise NotImplementedError( # pragma: no cover "`%s` must implement `.__contains__()`" % type(self).__name__ ) + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Blob for part corresponding to `pack_uri`.""" + raise NotImplementedError( # pragma: no cover + f"`{type(self).__name__}` must implement `.__contains__()`" + ) + @classmethod - def factory(cls, pkg_file): + def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader: """Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`.""" # --- for pkg_file other than str, assume it's a stream and pass it to Zip # --- reader to sort out - if not is_string(pkg_file): + if not isinstance(pkg_file, str): return _ZipPkgReader(pkg_file) # --- otherwise we treat `pkg_file` as a path --- @@ -141,14 +151,16 @@ class _DirPkgReader(_PhysPkgReader): `path` is the path to a directory containing an expanded package. """ - def __init__(self, path): + def __init__(self, path: str): self._path = os.path.abspath(path) - def __contains__(self, pack_uri): + def __contains__(self, pack_uri: object) -> bool: """Return True when part identified by `pack_uri` is present in zip archive.""" + if not isinstance(pack_uri, PackURI): + return False return os.path.exists(posixpath.join(self._path, pack_uri.membername)) - def __getitem__(self, pack_uri): + def __getitem__(self, pack_uri: PackURI) -> bytes: """Return bytes of file corresponding to `pack_uri` in package directory.""" path = os.path.join(self._path, pack_uri.membername) try: @@ -161,14 +173,14 @@ def __getitem__(self, pack_uri): class _ZipPkgReader(_PhysPkgReader): """Implements |PhysPkgReader| interface for a zip-file OPC package.""" - def __init__(self, pkg_file): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file - def __contains__(self, pack_uri): + def __contains__(self, pack_uri: object) -> bool: """Return True when part identified by `pack_uri` is present in zip archive.""" return pack_uri in self._blobs - def __getitem__(self, pack_uri): + def __getitem__(self, pack_uri: PackURI) -> bytes: """Return bytes for part corresponding to `pack_uri`. Raises |KeyError| if no matching member is present in zip archive. @@ -178,76 +190,80 @@ def __getitem__(self, pack_uri): return self._blobs[pack_uri] @lazyproperty - def _blobs(self): + def _blobs(self) -> dict[PackURI, bytes]: """dict mapping partname to package part binaries.""" with zipfile.ZipFile(self._pkg_file, "r") as z: return {PackURI("/%s" % name): z.read(name) for name in z.namelist()} -class _PhysPkgWriter(object): +class _PhysPkgWriter: """Base class for physical package writer objects.""" @classmethod - def factory(cls, pkg_file): + def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter: """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`. - Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be - implemented or even a `_StreamPkgWriter`. + Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented + or even a `_StreamPkgWriter`. """ return _ZipPkgWriter(pkg_file) + def write(self, pack_uri: PackURI, blob: bytes) -> None: + """Write `blob` to package with membername corresponding to `pack_uri`.""" + raise NotImplementedError( # pragma: no cover + f"`{type(self).__name__}` must implement `.write()`" + ) + class _ZipPkgWriter(_PhysPkgWriter): """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package.""" - def __init__(self, pkg_file): + def __init__(self, pkg_file: str | IO[bytes]): self._pkg_file = pkg_file - def __enter__(self): + def __enter__(self) -> _ZipPkgWriter: """Enable use as a context-manager. Opening zip for writing happens here.""" return self - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__(self, *exc: list[Any]) -> None: """Close the zip archive on exit from context. - Closing flushes any pending physical writes and releasing any resources it's - using. + Closing flushes any pending physical writes and releasing any resources it's using. """ self._zipf.close() - def write(self, pack_uri, blob): + def write(self, pack_uri: PackURI, blob: bytes) -> None: """Write `blob` to zip package with membername corresponding to `pack_uri`.""" self._zipf.writestr(pack_uri.membername, blob) @lazyproperty - def _zipf(self): + def _zipf(self) -> zipfile.ZipFile: """`ZipFile` instance open for writing.""" return zipfile.ZipFile(self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED) -class _ContentTypesItem(object): +class _ContentTypesItem: """Composes content-types "part" ([Content_Types].xml) for a collection of parts.""" - def __init__(self, parts): + def __init__(self, parts: Sequence[Part]): self._parts = parts @classmethod - def xml_for(cls, parts): + def xml_for(cls, parts: Sequence[Part]) -> CT_Types: """Return content-types XML mapping each part in `parts` to a content-type. - The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC - package. + The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package. """ return cls(parts)._xml @lazyproperty - def _xml(self): + def _xml(self) -> CT_Types: """lxml.etree._Element containing the content-types item. - This XML object is suitable for serialization to the `[Content_Types].xml` item - for an OPC package. Although the sequence of elements is not strictly - significant, as an aid to testing and readability Default elements are sorted by - extension and Override elements are sorted by partname. + This XML object is suitable for serialization to the `[Content_Types].xml` item for an OPC + package. Although the sequence of elements is not strictly significant, as an aid to + testing and readability Default elements are sorted by extension and Override elements are + sorted by partname. """ defaults, overrides = self._defaults_and_overrides _types_elm = CT_Types.new() @@ -260,13 +276,13 @@ def _xml(self): return _types_elm @lazyproperty - def _defaults_and_overrides(self): + def _defaults_and_overrides(self) -> tuple[dict[str, str], dict[PackURI, str]]: """pair of dict (defaults, overrides) accounting for all parts. `defaults` is {ext: content_type} and overrides is {partname: content_type}. """ defaults = CaseInsensitiveDict(rels=CT.OPC_RELATIONSHIPS, xml=CT.XML) - overrides = dict() + overrides: dict[PackURI, str] = {} for part in self._parts: partname, content_type = part.partname, part.content_type diff --git a/src/pptx/opc/shared.py b/src/pptx/opc/shared.py index 95e379984..cc7fce8c1 100644 --- a/src/pptx/opc/shared.py +++ b/src/pptx/opc/shared.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Objects shared by modules in the pptx.opc sub-package.""" +from __future__ import annotations + class CaseInsensitiveDict(dict): """Mapping type like dict except it matches key without respect to case. diff --git a/src/pptx/opc/spec.py b/src/pptx/opc/spec.py index 5b63f425c..a83caf8bd 100644 --- a/src/pptx/opc/spec.py +++ b/src/pptx/opc/spec.py @@ -1,11 +1,6 @@ -# encoding: utf-8 - -""" -Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500. -""" - -from .constants import CONTENT_TYPE as CT +"""Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500.""" +from pptx.opc.constants import CONTENT_TYPE as CT default_content_types = ( ("bin", CT.PML_PRINTER_SETTINGS), diff --git a/src/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py index 099960d72..21afaa921 100644 --- a/src/pptx/oxml/__init__.py +++ b/src/pptx/oxml/__init__.py @@ -1,64 +1,58 @@ -# encoding: utf-8 - """Initializes lxml parser, particularly the custom element classes. Also makes available a handful of functions that wrap its typical uses. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import os +from typing import TYPE_CHECKING, Type from lxml import etree -from .ns import NamespacePrefixedTag +from pptx.oxml.ns import NamespacePrefixedTag + +if TYPE_CHECKING: + from pptx.oxml.xmlchemy import BaseOxmlElement -# configure etree XML parser ------------------------------- +# -- configure etree XML parser ---------------------------- element_class_lookup = etree.ElementNamespaceClassLookup() oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False) oxml_parser.set_element_class_lookup(element_class_lookup) -def parse_from_template(template_name): - """ - Return an element loaded from the XML in the template file identified by - *template_name*. - """ +def parse_from_template(template_file_name: str): + """Return an element loaded from the XML in the template file identified by `template_name`.""" thisdir = os.path.split(__file__)[0] - filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_name) + filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_file_name) with open(filename, "rb") as f: xml = f.read() return parse_xml(xml) -def parse_xml(xml): - """ - Return root lxml element obtained by parsing XML character string in - *xml*, which can be either a Python 2.x string or unicode. - """ - root_element = etree.fromstring(xml, oxml_parser) - return root_element +def parse_xml(xml: str | bytes): + """Return root lxml element obtained by parsing XML character string in `xml`.""" + return etree.fromstring(xml, oxml_parser) -def register_element_cls(nsptagname, cls): - """ - Register *cls* to be constructed when the oxml parser encounters an - element having name *nsptag_name*. *nsptag_name* is a string of the form - ``nspfx:tagroot``, e.g. ``'w:document'``. +def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]): + """Register `cls` to be constructed when oxml parser encounters element having `nsptag_name`. + + `nsptag_name` is a string of the form `nspfx:tagroot`, e.g. `"w:document"`. """ nsptag = NamespacePrefixedTag(nsptagname) namespace = element_class_lookup.get_namespace(nsptag.nsuri) namespace[nsptag.local_part] = cls -from .action import CT_Hyperlink # noqa: E402 +from pptx.oxml.action import CT_Hyperlink # noqa: E402 register_element_cls("a:hlinkClick", CT_Hyperlink) register_element_cls("a:hlinkHover", CT_Hyperlink) -from .chart.axis import ( # noqa: E402 +from pptx.oxml.chart.axis import ( # noqa: E402 CT_AxisUnit, CT_CatAx, CT_ChartLines, @@ -87,7 +81,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:valAx", CT_ValAx) -from .chart.chart import ( # noqa: E402 +from pptx.oxml.chart.chart import ( # noqa: E402 CT_Chart, CT_ChartSpace, CT_ExternalData, @@ -102,27 +96,27 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:style", CT_Style) -from .chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls # noqa: E402 +from pptx.oxml.chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls # noqa: E402 register_element_cls("c:dLbl", CT_DLbl) register_element_cls("c:dLblPos", CT_DLblPos) register_element_cls("c:dLbls", CT_DLbls) -from .chart.legend import CT_Legend, CT_LegendPos # noqa: E402 +from pptx.oxml.chart.legend import CT_Legend, CT_LegendPos # noqa: E402 register_element_cls("c:legend", CT_Legend) register_element_cls("c:legendPos", CT_LegendPos) -from .chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle # noqa: E402 +from pptx.oxml.chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle # noqa: E402 register_element_cls("c:marker", CT_Marker) register_element_cls("c:size", CT_MarkerSize) register_element_cls("c:symbol", CT_MarkerStyle) -from .chart.plot import ( # noqa: E402 +from pptx.oxml.chart.plot import ( # noqa: E402 CT_Area3DChart, CT_AreaChart, CT_BarChart, @@ -155,7 +149,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:scatterChart", CT_ScatterChart) -from .chart.series import ( # noqa: E402 +from pptx.oxml.chart.series import ( # noqa: E402 CT_AxDataSource, CT_DPt, CT_Lvl, @@ -175,7 +169,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:yVal", CT_NumDataSource) -from .chart.shared import ( # noqa: E402 +from pptx.oxml.chart.shared import ( # noqa: E402 CT_Boolean, CT_Boolean_Explicit, CT_Double, @@ -218,12 +212,12 @@ def register_element_cls(nsptagname, cls): register_element_cls("c:xMode", CT_LayoutMode) -from .coreprops import CT_CoreProperties # noqa: E402 +from pptx.oxml.coreprops import CT_CoreProperties # noqa: E402 register_element_cls("cp:coreProperties", CT_CoreProperties) -from .dml.color import ( # noqa: E402 +from pptx.oxml.dml.color import ( # noqa: E402 CT_Color, CT_HslColor, CT_Percentage, @@ -246,7 +240,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("a:sysClr", CT_SystemColor) -from .dml.fill import ( # noqa: E402 +from pptx.oxml.dml.fill import ( # noqa: E402 CT_Blip, CT_BlipFillProperties, CT_GradientFillProperties, @@ -273,12 +267,12 @@ def register_element_cls(nsptagname, cls): register_element_cls("a:srcRect", CT_RelativeRect) -from .dml.line import CT_PresetLineDashProperties # noqa: E402 +from pptx.oxml.dml.line import CT_PresetLineDashProperties # noqa: E402 register_element_cls("a:prstDash", CT_PresetLineDashProperties) -from .presentation import ( # noqa: E402 +from pptx.oxml.presentation import ( # noqa: E402 CT_Presentation, CT_SlideId, CT_SlideIdList, @@ -295,7 +289,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:sldSz", CT_SlideSize) -from .shapes.autoshape import ( # noqa: E402 +from pptx.oxml.shapes.autoshape import ( # noqa: E402 CT_AdjPoint2D, CT_CustomGeometry2D, CT_GeomGuide, @@ -326,7 +320,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:sp", CT_Shape) -from .shapes.connector import ( # noqa: E402 +from pptx.oxml.shapes.connector import ( # noqa: E402 CT_Connection, CT_Connector, CT_ConnectorNonVisual, @@ -340,7 +334,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:nvCxnSpPr", CT_ConnectorNonVisual) -from .shapes.graphfrm import ( # noqa: E402 +from pptx.oxml.shapes.graphfrm import ( # noqa: E402 CT_GraphicalObject, CT_GraphicalObjectData, CT_GraphicalObjectFrame, @@ -355,7 +349,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:oleObj", CT_OleObject) -from .shapes.groupshape import ( # noqa: E402 +from pptx.oxml.shapes.groupshape import ( # noqa: E402 CT_GroupShape, CT_GroupShapeNonVisual, CT_GroupShapeProperties, @@ -367,14 +361,14 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:spTree", CT_GroupShape) -from .shapes.picture import CT_Picture, CT_PictureNonVisual # noqa: E402 +from pptx.oxml.shapes.picture import CT_Picture, CT_PictureNonVisual # noqa: E402 register_element_cls("p:blipFill", CT_BlipFillProperties) register_element_cls("p:nvPicPr", CT_PictureNonVisual) register_element_cls("p:pic", CT_Picture) -from .shapes.shared import ( # noqa: E402 +from pptx.oxml.shapes.shared import ( # noqa: E402 CT_ApplicationNonVisualDrawingProps, CT_LineProperties, CT_NonVisualDrawingProps, @@ -399,7 +393,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:xfrm", CT_Transform2D) -from .slide import ( # noqa: E402 +from pptx.oxml.slide import ( # noqa: E402 CT_Background, CT_BackgroundProperties, CT_CommonSlideData, @@ -430,7 +424,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:video", CT_TLMediaNodeVideo) -from .table import ( # noqa: E402 +from pptx.oxml.table import ( # noqa: E402 CT_Table, CT_TableCell, CT_TableCellProperties, @@ -449,7 +443,7 @@ def register_element_cls(nsptagname, cls): register_element_cls("a:tr", CT_TableRow) -from .text import ( # noqa: E402 +from pptx.oxml.text import ( # noqa: E402 CT_RegularTextRun, CT_TextBody, CT_TextBodyProperties, @@ -487,6 +481,6 @@ def register_element_cls(nsptagname, cls): register_element_cls("p:txBody", CT_TextBody) -from .theme import CT_OfficeStyleSheet # noqa: E402 +from pptx.oxml.theme import CT_OfficeStyleSheet # noqa: E402 register_element_cls("a:theme", CT_OfficeStyleSheet) diff --git a/src/pptx/oxml/action.py b/src/pptx/oxml/action.py index baaeb923d..9b31a9e16 100644 --- a/src/pptx/oxml/action.py +++ b/src/pptx/oxml/action.py @@ -1,31 +1,26 @@ -# encoding: utf-8 +"""lxml custom element classes for text-related XML elements.""" -""" -lxml custom element classes for text-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import - -from .simpletypes import XsdString -from .xmlchemy import BaseOxmlElement, OptionalAttribute +from pptx.oxml.simpletypes import XsdString +from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute class CT_Hyperlink(BaseOxmlElement): - """ - Custom element class for elements. - """ + """Custom element class for elements.""" - rId = OptionalAttribute("r:id", XsdString) - action = OptionalAttribute("action", XsdString) + rId: str = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + action: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "action", XsdString + ) @property - def action_fields(self): - """ - A dictionary containing any key-value pairs present in the query - portion of the `ppaction://` URL in the action attribute. For example - `{'id':'0', 'return':'true'}` in - 'ppaction://customshow?id=0&return=true'. Returns an empty dictionary - if the URL contains no query string or if no action attribute is + def action_fields(self) -> dict[str, str]: + """Query portion of the `ppaction://` URL as dict. + + For example `{'id':'0', 'return':'true'}` in 'ppaction://customshow?id=0&return=true'. + + Returns an empty dict if the URL contains no query string or if no action attribute is present. """ url = self.action @@ -41,12 +36,11 @@ def action_fields(self): return dict([pair.split("=") for pair in key_value_pairs]) @property - def action_verb(self): - """ - The host portion of the `ppaction://` URL contained in the action - attribute. For example 'customshow' in - 'ppaction://customshow?id=0&return=true'. Returns |None| if no action - attribute is present. + def action_verb(self) -> str | None: + """The host portion of the `ppaction://` URL contained in the action attribute. + + For example 'customshow' in 'ppaction://customshow?id=0&return=true'. Returns |None| if no + action attribute is present. """ url = self.action diff --git a/src/pptx/oxml/chart/axis.py b/src/pptx/oxml/chart/axis.py index c59d2440a..7129810c9 100644 --- a/src/pptx/oxml/chart/axis.py +++ b/src/pptx/oxml/chart/axis.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Axis-related oxml objects.""" -from __future__ import unicode_literals +from __future__ import annotations from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK from pptx.oxml.chart.shared import CT_Title diff --git a/src/pptx/oxml/chart/chart.py b/src/pptx/oxml/chart/chart.py index 65a0191e7..f4cd0dc7c 100644 --- a/src/pptx/oxml/chart/chart.py +++ b/src/pptx/oxml/chart/chart.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Custom element classes for top-level chart-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import cast from pptx.oxml import parse_xml from pptx.oxml.chart.shared import CT_Title @@ -40,9 +40,7 @@ class CT_Chart(BaseOxmlElement): autoTitleDeleted = ZeroOrOne("c:autoTitleDeleted", successors=_tag_seq[2:]) plotArea = OneAndOnlyOne("c:plotArea") legend = ZeroOrOne("c:legend", successors=_tag_seq[9:]) - rId = RequiredAttribute("r:id", XsdString) - - _chart_tmpl = '' % (nsdecls("c"), nsdecls("r")) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] @property def has_legend(self): @@ -69,13 +67,9 @@ def has_legend(self, bool_value): self._add_legend() @staticmethod - def new_chart(rId): - """ - Return a new ```` element - """ - xml = CT_Chart._chart_tmpl % (rId) - chart = parse_xml(xml) - return chart + def new_chart(rId: str) -> CT_Chart: + """Return a new `c:chart` element.""" + return cast(CT_Chart, parse_xml(f'')) def _new_title(self): return CT_Title.new_title() diff --git a/src/pptx/oxml/chart/datalabel.py b/src/pptx/oxml/chart/datalabel.py index 091693919..b6aac2fd5 100644 --- a/src/pptx/oxml/chart/datalabel.py +++ b/src/pptx/oxml/chart/datalabel.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Chart data-label related oxml objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.enum.chart import XL_DATA_LABEL_POSITION from pptx.oxml import parse_xml diff --git a/src/pptx/oxml/chart/legend.py b/src/pptx/oxml/chart/legend.py index 7a2eadb8e..196ca15de 100644 --- a/src/pptx/oxml/chart/legend.py +++ b/src/pptx/oxml/chart/legend.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""lxml custom element classes for legend-related XML elements.""" -""" -lxml custom element classes for legend-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from ...enum.chart import XL_LEGEND_POSITION -from ..text import CT_TextBody -from ..xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne +from pptx.enum.chart import XL_LEGEND_POSITION +from pptx.oxml.text import CT_TextBody +from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne class CT_Legend(BaseOxmlElement): diff --git a/src/pptx/oxml/chart/marker.py b/src/pptx/oxml/chart/marker.py index e849e3be2..34afd13d5 100644 --- a/src/pptx/oxml/chart/marker.py +++ b/src/pptx/oxml/chart/marker.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""Series-related oxml objects.""" -""" -Series-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals - -from ...enum.chart import XL_MARKER_STYLE -from ..simpletypes import ST_MarkerSize -from ..xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne +from pptx.enum.chart import XL_MARKER_STYLE +from pptx.oxml.simpletypes import ST_MarkerSize +from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne class CT_Marker(BaseOxmlElement): diff --git a/src/pptx/oxml/chart/plot.py b/src/pptx/oxml/chart/plot.py index f917913df..9c695a43a 100644 --- a/src/pptx/oxml/chart/plot.py +++ b/src/pptx/oxml/chart/plot.py @@ -1,25 +1,21 @@ -# encoding: utf-8 +"""Plot-related oxml objects.""" -""" -Plot-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from .datalabel import CT_DLbls -from ..simpletypes import ( +from pptx.oxml.chart.datalabel import CT_DLbls +from pptx.oxml.simpletypes import ( ST_BarDir, ST_BubbleScale, ST_GapAmount, ST_Grouping, ST_Overlap, ) -from ..xmlchemy import ( +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, - ZeroOrOne, ZeroOrMore, + ZeroOrOne, ) diff --git a/src/pptx/oxml/chart/series.py b/src/pptx/oxml/chart/series.py index 2974a2269..9264d552d 100644 --- a/src/pptx/oxml/chart/series.py +++ b/src/pptx/oxml/chart/series.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""Series-related oxml objects.""" -""" -Series-related oxml objects. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals - -from .datalabel import CT_DLbls -from ..simpletypes import XsdUnsignedInt -from ..xmlchemy import ( +from pptx.oxml.chart.datalabel import CT_DLbls +from pptx.oxml.simpletypes import XsdUnsignedInt +from pptx.oxml.xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OxmlElement, diff --git a/src/pptx/oxml/chart/shared.py b/src/pptx/oxml/chart/shared.py index ddea5132c..5515aa4be 100644 --- a/src/pptx/oxml/chart/shared.py +++ b/src/pptx/oxml/chart/shared.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Shared oxml objects for charts.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.oxml import parse_xml from pptx.oxml.ns import nsdecls @@ -186,10 +184,7 @@ def tx_rich(self): def new_title(): """Return "loose" `c:title` element containing default children.""" return parse_xml( - "" - " " - ' ' - "" % nsdecls("c") + "" " " ' ' "" % nsdecls("c") ) diff --git a/src/pptx/oxml/coreprops.py b/src/pptx/oxml/coreprops.py index 2993e88bc..de6b26b24 100644 --- a/src/pptx/oxml/coreprops.py +++ b/src/pptx/oxml/coreprops.py @@ -1,30 +1,29 @@ -# encoding: utf-8 +"""lxml custom element classes for core properties-related XML elements.""" -""" -lxml custom element classes for core properties-related XML elements. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations +import datetime as dt import re +from typing import Callable, cast -from datetime import datetime, timedelta +from lxml.etree import _Element # pyright: ignore[reportPrivateUsage] -from pptx.compat import to_unicode -from . import parse_xml -from .ns import nsdecls, qn -from .xmlchemy import BaseOxmlElement, ZeroOrOne +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls, qn +from pptx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne class CT_CoreProperties(BaseOxmlElement): - """ - ```` element, the root element of the Core Properties - part stored as ``/docProps/core.xml``. Implements many of the Dublin Core - document metadata elements. String elements resolve to an empty string - ('') if the element is not present in the XML. String elements are - limited in length to 255 unicode characters. + """`cp:coreProperties` element. + + The root element of the Core Properties part stored as `/docProps/core.xml`. Implements many + of the Dublin Core document metadata elements. String elements resolve to an empty string ('') + if the element is not present in the XML. String elements are limited in length to 255 unicode + characters. """ + get_or_add_revision: Callable[[], _Element] + category = ZeroOrOne("cp:category", successors=()) contentStatus = ZeroOrOne("cp:contentStatus", successors=()) created = ZeroOrOne("dcterms:created", successors=()) @@ -36,7 +35,9 @@ class CT_CoreProperties(BaseOxmlElement): lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=()) lastPrinted = ZeroOrOne("cp:lastPrinted", successors=()) modified = ZeroOrOne("dcterms:modified", successors=()) - revision = ZeroOrOne("cp:revision", successors=()) + revision: _Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "cp:revision", successors=() + ) subject = ZeroOrOne("dc:subject", successors=()) title = ZeroOrOne("dc:title", successors=()) version = ZeroOrOne("cp:version", successors=()) @@ -44,42 +45,40 @@ class CT_CoreProperties(BaseOxmlElement): _coreProperties_tmpl = "\n" % nsdecls("cp", "dc", "dcterms") @staticmethod - def new_coreProperties(): - """Return a new ```` element""" - xml = CT_CoreProperties._coreProperties_tmpl - coreProperties = parse_xml(xml) - return coreProperties + def new_coreProperties() -> CT_CoreProperties: + """Return a new `cp:coreProperties` element""" + return cast(CT_CoreProperties, parse_xml(CT_CoreProperties._coreProperties_tmpl)) @property - def author_text(self): + def author_text(self) -> str: return self._text_of_element("creator") @author_text.setter - def author_text(self, value): + def author_text(self, value: str): self._set_element_text("creator", value) @property - def category_text(self): + def category_text(self) -> str: return self._text_of_element("category") @category_text.setter - def category_text(self, value): + def category_text(self, value: str): self._set_element_text("category", value) @property - def comments_text(self): + def comments_text(self) -> str: return self._text_of_element("description") @comments_text.setter - def comments_text(self, value): + def comments_text(self, value: str): self._set_element_text("description", value) @property - def contentStatus_text(self): + def contentStatus_text(self) -> str: return self._text_of_element("contentStatus") @contentStatus_text.setter - def contentStatus_text(self, value): + def contentStatus_text(self, value: str): self._set_element_text("contentStatus", value) @property @@ -87,39 +86,39 @@ def created_datetime(self): return self._datetime_of_element("created") @created_datetime.setter - def created_datetime(self, value): + def created_datetime(self, value: dt.datetime): self._set_element_datetime("created", value) @property - def identifier_text(self): + def identifier_text(self) -> str: return self._text_of_element("identifier") @identifier_text.setter - def identifier_text(self, value): + def identifier_text(self, value: str): self._set_element_text("identifier", value) @property - def keywords_text(self): + def keywords_text(self) -> str: return self._text_of_element("keywords") @keywords_text.setter - def keywords_text(self, value): + def keywords_text(self, value: str): self._set_element_text("keywords", value) @property - def language_text(self): + def language_text(self) -> str: return self._text_of_element("language") @language_text.setter - def language_text(self, value): + def language_text(self, value: str): self._set_element_text("language", value) @property - def lastModifiedBy_text(self): + def lastModifiedBy_text(self) -> str: return self._text_of_element("lastModifiedBy") @lastModifiedBy_text.setter - def lastModifiedBy_text(self, value): + def lastModifiedBy_text(self, value: str): self._set_element_text("lastModifiedBy", value) @property @@ -127,7 +126,7 @@ def lastPrinted_datetime(self): return self._datetime_of_element("lastPrinted") @lastPrinted_datetime.setter - def lastPrinted_datetime(self, value): + def lastPrinted_datetime(self, value: dt.datetime): self._set_element_datetime("lastPrinted", value) @property @@ -135,104 +134,101 @@ def modified_datetime(self): return self._datetime_of_element("modified") @modified_datetime.setter - def modified_datetime(self, value): + def modified_datetime(self, value: dt.datetime): self._set_element_datetime("modified", value) @property - def revision_number(self): - """ - Integer value of revision property. - """ + def revision_number(self) -> int: + """Integer value of revision property.""" revision = self.revision if revision is None: return 0 revision_str = revision.text + if revision_str is None: + return 0 try: revision = int(revision_str) except ValueError: - # non-integer revision strings also resolve to 0 - revision = 0 - # as do negative integers + # -- non-integer revision strings also resolve to 0 -- + return 0 + # -- as do negative integers -- if revision < 0: - revision = 0 + return 0 return revision @revision_number.setter - def revision_number(self, value): - """ - Set revision property to string value of integer *value*. - """ - if not isinstance(value, int) or value < 1: + def revision_number(self, value: int): + """Set revision property to string value of integer `value`.""" + if not isinstance(value, int) or value < 1: # pyright: ignore[reportUnnecessaryIsInstance] tmpl = "revision property requires positive int, got '%s'" raise ValueError(tmpl % value) revision = self.get_or_add_revision() revision.text = str(value) @property - def subject_text(self): + def subject_text(self) -> str: return self._text_of_element("subject") @subject_text.setter - def subject_text(self, value): + def subject_text(self, value: str): self._set_element_text("subject", value) @property - def title_text(self): + def title_text(self) -> str: return self._text_of_element("title") @title_text.setter - def title_text(self, value): + def title_text(self, value: str): self._set_element_text("title", value) @property - def version_text(self): + def version_text(self) -> str: return self._text_of_element("version") @version_text.setter - def version_text(self, value): + def version_text(self, value: str): self._set_element_text("version", value) - def _datetime_of_element(self, property_name): - element = getattr(self, property_name) + def _datetime_of_element(self, property_name: str) -> dt.datetime | None: + element = cast("_Element | None", getattr(self, property_name)) if element is None: return None datetime_str = element.text + if datetime_str is None: + return None try: return self._parse_W3CDTF_to_datetime(datetime_str) except ValueError: # invalid datetime strings are ignored return None - def _get_or_add(self, prop_name): - """ - Return element returned by 'get_or_add_' method for *prop_name*. - """ + def _get_or_add(self, prop_name: str): + """Return element returned by 'get_or_add_' method for `prop_name`.""" get_or_add_method_name = "get_or_add_%s" % prop_name get_or_add_method = getattr(self, get_or_add_method_name) element = get_or_add_method() return element @classmethod - def _offset_dt(cls, dt, offset_str): - """ - Return a |datetime| instance that is offset from datetime *dt* by - the timezone offset specified in *offset_str*, a string like - ``'-07:00'``. + def _offset_dt(cls, datetime: dt.datetime, offset_str: str): + """Return |datetime| instance offset from `datetime` by offset specified in `offset_str`. + + `offset_str` is a string like `'-07:00'`. """ match = cls._offset_pattern.match(offset_str) if match is None: - raise ValueError("'%s' is not a valid offset string" % offset_str) + raise ValueError(f"{repr(offset_str)} is not a valid offset string") sign, hours_str, minutes_str = match.groups() sign_factor = -1 if sign == "+" else 1 hours = int(hours_str) * sign_factor minutes = int(minutes_str) * sign_factor - td = timedelta(hours=hours, minutes=minutes) - return dt + td + td = dt.timedelta(hours=hours, minutes=minutes) + return datetime + td _offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)") @classmethod - def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): + def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime: # valid W3CDTF date cases: # yyyy e.g. '2003' # yyyy-mm e.g. '2003-12' @@ -244,24 +240,22 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): # '-07:30', so we have to do it ourselves parseable_part = w3cdtf_str[:19] offset_str = w3cdtf_str[19:] - dt = None + timestamp = None for tmpl in templates: try: - dt = datetime.strptime(parseable_part, tmpl) + timestamp = dt.datetime.strptime(parseable_part, tmpl) except ValueError: continue - if dt is None: + if timestamp is None: tmpl = "could not parse W3CDTF datetime string '%s'" raise ValueError(tmpl % w3cdtf_str) if len(offset_str) == 6: - return cls._offset_dt(dt, offset_str) - return dt + return cls._offset_dt(timestamp, offset_str) + return timestamp - def _set_element_datetime(self, prop_name, value): - """ - Set date/time value of child element having *prop_name* to *value*. - """ - if not isinstance(value, datetime): + def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None: + """Set date/time value of child element having `prop_name` to `value`.""" + if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance] tmpl = "property requires object, got %s" raise ValueError(tmpl % type(value)) element = self._get_or_add(prop_name) @@ -276,16 +270,16 @@ def _set_element_datetime(self, prop_name, value): element.set(qn("xsi:type"), "dcterms:W3CDTF") del self.attrib[qn("xsi:foo")] - def _set_element_text(self, prop_name, value): - """Set string value of *name* property to *value*.""" - value = to_unicode(value) + def _set_element_text(self, prop_name: str, value: str) -> None: + """Set string value of `name` property to `value`.""" + value = str(value) if len(value) > 255: tmpl = "exceeded 255 char limit for property, got:\n\n'%s'" raise ValueError(tmpl % value) element = self._get_or_add(prop_name) element.text = value - def _text_of_element(self, property_name): + def _text_of_element(self, property_name: str) -> str: element = getattr(self, property_name) if element is None: return "" diff --git a/src/pptx/oxml/dml/color.py b/src/pptx/oxml/dml/color.py index 4aa796d5b..dfce90aa0 100644 --- a/src/pptx/oxml/dml/color.py +++ b/src/pptx/oxml/dml/color.py @@ -1,14 +1,10 @@ -# encoding: utf-8 +"""lxml custom element classes for DrawingML-related XML elements.""" -""" -lxml custom element classes for DrawingML-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import - -from ...enum.dml import MSO_THEME_COLOR -from ..simpletypes import ST_HexColorRGB, ST_Percentage -from ..xmlchemy import ( +from pptx.enum.dml import MSO_THEME_COLOR +from pptx.oxml.simpletypes import ST_HexColorRGB, ST_Percentage +from pptx.oxml.xmlchemy import ( BaseOxmlElement, Choice, RequiredAttribute, diff --git a/src/pptx/oxml/dml/fill.py b/src/pptx/oxml/dml/fill.py index a7b688a3e..2ff2255d7 100644 --- a/src/pptx/oxml/dml/fill.py +++ b/src/pptx/oxml/dml/fill.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""lxml custom element classes for DrawingML-related XML elements.""" -""" -lxml custom element classes for DrawingML-related XML elements. -""" - -from __future__ import absolute_import +from __future__ import annotations from pptx.enum.dml import MSO_PATTERN_TYPE from pptx.oxml import parse_xml @@ -165,17 +161,13 @@ class CT_PatternFillProperties(BaseOxmlElement): def _new_bgClr(self): """Override default to add minimum subtree.""" - xml = ( - "\n" ' \n' "\n" - ) % nsdecls("a") + xml = ("\n" ' \n' "\n") % nsdecls("a") bgClr = parse_xml(xml) return bgClr def _new_fgClr(self): """Override default to add minimum subtree.""" - xml = ( - "\n" ' \n' "\n" - ) % nsdecls("a") + xml = ("\n" ' \n' "\n") % nsdecls("a") fgClr = parse_xml(xml) return fgClr diff --git a/src/pptx/oxml/dml/line.py b/src/pptx/oxml/dml/line.py index 02d4e59c2..720ca8e07 100644 --- a/src/pptx/oxml/dml/line.py +++ b/src/pptx/oxml/dml/line.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """lxml custom element classes for DrawingML line-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.enum.dml import MSO_LINE_DASH_STYLE from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute diff --git a/src/pptx/oxml/ns.py b/src/pptx/oxml/ns.py index f83c1cd0b..d900c33bf 100644 --- a/src/pptx/oxml/ns.py +++ b/src/pptx/oxml/ns.py @@ -1,66 +1,57 @@ -# encoding: utf-8 +"""Namespace related objects.""" -""" -Namespace related objects. -""" +from __future__ import annotations -from __future__ import absolute_import - -#: Maps namespace prefix to namespace name for all known PowerPoint XML -#: namespaces. +# -- Maps namespace prefix to namespace name for all known PowerPoint XML namespaces -- _nsmap = { - "a": ("http://schemas.openxmlformats.org/drawingml/2006/main"), - "c": ("http://schemas.openxmlformats.org/drawingml/2006/chart"), - "cp": ( - "http://schemas.openxmlformats.org/package/2006/metadata/core-pro" "perties" - ), - "ct": ("http://schemas.openxmlformats.org/package/2006/content-types"), - "dc": ("http://purl.org/dc/elements/1.1/"), - "dcmitype": ("http://purl.org/dc/dcmitype/"), - "dcterms": ("http://purl.org/dc/terms/"), - "ep": ( - "http://schemas.openxmlformats.org/officeDocument/2006/extended-p" "roperties" - ), - "i": ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationsh" "ips/image" - ), - "m": ("http://schemas.openxmlformats.org/officeDocument/2006/math"), - "mo": ("http://schemas.microsoft.com/office/mac/office/2008/main"), - "mv": ("urn:schemas-microsoft-com:mac:vml"), - "o": ("urn:schemas-microsoft-com:office:office"), - "p": ("http://schemas.openxmlformats.org/presentationml/2006/main"), - "pd": ("http://schemas.openxmlformats.org/drawingml/2006/presentationDra" "wing"), - "pic": ("http://schemas.openxmlformats.org/drawingml/2006/picture"), - "pr": ("http://schemas.openxmlformats.org/package/2006/relationships"), - "r": ("http://schemas.openxmlformats.org/officeDocument/2006/relationsh" "ips"), - "sl": ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationsh" - "ips/slideLayout" - ), - "v": ("urn:schemas-microsoft-com:vml"), - "ve": ("http://schemas.openxmlformats.org/markup-compatibility/2006"), - "w": ("http://schemas.openxmlformats.org/wordprocessingml/2006/main"), - "w10": ("urn:schemas-microsoft-com:office:word"), - "wne": ("http://schemas.microsoft.com/office/word/2006/wordml"), - "wp": ("http://schemas.openxmlformats.org/drawingml/2006/wordprocessingD" "rawing"), - "xsi": ("http://www.w3.org/2001/XMLSchema-instance"), + "a": "http://schemas.openxmlformats.org/drawingml/2006/main", + "c": "http://schemas.openxmlformats.org/drawingml/2006/chart", + "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", + "ct": "http://schemas.openxmlformats.org/package/2006/content-types", + "dc": "http://purl.org/dc/elements/1.1/", + "dcmitype": "http://purl.org/dc/dcmitype/", + "dcterms": "http://purl.org/dc/terms/", + "ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties", + "i": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + "m": "http://schemas.openxmlformats.org/officeDocument/2006/math", + "mo": "http://schemas.microsoft.com/office/mac/office/2008/main", + "mv": "urn:schemas-microsoft-com:mac:vml", + "o": "urn:schemas-microsoft-com:office:office", + "p": "http://schemas.openxmlformats.org/presentationml/2006/main", + "pd": "http://schemas.openxmlformats.org/drawingml/2006/presentationDrawing", + "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", + "pr": "http://schemas.openxmlformats.org/package/2006/relationships", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "sl": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout", + "v": "urn:schemas-microsoft-com:vml", + "ve": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "w10": "urn:schemas-microsoft-com:office:word", + "wne": "http://schemas.microsoft.com/office/word/2006/wordml", + "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", } +pfxmap = {value: key for key, value in _nsmap.items()} + class NamespacePrefixedTag(str): - """ - Value object that knows the semantics of an XML tag having a namespace - prefix. - """ + """Value object that knows the semantics of an XML tag having a namespace prefix.""" - def __new__(cls, nstag, *args): + def __new__(cls, nstag: str): return super(NamespacePrefixedTag, cls).__new__(cls, nstag) - def __init__(self, nstag): + def __init__(self, nstag: str): self._pfx, self._local_part = nstag.split(":") self._ns_uri = _nsmap[self._pfx] + @classmethod + def from_clark_name(cls, clark_name: str) -> NamespacePrefixedTag: + nsuri, local_name = clark_name[1:].split("}") + nstag = "%s:%s" % (pfxmap[nsuri], local_name) + return cls(nstag) + @property def clark_name(self): return "{%s}%s" % (self._ns_uri, self._local_part) @@ -100,40 +91,39 @@ def nsuri(self): return self._ns_uri -def namespaces(*prefixes): - """ - Return a dict containing the subset namespace prefix mappings specified by - *prefixes*. Any number of namespace prefixes can be supplied, e.g. - namespaces('a', 'r', 'p'). +def namespaces(*prefixes: str): + """Return a dict containing the subset namespace prefix mappings specified by *prefixes*. + + Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p'). """ - namespaces = {} - for prefix in prefixes: - namespaces[prefix] = _nsmap[prefix] - return namespaces + return {pfx: _nsmap[pfx] for pfx in prefixes} nsmap = namespaces # alias for more compact use with Element() -def nsdecls(*prefixes): +def nsdecls(*prefixes: str): return " ".join(['xmlns:%s="%s"' % (pfx, _nsmap[pfx]) for pfx in prefixes]) -def nsuri(nspfx): - """ - Return the namespace URI corresponding to *nspfx*. For example, it would - return 'http://foo/bar' for an *nspfx* of 'f' if the 'f' prefix maps to - 'http://foo/bar' in _nsmap. +def nsuri(nspfx: str): + """Return the namespace URI corresponding to `nspfx`. + + Example: + + >>> nsuri("p") + "http://schemas.openxmlformats.org/presentationml/2006/main" """ return _nsmap[nspfx] -def qn(namespace_prefixed_tag): - """ - Return a Clark-notation qualified tag name corresponding to - *namespace_prefixed_tag*, a string like 'p:body'. 'qn' stands for - *qualified name*. As an example, ``qn('p:cSld')`` returns - ``'{http://schemas.../main}cSld'``. +def qn(namespace_prefixed_tag: str) -> str: + """Return a Clark-notation qualified tag name corresponding to `namespace_prefixed_tag`. + + `namespace_prefixed_tag` is a string like 'p:body'. 'qn' stands for `qualified name`. + + As an example, `qn("p:cSld")` returns: + `"{http://schemas.openxmlformats.org/drawingml/2006/main}cSld"`. """ nsptag = NamespacePrefixedTag(namespace_prefixed_tag) return nsptag.clark_name diff --git a/src/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py index 17616cb4f..12c6751f1 100644 --- a/src/pptx/oxml/presentation.py +++ b/src/pptx/oxml/presentation.py @@ -1,78 +1,91 @@ -# encoding: utf-8 +"""Custom element classes for presentation-related XML elements.""" -""" -Custom element classes for presentation-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import TYPE_CHECKING, Callable -from .simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString -from .xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne, ZeroOrMore +from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString +from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne +if TYPE_CHECKING: + from pptx.util import Length -class CT_Presentation(BaseOxmlElement): - """ - ```` element, root of the Presentation part stored as - ``/ppt/presentation.xml``. - """ - sldMasterIdLst = ZeroOrOne( - "p:sldMasterIdLst", - successors=( - "p:notesMasterIdLst", - "p:handoutMasterIdLst", - "p:sldIdLst", - "p:sldSz", - "p:notesSz", - ), +class CT_Presentation(BaseOxmlElement): + """`p:presentation` element, root of the Presentation part stored as `/ppt/presentation.xml`.""" + + get_or_add_sldSz: Callable[[], CT_SlideSize] + get_or_add_sldIdLst: Callable[[], CT_SlideIdList] + get_or_add_sldMasterIdLst: Callable[[], CT_SlideMasterIdList] + + sldMasterIdLst: CT_SlideMasterIdList | None = ( + ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldMasterIdLst", + successors=( + "p:notesMasterIdLst", + "p:handoutMasterIdLst", + "p:sldIdLst", + "p:sldSz", + "p:notesSz", + ), + ) + ) + sldIdLst: CT_SlideIdList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldIdLst", successors=("p:sldSz", "p:notesSz") + ) + sldSz: CT_SlideSize | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldSz", successors=("p:notesSz",) ) - sldIdLst = ZeroOrOne("p:sldIdLst", successors=("p:sldSz", "p:notesSz")) - sldSz = ZeroOrOne("p:sldSz", successors=("p:notesSz",)) class CT_SlideId(BaseOxmlElement): - """ - ```` element, direct child of that contains an rId - reference to a slide in the presentation. + """`p:sldId` element. + + Direct child of `p:sldIdLst` that contains an `rId` reference to a slide in the presentation. """ - id = RequiredAttribute("id", ST_SlideId) - rId = RequiredAttribute("r:id", XsdString) + id: int = RequiredAttribute("id", ST_SlideId) # pyright: ignore[reportAssignmentType] + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] class CT_SlideIdList(BaseOxmlElement): + """`p:sldIdLst` element. + + Direct child of that contains a list of the slide parts in the presentation. """ - ```` element, direct child of that contains - a list of the slide parts in the presentation. - """ + sldId_lst: list[CT_SlideId] + + _add_sldId: Callable[..., CT_SlideId] sldId = ZeroOrMore("p:sldId") - def add_sldId(self, rId): - """ - Return a reference to a newly created child element having - its r:id attribute set to *rId*. + def add_sldId(self, rId: str) -> CT_SlideId: + """Create and return a reference to a new `p:sldId` child element. + + The new `p:sldId` element has its r:id attribute set to `rId`. """ return self._add_sldId(id=self._next_id, rId=rId) @property def _next_id(self): - """ - Return the next available slide ID as an int. Valid slide IDs start - at 256. The next integer value greater than the max value in use is - chosen, which minimizes that chance of reusing the id of a deleted - slide. + """The next available slide ID as an `int`. + + Valid slide IDs start at 256. The next integer value greater than the max value in use is + chosen, which minimizes that chance of reusing the id of a deleted slide. """ id_str_lst = self.xpath("./p:sldId/@id") return max([255] + [int(id_str) for id_str in id_str_lst]) + 1 class CT_SlideMasterIdList(BaseOxmlElement): - """ - ```` element, child of ```` containing - references to the slide masters that belong to the presentation. + """`p:sldMasterIdLst` element. + + Child of `p:presentation` containing references to the slide masters that belong to the + presentation. """ + sldMasterId_lst: list[CT_SlideMasterIdListEntry] + sldMasterId = ZeroOrMore("p:sldMasterId") @@ -82,14 +95,19 @@ class CT_SlideMasterIdListEntry(BaseOxmlElement): a reference to a slide master. """ - rId = RequiredAttribute("r:id", XsdString) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] class CT_SlideSize(BaseOxmlElement): - """ - ```` element, direct child of that contains the - width and height of slides in the presentation. + """`p:sldSz` element. + + Direct child of that contains the width and height of slides in the + presentation. """ - cx = RequiredAttribute("cx", ST_SlideSizeCoordinate) - cy = RequiredAttribute("cy", ST_SlideSizeCoordinate) + cx: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "cx", ST_SlideSizeCoordinate + ) + cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "cy", ST_SlideSizeCoordinate + ) diff --git a/src/pptx/oxml/shapes/__init__.py b/src/pptx/oxml/shapes/__init__.py index e69de29bb..37f8ef60e 100644 --- a/src/pptx/oxml/shapes/__init__.py +++ b/src/pptx/oxml/shapes/__init__.py @@ -0,0 +1,19 @@ +"""Base shape-related objects such as BaseShape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from pptx.oxml.shapes.autoshape import CT_Shape + from pptx.oxml.shapes.connector import CT_Connector + from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame + from pptx.oxml.shapes.groupshape import CT_GroupShape + from pptx.oxml.shapes.picture import CT_Picture + + +ShapeElement: TypeAlias = ( + "CT_Connector | CT_GraphicalObjectFrame | CT_GroupShape | CT_Picture | CT_Shape" +) diff --git a/src/pptx/oxml/shapes/autoshape.py b/src/pptx/oxml/shapes/autoshape.py index 3da31d132..5d78f624f 100644 --- a/src/pptx/oxml/shapes/autoshape.py +++ b/src/pptx/oxml/shapes/autoshape.py @@ -1,10 +1,10 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -lxml custom element classes for shape-related XML elements. -""" +"""lxml custom element classes for shape-related XML elements.""" -from __future__ import absolute_import +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.oxml import parse_xml @@ -22,69 +22,91 @@ OneAndOnlyOne, OptionalAttribute, RequiredAttribute, - ZeroOrOne, ZeroOrMore, + ZeroOrOne, ) +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import ( + CT_ApplicationNonVisualDrawingProps, + CT_NonVisualDrawingProps, + CT_ShapeProperties, + ) + from pptx.util import Length + class CT_AdjPoint2D(BaseOxmlElement): """`a:pt` custom element class.""" - x = RequiredAttribute("x", ST_Coordinate) - y = RequiredAttribute("y", ST_Coordinate) + x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType] + y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType] class CT_CustomGeometry2D(BaseOxmlElement): """`a:custGeom` custom element class.""" + get_or_add_pathLst: Callable[[], CT_Path2DList] + _tag_seq = ("a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst", "a:rect", "a:pathLst") - pathLst = ZeroOrOne("a:pathLst", successors=_tag_seq[6:]) + pathLst: CT_Path2DList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:pathLst", successors=_tag_seq[6:] + ) class CT_GeomGuide(BaseOxmlElement): - """ - ```` custom element class, defining a "guide", corresponding to - a yellow diamond-shaped handle on an autoshape. + """`a:gd` custom element class. + + Defines a "guide", corresponding to a yellow diamond-shaped handle on an autoshape. """ - name = RequiredAttribute("name", XsdString) - fmla = RequiredAttribute("fmla", XsdString) + name: str = RequiredAttribute("name", XsdString) # pyright: ignore[reportAssignmentType] + fmla: str = RequiredAttribute("fmla", XsdString) # pyright: ignore[reportAssignmentType] class CT_GeomGuideList(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:avLst` custom element class.""" + + _add_gd: Callable[[], CT_GeomGuide] + + gd_lst: list[CT_GeomGuide] gd = ZeroOrMore("a:gd") class CT_NonVisualDrawingShapeProps(BaseShapeElement): - """ - ```` custom element class - """ + """`p:cNvSpPr` custom element class.""" spLocks = ZeroOrOne("a:spLocks") - txBox = OptionalAttribute("txBox", XsdBoolean) + txBox: bool | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "txBox", XsdBoolean + ) class CT_Path2D(BaseOxmlElement): """`a:path` custom element class.""" + _add_close: Callable[[], CT_Path2DClose] + _add_lnTo: Callable[[], CT_Path2DLineTo] + _add_moveTo: Callable[[], CT_Path2DMoveTo] + close = ZeroOrMore("a:close", successors=()) lnTo = ZeroOrMore("a:lnTo", successors=()) moveTo = ZeroOrMore("a:moveTo", successors=()) - w = OptionalAttribute("w", ST_PositiveCoordinate) - h = OptionalAttribute("h", ST_PositiveCoordinate) - - def add_close(self): + w: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "w", ST_PositiveCoordinate + ) + h: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "h", ST_PositiveCoordinate + ) + + def add_close(self) -> CT_Path2DClose: """Return a newly created `a:close` element. The new `a:close` element is appended to this `a:path` element. """ return self._add_close() - def add_lnTo(self, x, y): + def add_lnTo(self, x: Length, y: Length) -> CT_Path2DLineTo: """Return a newly created `a:lnTo` subtree with end point *(x, y)*. The new `a:lnTo` element is appended to this `a:path` element. @@ -94,8 +116,8 @@ def add_lnTo(self, x, y): pt.x, pt.y = x, y return lnTo - def add_moveTo(self, x, y): - """Return a newly created `a:moveTo` subtree with point *(x, y)*. + def add_moveTo(self, x: Length, y: Length): + """Return a newly created `a:moveTo` subtree with point `(x, y)`. The new `a:moveTo` element is appended to this `a:path` element. """ @@ -112,15 +134,19 @@ class CT_Path2DClose(BaseOxmlElement): class CT_Path2DLineTo(BaseOxmlElement): """`a:lnTo` custom element class.""" + _add_pt: Callable[[], CT_AdjPoint2D] + pt = ZeroOrOne("a:pt", successors=()) class CT_Path2DList(BaseOxmlElement): """`a:pathLst` custom element class.""" + _add_path: Callable[[], CT_Path2D] + path = ZeroOrMore("a:path", successors=()) - def add_path(self, w, h): + def add_path(self, w: Length, h: Length): """Return a newly created `a:path` child element.""" path = self._add_path() path.w, path.h = w, h @@ -130,33 +156,32 @@ def add_path(self, w, h): class CT_Path2DMoveTo(BaseOxmlElement): """`a:moveTo` custom element class.""" + _add_pt: Callable[[], CT_AdjPoint2D] + pt = ZeroOrOne("a:pt", successors=()) class CT_PresetGeometry2D(BaseOxmlElement): - """ - custom element class - """ + """`a:prstGeom` custom element class.""" - avLst = ZeroOrOne("a:avLst") - prst = RequiredAttribute("prst", MSO_AUTO_SHAPE_TYPE) + _add_avLst: Callable[[], CT_GeomGuideList] + _remove_avLst: Callable[[], None] + + avLst: CT_GeomGuideList | None = ZeroOrOne("a:avLst") # pyright: ignore[reportAssignmentType] + prst: MSO_AUTO_SHAPE_TYPE = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "prst", MSO_AUTO_SHAPE_TYPE + ) @property - def gd_lst(self): - """ - Sequence containing the ``gd`` element children of ```` - child element, empty if none are present. - """ + def gd_lst(self) -> list[CT_GeomGuide]: + """Sequence of `a:gd` element children of `a:avLst`. Empty if none are present.""" avLst = self.avLst if avLst is None: return [] return avLst.gd_lst - def rewrite_guides(self, guides): - """ - Remove any ```` element children of ```` and replace - them with ones having (name, val) in *guides*. - """ + def rewrite_guides(self, guides: list[tuple[str, int]]): + """Replace any `a:gd` element children of `a:avLst` with ones forme from `guides`.""" self._remove_avLst() avLst = self._add_avLst() for name, val in guides: @@ -166,16 +191,15 @@ def rewrite_guides(self, guides): class CT_Shape(BaseShapeElement): - """ - ```` custom element class - """ + """`p:sp` custom element class.""" + + get_or_add_txBody: Callable[[], CT_TextBody] - nvSpPr = OneAndOnlyOne("p:nvSpPr") - spPr = OneAndOnlyOne("p:spPr") - txBody = ZeroOrOne("p:txBody", successors=("p:extLst",)) + nvSpPr: CT_ShapeNonVisual = OneAndOnlyOne("p:nvSpPr") # pyright: ignore[reportAssignmentType] + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] + txBody: CT_TextBody | None = ZeroOrOne("p:txBody", successors=("p:extLst",)) # pyright: ignore - def add_path(self, w, h): - """Reference to `a:custGeom` descendant or |None| if not present.""" + def add_path(self, w: Length, h: Length) -> CT_Path2D: custGeom = self.spPr.custGeom if custGeom is None: raise ValueError("shape must be freeform") @@ -183,9 +207,7 @@ def add_path(self, w, h): return pathLst.add_path(w=w, h=h) def get_or_add_ln(self): - """ - Return the grandchild element, newly added if not present. - """ + """Return the `a:ln` grandchild element, newly added if not present.""" return self.spPr.get_or_add_ln() @property @@ -199,120 +221,36 @@ def has_custom_geometry(self): @property def is_autoshape(self): - """ - True if this shape is an auto shape. A shape is an auto shape if it - has a ```` element and does not have a txBox="1" attribute - on cNvSpPr. + """True if this shape is an auto shape. + + A shape is an auto shape if it has a `a:prstGeom` element and does not have a txBox="1" + attribute on cNvSpPr. """ prstGeom = self.prstGeom if prstGeom is None: return False - if self.nvSpPr.cNvSpPr.txBox is True: - return False - return True + return self.nvSpPr.cNvSpPr.txBox is not True @property def is_textbox(self): + """True if this shape is a text box. + + A shape is a text box if it has a `txBox` attribute on cNvSpPr that resolves to |True|. + The default when the txBox attribute is missing is |False|. """ - True if this shape is a text box. A shape is a text box if it has a - ``txBox`` attribute on cNvSpPr that resolves to |True|. The default - when the txBox attribute is missing is |False|. - """ - if self.nvSpPr.cNvSpPr.txBox is True: - return True - return False + return self.nvSpPr.cNvSpPr.txBox is True @property def ln(self): - """ - ```` grand-child element or |None| if not present - """ + """`a:ln` grand-child element or |None| if not present.""" return self.spPr.ln @staticmethod - def new_autoshape_sp(id_, name, prst, left, top, width, height): - """ - Return a new ```` element tree configured as a base auto shape. - """ - tmpl = CT_Shape._autoshape_sp_tmpl() - xml = tmpl % (id_, name, left, top, width, height, prst) - sp = parse_xml(xml) - return sp - - @staticmethod - def new_freeform_sp(shape_id, name, x, y, cx, cy): - """Return new `p:sp` element tree configured as freeform shape. - - The returned shape has a `a:custGeom` subtree but no paths in its - path list. - """ - tmpl = CT_Shape._freeform_sp_tmpl() - xml = tmpl % (shape_id, name, x, y, cx, cy) - sp = parse_xml(xml) - return sp - - @staticmethod - def new_placeholder_sp(id_, name, ph_type, orient, sz, idx): - """ - Return a new ```` element tree configured as a placeholder - shape. - """ - tmpl = CT_Shape._ph_sp_tmpl() - xml = tmpl % (id_, name) - sp = parse_xml(xml) - - ph = sp.nvSpPr.nvPr.get_or_add_ph() - ph.type = ph_type - ph.idx = idx - ph.orient = orient - ph.sz = sz - - placeholder_types_that_have_a_text_frame = ( - PP_PLACEHOLDER.TITLE, - PP_PLACEHOLDER.CENTER_TITLE, - PP_PLACEHOLDER.SUBTITLE, - PP_PLACEHOLDER.BODY, - PP_PLACEHOLDER.OBJECT, - ) - - if ph_type in placeholder_types_that_have_a_text_frame: - sp.append(CT_TextBody.new()) - - return sp - - @staticmethod - def new_textbox_sp(id_, name, left, top, width, height): - """ - Return a new ```` element tree configured as a base textbox - shape. - """ - tmpl = CT_Shape._textbox_sp_tmpl() - xml = tmpl % (id_, name, left, top, width, height) - sp = parse_xml(xml) - return sp - - @property - def prst(self): - """ - Value of ``prst`` attribute of ```` element or |None| if - not present. - """ - prstGeom = self.prstGeom - if prstGeom is None: - return None - return prstGeom.prst - - @property - def prstGeom(self): - """ - Reference to ```` child element or |None| if this shape - doesn't have one, for example, if it's a placeholder shape. - """ - return self.spPr.prstGeom - - @staticmethod - def _autoshape_sp_tmpl(): - return ( + def new_autoshape_sp( + id_: int, name: str, prst: str, left: int, top: int, width: int, height: int + ) -> CT_Shape: + """Return a new `p:sp` element tree configured as a base auto shape.""" + xml = ( "\n" " \n" ' \n' @@ -350,11 +288,17 @@ def _autoshape_sp_tmpl(): " \n" " \n" "" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d", "%s") - ) + ) % (id_, name, left, top, width, height, prst) + return cast(CT_Shape, parse_xml(xml)) @staticmethod - def _freeform_sp_tmpl(): - return ( + def new_freeform_sp(shape_id: int, name: str, x: int, y: int, cx: int, cy: int): + """Return new `p:sp` element tree configured as freeform shape. + + The returned shape has a `a:custGeom` subtree but no paths in its + path list. + """ + xml = ( "\n" " \n" ' \n' @@ -397,26 +341,76 @@ def _freeform_sp_tmpl(): " \n" " \n" "" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d") + ) % (shape_id, name, x, y, cx, cy) + return cast(CT_Shape, parse_xml(xml)) + + @staticmethod + def new_placeholder_sp( + id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz, idx + ) -> CT_Shape: + """Return a new `p:sp` element tree configured as a placeholder shape.""" + sp = cast( + CT_Shape, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), ) - def _new_txBody(self): - return CT_TextBody.new_p_txBody() + ph = sp.nvSpPr.nvPr.get_or_add_ph() + ph.type = ph_type + ph.idx = idx + ph.orient = orient + ph.sz = sz - @staticmethod - def _ph_sp_tmpl(): - return ( - "\n" - " \n" - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - "" % (nsdecls("a", "p"), "%d", "%s") + placeholder_types_that_have_a_text_frame = ( + PP_PLACEHOLDER.TITLE, + PP_PLACEHOLDER.CENTER_TITLE, + PP_PLACEHOLDER.SUBTITLE, + PP_PLACEHOLDER.BODY, + PP_PLACEHOLDER.OBJECT, ) + if ph_type in placeholder_types_that_have_a_text_frame: + sp.append(CT_TextBody.new()) + + return sp + + @staticmethod + def new_textbox_sp(id_, name, left, top, width, height): + """Return a new `p:sp` element tree configured as a base textbox shape.""" + tmpl = CT_Shape._textbox_sp_tmpl() + xml = tmpl % (id_, name, left, top, width, height) + sp = parse_xml(xml) + return sp + + @property + def prst(self): + """Value of `prst` attribute of `a:prstGeom` element or |None| if not present.""" + prstGeom = self.prstGeom + if prstGeom is None: + return None + return prstGeom.prst + + @property + def prstGeom(self) -> CT_PresetGeometry2D: + """Reference to `a:prstGeom` child element. + + |None| if this shape doesn't have one, for example, if it's a placeholder shape. + """ + return self.spPr.prstGeom + + def _new_txBody(self): + return CT_TextBody.new_p_txBody() + @staticmethod def _textbox_sp_tmpl(): return ( @@ -448,10 +442,14 @@ def _textbox_sp_tmpl(): class CT_ShapeNonVisual(BaseShapeElement): - """ - ```` custom element class - """ - - cNvPr = OneAndOnlyOne("p:cNvPr") - cNvSpPr = OneAndOnlyOne("p:cNvSpPr") - nvPr = OneAndOnlyOne("p:nvPr") + """`p:nvSpPr` custom element class.""" + + cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvPr" + ) + cNvSpPr: CT_NonVisualDrawingShapeProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvSpPr" + ) + nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvPr") + ) diff --git a/src/pptx/oxml/shapes/connector.py b/src/pptx/oxml/shapes/connector.py index ebbe1045f..91261f780 100644 --- a/src/pptx/oxml/shapes/connector.py +++ b/src/pptx/oxml/shapes/connector.py @@ -1,22 +1,23 @@ -# encoding: utf-8 +"""lxml custom element classes for XML elements related to the Connector shape.""" -""" -lxml custom element classes for shape-related XML elements. -""" +from __future__ import annotations -from __future__ import absolute_import +from typing import TYPE_CHECKING, cast -from .. import parse_xml -from ..ns import nsdecls -from .shared import BaseShapeElement -from ..simpletypes import ST_DrawingElementId, XsdUnsignedInt -from ..xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls +from pptx.oxml.shapes.shared import BaseShapeElement +from pptx.oxml.simpletypes import ST_DrawingElementId, XsdUnsignedInt +from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne + +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import CT_ShapeProperties class CT_Connection(BaseShapeElement): - """ - A `a:stCxn` or `a:endCxn` element specifying a connection between - an end-point of a connector and a shape connection point. + """A `a:stCxn` or `a:endCxn` element. + + Specifies a connection between an end-point of a connector and a shape connection point. """ id = RequiredAttribute("id", ST_DrawingElementId) @@ -24,77 +25,68 @@ class CT_Connection(BaseShapeElement): class CT_Connector(BaseShapeElement): - """ - A line/connector shape ```` element - """ + """A line/connector shape `p:cxnSp` element""" _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst") nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr") - spPr = OneAndOnlyOne("p:spPr") + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] del _tag_seq @classmethod - def new_cxnSp(cls, id_, name, prst, x, y, cx, cy, flipH, flipV): - """ - Return a new ```` element tree configured as a base - connector. - """ - tmpl = cls._cxnSp_tmpl() + def new_cxnSp( + cls, + id_: int, + name: str, + prst: str, + x: int, + y: int, + cx: int, + cy: int, + flipH: bool, + flipV: bool, + ) -> CT_Connector: + """Return a new `p:cxnSp` element tree configured as a base connector.""" flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "") - xml = tmpl.format( - **{ - "nsdecls": nsdecls("a", "p"), - "id": id_, - "name": name, - "x": x, - "y": y, - "cx": cx, - "cy": cy, - "prst": prst, - "flip": flip, - } - ) - return parse_xml(xml) - - @staticmethod - def _cxnSp_tmpl(): - return ( - "\n" - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - ' \n' - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - ' \n' - ' \n' - " \n" - ' \n' - ' \n' - " \n" - ' \n' - ' \n' - " \n" - ' \n' - ' \n' - " \n" - " \n" - "" + return cast( + CT_Connector, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f' \n' + f" \n" + f" \n" + f"" + ), ) class CT_ConnectorNonVisual(BaseOxmlElement): """ - ```` element, container for the non-visual properties of + `p:nvCxnSpPr` element, container for the non-visual properties of a connector, such as name, id, etc. """ diff --git a/src/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py index 6f65da7f4..cf32377c2 100644 --- a/src/pptx/oxml/shapes/graphfrm.py +++ b/src/pptx/oxml/shapes/graphfrm.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """lxml custom element class for CT_GraphicalObjectFrame XML element.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + from pptx.oxml import parse_xml from pptx.oxml.chart.chart import CT_Chart from pptx.oxml.ns import nsdecls @@ -21,159 +23,165 @@ GRAPHIC_DATA_URI_TABLE, ) +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import ( + CT_ApplicationNonVisualDrawingProps, + CT_NonVisualDrawingProps, + CT_Transform2D, + ) + class CT_GraphicalObject(BaseOxmlElement): - """ - ```` element, which is the container for the reference to or - definition of the framed graphical object (table, chart, etc.). + """`a:graphic` element. + + The container for the reference to or definition of the framed graphical object (table, chart, + etc.). """ - graphicData = OneAndOnlyOne("a:graphicData") + graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:graphicData" + ) @property - def chart(self): - """ - The ```` grandchild element, or |None| if not present. - """ + def chart(self) -> CT_Chart | None: + """The `c:chart` grandchild element, or |None| if not present.""" return self.graphicData.chart class CT_GraphicalObjectData(BaseShapeElement): - """ - ```` element, the direct container for a table, a chart, - or another graphical object. + """`p:graphicData` element. + + The direct container for a table, a chart, or another graphical object. """ - chart = ZeroOrOne("c:chart") - tbl = ZeroOrOne("a:tbl") - uri = RequiredAttribute("uri", XsdString) + chart: CT_Chart | None = ZeroOrOne("c:chart") # pyright: ignore[reportAssignmentType] + tbl: CT_Table | None = ZeroOrOne("a:tbl") # pyright: ignore[reportAssignmentType] + uri: str = RequiredAttribute("uri", XsdString) # pyright: ignore[reportAssignmentType] @property - def blob_rId(self): - """Optional "r:id" attribute value of `` descendent element. + def blob_rId(self) -> str | None: + """Optional `r:id` attribute value of `p:oleObj` descendent element. - This value is `None` when this `p:graphicData` element does not enclose an OLE - object. This value could also be `None` if an enclosed OLE object does not - specify this attribute (it is specified optional in the schema) but so far, all - OLE objects we've encountered specify this value. + This value is `None` when this `p:graphicData` element does not enclose an OLE object. + This value could also be `None` if an enclosed OLE object does not specify this attribute + (it is specified optional in the schema) but so far, all OLE objects we've encountered + specify this value. """ return None if self._oleObj is None else self._oleObj.rId @property - def is_embedded_ole_obj(self): + def is_embedded_ole_obj(self) -> bool | None: """Optional boolean indicating an embedded OLE object. - Returns `None` when this `p:graphicData` element does not enclose an OLE object. - `True` indicates an embedded OLE object and `False` indicates a linked OLE - object. + Returns `None` when this `p:graphicData` element does not enclose an OLE object. `True` + indicates an embedded OLE object and `False` indicates a linked OLE object. """ return None if self._oleObj is None else self._oleObj.is_embedded @property - def progId(self): - """Optional str value of "progId" attribute of `` descendent. + def progId(self) -> str | None: + """Optional str value of "progId" attribute of `p:oleObj` descendent. - This value identifies the "type" of the embedded object in terms of the - application used to open it. + This value identifies the "type" of the embedded object in terms of the application used + to open it. - This value is `None` when this `p:graphicData` element does not enclose an OLE - object. This could also be `None` if an enclosed OLE object does not specify - this attribute (it is specified optional in the schema) but so far, all OLE - objects we've encountered specify this value. + This value is `None` when this `p:graphicData` element does not enclose an OLE object. + This could also be `None` if an enclosed OLE object does not specify this attribute (it is + specified optional in the schema) but so far, all OLE objects we've encountered specify + this value. """ return None if self._oleObj is None else self._oleObj.progId @property - def showAsIcon(self): + def showAsIcon(self) -> bool | None: """Optional value of "showAsIcon" attribute value of `p:oleObj` descendent. - This value is `None` when this `p:graphicData` element does not enclose an OLE - object. It is False when the `showAsIcon` attribute is omitted on the `p:oleObj` - element. + This value is `None` when this `p:graphicData` element does not enclose an OLE object. It + is False when the `showAsIcon` attribute is omitted on the `p:oleObj` element. """ return None if self._oleObj is None else self._oleObj.showAsIcon @property - def _oleObj(self): - """Optional `` element contained in this `p:graphicData' element. - - Returns `None` when this graphic-data element does not enclose an OLE object. - Note that this returns the last `p:oleObj` element found. There can be more - than one `p:oleObj` element because an `` element may - appear as the child of `p:graphicData` and that alternate-content subtree can - contain multiple compatibility choices. The last one should suit best for - reading purposes because it contains the lowest common denominator. + def _oleObj(self) -> CT_OleObject | None: + """Optional `p:oleObj` element contained in this `p:graphicData' element. + + Returns `None` when this graphic-data element does not enclose an OLE object. Note that + this returns the last `p:oleObj` element found. There can be more than one `p:oleObj` + element because an `mc.AlternateContent` element may appear as the child of + `p:graphicData` and that alternate-content subtree can contain multiple compatibility + choices. The last one should suit best for reading purposes because it contains the lowest + common denominator. """ - oleObjs = self.xpath(".//p:oleObj") + oleObjs = cast(list[CT_OleObject], self.xpath(".//p:oleObj")) return oleObjs[-1] if oleObjs else None class CT_GraphicalObjectFrame(BaseShapeElement): - """ - ```` element, which is a container for a table, a chart, - or another graphical object. + """`p:graphicFrame` element. + + A container for a table, a chart, or another graphical object. """ - nvGraphicFramePr = OneAndOnlyOne("p:nvGraphicFramePr") - xfrm = OneAndOnlyOne("p:xfrm") - graphic = OneAndOnlyOne("a:graphic") + nvGraphicFramePr: CT_GraphicalObjectFrameNonVisual = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvGraphicFramePr") + ) + xfrm: CT_Transform2D = OneAndOnlyOne("p:xfrm") # pyright: ignore + graphic: CT_GraphicalObject = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:graphic" + ) @property - def chart(self): - """ - The ```` great-grandchild element, or |None| if not present. - """ + def chart(self) -> CT_Chart | None: + """The `c:chart` great-grandchild element, or |None| if not present.""" return self.graphic.chart @property - def chart_rId(self): - """ - The ``rId`` attribute of the ```` great-grandchild element, - or |None| if not present. + def chart_rId(self) -> str | None: + """The `rId` attribute of the `c:chart` great-grandchild element. + + |None| if not present. """ chart = self.chart if chart is None: return None return chart.rId - def get_or_add_xfrm(self): - """ - Return the required ```` child element. Overrides version on - BaseShapeElement. + def get_or_add_xfrm(self) -> CT_Transform2D: + """Return the required `p:xfrm` child element. + + Overrides version on BaseShapeElement. """ return self.xfrm @property - def graphicData(self): - """` grandchild of this graphic-frame element.""" + def graphicData(self) -> CT_GraphicalObjectData: + """`a:graphicData` grandchild of this graphic-frame element.""" return self.graphic.graphicData @property - def graphicData_uri(self): - """str value of `uri` attribute of ` grandchild.""" + def graphicData_uri(self) -> str: + """str value of `uri` attribute of `a:graphicData` grandchild.""" return self.graphic.graphicData.uri @property - def has_oleobj(self): - """True for graphicFrame containing an OLE object, False otherwise.""" + def has_oleobj(self) -> bool: + """`True` for graphicFrame containing an OLE object, `False` otherwise.""" return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ @property - def is_embedded_ole_obj(self): + def is_embedded_ole_obj(self) -> bool | None: """Optional boolean indicating an embedded OLE object. - Returns `None` when this `p:graphicFrame` element does not enclose an OLE - object. `True` indicates an embedded OLE object and `False` indicates a linked - OLE object. + Returns `None` when this `p:graphicFrame` element does not enclose an OLE object. `True` + indicates an embedded OLE object and `False` indicates a linked OLE object. """ return self.graphicData.is_embedded_ole_obj @classmethod - def new_chart_graphicFrame(cls, id_, name, rId, x, y, cx, cy): - """ - Return a ```` element tree populated with a chart - element. - """ + def new_chart_graphicFrame( + cls, id_: int, name: str, rId: str, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Return a `p:graphicFrame` element tree populated with a chart element.""" graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy) graphicData = graphicFrame.graphic.graphicData graphicData.uri = GRAPHIC_DATA_URI_CHART @@ -181,160 +189,154 @@ def new_chart_graphicFrame(cls, id_, name, rId, x, y, cx, cy): return graphicFrame @classmethod - def new_graphicFrame(cls, id_, name, x, y, cx, cy): - """ - Return a new ```` element tree suitable for - containing a table or chart. Note that a graphicFrame element is not - a valid shape until it contains a graphical object such as a table. + def new_graphicFrame( + cls, id_: int, name: str, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Return a new `p:graphicFrame` element tree suitable for containing a table or chart. + + Note that a graphicFrame element is not a valid shape until it contains a graphical object + such as a table. """ - xml = cls._graphicFrame_tmpl() % (id_, name, x, y, cx, cy) - graphicFrame = parse_xml(xml) - return graphicFrame + return cast( + CT_GraphicalObjectFrame, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), + ) @classmethod def new_ole_object_graphicFrame( - cls, id_, name, ole_object_rId, progId, icon_rId, x, y, cx, cy, imgW, imgH - ): - """Return newly-created `` for embedded OLE-object. + cls, + id_: int, + name: str, + ole_object_rId: str, + progId: str, + icon_rId: str, + x: int, + y: int, + cx: int, + cy: int, + imgW: int, + imgH: int, + ) -> CT_GraphicalObjectFrame: + """Return newly-created `p:graphicFrame` for embedded OLE-object. `ole_object_rId` identifies the relationship to the OLE-object part. - `progId` is a str identifying the object-type in terms of the application - (program) used to open it. This becomes an attribute of the same name in the - `p:oleObj` element. + `progId` is a str identifying the object-type in terms of the application (program) used + to open it. This becomes an attribute of the same name in the `p:oleObj` element. - `icon_rId` identifies the relationship to an image part used to display the - OLE-object as an icon (vs. a preview). + `icon_rId` identifies the relationship to an image part used to display the OLE-object as + an icon (vs. a preview). """ - return parse_xml( - cls._graphicFrame_xml_for_ole_object( - id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId, imgW, imgH - ) + return cast( + CT_GraphicalObjectFrame, + parse_xml( + f"\n" + f" \n" + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f" \n" + f" \n' + f' \n' + f" \n" + f" \n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f' \n' + f' \n' + f" \n" + f' \n' + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), ) @classmethod - def new_table_graphicFrame(cls, id_, name, rows, cols, x, y, cx, cy): - """ - Return a ```` element tree populated with a table - element. - """ + def new_table_graphicFrame( + cls, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Return a `p:graphicFrame` element tree populated with a table element.""" graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy) graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy)) return graphicFrame - @classmethod - def _graphicFrame_tmpl(cls): - return ( - "\n" - " \n" - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - ' \n' - ' \n' - " \n" - " \n" - " \n" - " \n" - "" - % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d") - ) - - @classmethod - def _graphicFrame_xml_for_ole_object( - cls, id_, name, x, y, cx, cy, ole_object_rId, progId, icon_rId, imgW, imgH - ): - """str XML for element of an embedded OLE-object shape.""" - return ( - "\n" - " \n" - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - ' \n' - ' \n' - " \n" - " \n" - " \n' - ' \n' - " \n" - " \n" - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - ' \n' - ' \n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "" - ).format( - nsdecls=nsdecls("a", "p", "r"), - id_=id_, - name=name, - x=x, - y=y, - cx=cx, - cy=cy, - ole_object_rId=ole_object_rId, - progId=progId, - icon_rId=icon_rId, - imgW=imgW, - imgH=imgH, - ) - class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement): - """`` element. + """`p:nvGraphicFramePr` element. This contains the non-visual properties of a graphic frame, such as name, id, etc. """ - cNvPr = OneAndOnlyOne("p:cNvPr") - nvPr = OneAndOnlyOne("p:nvPr") + cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:cNvPr" + ) + nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType] + OneAndOnlyOne("p:nvPr") + ) class CT_OleObject(BaseOxmlElement): - """`` element, container for an OLE object (e.g. Excel file). + """`p:oleObj` element, container for an OLE object (e.g. Excel file). An OLE object can be either linked or embedded (hence the name). """ - progId = OptionalAttribute("progId", XsdString) - rId = OptionalAttribute("r:id", XsdString) - showAsIcon = OptionalAttribute("showAsIcon", XsdBoolean, default=False) + progId: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "progId", XsdString + ) + rId: str | None = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] + showAsIcon: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "showAsIcon", XsdBoolean, default=False + ) @property - def is_embedded(self): + def is_embedded(self) -> bool: """True when this OLE object is embedded, False when it is linked.""" - return True if len(self.xpath("./p:embed")) > 0 else False + return len(self.xpath("./p:embed")) > 0 diff --git a/src/pptx/oxml/shapes/groupshape.py b/src/pptx/oxml/shapes/groupshape.py index e428bd79e..f62bc6662 100644 --- a/src/pptx/oxml/shapes/groupshape.py +++ b/src/pptx/oxml/shapes/groupshape.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """lxml custom element classes for shape-tree-related XML elements.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Iterator from pptx.enum.shapes import MSO_CONNECTOR_TYPE from pptx.oxml import parse_xml @@ -15,15 +15,21 @@ from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne from pptx.util import Emu +if TYPE_CHECKING: + from pptx.enum.shapes import PP_PLACEHOLDER + from pptx.oxml.shapes import ShapeElement + from pptx.oxml.shapes.shared import CT_Transform2D + class CT_GroupShape(BaseShapeElement): - """ - Used for the shape tree (````) element as well as the group - shape (````) element. - """ + """Used for shape tree (`p:spTree`) as well as the group shape (`p:grpSp`) elements.""" - nvGrpSpPr = OneAndOnlyOne("p:nvGrpSpPr") - grpSpPr = OneAndOnlyOne("p:grpSpPr") + nvGrpSpPr: CT_GroupShapeNonVisual = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:nvGrpSpPr" + ) + grpSpPr: CT_GroupShapeProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "p:grpSpPr" + ) _shape_tags = ( qn("p:sp"), @@ -34,26 +40,33 @@ class CT_GroupShape(BaseShapeElement): qn("p:contentPart"), ) - def add_autoshape(self, id_, name, prst, x, y, cx, cy): - """ - Append a new ```` shape to the group/shapetree having the - properties specified in call. - """ + def add_autoshape( + self, id_: int, name: str, prst: str, x: int, y: int, cx: int, cy: int + ) -> CT_Shape: + """Return new `p:sp` appended to the group/shapetree with specified attributes.""" sp = CT_Shape.new_autoshape_sp(id_, name, prst, x, y, cx, cy) self.insert_element_before(sp, "p:extLst") return sp - def add_cxnSp(self, id_, name, type_member, x, y, cx, cy, flipH, flipV): - """ - Append a new ```` shape to the group/shapetree having the - properties specified in call. - """ + def add_cxnSp( + self, + id_: int, + name: str, + type_member: MSO_CONNECTOR_TYPE, + x: int, + y: int, + cx: int, + cy: int, + flipH: bool, + flipV: bool, + ) -> CT_Connector: + """Return new `p:cxnSp` appended to the group/shapetree with the specified attribues.""" prst = MSO_CONNECTOR_TYPE.to_xml(type_member) cxnSp = CT_Connector.new_cxnSp(id_, name, prst, x, y, cx, cy, flipH, flipV) self.insert_element_before(cxnSp, "p:extLst") return cxnSp - def add_freeform_sp(self, x, y, cx, cy): + def add_freeform_sp(self, x: int, y: int, cx: int, cy: int) -> CT_Shape: """Append a new freeform `p:sp` with specified position and size.""" shape_id = self._next_shape_id name = "Freeform %d" % (shape_id - 1,) @@ -61,7 +74,7 @@ def add_freeform_sp(self, x, y, cx, cy): self.insert_element_before(sp, "p:extLst") return sp - def add_grpSp(self): + def add_grpSp(self) -> CT_GroupShape: """Return `p:grpSp` element newly appended to this shape tree. The element contains no sub-shapes, is positioned at (0, 0), and has @@ -73,40 +86,34 @@ def add_grpSp(self): self.insert_element_before(grpSp, "p:extLst") return grpSp - def add_pic(self, id_, name, desc, rId, x, y, cx, cy): - """ - Append a ```` shape to the group/shapetree having properties - as specified in call. - """ + def add_pic( + self, id_: int, name: str, desc: str, rId: str, x: int, y: int, cx: int, cy: int + ) -> CT_Picture: + """Append a `p:pic` shape to the group/shapetree having properties as specified in call.""" pic = CT_Picture.new_pic(id_, name, desc, rId, x, y, cx, cy) self.insert_element_before(pic, "p:extLst") return pic - def add_placeholder(self, id_, name, ph_type, orient, sz, idx): - """ - Append a newly-created placeholder ```` shape having the - specified placeholder properties. - """ + def add_placeholder( + self, id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz: str, idx: int + ) -> CT_Shape: + """Append a newly-created placeholder `p:sp` shape having the specified properties.""" sp = CT_Shape.new_placeholder_sp(id_, name, ph_type, orient, sz, idx) self.insert_element_before(sp, "p:extLst") return sp - def add_table(self, id_, name, rows, cols, x, y, cx, cy): - """ - Append a ```` shape containing a table as specified - in call. - """ + def add_table( + self, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int + ) -> CT_GraphicalObjectFrame: + """Append a `p:graphicFrame` shape containing a table as specified in call.""" graphicFrame = CT_GraphicalObjectFrame.new_table_graphicFrame( id_, name, rows, cols, x, y, cx, cy ) self.insert_element_before(graphicFrame, "p:extLst") return graphicFrame - def add_textbox(self, id_, name, x, y, cx, cy): - """ - Append a newly-created textbox ```` shape having the specified - position and size. - """ + def add_textbox(self, id_: int, name: str, x: int, y: int, cx: int, cy: int) -> CT_Shape: + """Append a newly-created textbox `p:sp` shape having the specified position and size.""" sp = CT_Shape.new_textbox_sp(id_, name, x, y, cx, cy) self.insert_element_before(sp, "p:extLst") return sp @@ -121,32 +128,27 @@ def chOff(self): """Descendent `p:grpSpPr/a:xfrm/a:chOff` element.""" return self.grpSpPr.get_or_add_xfrm().get_or_add_chOff() - def get_or_add_xfrm(self): - """ - Return the ```` grandchild element, newly-added if not - present. - """ + def get_or_add_xfrm(self) -> CT_Transform2D: + """Return the `a:xfrm` grandchild element, newly-added if not present.""" return self.grpSpPr.get_or_add_xfrm() def iter_ph_elms(self): - """ - Generate each placeholder shape child element in document order. - """ + """Generate each placeholder shape child element in document order.""" for e in self.iter_shape_elms(): if e.has_ph_elm: yield e - def iter_shape_elms(self): - """ - Generate each child of this ```` element that corresponds - to a shape, in the sequence they appear in the XML. + def iter_shape_elms(self) -> Iterator[ShapeElement]: + """Generate each child of this `p:spTree` element that corresponds to a shape. + + Items appear in XML document order. """ for elm in self.iterchildren(): if elm.tag in self._shape_tags: yield elm @property - def max_shape_id(self): + def max_shape_id(self) -> int: """Maximum int value assigned as @id in this slide. This is generally a shape-id, but ids can be assigned to other @@ -161,8 +163,8 @@ def max_shape_id(self): return max(used_ids) if used_ids else 0 @classmethod - def new_grpSp(cls, id_, name): - """Return new "loose" `p:grpSp` element having *id_* and *name*.""" + def new_grpSp(cls, id_: int, name: str) -> CT_GroupShape: + """Return new "loose" `p:grpSp` element having `id_` and `name`.""" xml = ( "\n" " \n" @@ -183,7 +185,7 @@ def new_grpSp(cls, id_, name): grpSp = parse_xml(xml) return grpSp - def recalculate_extents(self): + def recalculate_extents(self) -> None: """Adjust x, y, cx, and cy to incorporate all contained shapes. This would typically be called when a contained shape is added, @@ -204,14 +206,12 @@ def recalculate_extents(self): self.getparent().recalculate_extents() @property - def xfrm(self): - """ - The ```` grandchild element or |None| if not found - """ + def xfrm(self) -> CT_Transform2D | None: + """The `a:xfrm` grandchild element or |None| if not found.""" return self.grpSpPr.xfrm @property - def _child_extents(self): + def _child_extents(self) -> tuple[int, int, int, int]: """(x, y, cx, cy) tuple representing net position and size. The values are formed as a composite of the contained child shapes. @@ -234,7 +234,7 @@ def _child_extents(self): return x, y, cx, cy @property - def _next_shape_id(self): + def _next_shape_id(self) -> int: """Return unique shape id suitable for use with a new shape element. The returned id is the next available positive integer drawing object @@ -250,15 +250,15 @@ def _next_shape_id(self): class CT_GroupShapeNonVisual(BaseShapeElement): - """ - ```` element. - """ + """`p:nvGrpSpPr` element.""" cNvPr = OneAndOnlyOne("p:cNvPr") class CT_GroupShapeProperties(BaseOxmlElement): - """p:grpSpPr element """ + """p:grpSpPr element""" + + get_or_add_xfrm: Callable[[], CT_Transform2D] _tag_seq = ( "a:xfrm", @@ -273,6 +273,8 @@ class CT_GroupShapeProperties(BaseOxmlElement): "a:scene3d", "a:extLst", ) - xfrm = ZeroOrOne("a:xfrm", successors=_tag_seq[1:]) + xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:xfrm", successors=_tag_seq[1:] + ) effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[8:]) del _tag_seq diff --git a/src/pptx/oxml/shapes/picture.py b/src/pptx/oxml/shapes/picture.py index 39904385d..bacc97194 100644 --- a/src/pptx/oxml/shapes/picture.py +++ b/src/pptx/oxml/shapes/picture.py @@ -1,9 +1,8 @@ -# encoding: utf-8 - """lxml custom element classes for picture-related XML elements.""" -from __future__ import division +from __future__ import annotations +from typing import TYPE_CHECKING, cast from xml.sax.saxutils import escape from pptx.oxml import parse_xml @@ -11,6 +10,10 @@ from pptx.oxml.shapes.shared import BaseShapeElement from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne +if TYPE_CHECKING: + from pptx.oxml.shapes.shared import CT_ShapeProperties + from pptx.util import Length + class CT_Picture(BaseShapeElement): """`p:pic` element. @@ -20,10 +23,10 @@ class CT_Picture(BaseShapeElement): nvPicPr = OneAndOnlyOne("p:nvPicPr") blipFill = OneAndOnlyOne("p:blipFill") - spPr = OneAndOnlyOne("p:spPr") + spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType] @property - def blip_rId(self): + def blip_rId(self) -> str | None: """Value of `p:blipFill/a:blip/@r:embed`. Returns |None| if not present. @@ -65,28 +68,38 @@ def new_ph_pic(cls, id_, name, desc, rId): @classmethod def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy): """Return new `` element tree configured with supplied parameters.""" - return parse_xml( - cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy) - ) + return parse_xml(cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy)) @classmethod def new_video_pic( - cls, shape_id, shape_name, video_rId, media_rId, poster_frame_rId, x, y, cx, cy - ): + cls, + shape_id: int, + shape_name: str, + video_rId: str, + media_rId: str, + poster_frame_rId: str, + x: Length, + y: Length, + cx: Length, + cy: Length, + ) -> CT_Picture: """Return a new `p:pic` populated with the specified video.""" - return parse_xml( - cls._pic_video_tmpl() - % ( - shape_id, - shape_name, - video_rId, - media_rId, - poster_frame_rId, - x, - y, - cx, - cy, - ) + return cast( + CT_Picture, + parse_xml( + cls._pic_video_tmpl() + % ( + shape_id, + shape_name, + video_rId, + media_rId, + poster_frame_rId, + x, + y, + cx, + cy, + ) + ), ) @property diff --git a/src/pptx/oxml/shapes/shared.py b/src/pptx/oxml/shapes/shared.py index 74eb562d3..d9f945697 100644 --- a/src/pptx/oxml/shapes/shared.py +++ b/src/pptx/oxml/shapes/shared.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Common shape-related oxml objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable from pptx.dml.fill import CT_GradientFillProperties from pptx.enum.shapes import PP_PLACEHOLDER @@ -30,15 +30,19 @@ ) from pptx.util import Emu +if TYPE_CHECKING: + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.shapes.autoshape import CT_CustomGeometry2D, CT_PresetGeometry2D + from pptx.util import Length + class BaseShapeElement(BaseOxmlElement): - """ - Provides common behavior for shape element classes like CT_Shape, - CT_Picture, etc. - """ + """Provides common behavior for shape element classes like CT_Shape, CT_Picture, etc.""" + + spPr: CT_ShapeProperties @property - def cx(self): + def cx(self) -> Length: return self._get_xfrm_attr("cx") @cx.setter @@ -46,7 +50,7 @@ def cx(self, value): self._set_xfrm_attr("cx", value) @property - def cy(self): + def cy(self) -> Length: return self._get_xfrm_attr("cy") @cy.setter @@ -70,36 +74,34 @@ def flipV(self, value): self._set_xfrm_attr("flipV", value) def get_or_add_xfrm(self): - """ - Return the ```` grandchild element, newly-added if not - present. This version works for ````, ````, and - ```` elements, others will need to override. + """Return the `a:xfrm` grandchild element, newly-added if not present. + + This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to + override. """ return self.spPr.get_or_add_xfrm() @property def has_ph_elm(self): """ - True if this shape element has a ```` descendant, indicating it + True if this shape element has a `p:ph` descendant, indicating it is a placeholder shape. False otherwise. """ return self.ph is not None @property - def ph(self): - """ - The ```` descendant element if there is one, None otherwise. - """ + def ph(self) -> CT_Placeholder | None: + """The `p:ph` descendant element if there is one, None otherwise.""" ph_elms = self.xpath("./*[1]/p:nvPr/p:ph") if len(ph_elms) == 0: return None return ph_elms[0] @property - def ph_idx(self): - """ - Integer value of placeholder idx attribute. Raises |ValueError| if - shape is not a placeholder. + def ph_idx(self) -> int: + """Integer value of placeholder idx attribute. + + Raises |ValueError| if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -107,10 +109,10 @@ def ph_idx(self): return ph.idx @property - def ph_orient(self): - """ - Placeholder orientation, e.g. 'vert'. Raises |ValueError| if shape is - not a placeholder. + def ph_orient(self) -> str: + """Placeholder orientation, e.g. 'vert'. + + Raises |ValueError| if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -118,10 +120,10 @@ def ph_orient(self): return ph.orient @property - def ph_sz(self): - """ - Placeholder size, e.g. ST_PlaceholderSize.HALF, None if shape has no - ```` descendant. + def ph_sz(self) -> str: + """Placeholder size, e.g. ST_PlaceholderSize.HALF. + + Raises `ValueError` if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -130,9 +132,9 @@ def ph_sz(self): @property def ph_type(self): - """ - Placeholder type, e.g. ST_PlaceholderType.TITLE ('title'), none if - shape has no ```` descendant. + """Placeholder type, e.g. ST_PlaceholderType.TITLE ('title'). + + Raises `ValueError` if shape is not a placeholder. """ ph = self.ph if ph is None: @@ -140,17 +142,15 @@ def ph_type(self): return ph.type @property - def rot(self): - """ - Float representing degrees this shape is rotated clockwise. - """ + def rot(self) -> float: + """Float representing degrees this shape is rotated clockwise.""" xfrm = self.xfrm - if xfrm is None: + if xfrm is None or xfrm.rot is None: return 0.0 return xfrm.rot @rot.setter - def rot(self, value): + def rot(self, value: float): self.get_or_add_xfrm().rot = value @property @@ -169,13 +169,11 @@ def shape_name(self): @property def txBody(self): - """ - Child ```` element, None if not present - """ + """Child `p:txBody` element, None if not present.""" return self.find(qn("p:txBody")) @property - def x(self): + def x(self) -> Length: return self._get_xfrm_attr("x") @x.setter @@ -184,15 +182,15 @@ def x(self, value): @property def xfrm(self): - """ - The ```` grandchild element or |None| if not found. This - version works for ````, ````, and ```` - elements, others will need to override. + """The `a:xfrm` grandchild element or |None| if not found. + + This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to + override. """ return self.spPr.xfrm @property - def y(self): + def y(self) -> Length: return self._get_xfrm_attr("y") @y.setter @@ -203,12 +201,12 @@ def y(self, value): def _nvXxPr(self): """ Required non-visual shape properties element for this shape. Actual - name depends on the shape type, e.g. ```` for picture + name depends on the shape type, e.g. `p:nvPicPr` for picture shape. """ return self.xpath("./*[1]")[0] - def _get_xfrm_attr(self, name): + def _get_xfrm_attr(self, name: str) -> Length | None: xfrm = self.xfrm if xfrm is None: return None @@ -220,9 +218,9 @@ def _set_xfrm_attr(self, name, value): class CT_ApplicationNonVisualDrawingProps(BaseOxmlElement): - """ - ```` element - """ + """`p:nvPr` element.""" + + get_or_add_ph: Callable[[], CT_Placeholder] ph = ZeroOrOne( "p:ph", @@ -295,27 +293,34 @@ def prstDash_val(self, val): class CT_NonVisualDrawingProps(BaseOxmlElement): - """ - ```` custom element class. - """ + """`p:cNvPr` custom element class.""" + + get_or_add_hlinkClick: Callable[[], CT_Hyperlink] + get_or_add_hlinkHover: Callable[[], CT_Hyperlink] _tag_seq = ("a:hlinkClick", "a:hlinkHover", "a:extLst") - hlinkClick = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:]) - hlinkHover = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:]) + hlinkClick: CT_Hyperlink | None = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:]) + hlinkHover: CT_Hyperlink | None = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:]) id = RequiredAttribute("id", ST_DrawingElementId) name = RequiredAttribute("name", XsdString) del _tag_seq class CT_Placeholder(BaseOxmlElement): - """ - ```` custom element class. - """ + """`p:ph` custom element class.""" - type = OptionalAttribute("type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT) - orient = OptionalAttribute("orient", ST_Direction, default=ST_Direction.HORZ) - sz = OptionalAttribute("sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL) - idx = OptionalAttribute("idx", XsdUnsignedInt, default=0) + type: PP_PLACEHOLDER = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT + ) + orient: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "orient", ST_Direction, default=ST_Direction.HORZ + ) + sz: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL + ) + idx: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "idx", XsdUnsignedInt, default=0 + ) class CT_Point2D(BaseOxmlElement): @@ -323,8 +328,8 @@ class CT_Point2D(BaseOxmlElement): Custom element class for element. """ - x = RequiredAttribute("x", ST_Coordinate) - y = RequiredAttribute("y", ST_Coordinate) + x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType] + y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType] class CT_PositiveSize2D(BaseOxmlElement): @@ -339,10 +344,14 @@ class CT_PositiveSize2D(BaseOxmlElement): class CT_ShapeProperties(BaseOxmlElement): """Custom element class for `p:spPr` element. - Shared by `p:sp`, `p:cxnSp`, and `p:pic` elements as well as a few more - obscure ones. + Shared by `p:sp`, `p:cxnSp`, and `p:pic` elements as well as a few more obscure ones. """ + get_or_add_xfrm: Callable[[], CT_Transform2D] + get_or_add_ln: Callable[[], CT_LineProperties] + _add_prstGeom: Callable[[], CT_PresetGeometry2D] + _remove_custGeom: Callable[[], None] + _tag_seq = ( "a:xfrm", "a:custGeom", @@ -360,9 +369,15 @@ class CT_ShapeProperties(BaseOxmlElement): "a:sp3d", "a:extLst", ) - xfrm = ZeroOrOne("a:xfrm", successors=_tag_seq[1:]) - custGeom = ZeroOrOne("a:custGeom", successors=_tag_seq[2:]) - prstGeom = ZeroOrOne("a:prstGeom", successors=_tag_seq[3:]) + xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:xfrm", successors=_tag_seq[1:] + ) + custGeom: CT_CustomGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:custGeom", successors=_tag_seq[2:] + ) + prstGeom: CT_PresetGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:prstGeom", successors=_tag_seq[3:] + ) eg_fillProperties = ZeroOrOneChoice( ( Choice("a:noFill"), @@ -374,7 +389,9 @@ class CT_ShapeProperties(BaseOxmlElement): ), successors=_tag_seq[9:], ) - ln = ZeroOrOne("a:ln", successors=_tag_seq[10:]) + ln: CT_LineProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:ln", successors=_tag_seq[10:] + ) effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[11:]) del _tag_seq @@ -399,11 +416,10 @@ def cy(self): return Emu(cy_str_lst[0]) @property - def x(self): - """ - The offset of the left edge of the shape from the left edge of the - slide, as an instance of Emu. Corresponds to the value of the - `./xfrm/off/@x` attribute. None if not present. + def x(self) -> Length | None: + """Distance between the left edge of the slide and left edge of the shape. + + 0 if not present. """ x_str_lst = self.xpath("./a:xfrm/a:off/@x") if not x_str_lst: @@ -433,12 +449,16 @@ class CT_Transform2D(BaseOxmlElement): """ _tag_seq = ("a:off", "a:ext", "a:chOff", "a:chExt") - off = ZeroOrOne("a:off", successors=_tag_seq[1:]) + off: CT_Point2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:off", successors=_tag_seq[1:] + ) ext = ZeroOrOne("a:ext", successors=_tag_seq[2:]) chOff = ZeroOrOne("a:chOff", successors=_tag_seq[3:]) chExt = ZeroOrOne("a:chExt", successors=_tag_seq[4:]) del _tag_seq - rot = OptionalAttribute("rot", ST_Angle, default=0.0) + rot: float | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "rot", ST_Angle, default=0.0 + ) flipH = OptionalAttribute("flipH", XsdBoolean, default=False) flipV = OptionalAttribute("flipV", XsdBoolean, default=False) diff --git a/src/pptx/oxml/simpletypes.py b/src/pptx/oxml/simpletypes.py index 7c3ed34e5..6ceb06f7c 100644 --- a/src/pptx/oxml/simpletypes.py +++ b/src/pptx/oxml/simpletypes.py @@ -1,37 +1,35 @@ -# encoding: utf-8 - """Simple-type classes. -A "simple-type" is a scalar type, generally serving as an XML attribute. This is in -contrast to a "complex-type" which would specify an XML element. +A "simple-type" is a scalar type, generally serving as an XML attribute. This is in contrast to a +"complex-type" which would specify an XML element. -These objects providing validation and format translation for values stored in XML -element attributes. Naming generally corresponds to the simple type in the associated -XML schema. +These objects providing validation and format translation for values stored in XML element +attributes. Naming generally corresponds to the simple type in the associated XML schema. """ +from __future__ import annotations + import numbers +from typing import Any from pptx.exc import InvalidXmlError from pptx.util import Centipoints, Emu -class BaseSimpleType(object): +class BaseSimpleType: @classmethod - def from_xml(cls, str_value): - return cls.convert_from_xml(str_value) + def from_xml(cls, xml_value: str) -> Any: + return cls.convert_from_xml(xml_value) @classmethod - def to_xml(cls, value): + def to_xml(cls, value: Any) -> str: cls.validate(value) str_value = cls.convert_to_xml(value) return str_value @classmethod - def validate_float(cls, value): - """ - Note that int values are accepted. - """ + def validate_float(cls, value: Any): + """Note that int values are accepted.""" if not isinstance(value, (int, float)): raise TypeError("value must be a number, got %s" % type(value)) @@ -151,8 +149,7 @@ def convert_to_xml(cls, value): def validate(cls, value): if value not in (True, False): raise TypeError( - "only True or False (and possibly None) may be assigned, got" - " '%s'" % value + "only True or False (and possibly None) may be assigned, got" " '%s'" % value ) @@ -231,7 +228,7 @@ class ST_Angle(XsdInt): THREE_SIXTY = 360 * DEGREE_INCREMENTS @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> float: rot = int(str_value) % cls.THREE_SIXTY return float(rot) / cls.DEGREE_INCREMENTS @@ -349,9 +346,7 @@ def validate(cls, value): class ST_Direction(XsdTokenEnumeration): - """ - Valid values for attribute - """ + """Valid values for `` attribute.""" HORZ = "horz" VERT = "vert" @@ -419,17 +414,13 @@ def validate(cls, value): # must be 6 chars long---------- if len(str_value) != 6: - raise ValueError( - "RGB string must be six characters long, got '%s'" % str_value - ) + raise ValueError("RGB string must be six characters long, got '%s'" % str_value) # must parse as hex int -------- try: int(str_value, 16) except ValueError: - raise ValueError( - "RGB string must be valid hex string, got '%s'" % str_value - ) + raise ValueError("RGB string must be valid hex string, got '%s'" % str_value) class ST_LayoutMode(XsdStringEnumeration): @@ -471,8 +462,7 @@ def validate(cls, value): super(ST_LineWidth, cls).validate(value) if value < 0 or value > 20116800: raise ValueError( - "value must be in range 0-20116800 inclusive (0-1584 points)" - ", got %d" % value + "value must be in range 0-20116800 inclusive (0-1584 points)" ", got %d" % value ) @@ -615,8 +605,7 @@ def validate(cls, value): cls.validate_int(value) if value < 914400 or value > 51206400: raise ValueError( - "value must be in range(914400, 51206400) (1-56 inches), got" - " %d" % value + "value must be in range(914400, 51206400) (1-56 inches), got" " %d" % value ) @@ -636,9 +625,7 @@ class ST_TargetMode(XsdString): def validate(cls, value): cls.validate_string(value) if value not in ("External", "Internal"): - raise ValueError( - "must be one of 'Internal' or 'External', got '%s'" % value - ) + raise ValueError("must be one of 'Internal' or 'External', got '%s'" % value) class ST_TextFontScalePercentOrPercentString(BaseFloatType): @@ -661,9 +648,7 @@ def convert_to_xml(cls, value): def validate(cls, value): BaseFloatType.validate(value) if value < 1.0 or value > 100.0: - raise ValueError( - "value must be in range 1.0..100.0 (percent), got %s" % value - ) + raise ValueError("value must be in range 1.0..100.0 (percent), got %s" % value) class ST_TextFontSize(BaseIntType): diff --git a/src/pptx/oxml/slide.py b/src/pptx/oxml/slide.py index 36b868cf8..37a9780f6 100644 --- a/src/pptx/oxml/slide.py +++ b/src/pptx/oxml/slide.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Slide-related custom element classes, including those for masters.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast from pptx.oxml import parse_from_template, parse_xml from pptx.oxml.dml.fill import CT_GradientFillProperties @@ -19,39 +19,39 @@ ZeroOrOneChoice, ) +if TYPE_CHECKING: + from pptx.oxml.shapes.groupshape import CT_GroupShape + class _BaseSlideElement(BaseOxmlElement): - """ - Base class for the six slide types, providing common methods. - """ + """Base class for the six slide types, providing common methods.""" + + cSld: CT_CommonSlideData @property - def spTree(self): - """ - Return required `p:cSld/p:spTree` grandchild. - """ + def spTree(self) -> CT_GroupShape: + """Return required `p:cSld/p:spTree` grandchild.""" return self.cSld.spTree class CT_Background(BaseOxmlElement): """`p:bg` element.""" + _insert_bgPr: Callable[[CT_BackgroundProperties], None] + # ---these two are actually a choice, not a sequence, but simpler for # ---present purposes this way. _tag_seq = ("p:bgPr", "p:bgRef") - bgPr = ZeroOrOne("p:bgPr", successors=()) + bgPr: CT_BackgroundProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:bgPr", successors=() + ) bgRef = ZeroOrOne("p:bgRef", successors=()) del _tag_seq def add_noFill_bgPr(self): """Return a new `p:bgPr` element with noFill properties.""" - xml = ( - "\n" - " \n" - " \n" - "" % nsdecls("a", "p") - ) - bgPr = parse_xml(xml) + xml = "\n" " \n" " \n" "" % nsdecls("a", "p") + bgPr = cast(CT_BackgroundProperties, parse_xml(xml)) self._insert_bgPr(bgPr) return bgPr @@ -91,24 +91,31 @@ def _new_gradFill(self): class CT_CommonSlideData(BaseOxmlElement): """`p:cSld` element.""" + _remove_bg: Callable[[], None] + get_or_add_bg: Callable[[], CT_Background] + _tag_seq = ("p:bg", "p:spTree", "p:custDataLst", "p:controls", "p:extLst") - bg = ZeroOrOne("p:bg", successors=_tag_seq[1:]) - spTree = OneAndOnlyOne("p:spTree") + bg: CT_Background | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:bg", successors=_tag_seq[1:] + ) + spTree: CT_GroupShape = OneAndOnlyOne("p:spTree") # pyright: ignore[reportAssignmentType] del _tag_seq - name = OptionalAttribute("name", XsdString, default="") + name: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "name", XsdString, default="" + ) - def get_or_add_bgPr(self): + def get_or_add_bgPr(self) -> CT_BackgroundProperties: """Return `p:bg/p:bgPr` grandchild. - If no such grandchild is present, any existing `p:bg` child is first - removed and a new default `p:bg` with noFill settings is added. + If no such grandchild is present, any existing `p:bg` child is first removed and a new + default `p:bg` with noFill settings is added. """ bg = self.bg if bg is None or bg.bgPr is None: - self._change_to_noFill_bg() - return self.bg.bgPr + bg = self._change_to_noFill_bg() + return cast(CT_BackgroundProperties, bg.bgPr) - def _change_to_noFill_bg(self): + def _change_to_noFill_bg(self) -> CT_Background: """Establish a `p:bg` child with no-fill settings. Any existing `p:bg` child is first removed. @@ -120,55 +127,48 @@ def _change_to_noFill_bg(self): class CT_NotesMaster(_BaseSlideElement): - """ - ```` element, root of a notes master part - """ + """`p:notesMaster` element, root of a notes master part.""" _tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] del _tag_seq @classmethod - def new_default(cls): - """ - Return a new ```` element based on the built-in - default template. - """ - return parse_from_template("notesMaster") + def new_default(cls) -> CT_NotesMaster: + """Return a new `p:notesMaster` element based on the built-in default template.""" + return cast(CT_NotesMaster, parse_from_template("notesMaster")) class CT_NotesSlide(_BaseSlideElement): - """ - ```` element, root of a notes slide part - """ + """`p:notes` element, root of a notes slide part.""" _tag_seq = ("p:cSld", "p:clrMapOvr", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] del _tag_seq @classmethod - def new(cls): - """ - Return a new ```` element based on the default template. - Note that the template does not include placeholders, which must be - subsequently cloned from the notes master. + def new(cls) -> CT_NotesSlide: + """Return a new ```` element based on the default template. + + Note that the template does not include placeholders, which must be subsequently cloned + from the notes master. """ - return parse_from_template("notes") + return cast(CT_NotesSlide, parse_from_template("notes")) class CT_Slide(_BaseSlideElement): """`p:sld` element, root element of a slide part (XML document).""" _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=_tag_seq[2:]) timing = ZeroOrOne("p:timing", successors=_tag_seq[4:]) del _tag_seq @classmethod - def new(cls): + def new(cls) -> CT_Slide: """Return new `p:sld` element configured as base slide shape.""" - return parse_xml(cls._sld_xml()) + return cast(CT_Slide, parse_xml(cls._sld_xml())) @property def bg(self): @@ -252,37 +252,37 @@ def _sld_xml(): class CT_SlideLayout(_BaseSlideElement): - """ - ```` element, root of a slide layout part - """ + """`p:sldLayout` element, root of a slide layout part.""" _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst") - cSld = OneAndOnlyOne("p:cSld") + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] del _tag_seq class CT_SlideLayoutIdList(BaseOxmlElement): + """`p:sldLayoutIdLst` element, child of `p:sldMaster`. + + Contains references to the slide layouts that inherit from the slide master. """ - ```` element, child of ```` containing - references to the slide layouts that inherit from the slide master. - """ + + sldLayoutId_lst: list[CT_SlideLayoutIdListEntry] sldLayoutId = ZeroOrMore("p:sldLayoutId") class CT_SlideLayoutIdListEntry(BaseOxmlElement): - """ - ```` element, child of ```` containing - a reference to a slide layout. + """`p:sldLayoutId` element, child of `p:sldLayoutIdLst`. + + Contains a reference to a slide layout. """ - rId = RequiredAttribute("r:id", XsdString) + rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType] class CT_SlideMaster(_BaseSlideElement): - """ - ```` element, root of a slide master part - """ + """`p:sldMaster` element, root of a slide master part.""" + + get_or_add_sldLayoutIdLst: Callable[[], CT_SlideLayoutIdList] _tag_seq = ( "p:cSld", @@ -294,8 +294,10 @@ class CT_SlideMaster(_BaseSlideElement): "p:txStyles", "p:extLst", ) - cSld = OneAndOnlyOne("p:cSld") - sldLayoutIdLst = ZeroOrOne("p:sldLayoutIdLst", successors=_tag_seq[3:]) + cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType] + sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "p:sldLayoutIdLst", successors=_tag_seq[3:] + ) del _tag_seq diff --git a/src/pptx/oxml/table.py b/src/pptx/oxml/table.py index 5b0bd5b6d..cd3e9ebc3 100644 --- a/src/pptx/oxml/table.py +++ b/src/pptx/oxml/table.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Custom element classes for table-related XML elements""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Iterator, cast from pptx.enum.text import MSO_VERTICAL_ANCHOR from pptx.oxml import parse_xml @@ -22,87 +22,95 @@ ) from pptx.util import Emu, lazyproperty +if TYPE_CHECKING: + from pptx.util import Length + class CT_Table(BaseOxmlElement): """`a:tbl` custom element class""" + get_or_add_tblPr: Callable[[], CT_TableProperties] + tr_lst: list[CT_TableRow] + _add_tr: Callable[..., CT_TableRow] + _tag_seq = ("a:tblPr", "a:tblGrid", "a:tr") - tblPr = ZeroOrOne("a:tblPr", successors=_tag_seq[1:]) - tblGrid = OneAndOnlyOne("a:tblGrid") + tblPr: CT_TableProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:tblPr", successors=_tag_seq[1:] + ) + tblGrid: CT_TableGrid = OneAndOnlyOne("a:tblGrid") # pyright: ignore[reportAssignmentType] tr = ZeroOrMore("a:tr", successors=_tag_seq[3:]) del _tag_seq - def add_tr(self, height): - """ - Return a reference to a newly created child element having its - ``h`` attribute set to *height*. - """ + def add_tr(self, height: Length) -> CT_TableRow: + """Return a newly created `a:tr` child element having its `h` attribute set to `height`.""" return self._add_tr(h=height) @property - def bandCol(self): + def bandCol(self) -> bool: return self._get_boolean_property("bandCol") @bandCol.setter - def bandCol(self, value): + def bandCol(self, value: bool): self._set_boolean_property("bandCol", value) @property - def bandRow(self): + def bandRow(self) -> bool: return self._get_boolean_property("bandRow") @bandRow.setter - def bandRow(self, value): + def bandRow(self, value: bool): self._set_boolean_property("bandRow", value) @property - def firstCol(self): + def firstCol(self) -> bool: return self._get_boolean_property("firstCol") @firstCol.setter - def firstCol(self, value): + def firstCol(self, value: bool): self._set_boolean_property("firstCol", value) @property - def firstRow(self): + def firstRow(self) -> bool: return self._get_boolean_property("firstRow") @firstRow.setter - def firstRow(self, value): + def firstRow(self, value: bool): self._set_boolean_property("firstRow", value) - def iter_tcs(self): + def iter_tcs(self) -> Iterator[CT_TableCell]: """Generate each `a:tc` element in this tbl. - tc elements are generated left-to-right, top-to-bottom. + `a:tc` elements are generated left-to-right, top-to-bottom. """ return (tc for tr in self.tr_lst for tc in tr.tc_lst) @property - def lastCol(self): + def lastCol(self) -> bool: return self._get_boolean_property("lastCol") @lastCol.setter - def lastCol(self, value): + def lastCol(self, value: bool): self._set_boolean_property("lastCol", value) @property - def lastRow(self): + def lastRow(self) -> bool: return self._get_boolean_property("lastRow") @lastRow.setter - def lastRow(self, value): + def lastRow(self, value: bool): self._set_boolean_property("lastRow", value) @classmethod - def new_tbl(cls, rows, cols, width, height, tableStyleId=None): - """Return a new ```` element tree.""" + def new_tbl( + cls, rows: int, cols: int, width: int, height: int, tableStyleId: str | None = None + ) -> CT_Table: + """Return a new `p:tbl` element tree.""" # working hypothesis is this is the default table style GUID if tableStyleId is None: tableStyleId = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}" xml = cls._tbl_tmpl() % (tableStyleId) - tbl = parse_xml(xml) + tbl = cast(CT_Table, parse_xml(xml)) # add specified number of rows and columns rowheight = height // rows @@ -112,27 +120,27 @@ def new_tbl(cls, rows, cols, width, height, tableStyleId=None): # adjust width of last col to absorb any div error if col == cols - 1: colwidth = width - ((cols - 1) * colwidth) - tbl.tblGrid.add_gridCol(width=colwidth) + tbl.tblGrid.add_gridCol(width=Emu(colwidth)) for row in range(rows): # adjust height of last row to absorb any div error if row == rows - 1: rowheight = height - ((rows - 1) * rowheight) - tr = tbl.add_tr(height=rowheight) + tr = tbl.add_tr(height=Emu(rowheight)) for col in range(cols): tr.add_tc() return tbl - def tc(self, row_idx, col_idx): - """Return `a:tc` element at *row_idx*, *col_idx*.""" + def tc(self, row_idx: int, col_idx: int) -> CT_TableCell: + """Return `a:tc` element at `row_idx`, `col_idx`.""" return self.tr_lst[row_idx].tc_lst[col_idx] - def _get_boolean_property(self, propname): - """ - Generalized getter for the boolean properties on the ```` - child element. Defaults to False if *propname* attribute is missing - or ```` element itself is not present. + def _get_boolean_property(self, propname: str) -> bool: + """Generalized getter for the boolean properties on the `a:tblPr` child element. + + Defaults to False if `propname` attribute is missing or `a:tblPr` element itself is not + present. """ tblPr = self.tblPr if tblPr is None: @@ -140,19 +148,16 @@ def _get_boolean_property(self, propname): propval = getattr(tblPr, propname) return {True: True, False: False, None: False}[propval] - def _set_boolean_property(self, propname, value): - """ - Generalized setter for boolean properties on the ```` child - element, setting *propname* attribute appropriately based on *value*. - If *value* is True, the attribute is set to "1"; a tblPr child - element is added if necessary. If *value* is False, the *propname* - attribute is removed if present, allowing its default value of False - to be its effective value. + def _set_boolean_property(self, propname: str, value: bool) -> None: + """Generalized setter for boolean properties on the `a:tblPr` child element. + + Sets `propname` attribute appropriately based on `value`. If `value` is True, the + attribute is set to "1"; a tblPr child element is added if necessary. If `value` is False, + the `propname` attribute is removed if present, allowing its default value of False to be + its effective value. """ if value not in (True, False): - raise ValueError( - "assigned value must be either True or False, got %s" % value - ) + raise ValueError("assigned value must be either True or False, got %s" % value) tblPr = self.get_or_add_tblPr() setattr(tblPr, propname, value) @@ -171,43 +176,52 @@ def _tbl_tmpl(cls): class CT_TableCell(BaseOxmlElement): """`a:tc` custom element class""" + get_or_add_tcPr: Callable[[], CT_TableCellProperties] + get_or_add_txBody: Callable[[], CT_TextBody] + _tag_seq = ("a:txBody", "a:tcPr", "a:extLst") - txBody = ZeroOrOne("a:txBody", successors=_tag_seq[1:]) - tcPr = ZeroOrOne("a:tcPr", successors=_tag_seq[2:]) + txBody: CT_TextBody | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:txBody", successors=_tag_seq[1:] + ) + tcPr: CT_TableCellProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:tcPr", successors=_tag_seq[2:] + ) del _tag_seq - gridSpan = OptionalAttribute("gridSpan", XsdInt, default=1) - rowSpan = OptionalAttribute("rowSpan", XsdInt, default=1) - hMerge = OptionalAttribute("hMerge", XsdBoolean, default=False) - vMerge = OptionalAttribute("vMerge", XsdBoolean, default=False) + gridSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "gridSpan", XsdInt, default=1 + ) + rowSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "rowSpan", XsdInt, default=1 + ) + hMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "hMerge", XsdBoolean, default=False + ) + vMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "vMerge", XsdBoolean, default=False + ) @property - def anchor(self): - """ - String held in ``anchor`` attribute of ```` child element of - this ```` element. - """ + def anchor(self) -> MSO_VERTICAL_ANCHOR | None: + """String held in `anchor` attribute of `a:tcPr` child element of this `a:tc` element.""" if self.tcPr is None: return None return self.tcPr.anchor @anchor.setter - def anchor(self, anchor_enum_idx): - """ - Set value of anchor attribute on ```` child element - """ + def anchor(self, anchor_enum_idx: MSO_VERTICAL_ANCHOR | None): + """Set value of anchor attribute on `a:tcPr` child element.""" if anchor_enum_idx is None and self.tcPr is None: return tcPr = self.get_or_add_tcPr() tcPr.anchor = anchor_enum_idx - def append_ps_from(self, spanned_tc): - """Append `a:p` elements taken from *spanned_tc*. + def append_ps_from(self, spanned_tc: CT_TableCell): + """Append `a:p` elements taken from `spanned_tc`. - Any non-empty paragraph elements in *spanned_tc* are removed and - appended to the text-frame of this cell. If *spanned_tc* is left with - no content after this process, a single empty `a:p` element is added - to ensure the cell is compliant with the spec. + Any non-empty paragraph elements in `spanned_tc` are removed and appended to the + text-frame of this cell. If `spanned_tc` is left with no content after this process, a + single empty `a:p` element is added to ensure the cell is compliant with the spec. """ source_txBody = spanned_tc.get_or_add_txBody() target_txBody = self.get_or_add_txBody() @@ -228,94 +242,96 @@ def append_ps_from(self, spanned_tc): target_txBody.unclear_content() @property - def col_idx(self): + def col_idx(self) -> int: """Offset of this cell's column in its table.""" # ---tc elements come before any others in `a:tr` element--- - return self.getparent().index(self) + return cast(CT_TableRow, self.getparent()).index(self) @property - def is_merge_origin(self): + def is_merge_origin(self) -> bool: """True if cell is top-left in merged cell range.""" if self.gridSpan > 1 and not self.vMerge: return True - if self.rowSpan > 1 and not self.hMerge: - return True - return False + return self.rowSpan > 1 and not self.hMerge @property - def is_spanned(self): + def is_spanned(self) -> bool: """True if cell is in merged cell range but not merge origin cell.""" return self.hMerge or self.vMerge @property - def marT(self): - """ - Read/write integer top margin value represented in ``marT`` attribute - of the ```` child element of this ```` element. If the - attribute is not present, the default value ``45720`` (0.05 inches) - is returned for top and bottom; ``91440`` (0.10 inches) is the - default for left and right. Assigning |None| to any ``marX`` - property clears that attribute from the element, effectively setting - it to the default value. + def marT(self) -> Length: + """Top margin for this cell. + + This value is stored in the `marT` attribute of the `a:tcPr` child element of this `a:tc`. + + Read/write. If the attribute is not present, the default value `45720` (0.05 inches) is + returned for top and bottom; `91440` (0.10 inches) is the default for left and right. + Assigning |None| to any `marX` property clears that attribute from the element, + effectively setting it to the default value. """ - return self._get_marX("marT", 45720) + return self._get_marX("marT", Emu(45720)) @marT.setter - def marT(self, value): + def marT(self, value: Length | None): self._set_marX("marT", value) @property - def marR(self): - """ - Right margin value represented in ``marR`` attribute. - """ - return self._get_marX("marR", 91440) + def marR(self) -> Length: + """Right margin value represented in `marR` attribute.""" + return self._get_marX("marR", Emu(91440)) @marR.setter - def marR(self, value): + def marR(self, value: Length | None): self._set_marX("marR", value) @property - def marB(self): - """ - Bottom margin value represented in ``marB`` attribute. - """ - return self._get_marX("marB", 45720) + def marB(self) -> Length: + """Bottom margin value represented in `marB` attribute.""" + return self._get_marX("marB", Emu(45720)) @marB.setter - def marB(self, value): + def marB(self, value: Length | None): self._set_marX("marB", value) @property - def marL(self): - """ - Left margin value represented in ``marL`` attribute. - """ - return self._get_marX("marL", 91440) + def marL(self) -> Length: + """Left margin value represented in `marL` attribute.""" + return self._get_marX("marL", Emu(91440)) @marL.setter - def marL(self, value): + def marL(self, value: Length | None): self._set_marX("marL", value) @classmethod - def new(cls): + def new(cls) -> CT_TableCell: """Return a new `a:tc` element subtree.""" - xml = cls._tc_tmpl() - tc = parse_xml(xml) - return tc + return cast( + CT_TableCell, + parse_xml( + f"\n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f"" + ), + ) @property - def row_idx(self): + def row_idx(self) -> int: """Offset of this cell's row in its table.""" - return self.getparent().row_idx + return cast(CT_TableRow, self.getparent()).row_idx @property - def tbl(self): + def tbl(self) -> CT_Table: """Table element this cell belongs to.""" - return self.xpath("ancestor::a:tbl")[0] + return cast(CT_Table, self.xpath("ancestor::a:tbl")[0]) @property - def text(self): + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] """str text contained in cell""" # ---note this shadows lxml _Element.text--- txBody = self.txBody @@ -323,41 +339,26 @@ def text(self): return "" return "\n".join([p.text for p in txBody.p_lst]) - def _get_marX(self, attr_name, default): - """ - Generalized method to get margin values. - """ + def _get_marX(self, attr_name: str, default: Length) -> Length: + """Generalized method to get margin values.""" if self.tcPr is None: return Emu(default) return Emu(int(self.tcPr.get(attr_name, default))) - def _new_txBody(self): + def _new_txBody(self) -> CT_TextBody: return CT_TextBody.new_a_txBody() - def _set_marX(self, marX, value): - """ - Set value of marX attribute on ```` child element. If *marX* - is |None|, the marX attribute is removed. *marX* is a string, one of - ``('marL', 'marR', 'marT', 'marB')``. + def _set_marX(self, marX: str, value: Length | None) -> None: + """Set value of marX attribute on `a:tcPr` child element. + + If `marX` is |None|, the marX attribute is removed. `marX` is a string, one of `('marL', + 'marR', 'marT', 'marB')`. """ if value is None and self.tcPr is None: return tcPr = self.get_or_add_tcPr() setattr(tcPr, marX, value) - @classmethod - def _tc_tmpl(cls): - return ( - "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "" % nsdecls("a") - ) - class CT_TableCellProperties(BaseOxmlElement): """`a:tcPr` custom element class""" @@ -373,43 +374,47 @@ class CT_TableCellProperties(BaseOxmlElement): ), successors=("a:headers", "a:extLst"), ) - anchor = OptionalAttribute("anchor", MSO_VERTICAL_ANCHOR) - marL = OptionalAttribute("marL", ST_Coordinate32) - marR = OptionalAttribute("marR", ST_Coordinate32) - marT = OptionalAttribute("marT", ST_Coordinate32) - marB = OptionalAttribute("marB", ST_Coordinate32) + anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "anchor", MSO_VERTICAL_ANCHOR + ) + marL: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marL", ST_Coordinate32 + ) + marR: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marR", ST_Coordinate32 + ) + marT: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marT", ST_Coordinate32 + ) + marB: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "marB", ST_Coordinate32 + ) def _new_gradFill(self): return CT_GradientFillProperties.new_gradFill() class CT_TableCol(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:gridCol` custom element class.""" - w = RequiredAttribute("w", ST_Coordinate) + w: Length = RequiredAttribute("w", ST_Coordinate) # pyright: ignore[reportAssignmentType] class CT_TableGrid(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:tblGrid` custom element class.""" + + gridCol_lst: list[CT_TableCol] + _add_gridCol: Callable[..., CT_TableCol] gridCol = ZeroOrMore("a:gridCol") - def add_gridCol(self, width): - """ - Return a reference to a newly created child element - having its ``w`` attribute set to *width*. - """ + def add_gridCol(self, width: Length) -> CT_TableCol: + """A newly appended `a:gridCol` child element having its `w` attribute set to `width`.""" return self._add_gridCol(w=width) class CT_TableProperties(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:tblPr` custom element class.""" bandRow = OptionalAttribute("bandRow", XsdBoolean, default=False) bandCol = OptionalAttribute("bandCol", XsdBoolean, default=False) @@ -420,24 +425,22 @@ class CT_TableProperties(BaseOxmlElement): class CT_TableRow(BaseOxmlElement): - """ - ```` custom element class - """ + """`a:tr` custom element class.""" + + tc_lst: list[CT_TableCell] + _add_tc: Callable[[], CT_TableCell] tc = ZeroOrMore("a:tc", successors=("a:extLst",)) - h = RequiredAttribute("h", ST_Coordinate) + h: Length = RequiredAttribute("h", ST_Coordinate) # pyright: ignore[reportAssignmentType] - def add_tc(self): - """ - Return a reference to a newly added minimal valid ```` child - element. - """ + def add_tc(self) -> CT_TableCell: + """A newly added minimal valid `a:tc` child element.""" return self._add_tc() @property - def row_idx(self): + def row_idx(self) -> int: """Offset of this row in its table.""" - return self.getparent().tr_lst.index(self) + return cast(CT_Table, self.getparent()).tr_lst.index(self) def _new_tc(self): return CT_TableCell.new() @@ -446,21 +449,19 @@ def _new_tc(self): class TcRange(object): """A 2D block of `a:tc` cell elements in a table. - This object assumes the structure of the underlying table does not change - during its lifetime. Structural changes in this context would be - insertion or removal of rows or columns. + This object assumes the structure of the underlying table does not change during its lifetime. + Structural changes in this context would be insertion or removal of rows or columns. - The client is expected to create, use, and then abandon an instance in - the context of a single user operation that is known to have no - structural side-effects of this type. + The client is expected to create, use, and then abandon an instance in the context of a single + user operation that is known to have no structural side-effects of this type. """ - def __init__(self, tc, other_tc): + def __init__(self, tc: CT_TableCell, other_tc: CT_TableCell): self._tc = tc self._other_tc = other_tc @classmethod - def from_merge_origin(cls, tc): + def from_merge_origin(cls, tc: CT_TableCell): """Return instance created from merge-origin tc element.""" other_tc = tc.tbl.tc( tc.row_idx + tc.rowSpan - 1, # ---other_row_idx @@ -469,7 +470,7 @@ def from_merge_origin(cls, tc): return cls(tc, other_tc) @lazyproperty - def contains_merged_cell(self): + def contains_merged_cell(self) -> bool: """True if one or more cells in range are part of a merged cell.""" for tc in self.iter_tcs(): if tc.gridSpan > 1: @@ -483,7 +484,7 @@ def contains_merged_cell(self): return False @lazyproperty - def dimensions(self): + def dimensions(self) -> tuple[int, int]: """(row_count, col_count) pair describing size of range.""" _, _, width, height = self._extents return height, width @@ -544,16 +545,15 @@ def _bottom(self): return top + height @lazyproperty - def _extents(self): + def _extents(self) -> tuple[int, int, int, int]: """A (left, top, width, height) tuple describing range extents. - Note this is normalized to accommodate the various orderings of the - corner cells provided on construction, which may be in any of four - configurations such as (top-left, bottom-right), - (bottom-left, top-right), etc. + Note this is normalized to accommodate the various orderings of the corner cells provided + on construction, which may be in any of four configurations such as (top-left, + bottom-right), (bottom-left, top-right), etc. """ - def start_and_size(idx, other_idx): + def start_and_size(idx: int, other_idx: int) -> tuple[int, int]: """Return beginning and length of range based on two indexes.""" return min(idx, other_idx), abs(idx - other_idx) + 1 @@ -566,23 +566,23 @@ def start_and_size(idx, other_idx): @lazyproperty def _left(self): - """Index of leftmost column in range""" + """Index of leftmost column in range.""" left, _, _, _ = self._extents return left @lazyproperty def _right(self): - """Index of column following the last column in range""" + """Index of column following the last column in range.""" left, _, width, _ = self._extents return left + width @lazyproperty def _tbl(self): - """`a:tbl` element containing this cell range""" + """`a:tbl` element containing this cell range.""" return self._tc.tbl @lazyproperty def _top(self): - """Index of topmost row in range""" + """Index of topmost row in range.""" _, top, _, _ = self._extents return top diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index ced0f8088..0f9ecc152 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -1,12 +1,10 @@ -# encoding: utf-8 - """Custom element classes for text-related XML elements""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import re +from typing import TYPE_CHECKING, Callable, cast -from pptx.compat import to_unicode from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import ( MSO_AUTO_SIZE, @@ -42,27 +40,33 @@ ) from pptx.util import Emu, Length +if TYPE_CHECKING: + from pptx.oxml.action import CT_Hyperlink + class CT_RegularTextRun(BaseOxmlElement): """`a:r` custom element class""" - rPr = ZeroOrOne("a:rPr", successors=("a:t",)) - t = OneAndOnlyOne("a:t") + get_or_add_rPr: Callable[[], CT_TextCharacterProperties] + + rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:rPr", successors=("a:t",) + ) + t: BaseOxmlElement = OneAndOnlyOne("a:t") # pyright: ignore[reportAssignmentType] @property - def text(self): - """(unicode) str containing text of (required) `a:t` child""" + def text(self) -> str: + """All text of (required) `a:t` child.""" text = self.t.text - # t.text is None when t element is empty, e.g. '' - return to_unicode(text) if text is not None else "" + # -- t.text is None when t element is empty, e.g. '' -- + return text or "" @text.setter - def text(self, str): - """*str* is unicode value to replace run text.""" - self.t.text = self._escape_ctrl_chars(str) + def text(self, value: str): # pyright: ignore[reportIncompatibleMethodOverride] + self.t.text = self._escape_ctrl_chars(value) @staticmethod - def _escape_ctrl_chars(s): + def _escape_ctrl_chars(s: str) -> str: """Return str after replacing each control character with a plain-text escape. For example, a BEL character (x07) would appear as "_x0007_". Horizontal-tab @@ -78,8 +82,13 @@ class CT_TextBody(BaseOxmlElement): Also used for `c:txPr` in charts and perhaps other elements. """ - bodyPr = OneAndOnlyOne("a:bodyPr") - p = OneOrMore("a:p") + add_p: Callable[[], CT_TextParagraph] + p_lst: list[CT_TextParagraph] + + bodyPr: CT_TextBodyProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType] + "a:bodyPr" + ) + p: CT_TextParagraph = OneOrMore("a:p") # pyright: ignore[reportAssignmentType] def clear_content(self): """Remove all `a:p` children, but leave any others. @@ -90,12 +99,11 @@ def clear_content(self): self.remove(p) @property - def defRPr(self): - """ - ```` element of required first ``p`` child, added with its - ancestors if not present. Used when element is a ```` in - a chart and the ``p`` element is used only to specify formatting, not - content. + def defRPr(self) -> CT_TextCharacterProperties: + """`a:defRPr` element of required first `p` child, added with its ancestors if not present. + + Used when element is a ``c:txPr`` in a chart and the `p` element is used only to specify + formatting, not content. """ p = self.p_lst[0] pPr = p.get_or_add_pPr() @@ -103,7 +111,7 @@ def defRPr(self): return defRPr @property - def is_empty(self): + def is_empty(self) -> bool: """True if only a single empty `a:p` element is present.""" ps = self.p_lst if len(ps) > 1: @@ -118,37 +126,32 @@ def is_empty(self): @classmethod def new(cls): - """ - Return a new ```` element tree - """ + """Return a new `p:txBody` element tree.""" xml = cls._txBody_tmpl() txBody = parse_xml(xml) return txBody @classmethod - def new_a_txBody(cls): - """ - Return a new ```` element tree, suitable for use in a table - cell and possibly other situations. + def new_a_txBody(cls) -> CT_TextBody: + """Return a new `a:txBody` element tree. + + Suitable for use in a table cell and possibly other situations. """ xml = cls._a_txBody_tmpl() - txBody = parse_xml(xml) + txBody = cast(CT_TextBody, parse_xml(xml)) return txBody @classmethod def new_p_txBody(cls): - """ - Return a new ```` element tree, suitable for use in an - ```` element. - """ + """Return a new `p:txBody` element tree, suitable for use in an `p:sp` element.""" xml = cls._p_txBody_tmpl() return parse_xml(xml) @classmethod def new_txPr(cls): - """ - Return a ```` element tree suitable for use in a chart object - like data labels or tick labels. + """Return a `c:txPr` element tree. + + Suitable for use in a chart object like data labels or tick labels. """ xml = ( "\n" @@ -167,8 +170,8 @@ def new_txPr(cls): def unclear_content(self): """Ensure p:txBody has at least one a:p child. - Intuitively, reverse a ".clear_content()" operation to minimum - conformance with spec (single empty paragraph). + Intuitively, reverse a ".clear_content()" operation to minimum conformance with spec + (single empty paragraph). """ if len(self.p_lst) > 0: return @@ -196,27 +199,43 @@ def _txBody_tmpl(cls): class CT_TextBodyProperties(BaseOxmlElement): - """ - custom element class - """ + """`a:bodyPr` custom element class.""" + + _add_noAutofit: Callable[[], BaseOxmlElement] + _add_normAutofit: Callable[[], CT_TextNormalAutofit] + _add_spAutoFit: Callable[[], BaseOxmlElement] + _remove_eg_textAutoFit: Callable[[], None] + + noAutofit: BaseOxmlElement | None + normAutofit: CT_TextNormalAutofit | None + spAutoFit: BaseOxmlElement | None eg_textAutoFit = ZeroOrOneChoice( (Choice("a:noAutofit"), Choice("a:normAutofit"), Choice("a:spAutoFit")), successors=("a:scene3d", "a:sp3d", "a:flatTx", "a:extLst"), ) - lIns = OptionalAttribute("lIns", ST_Coordinate32, default=Emu(91440)) - tIns = OptionalAttribute("tIns", ST_Coordinate32, default=Emu(45720)) - rIns = OptionalAttribute("rIns", ST_Coordinate32, default=Emu(91440)) - bIns = OptionalAttribute("bIns", ST_Coordinate32, default=Emu(45720)) - anchor = OptionalAttribute("anchor", MSO_VERTICAL_ANCHOR) - wrap = OptionalAttribute("wrap", ST_TextWrappingType) + lIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "lIns", ST_Coordinate32, default=Emu(91440) + ) + tIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "tIns", ST_Coordinate32, default=Emu(45720) + ) + rIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "rIns", ST_Coordinate32, default=Emu(91440) + ) + bIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "bIns", ST_Coordinate32, default=Emu(45720) + ) + anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "anchor", MSO_VERTICAL_ANCHOR + ) + wrap: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "wrap", ST_TextWrappingType + ) @property def autofit(self): - """ - The autofit setting for the text frame, a member of the - ``MSO_AUTO_SIZE`` enumeration. - """ + """The autofit setting for the text frame, a member of the `MSO_AUTO_SIZE` enumeration.""" if self.noAutofit is not None: return MSO_AUTO_SIZE.NONE if self.normAutofit is not None: @@ -226,11 +245,11 @@ def autofit(self): return None @autofit.setter - def autofit(self, value): + def autofit(self, value: MSO_AUTO_SIZE | None): if value is not None and value not in MSO_AUTO_SIZE: raise ValueError( - "only None or a member of the MSO_AUTO_SIZE enumeration can " - "be assigned to CT_TextBodyProperties.autofit, got %s" % value + f"only None or a member of the MSO_AUTO_SIZE enumeration can be assigned to" + f" CT_TextBodyProperties.autofit, got {value}" ) self._remove_eg_textAutoFit() if value == MSO_AUTO_SIZE.NONE: @@ -242,12 +261,16 @@ def autofit(self, value): class CT_TextCharacterProperties(BaseOxmlElement): - """`a:rPr, a:defRPr, and `a:endParaRPr` custom element class. + """Custom element class for `a:rPr`, `a:defRPr`, and `a:endParaRPr`. - 'rPr' is short for 'run properties', and it corresponds to the |Font| - proxy class. + 'rPr' is short for 'run properties', and it corresponds to the |Font| proxy class. """ + get_or_add_hlinkClick: Callable[[], CT_Hyperlink] + get_or_add_latin: Callable[[], CT_TextFont] + _remove_latin: Callable[[], None] + _remove_hlinkClick: Callable[[], None] + eg_fillProperties = ZeroOrOneChoice( ( Choice("a:noFill"), @@ -275,7 +298,7 @@ class CT_TextCharacterProperties(BaseOxmlElement): "a:extLst", ), ) - latin = ZeroOrOne( + latin: CT_TextFont | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "a:latin", successors=( "a:ea", @@ -287,62 +310,73 @@ class CT_TextCharacterProperties(BaseOxmlElement): "a:extLst", ), ) - hlinkClick = ZeroOrOne("a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst")) + hlinkClick: CT_Hyperlink | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst") + ) - lang = OptionalAttribute("lang", MSO_LANGUAGE_ID) - sz = OptionalAttribute("sz", ST_TextFontSize) - b = OptionalAttribute("b", XsdBoolean) - i = OptionalAttribute("i", XsdBoolean) - u = OptionalAttribute("u", MSO_TEXT_UNDERLINE_TYPE) + lang: MSO_LANGUAGE_ID | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "lang", MSO_LANGUAGE_ID + ) + sz: int | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "sz", ST_TextFontSize + ) + b: bool | None = OptionalAttribute("b", XsdBoolean) # pyright: ignore[reportAssignmentType] + i: bool | None = OptionalAttribute("i", XsdBoolean) # pyright: ignore[reportAssignmentType] + u: MSO_TEXT_UNDERLINE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "u", MSO_TEXT_UNDERLINE_TYPE + ) def _new_gradFill(self): return CT_GradientFillProperties.new_gradFill() - def add_hlinkClick(self, rId): - """ - Add an child element with r:id attribute set to *rId*. - """ + def add_hlinkClick(self, rId: str) -> CT_Hyperlink: + """Add an `a:hlinkClick` child element with r:id attribute set to `rId`.""" hlinkClick = self.get_or_add_hlinkClick() hlinkClick.rId = rId return hlinkClick class CT_TextField(BaseOxmlElement): - """ - field element, for either a slide number or date field - """ + """`a:fld` field element, for either a slide number or date field.""" - rPr = ZeroOrOne("a:rPr", successors=("a:pPr", "a:t")) - t = ZeroOrOne("a:t", successors=()) + get_or_add_rPr: Callable[[], CT_TextCharacterProperties] + + rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:rPr", successors=("a:pPr", "a:t") + ) + t: BaseOxmlElement | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:t", successors=() + ) @property - def text(self): - """ - The text of the ```` child element. - """ + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] + """The text of the `a:t` child element.""" t = self.t if t is None: return "" - text = t.text - return to_unicode(text) if text is not None else "" + return t.text or "" class CT_TextFont(BaseOxmlElement): - """ - Custom element class for , , , and child - elements of CT_TextCharacterProperties, e.g. . + """Custom element class for `a:latin`, `a:ea`, `a:cs`, and `a:sym`. + + These occur as child elements of CT_TextCharacterProperties, e.g. `a:rPr`. """ - typeface = RequiredAttribute("typeface", ST_TextTypeface) + typeface: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "typeface", ST_TextTypeface + ) class CT_TextLineBreak(BaseOxmlElement): """`a:br` line break element""" + get_or_add_rPr: Callable[[], CT_TextCharacterProperties] + rPr = ZeroOrOne("a:rPr", successors=()) @property - def text(self): + def text(self): # pyright: ignore[reportIncompatibleMethodOverride] """Unconditionally a single vertical-tab character. A line break element can contain no text other than the implicit line feed it @@ -352,9 +386,7 @@ def text(self): class CT_TextNormalAutofit(BaseOxmlElement): - """ - element specifying fit text to shape font reduction, etc. - """ + """`a:normAutofit` element specifying fit text to shape font reduction, etc.""" fontScale = OptionalAttribute( "fontScale", ST_TextFontScalePercentOrPercentString, default=100.0 @@ -364,36 +396,42 @@ class CT_TextNormalAutofit(BaseOxmlElement): class CT_TextParagraph(BaseOxmlElement): """`a:p` custom element class""" - pPr = ZeroOrOne("a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr")) + get_or_add_endParaRPr: Callable[[], CT_TextCharacterProperties] + get_or_add_pPr: Callable[[], CT_TextParagraphProperties] + r_lst: list[CT_RegularTextRun] + _add_br: Callable[[], CT_TextLineBreak] + _add_r: Callable[[], CT_RegularTextRun] + + pPr: CT_TextParagraphProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr") + ) r = ZeroOrMore("a:r", successors=("a:endParaRPr",)) br = ZeroOrMore("a:br", successors=("a:endParaRPr",)) - endParaRPr = ZeroOrOne("a:endParaRPr", successors=()) + endParaRPr: CT_TextCharacterProperties | None = ZeroOrOne( + "a:endParaRPr", successors=() + ) # pyright: ignore[reportAssignmentType] - def add_br(self): - """ - Return a newly appended element. - """ + def add_br(self) -> CT_TextLineBreak: + """Return a newly appended `a:br` element.""" return self._add_br() - def add_r(self, text=None): - """ - Return a newly appended element. - """ + def add_r(self, text: str | None = None) -> CT_RegularTextRun: + """Return a newly appended `a:r` element.""" r = self._add_r() if text: r.text = text return r - def append_text(self, text): - """Append `a:r` and `a:br` elements to *p* based on *text*. + def append_text(self, text: str): + """Append `a:r` and `a:br` elements to `p` based on `text`. - Any `\n` or `\v` (vertical-tab) characters in *text* delimit `a:r` (run) - elements and themselves are translated to `a:br` (line-break) elements. The - vertical-tab character appears in clipboard text from PowerPoint at "soft" - line-breaks (new-line, but not new paragraph). + Any `\n` or `\v` (vertical-tab) characters in `text` delimit `a:r` (run) elements and + themselves are translated to `a:br` (line-break) elements. The vertical-tab character + appears in clipboard text from PowerPoint at "soft" line-breaks (new-line, but not new + paragraph). """ for idx, r_str in enumerate(re.split("\n|\v", text)): - # ---breaks are only added *between* items, not at start--- + # ---breaks are only added _between_ items, not at start--- if idx > 0: self.add_br() # ---runs that would be empty are not added--- @@ -401,16 +439,17 @@ def append_text(self, text): self.add_r(r_str) @property - def content_children(self): + def content_children(self) -> tuple[CT_RegularTextRun | CT_TextLineBreak | CT_TextField, ...]: """Sequence containing text-container child elements of this `a:p` element. These include `a:r`, `a:br`, and `a:fld`. """ - text_types = {CT_RegularTextRun, CT_TextLineBreak, CT_TextField} - return tuple(elm for elm in self if type(elm) in text_types) + return tuple( + e for e in self if isinstance(e, (CT_RegularTextRun, CT_TextLineBreak, CT_TextField)) + ) @property - def text(self): + def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride] """str text contained in this paragraph.""" # ---note this shadows the lxml _Element.text--- return "".join([child.text for child in self.content_children]) @@ -421,9 +460,15 @@ def _new_r(self): class CT_TextParagraphProperties(BaseOxmlElement): - """ - custom element class - """ + """`a:pPr` custom element class.""" + + get_or_add_defRPr: Callable[[], CT_TextCharacterProperties] + _add_lnSpc: Callable[[], CT_TextSpacing] + _add_spcAft: Callable[[], CT_TextSpacing] + _add_spcBef: Callable[[], CT_TextSpacing] + _remove_lnSpc: Callable[[], None] + _remove_spcAft: Callable[[], None] + _remove_spcBef: Callable[[], None] _tag_seq = ( "a:lnSpc", @@ -444,31 +489,43 @@ class CT_TextParagraphProperties(BaseOxmlElement): "a:defRPr", "a:extLst", ) - lnSpc = ZeroOrOne("a:lnSpc", successors=_tag_seq[1:]) - spcBef = ZeroOrOne("a:spcBef", successors=_tag_seq[2:]) - spcAft = ZeroOrOne("a:spcAft", successors=_tag_seq[3:]) - defRPr = ZeroOrOne("a:defRPr", successors=_tag_seq[16:]) - lvl = OptionalAttribute("lvl", ST_TextIndentLevelType, default=0) - algn = OptionalAttribute("algn", PP_PARAGRAPH_ALIGNMENT) + lnSpc: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:lnSpc", successors=_tag_seq[1:] + ) + spcBef: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcBef", successors=_tag_seq[2:] + ) + spcAft: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcAft", successors=_tag_seq[3:] + ) + defRPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:defRPr", successors=_tag_seq[16:] + ) + lvl: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "lvl", ST_TextIndentLevelType, default=0 + ) + algn: PP_PARAGRAPH_ALIGNMENT | None = OptionalAttribute( + "algn", PP_PARAGRAPH_ALIGNMENT + ) # pyright: ignore[reportAssignmentType] del _tag_seq @property - def line_spacing(self): - """ - The spacing between baselines of successive lines in this paragraph. - A float value indicates a number of lines. A |Length| value indicates - a fixed spacing. Value is contained in `./a:lnSpc/a:spcPts/@val` or - `./a:lnSpc/a:spcPct/@val`. Value is |None| if no element is present. + def line_spacing(self) -> float | Length | None: + """The spacing between baselines of successive lines in this paragraph. + + A float value indicates a number of lines. A |Length| value indicates a fixed spacing. + Value is contained in `./a:lnSpc/a:spcPts/@val` or `./a:lnSpc/a:spcPct/@val`. Value is + |None| if no element is present. """ lnSpc = self.lnSpc if lnSpc is None: return None if lnSpc.spcPts is not None: return lnSpc.spcPts.val - return lnSpc.spcPct.val + return cast(CT_TextSpacingPercent, lnSpc.spcPct).val @line_spacing.setter - def line_spacing(self, value): + def line_spacing(self, value: float | Length | None): self._remove_lnSpc() if value is None: return @@ -478,11 +535,8 @@ def line_spacing(self, value): self._add_lnSpc().set_spcPct(value) @property - def space_after(self): - """ - The EMU equivalent of the centipoints value in - `./a:spcAft/a:spcPts/@val`. - """ + def space_after(self) -> Length | None: + """The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`.""" spcAft = self.spcAft if spcAft is None: return None @@ -492,17 +546,14 @@ def space_after(self): return spcPts.val @space_after.setter - def space_after(self, value): + def space_after(self, value: Length | None): self._remove_spcAft() if value is not None: self._add_spcAft().set_spcPts(value) @property def space_before(self): - """ - The EMU equivalent of the centipoints value in - `./a:spcBef/a:spcPts/@val`. - """ + """The EMU equivalent of the centipoints value in `./a:spcBef/a:spcPts/@val`.""" spcBef = self.spcBef if spcBef is None: return None @@ -512,54 +563,56 @@ def space_before(self): return spcPts.val @space_before.setter - def space_before(self, value): + def space_before(self, value: Length | None): self._remove_spcBef() if value is not None: self._add_spcBef().set_spcPts(value) class CT_TextSpacing(BaseOxmlElement): - """ - Used for , , and elements. - """ + """Used for `a:lnSpc`, `a:spcBef`, and `a:spcAft` elements.""" + + get_or_add_spcPct: Callable[[], CT_TextSpacingPercent] + get_or_add_spcPts: Callable[[], CT_TextSpacingPoint] + _remove_spcPct: Callable[[], None] + _remove_spcPts: Callable[[], None] # this should actually be a OneAndOnlyOneChoice, but that's not # implemented yet. - spcPct = ZeroOrOne("a:spcPct") - spcPts = ZeroOrOne("a:spcPts") + spcPct: CT_TextSpacingPercent | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcPct" + ) + spcPts: CT_TextSpacingPoint | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:spcPts" + ) - def set_spcPct(self, value): - """ - Set spacing to *value* lines, e.g. 1.75 lines. A ./a:spcPts child is - removed if present. + def set_spcPct(self, value: float): + """Set spacing to `value` lines, e.g. 1.75 lines. + + A ./a:spcPts child is removed if present. """ self._remove_spcPts() spcPct = self.get_or_add_spcPct() spcPct.val = value - def set_spcPts(self, value): - """ - Set spacing to *value* points. A ./a:spcPct child is removed if - present. - """ + def set_spcPts(self, value: Length): + """Set spacing to `value` points. A ./a:spcPct child is removed if present.""" self._remove_spcPct() spcPts = self.get_or_add_spcPts() spcPts.val = value class CT_TextSpacingPercent(BaseOxmlElement): - """ - element, specifying spacing in thousandths of a percent in its - `val` attribute. - """ + """`a:spcPct` element, specifying spacing in thousandths of a percent in its `val` attribute.""" - val = RequiredAttribute("val", ST_TextSpacingPercentOrPercentString) + val: float = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "val", ST_TextSpacingPercentOrPercentString + ) class CT_TextSpacingPoint(BaseOxmlElement): - """ - element, specifying spacing in centipoints in its `val` - attribute. - """ + """`a:spcPts` element, specifying spacing in centipoints in its `val` attribute.""" - val = RequiredAttribute("val", ST_TextSpacingPoint) + val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "val", ST_TextSpacingPoint + ) diff --git a/src/pptx/oxml/theme.py b/src/pptx/oxml/theme.py index 9e3737311..19ac8dea6 100644 --- a/src/pptx/oxml/theme.py +++ b/src/pptx/oxml/theme.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""lxml custom element classes for theme-related XML elements.""" -""" -lxml custom element classes for theme-related XML elements. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from . import parse_from_template from .xmlchemy import BaseOxmlElement diff --git a/src/pptx/oxml/xmlchemy.py b/src/pptx/oxml/xmlchemy.py index b84ef4ddb..41fb2e171 100644 --- a/src/pptx/oxml/xmlchemy.py +++ b/src/pptx/oxml/xmlchemy.py @@ -1,36 +1,49 @@ -# encoding: utf-8 +"""Base and meta classes enabling declarative definition of custom element classes.""" -""" -Base and meta classes that enable declarative definition of custom element -classes. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import re +from typing import Any, Callable, Iterable, Protocol, Sequence, Type, cast from lxml import etree +from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage] + +from pptx.exc import InvalidXmlError +from pptx.oxml import oxml_parser +from pptx.oxml.ns import NamespacePrefixedTag, _nsmap, qn # pyright: ignore[reportPrivateUsage] +from pptx.util import lazyproperty -from . import oxml_parser -from ..compat import Unicode -from ..exc import InvalidXmlError -from .ns import NamespacePrefixedTag, _nsmap, qn -from ..util import lazyproperty +class AttributeType(Protocol): + """Interface for an object that can act as an attribute type. -def OxmlElement(nsptag_str, nsmap=None): + An attribute-type specifies how values are transformed to and from the XML "string" value of the + attribute. """ - Return a 'loose' lxml element having the tag specified by *nsptag_str*. - *nsptag_str* must contain the standard namespace prefix, e.g. 'a:tbl'. - The resulting element is an instance of the custom element class for this - tag name if one is defined. + + @classmethod + def from_xml(cls, xml_value: str) -> Any: + """Transform an attribute value to a Python value.""" + ... + + @classmethod + def to_xml(cls, value: Any) -> str: + """Transform a Python value to a str value suitable to this XML attribute.""" + ... + + +def OxmlElement(nsptag_str: str, nsmap: dict[str, str] | None = None) -> BaseOxmlElement: + """Return a "loose" lxml element having the tag specified by `nsptag_str`. + + `nsptag_str` must contain the standard namespace prefix, e.g. 'a:tbl'. The resulting element is + an instance of the custom element class for this tag name if one is defined. """ nsptag = NamespacePrefixedTag(nsptag_str) nsmap = nsmap if nsmap is not None else nsptag.nsmap return oxml_parser.makeelement(nsptag.clark_name, nsmap=nsmap) -def serialize_for_reading(element): +def serialize_for_reading(element: ElementBase): """ Serialize *element* to human-readable XML suitable for tests. No XML declaration. @@ -39,11 +52,8 @@ def serialize_for_reading(element): return XmlString(xml) -class XmlString(Unicode): - """ - Provides string comparison override suitable for serialized XML that is - useful for tests. - """ +class XmlString(str): + """Provides string comparison override suitable for serialized XML; useful for tests.""" # ' text' # | | || | @@ -53,7 +63,9 @@ class XmlString(Unicode): _xml_elm_line_patt = re.compile(r"( *)([^<]*)?") - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, str): + return False lines = self.splitlines() lines_other = other.splitlines() if len(lines) != len(lines_other): @@ -63,22 +75,22 @@ def __eq__(self, other): return False return True - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not self.__eq__(other) - def _attr_seq(self, attrs): - """ - Return a sequence of attribute strings parsed from *attrs*. Each - attribute string is stripped of whitespace on both ends. + def _attr_seq(self, attrs: str) -> list[str]: + """Return a sequence of attribute strings parsed from *attrs*. + + Each attribute string is stripped of whitespace on both ends. """ attrs = attrs.strip() attr_lst = attrs.split() return sorted(attr_lst) - def _eq_elm_strs(self, line, line_2): - """ - Return True if the element in *line_2* is XML equivalent to the - element in *line*. + def _eq_elm_strs(self, line: str, line_2: str) -> bool: + """True if the element in `line_2` is XML-equivalent to the element in `line`. + + In particular, the order of attributes in XML is not significant. """ front, attrs, close, text = self._parse_line(line) front_2, attrs_2, close_2, text_2 = self._parse_line(line_2) @@ -92,22 +104,19 @@ def _eq_elm_strs(self, line, line_2): return False return True - def _parse_line(self, line): - """ - Return front, attrs, close, text 4-tuple result of parsing XML element - string *line*. - """ + def _parse_line(self, line: str): + """Return front, attrs, close, text 4-tuple result of parsing XML element string `line`.""" match = self._xml_elm_line_patt.match(line) + if match is None: + raise ValueError("`line` does not match pattern for an XML element") front, attrs, close, text = [match.group(n) for n in range(1, 5)] return front, attrs, close, text class MetaOxmlElement(type): - """ - Metaclass for BaseOxmlElement - """ + """Metaclass for BaseOxmlElement.""" - def __init__(cls, clsname, bases, clsdict): + def __init__(cls, clsname: str, bases: tuple[type, ...], clsdict: dict[str, Any]): dispatchable = ( OneAndOnlyOne, OneOrMore, @@ -122,18 +131,14 @@ def __init__(cls, clsname, bases, clsdict): value.populate_class_members(cls, key) -class BaseAttribute(object): - """ - Base class for OptionalAttribute and RequiredAttribute, providing common - methods. - """ +class BaseAttribute: + """Base class for OptionalAttribute and RequiredAttribute, providing common methods.""" - def __init__(self, attr_name, simple_type): - super(BaseAttribute, self).__init__() + def __init__(self, attr_name: str, simple_type: type[AttributeType]): self._attr_name = attr_name self._simple_type = simple_type - def populate_class_members(self, element_cls, prop_name): + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): """ Add the appropriate methods to *element_cls*. """ @@ -143,10 +148,10 @@ def populate_class_members(self, element_cls, prop_name): self._add_attr_property() def _add_attr_property(self): - """ - Add a read/write ``{prop_name}`` property to the element class that - returns the interpreted value of this attribute on access and changes - the attribute value to its ST_* counterpart on assignment. + """Add a read/write `{prop_name}` property to the element class. + + The property returns the interpreted value of this attribute on access and changes the + attribute value to its ST_* counterpart on assignment. """ property_ = property(self._getter, self._setter, None) # assign unconditionally to overwrite element name definition @@ -158,15 +163,25 @@ def _clark_name(self): return qn(self._attr_name) return self._attr_name + @property + def _getter(self) -> Callable[[BaseOxmlElement], Any]: + """Callable suitable for the "get" side of the attribute property descriptor.""" + raise NotImplementedError("must be implemented by each subclass") + + @property + def _setter(self) -> Callable[[BaseOxmlElement, Any], None]: + """Callable suitable for the "set" side of the attribute property descriptor.""" + raise NotImplementedError("must be implemented by each subclass") + class OptionalAttribute(BaseAttribute): - """ - Defines an optional attribute on a custom element class. An optional - attribute returns a default value when not present for reading. When - assigned |None|, the attribute is removed. + """Defines an optional attribute on a custom element class. + + An optional attribute returns a default value when not present for reading. When assigned + |None|, the attribute is removed. """ - def __init__(self, attr_name, simple_type, default=None): + def __init__(self, attr_name: str, simple_type: type[AttributeType], default: Any = None): super(OptionalAttribute, self).__init__(attr_name, simple_type) self._default = default @@ -184,13 +199,10 @@ def _docstring(self): ) @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the attribute - property descriptor. - """ + def _getter(self) -> Callable[[BaseOxmlElement], Any]: + """Callable suitable for the "get" side of the attribute property descriptor.""" - def get_attr_value(obj): + def get_attr_value(obj: BaseOxmlElement) -> Any: attr_str_value = obj.get(self._clark_name) if attr_str_value is None: return self._default @@ -200,13 +212,12 @@ def get_attr_value(obj): return get_attr_value @property - def _setter(self): - """ - Return a function object suitable for the "set" side of the attribute - property descriptor. - """ + def _setter(self) -> Callable[[BaseOxmlElement, Any], None]: + """Callable suitable for the "set" side of the attribute property descriptor.""" - def set_attr_value(obj, value): + def set_attr_value(obj: BaseOxmlElement, value: Any) -> None: + # -- when an XML attribute has a default value, setting it to that default removes the + # -- attribute from the element (when it is present) if value == self._default: if self._clark_name in obj.attrib: del obj.attrib[self._clark_name] @@ -218,28 +229,23 @@ def set_attr_value(obj, value): class RequiredAttribute(BaseAttribute): - """ - Defines a required attribute on a custom element class. A required - attribute is assumed to be present for reading, so does not have - a default value; its actual value is always used. If missing on read, - an |InvalidXmlError| is raised. It also does not remove the attribute if - |None| is assigned. Assigning |None| raises |TypeError| or |ValueError|, - depending on the simple type of the attribute. + """Defines a required attribute on a custom element class. + + A required attribute is assumed to be present for reading, so does not have a default value; + its actual value is always used. If missing on read, an |InvalidXmlError| is raised. It also + does not remove the attribute if |None| is assigned. Assigning |None| raises |TypeError| or + |ValueError|, depending on the simple type of the attribute. """ @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the attribute - property descriptor. - """ + def _getter(self) -> Callable[[BaseOxmlElement], Any]: + """Callable suitable for the "get" side of the attribute property descriptor.""" - def get_attr_value(obj): + def get_attr_value(obj: BaseOxmlElement) -> Any: attr_str_value = obj.get(self._clark_name) if attr_str_value is None: raise InvalidXmlError( - "required '%s' attribute not present on element %s" - % (self._attr_name, obj.tag) + "required '%s' attribute not present on element %s" % (self._attr_name, obj.tag) ) return self._simple_type.from_xml(attr_str_value) @@ -258,45 +264,36 @@ def _docstring(self): ) @property - def _setter(self): - """ - Return a function object suitable for the "set" side of the attribute - property descriptor. - """ + def _setter(self) -> Callable[[BaseOxmlElement, Any], None]: + """Callable suitable for the "set" side of the attribute property descriptor.""" - def set_attr_value(obj, value): + def set_attr_value(obj: BaseOxmlElement, value: Any) -> None: str_value = self._simple_type.to_xml(value) obj.set(self._clark_name, str_value) return set_attr_value -class _BaseChildElement(object): - """ - Base class for the child element classes corresponding to varying - cardinalities, such as ZeroOrOne and ZeroOrMore. +class _BaseChildElement: + """Base class for the child element classes corresponding to varying cardinalities. + + Subclasses include ZeroOrOne and ZeroOrMore. """ - def __init__(self, nsptagname, successors=()): + def __init__(self, nsptagname: str, successors: Sequence[str] = ()): super(_BaseChildElement, self).__init__() self._nsptagname = nsptagname self._successors = successors - def populate_class_members(self, element_cls, prop_name): - """ - Baseline behavior for adding the appropriate methods to - *element_cls*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """Baseline behavior for adding the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._prop_name = prop_name def _add_adder(self): - """ - Add an ``_add_x()`` method to the element class for this child - element. - """ + """Add an ``_add_x()`` method to the element class for this child element.""" - def _add_child(obj, **attrs): + def _add_child(obj: BaseOxmlElement, **attrs: Any): new_method = getattr(obj, self._new_method_name) child = new_method() for key, value in attrs.items(): @@ -312,9 +309,9 @@ def _add_child(obj, **attrs): self._add_to_class(self._add_method_name, _add_child) def _add_creator(self): - """ - Add a ``_new_{prop_name}()`` method to the element class that creates - a new, empty element of the correct type, having no attributes. + """Add a `_new_{prop_name}()` method to the element class. + + This method creates a new, empty element of the correct type, having no attributes. """ creator = self._creator creator.__doc__ = ( @@ -324,21 +321,18 @@ def _add_creator(self): self._add_to_class(self._new_method_name, creator) def _add_getter(self): - """ - Add a read-only ``{prop_name}`` property to the element class for - this child element. + """Add a read-only `{prop_name}` property to the parent element class. + + The property locates and returns this child element or `None` if not present. """ property_ = property(self._getter, None, None) # assign unconditionally to overwrite element name definition setattr(self._element_cls, self._prop_name, property_) def _add_inserter(self): - """ - Add an ``_insert_x()`` method to the element class for this child - element. - """ + """Add an ``_insert_x()`` method to the element class for this child element.""" - def _insert_child(obj, child): + def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement): obj.insert_element_before(child, *self._successors) return child @@ -353,7 +347,7 @@ def _add_list_getter(self): Add a read-only ``{prop_name}_lst`` property to the element class to retrieve a list of child elements matching this type. """ - prop_name = "%s_lst" % self._prop_name + prop_name = f"{self._prop_name}_lst" property_ = property(self._list_getter, None, None) setattr(self._element_cls, prop_name, property_) @@ -361,36 +355,30 @@ def _add_list_getter(self): def _add_method_name(self): return "_add_%s" % self._prop_name - def _add_to_class(self, name, method): - """ - Add *method* to the target class as *name*, unless *name* is already - defined on the class. - """ + def _add_to_class(self, name: str, method: Callable[..., Any]): + """Add `method` to the target class as `name`, unless `name` is already defined there.""" if hasattr(self._element_cls, name): return setattr(self._element_cls, name, method) @property - def _creator(self): - """ - Return a function object that creates a new, empty element of the - right type, having no attributes. - """ + def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]: + """Callable that creates a new, empty element of the child type, having no attributes.""" - def new_child_element(obj): + def new_child_element(obj: BaseOxmlElement): return OxmlElement(self._nsptagname) return new_child_element @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the property - descriptor. This default getter returns the child element with - matching tag name or |None| if not present. + def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement | None]: + """Callable suitable for the "get" side of the property descriptor. + + This default getter returns the child element with matching tag name or |None| if not + present. """ - def get_child_element(obj): + def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement | None: return obj.find(qn(self._nsptagname)) get_child_element.__doc__ = ( @@ -403,14 +391,11 @@ def _insert_method_name(self): return "_insert_%s" % self._prop_name @property - def _list_getter(self): - """ - Return a function object suitable for the "get" side of a list - property descriptor. - """ + def _list_getter(self) -> Callable[[BaseOxmlElement], list[BaseOxmlElement]]: + """Callable suitable for the "get" side of a list property descriptor.""" - def get_child_element_list(obj): - return obj.findall(qn(self._nsptagname)) + def get_child_element_list(obj: BaseOxmlElement) -> list[BaseOxmlElement]: + return cast("list[BaseOxmlElement]", obj.findall(qn(self._nsptagname))) get_child_element_list.__doc__ = ( "A list containing each of the ``<%s>`` child elements, in the o" @@ -428,19 +413,16 @@ def _new_method_name(self): class Choice(_BaseChildElement): - """ - Defines a child element belonging to a group, only one of which may - appear as a child. - """ + """Defines a child element belonging to a group, only one of which may appear as a child.""" @property def nsptagname(self): return self._nsptagname - def populate_class_members(self, element_cls, group_prop_name, successors): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members( # pyright: ignore[reportIncompatibleMethodOverride] + self, element_cls: Type[BaseOxmlElement], group_prop_name: str, successors: Sequence[str] + ): + """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._group_prop_name = group_prop_name self._successors = successors @@ -451,13 +433,10 @@ def populate_class_members(self, element_cls, group_prop_name, successors): self._add_adder() self._add_get_or_change_to_method() - def _add_get_or_change_to_method(self): - """ - Add a ``get_or_change_to_x()`` method to the element class for this - child element. - """ + def _add_get_or_change_to_method(self) -> None: + """Add a `get_or_change_to_x()` method to the element class for this child element.""" - def get_or_change_to_child(obj): + def get_or_change_to_child(obj: BaseOxmlElement): child = getattr(obj, self._prop_name) if child is not None: return child @@ -493,14 +472,12 @@ def _remove_group_method_name(self): class OneAndOnlyOne(_BaseChildElement): - """ - Defines a required child element for MetaOxmlElement. - """ + """Defines a required child element for MetaOxmlElement.""" - def __init__(self, nsptagname): - super(OneAndOnlyOne, self).__init__(nsptagname, None) + def __init__(self, nsptagname: str): + super(OneAndOnlyOne, self).__init__(nsptagname, ()) - def populate_class_members(self, element_cls, prop_name): + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): """ Add the appropriate methods to *element_cls*. """ @@ -508,13 +485,10 @@ def populate_class_members(self, element_cls, prop_name): self._add_getter() @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the property - descriptor. - """ + def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]: + """Callable suitable for the "get" side of the property descriptor.""" - def get_child_element(obj): + def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement: child = obj.find(qn(self._nsptagname)) if child is None: raise InvalidXmlError( @@ -522,22 +496,15 @@ def get_child_element(obj): ) return child - get_child_element.__doc__ = ( - "Required ``<%s>`` child element." % self._nsptagname - ) + get_child_element.__doc__ = "Required ``<%s>`` child element." % self._nsptagname return get_child_element class OneOrMore(_BaseChildElement): - """ - Defines a repeating child element for MetaOxmlElement that must appear at - least once. - """ + """Defines a repeating child element for MetaOxmlElement that must appear at least once.""" - def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """Add the appropriate methods to *element_cls*.""" super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() @@ -546,12 +513,10 @@ def populate_class_members(self, element_cls, prop_name): self._add_public_adder() delattr(element_cls, prop_name) - def _add_public_adder(self): - """ - Add a public ``add_x()`` method to the parent element class. - """ + def _add_public_adder(self) -> None: + """Add a public `.add_x()` method to the parent element class.""" - def add_child(obj): + def add_child(obj: BaseOxmlElement) -> BaseOxmlElement: private_add_method = getattr(obj, self._add_method_name) child = private_add_method() return child @@ -578,7 +543,7 @@ class ZeroOrMore(_BaseChildElement): Defines an optional repeating child element for MetaOxmlElement. """ - def populate_class_members(self, element_cls, prop_name): + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): """ Add the appropriate methods to *element_cls*. """ @@ -591,14 +556,10 @@ def populate_class_members(self, element_cls, prop_name): class ZeroOrOne(_BaseChildElement): - """ - Defines an optional child element for MetaOxmlElement. - """ + """Defines an optional child element for MetaOxmlElement.""" - def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """Add the appropriate methods to `element_cls`.""" super(ZeroOrOne, self).populate_class_members(element_cls, prop_name) self._add_getter() self._add_creator() @@ -608,12 +569,9 @@ def populate_class_members(self, element_cls, prop_name): self._add_remover() def _add_get_or_adder(self): - """ - Add a ``get_or_add_x()`` method to the element class for this - child element. - """ + """Add a `.get_or_add_x()` method to the element class for this child element.""" - def get_or_add_child(obj): + def get_or_add_child(obj: BaseOxmlElement) -> BaseOxmlElement: child = getattr(obj, self._prop_name) if child is None: add_method = getattr(obj, self._add_method_name) @@ -626,17 +584,12 @@ def get_or_add_child(obj): self._add_to_class(self._get_or_add_method_name, get_or_add_child) def _add_remover(self): - """ - Add a ``_remove_x()`` method to the element class for this child - element. - """ + """Add a `._remove_x()` method to the element class for this child element.""" - def _remove_child(obj): + def _remove_child(obj: BaseOxmlElement) -> None: obj.remove_all(self._nsptagname) - _remove_child.__doc__ = ( - "Remove all ``<%s>`` child elements." - ) % self._nsptagname + _remove_child.__doc__ = f"Remove all `{self._nsptagname}` child elements." self._add_to_class(self._remove_method_name, _remove_child) @lazyproperty @@ -645,50 +598,37 @@ def _get_or_add_method_name(self): class ZeroOrOneChoice(_BaseChildElement): - """ - Correspondes to an ``EG_*`` element group where at most one of its - members may appear as a child. - """ + """An `EG_*` element group where at most one of its members may appear as a child.""" - def __init__(self, choices, successors=()): - self._choices = choices - self._successors = successors + def __init__(self, choices: Iterable[Choice], successors: Iterable[str] = ()): + self._choices = tuple(choices) + self._successors = tuple(successors) - def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str): + """Add the appropriate methods to `element_cls`.""" super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name) self._add_choice_getter() for choice in self._choices: - choice.populate_class_members( - element_cls, self._prop_name, self._successors - ) + choice.populate_class_members(element_cls, self._prop_name, self._successors) self._add_group_remover() def _add_choice_getter(self): - """ - Add a read-only ``{prop_name}`` property to the element class that - returns the present member of this group, or |None| if none are - present. + """Add a read-only `.{prop_name}` property to the element class. + + The property returns the present member of this group, or |None| if none are present. """ property_ = property(self._choice_getter, None, None) # assign unconditionally to overwrite element name definition setattr(self._element_cls, self._prop_name, property_) def _add_group_remover(self): - """ - Add a ``_remove_eg_x()`` method to the element class for this choice - group. - """ + """Add a `._remove_eg_x()` method to the element class for this choice group.""" - def _remove_choice_group(obj): + def _remove_choice_group(obj: BaseOxmlElement) -> None: for tagname in self._member_nsptagnames: obj.remove_all(tagname) - _remove_choice_group.__doc__ = ( - "Remove the current choice group child element if present." - ) + _remove_choice_group.__doc__ = "Remove the current choice group child element if present." self._add_to_class(self._remove_choice_group_method_name, _remove_choice_group) @property @@ -698,8 +638,10 @@ def _choice_getter(self): descriptor. """ - def get_group_member_element(obj): - return obj.first_child_found_in(*self._member_nsptagnames) + def get_group_member_element(obj: BaseOxmlElement) -> BaseOxmlElement | None: + return cast( + "BaseOxmlElement | None", obj.first_child_found_in(*self._member_nsptagnames) + ) get_group_member_element.__doc__ = ( "Return the child element belonging to this element group, or " @@ -708,49 +650,39 @@ def get_group_member_element(obj): return get_group_member_element @lazyproperty - def _member_nsptagnames(self): - """ - Sequence of namespace-prefixed tagnames, one for each of the member - elements of this choice group. - """ + def _member_nsptagnames(self) -> list[str]: + """Sequence of namespace-prefixed tagnames, one for each member element of choice group.""" return [choice.nsptagname for choice in self._choices] @lazyproperty def _remove_choice_group_method_name(self): - return "_remove_%s" % self._prop_name + """Function-name for choice remover.""" + return f"_remove_{self._prop_name}" -class _OxmlElementBase(etree.ElementBase): - """ - Provides common behavior for oxml element classes - """ +# -- lxml typing isn't quite right here, just ignore this error on _Element -- +class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement): + """Effective base class for all custom element classes. - @classmethod - def child_tagnames_after(cls, tagname): - """ - Return a sequence containing the namespace prefixed child tagnames, - e.g. 'a:prstGeom', that occur after *tagname* in this element. - """ - return cls.child_tagnames.tagnames_after(tagname) + Adds standardized behavior to all classes in one place. + """ - def delete(self): - """ - Remove this element from the XML tree. - """ - self.getparent().remove(self) + def __repr__(self): + return "<%s '<%s>' at 0x%0x>" % ( + self.__class__.__name__, + self._nsptag, + id(self), + ) - def first_child_found_in(self, *tagnames): - """ - Return the first child found with tag in *tagnames*, or None if - not found. - """ + def first_child_found_in(self, *tagnames: str) -> _Element | None: + """First child with tag in `tagnames`, or None if not found.""" for tagname in tagnames: child = self.find(qn(tagname)) if child is not None: return child return None - def insert_element_before(self, elm, *tagnames): + def insert_element_before(self, elm: ElementBase, *tagnames: str): successor = self.first_child_found_in(*tagnames) if successor is not None: successor.addprevious(elm) @@ -758,40 +690,28 @@ def insert_element_before(self, elm, *tagnames): self.append(elm) return elm - def remove_all(self, tagname): - """ - Remove all child elements having *tagname*. - """ - matching = self.findall(qn(tagname)) - for child in matching: - self.remove(child) - - def remove_if_present(self, *tagnames): - """ - Remove all child elements having tagname in *tagnames*. - """ + def remove_all(self, *tagnames: str) -> None: + """Remove child elements with tagname (e.g. "a:p") in `tagnames`.""" for tagname in tagnames: - element = self.find(qn(tagname)) - if element is not None: - self.remove(element) + matching = self.findall(qn(tagname)) + for child in matching: + self.remove(child) @property - def xml(self): - """ - Return XML string for this element, suitable for testing purposes. - Pretty printed for readability and without an XML declaration at the - top. + def xml(self) -> str: + """XML string for this element, suitable for testing purposes. + + Pretty printed for readability and without an XML declaration at the top. """ return serialize_for_reading(self) - def xpath(self, xpath_str): - """ - Override of ``lxml`` _Element.xpath() method to provide standard Open - XML namespace mapping in centralized location. - """ - return super(BaseOxmlElement, self).xpath(xpath_str, namespaces=_nsmap) + def xpath(self, xpath_str: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride] + """Override of `lxml` _Element.xpath() method. + Provides standard Open XML namespace mapping (`nsmap`) in centralized location. + """ + return super().xpath(xpath_str, namespaces=_nsmap) -BaseOxmlElement = MetaOxmlElement( - "BaseOxmlElement", (etree.ElementBase,), dict(_OxmlElementBase.__dict__) -) + @property + def _nsptag(self) -> str: + return NamespacePrefixedTag.from_clark_name(self.tag) diff --git a/src/pptx/package.py b/src/pptx/package.py index 1d5e73cd6..79703cd6c 100644 --- a/src/pptx/package.py +++ b/src/pptx/package.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """Overall .pptx package.""" +from __future__ import annotations + +from typing import IO, Iterator + from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import OpcPackage from pptx.opc.packuri import PackURI @@ -15,7 +17,7 @@ class Package(OpcPackage): """An overall .pptx package.""" @lazyproperty - def core_properties(self): + def core_properties(self) -> CorePropertiesPart: """Instance of |CoreProperties| holding read/write Dublin Core doc properties. Creates a default core properties part if one is not present (not common). @@ -27,7 +29,7 @@ def core_properties(self): self.relate_to(core_props, RT.CORE_PROPERTIES) return core_props - def get_or_add_image_part(self, image_file): + def get_or_add_image_part(self, image_file: str | IO[bytes]): """ Return an |ImagePart| object containing the image in *image_file*. If the image part already exists in this package, it is reused, @@ -43,10 +45,10 @@ def get_or_add_media_part(self, media): """ return self._media_parts.get_or_add_media_part(media) - def next_image_partname(self, ext): - """ - Return a |PackURI| instance representing the next available image - partname, by sequence number. *ext* is used as the extention on the + def next_image_partname(self, ext: str) -> PackURI: + """Return a |PackURI| instance representing the next available image partname. + + Partname uses the next available sequence number. *ext* is used as the extention on the returned partname. """ @@ -127,10 +129,8 @@ def __init__(self, package): super(_ImageParts, self).__init__() self._package = package - def __iter__(self): - """ - Generate a reference to each |ImagePart| object in the package. - """ + def __iter__(self) -> Iterator[ImagePart]: + """Generate a reference to each |ImagePart| object in the package.""" image_parts = [] for rel in self._package.iter_rels(): if rel.is_external: @@ -143,7 +143,7 @@ def __iter__(self): image_parts.append(image_part) yield image_part - def get_or_add_image_part(self, image_file): + def get_or_add_image_part(self, image_file: str | IO[bytes]) -> ImagePart: """Return |ImagePart| object containing the image in `image_file`. `image_file` can be either a path to an image file or a file-like object @@ -152,9 +152,9 @@ def get_or_add_image_part(self, image_file): """ image = Image.from_file(image_file) image_part = self._find_by_sha1(image.sha1) - return ImagePart.new(self._package, image) if image_part is None else image_part + return image_part if image_part else ImagePart.new(self._package, image) - def _find_by_sha1(self, sha1): + def _find_by_sha1(self, sha1: str) -> ImagePart | None: """ Return an |ImagePart| object belonging to this package or |None| if no matching image part is found. The image part is identified by the diff --git a/src/pptx/parts/chart.py b/src/pptx/parts/chart.py index 2a8a04283..7208071b4 100644 --- a/src/pptx/parts/chart.py +++ b/src/pptx/parts/chart.py @@ -1,13 +1,21 @@ -# encoding: utf-8 - """Chart part objects, including Chart and Charts.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from pptx.chart.chart import Chart -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import XmlPart from pptx.parts.embeddedpackage import EmbeddedXlsxPart from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.chart.data import ChartData + from pptx.enum.chart import XL_CHART_TYPE + from pptx.package import Package + class ChartPart(XmlPart): """A chart part. @@ -18,7 +26,7 @@ class ChartPart(XmlPart): partname_template = "/ppt/charts/chart%d.xml" @classmethod - def new(cls, chart_type, chart_data, package): + def new(cls, chart_type: XL_CHART_TYPE, chart_data: ChartData, package: Package): """Return new |ChartPart| instance added to `package`. Returned chart-part contains a chart of `chart_type` depicting `chart_data`. @@ -74,11 +82,7 @@ def xlsx_part(self): is |None| if there is no `` element. """ xlsx_part_rId = self._chartSpace.xlsx_part_rId - return ( - None - if xlsx_part_rId is None - else self._chart_part.related_part(xlsx_part_rId) - ) + return None if xlsx_part_rId is None else self._chart_part.related_part(xlsx_part_rId) @xlsx_part.setter def xlsx_part(self, xlsx_part): diff --git a/src/pptx/parts/coreprops.py b/src/pptx/parts/coreprops.py index e39b154d0..8471cc8ef 100644 --- a/src/pptx/parts/coreprops.py +++ b/src/pptx/parts/coreprops.py @@ -3,12 +3,16 @@ from __future__ import annotations import datetime as dt +from typing import TYPE_CHECKING from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import XmlPart from pptx.opc.packuri import PackURI from pptx.oxml.coreprops import CT_CoreProperties +if TYPE_CHECKING: + from pptx.package import Package + class CorePropertiesPart(XmlPart): """Corresponds to part named `/docProps/core.xml`. @@ -16,8 +20,10 @@ class CorePropertiesPart(XmlPart): Contains the core document properties for this document package. """ + _element: CT_CoreProperties + @classmethod - def default(cls, package): + def default(cls, package: Package): """Return default new |CorePropertiesPart| instance suitable as starting point. This provides a base for adding core-properties to a package that doesn't yet @@ -31,35 +37,35 @@ def default(cls, package): return core_props @property - def author(self): + def author(self) -> str: return self._element.author_text @author.setter - def author(self, value): + def author(self, value: str): self._element.author_text = value @property - def category(self): + def category(self) -> str: return self._element.category_text @category.setter - def category(self, value): + def category(self, value: str): self._element.category_text = value @property - def comments(self): + def comments(self) -> str: return self._element.comments_text @comments.setter - def comments(self, value): + def comments(self, value: str): self._element.comments_text = value @property - def content_status(self): + def content_status(self) -> str: return self._element.contentStatus_text @content_status.setter - def content_status(self, value): + def content_status(self, value: str): self._element.contentStatus_text = value @property @@ -67,39 +73,39 @@ def created(self): return self._element.created_datetime @created.setter - def created(self, value): + def created(self, value: dt.datetime): self._element.created_datetime = value @property - def identifier(self): + def identifier(self) -> str: return self._element.identifier_text @identifier.setter - def identifier(self, value): + def identifier(self, value: str): self._element.identifier_text = value @property - def keywords(self): + def keywords(self) -> str: return self._element.keywords_text @keywords.setter - def keywords(self, value): + def keywords(self, value: str): self._element.keywords_text = value @property - def language(self): + def language(self) -> str: return self._element.language_text @language.setter - def language(self, value): + def language(self, value: str): self._element.language_text = value @property - def last_modified_by(self): + def last_modified_by(self) -> str: return self._element.lastModifiedBy_text @last_modified_by.setter - def last_modified_by(self, value): + def last_modified_by(self, value: str): self._element.lastModifiedBy_text = value @property @@ -107,7 +113,7 @@ def last_printed(self): return self._element.lastPrinted_datetime @last_printed.setter - def last_printed(self, value): + def last_printed(self, value: dt.datetime): self._element.lastPrinted_datetime = value @property @@ -115,7 +121,7 @@ def modified(self): return self._element.modified_datetime @modified.setter - def modified(self, value): + def modified(self, value: dt.datetime): self._element.modified_datetime = value @property @@ -123,35 +129,35 @@ def revision(self): return self._element.revision_number @revision.setter - def revision(self, value): + def revision(self, value: int): self._element.revision_number = value @property - def subject(self): + def subject(self) -> str: return self._element.subject_text @subject.setter - def subject(self, value): + def subject(self, value: str): self._element.subject_text = value @property - def title(self): + def title(self) -> str: return self._element.title_text @title.setter - def title(self, value): + def title(self, value: str): self._element.title_text = value @property - def version(self): + def version(self) -> str: return self._element.version_text @version.setter - def version(self, value): + def version(self, value: str): self._element.version_text = value @classmethod - def _new(cls, package): + def _new(cls, package: Package) -> CorePropertiesPart: """Return new empty |CorePropertiesPart| instance.""" return CorePropertiesPart( PackURI("/docProps/core.xml"), diff --git a/src/pptx/parts/embeddedpackage.py b/src/pptx/parts/embeddedpackage.py index c2d434e04..7aa2cf408 100644 --- a/src/pptx/parts/embeddedpackage.py +++ b/src/pptx/parts/embeddedpackage.py @@ -1,14 +1,19 @@ -# encoding: utf-8 - """Embedded Package part objects. "Package" in this context means another OPC package, i.e. a DOCX, PPTX, or XLSX "file". """ +from __future__ import annotations + +from typing import TYPE_CHECKING + from pptx.enum.shapes import PROG_ID from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import Part +if TYPE_CHECKING: + from pptx.package import Package + class EmbeddedPackagePart(Part): """A distinct OPC package, e.g. an Excel file, embedded in this PPTX package. @@ -17,7 +22,7 @@ class EmbeddedPackagePart(Part): """ @classmethod - def factory(cls, prog_id, object_blob, package): + def factory(cls, prog_id: PROG_ID | str, object_blob: bytes, package: Package): """Return a new |EmbeddedPackagePart| subclass instance added to *package*. The subclass is determined by `prog_id` which corresponds to the "application" @@ -43,7 +48,7 @@ def factory(cls, prog_id, object_blob, package): return EmbeddedPartCls.new(object_blob, package) @classmethod - def new(cls, blob, package): + def new(cls, blob: bytes, package: Package): """Return new |EmbeddedPackagePart| subclass object. The returned part object contains `blob` and is added to `package`. diff --git a/src/pptx/parts/image.py b/src/pptx/parts/image.py index db59c5fcc..9be5d02d6 100644 --- a/src/pptx/parts/image.py +++ b/src/pptx/parts/image.py @@ -1,36 +1,44 @@ -# encoding: utf-8 - """ImagePart and related objects.""" -from __future__ import division +from __future__ import annotations import hashlib +import io import os +from typing import IO, TYPE_CHECKING, Any, cast -try: - from PIL import Image as PIL_Image -except ImportError: - import Image as PIL_Image +from PIL import Image as PIL_Image -from pptx.compat import BytesIO, is_string from pptx.opc.package import Part from pptx.opc.spec import image_content_types -from pptx.util import lazyproperty +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from pptx.opc.packuri import PackURI + from pptx.package import Package + from pptx.util import Length class ImagePart(Part): """An image part. - An image part generally has a partname matching the regex - `ppt/media/image[1-9][0-9]*.*`. + An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`. """ - def __init__(self, partname, content_type, package, blob, filename=None): + def __init__( + self, + partname: PackURI, + content_type: str, + package: Package, + blob: bytes, + filename: str | None = None, + ): super(ImagePart, self).__init__(partname, content_type, package, blob) + self._blob = blob self._filename = filename @classmethod - def new(cls, package, image): + def new(cls, package: Package, image: Image) -> ImagePart: """Return new |ImagePart| instance containing `image`. `image` is an |Image| object. @@ -44,80 +52,76 @@ def new(cls, package, image): ) @property - def desc(self): - """ - The filename associated with this image, either the filename of - the original image or a generic name of the form ``image.ext`` - where ``ext`` is appropriate to the image file format, e.g. - ``'jpg'``. An image created using a path will have that filename; one - created with a file-like object will have a generic name. + def desc(self) -> str: + """The filename associated with this image. + + Either the filename of the original image or a generic name of the form `image.ext` where + `ext` is appropriate to the image file format, e.g. `'jpg'`. An image created using a path + will have that filename; one created with a file-like object will have a generic name. """ - # return generic filename if original filename is unknown + # -- return generic filename if original filename is unknown -- if self._filename is None: - return "image.%s" % self.ext + return f"image.{self.ext}" return self._filename @property - def ext(self): - """ - Return file extension for this image e.g. ``'png'``. - """ + def ext(self) -> str: + """File-name extension for this image e.g. `'png'`.""" return self.partname.ext @property - def image(self): - """ - An |Image| object containing the image in this image part. - """ - return Image(self.blob, self.desc) + def image(self) -> Image: + """An |Image| object containing the image in this image part. - def scale(self, scaled_cx, scaled_cy): + Note this is a `pptx.image.Image` object, not a PIL Image. """ - Return scaled image dimensions in EMU based on the combination of - parameters supplied. If *scaled_cx* and *scaled_cy* are both |None|, - the native image size is returned. If neither *scaled_cx* nor - *scaled_cy* is |None|, their values are returned unchanged. If - a value is provided for either *scaled_cx* or *scaled_cy* and the - other is |None|, the missing value is calculated such that the - image's aspect ratio is preserved. + return Image(self._blob, self.desc) + + def scale(self, scaled_cx: int | None, scaled_cy: int | None) -> tuple[int, int]: + """Return scaled image dimensions in EMU based on the combination of parameters supplied. + + If `scaled_cx` and `scaled_cy` are both |None|, the native image size is returned. If + neither `scaled_cx` nor `scaled_cy` is |None|, their values are returned unchanged. If a + value is provided for either `scaled_cx` or `scaled_cy` and the other is |None|, the + missing value is calculated such that the image's aspect ratio is preserved. """ image_cx, image_cy = self._native_size - if scaled_cx is None and scaled_cy is None: - scaled_cx = image_cx - scaled_cy = image_cy - elif scaled_cx is None: - scaling_factor = float(scaled_cy) / float(image_cy) - scaled_cx = int(round(image_cx * scaling_factor)) - elif scaled_cy is None: + if scaled_cx and scaled_cy: + return scaled_cx, scaled_cy + + if scaled_cx and not scaled_cy: scaling_factor = float(scaled_cx) / float(image_cx) scaled_cy = int(round(image_cy * scaling_factor)) + return scaled_cx, scaled_cy + + if not scaled_cx and scaled_cy: + scaling_factor = float(scaled_cy) / float(image_cy) + scaled_cx = int(round(image_cx * scaling_factor)) + return scaled_cx, scaled_cy - return scaled_cx, scaled_cy + # -- only remaining case is both `scaled_cx` and `scaled_cy` are `None` -- + return image_cx, image_cy @lazyproperty - def sha1(self): - """ - The SHA1 hash digest for the image binary of this image part, like: - ``'1be010ea47803b00e140b852765cdf84f491da47'``. + def sha1(self) -> str: + """The 40-character SHA1 hash digest for the image binary of this image part. + + like: `"1be010ea47803b00e140b852765cdf84f491da47"`. """ return hashlib.sha1(self._blob).hexdigest() @property - def _dpi(self): - """ - A (horz_dpi, vert_dpi) 2-tuple (ints) representing the dots-per-inch - property of this image. - """ - image = Image.from_blob(self.blob) + def _dpi(self) -> tuple[int, int]: + """(horz_dpi, vert_dpi) pair representing the dots-per-inch resolution of this image.""" + image = Image.from_blob(self._blob) return image.dpi @property - def _native_size(self): - """ - A (width, height) 2-tuple representing the native dimensions of the - image in EMU, calculated based on the image DPI value, if present, - assuming 72 dpi as a default. + def _native_size(self) -> tuple[Length, Length]: + """A (width, height) 2-tuple representing the native dimensions of the image in EMU. + + Calculated based on the image DPI value, if present, assuming 72 dpi as a default. """ EMU_PER_INCH = 914400 horz_dpi, vert_dpi = self._dpi @@ -126,38 +130,35 @@ def _native_size(self): width = EMU_PER_INCH * width_px / horz_dpi height = EMU_PER_INCH * height_px / vert_dpi - return width, height + return Emu(int(width)), Emu(int(height)) @property - def _px_size(self): - """ - A (width, height) 2-tuple representing the dimensions of this image - in pixels. - """ - image = Image.from_blob(self.blob) + def _px_size(self) -> tuple[int, int]: + """A (width, height) 2-tuple representing the dimensions of this image in pixels.""" + image = Image.from_blob(self._blob) return image.size class Image(object): """Immutable value object representing an image such as a JPEG, PNG, or GIF.""" - def __init__(self, blob, filename): + def __init__(self, blob: bytes, filename: str | None): super(Image, self).__init__() self._blob = blob self._filename = filename @classmethod - def from_blob(cls, blob, filename=None): - """Return a new |Image| object loaded from the image binary in *blob*.""" + def from_blob(cls, blob: bytes, filename: str | None = None) -> Image: + """Return a new |Image| object loaded from the image binary in `blob`.""" return cls(blob, filename) @classmethod - def from_file(cls, image_file): - """ - Return a new |Image| object loaded from *image_file*, which can be - either a path (string) or a file-like object. + def from_file(cls, image_file: str | IO[bytes]) -> Image: + """Return a new |Image| object loaded from `image_file`. + + `image_file` can be either a path (str) or a file-like object. """ - if is_string(image_file): + if isinstance(image_file, str): # treat image_file as a path with open(image_file, "rb") as f: blob = f.read() @@ -173,32 +174,27 @@ def from_file(cls, image_file): return cls.from_blob(blob, filename) @property - def blob(self): - """ - The binary image bytestream of this image. - """ + def blob(self) -> bytes: + """The binary image bytestream of this image.""" return self._blob @lazyproperty - def content_type(self): - """ - MIME-type of this image, e.g. ``'image/jpeg'``. - """ + def content_type(self) -> str: + """MIME-type of this image, e.g. `"image/jpeg"`.""" return image_content_types[self.ext] @lazyproperty - def dpi(self): - """ - A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch - resolution of this image. A default value of (72, 72) is used if the - dpi is not specified in the image file. + def dpi(self) -> tuple[int, int]: + """A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch resolution of this image. + + A default value of (72, 72) is used if the dpi is not specified in the image file. """ - def int_dpi(dpi): - """ - Return an integer dots-per-inch value corresponding to *dpi*. If - *dpi* is |None|, a non-numeric type, less than 1 or greater than - 2048, 72 is returned. + def int_dpi(dpi: Any): + """Return an integer dots-per-inch value corresponding to `dpi`. + + If `dpi` is |None|, a non-numeric type, less than 1 or greater than 2048, 72 is + returned. """ try: int_dpi = int(round(float(dpi))) @@ -208,12 +204,11 @@ def int_dpi(dpi): int_dpi = 72 return int_dpi - def normalize_pil_dpi(pil_dpi): - """ - Return a (horz_dpi, vert_dpi) 2-tuple corresponding to *pil_dpi*, - the value for the 'dpi' key in the ``info`` dict of a PIL image. - If the 'dpi' key is not present or contains an invalid value, - ``(72, 72)`` is returned. + def normalize_pil_dpi(pil_dpi: tuple[int, int] | None): + """Return a (horz_dpi, vert_dpi) 2-tuple corresponding to `pil_dpi`. + + The value for the 'dpi' key in the `info` dict of a PIL image. If the 'dpi' key is not + present or contains an invalid value, `(72, 72)` is returned. """ if isinstance(pil_dpi, tuple): return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1])) @@ -222,12 +217,11 @@ def normalize_pil_dpi(pil_dpi): return normalize_pil_dpi(self._pil_props[2]) @lazyproperty - def ext(self): - """ - Canonical file extension for this image e.g. ``'png'``. The returned - extension is all lowercase and is the canonical extension for the - content type of this image, regardless of what extension may have - been used in its filename, if any. + def ext(self) -> str: + """Canonical file extension for this image e.g. `'png'`. + + The returned extension is all lowercase and is the canonical extension for the content type + of this image, regardless of what extension may have been used in its filename, if any. """ ext_map = { "BMP": "bmp", @@ -244,46 +238,38 @@ def ext(self): return ext_map[format] @property - def filename(self): - """ - The filename from the path from which this image was loaded, if - loaded from the filesystem. |None| if no filename was used in - loading, such as when loaded from an in-memory stream. + def filename(self) -> str | None: + """Filename from path used to load this image, if loaded from the filesystem. + + |None| if no filename was used in loading, such as when loaded from an in-memory stream. """ return self._filename @lazyproperty - def sha1(self): - """ - SHA1 hash digest of the image blob - """ + def sha1(self) -> str: + """SHA1 hash digest of the image blob.""" return hashlib.sha1(self._blob).hexdigest() @lazyproperty - def size(self): - """ - A (width, height) 2-tuple specifying the dimensions of this image in - pixels. - """ + def size(self) -> tuple[int, int]: + """A (width, height) 2-tuple specifying the dimensions of this image in pixels.""" return self._pil_props[1] @property - def _format(self): - """ - The PIL Image format of this image, e.g. 'PNG'. - """ + def _format(self) -> str | None: + """The PIL Image format of this image, e.g. 'PNG'.""" return self._pil_props[0] @lazyproperty - def _pil_props(self): - """ - A tuple containing useful image properties extracted from this image - using Pillow (Python Imaging Library, or 'PIL'). - """ - stream = BytesIO(self._blob) - pil_image = PIL_Image.open(stream) + def _pil_props(self) -> tuple[str | None, tuple[int, int], tuple[int, int] | None]: + """tuple of image properties extracted from this image using Pillow.""" + stream = io.BytesIO(self._blob) + pil_image = PIL_Image.open(stream) # pyright: ignore[reportUnknownMemberType] format = pil_image.format width_px, height_px = pil_image.size - dpi = pil_image.info.get("dpi") + dpi = cast( + "tuple[int, int] | None", + pil_image.info.get("dpi"), # pyright: ignore[reportUnknownMemberType] + ) stream.close() return (format, (width_px, height_px), dpi) diff --git a/src/pptx/parts/media.py b/src/pptx/parts/media.py index 81efb5a5d..7e8bc2f21 100644 --- a/src/pptx/parts/media.py +++ b/src/pptx/parts/media.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """MediaPart and related objects.""" +from __future__ import annotations + import hashlib from pptx.opc.package import Part diff --git a/src/pptx/parts/presentation.py b/src/pptx/parts/presentation.py index 30b4ff016..1413de457 100644 --- a/src/pptx/parts/presentation.py +++ b/src/pptx/parts/presentation.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """Presentation part, the main part in a .pptx package.""" +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, Iterable + from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import XmlPart from pptx.opc.packuri import PackURI @@ -9,6 +11,10 @@ from pptx.presentation import Presentation from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.parts.coreprops import CorePropertiesPart + from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster + class PresentationPart(XmlPart): """Top level class in object model. @@ -16,10 +22,10 @@ class PresentationPart(XmlPart): Represents the contents of the /ppt directory of a .pptx file. """ - def add_slide(self, slide_layout): - """ - Return an (rId, slide) pair of a newly created blank slide that - inherits appearance from *slide_layout*. + def add_slide(self, slide_layout: SlideLayout): + """Return (rId, slide) pair of a newly created blank slide. + + New slide inherits appearance from `slide_layout`. """ partname = self._next_slide_partname slide_layout_part = slide_layout.part @@ -28,14 +34,14 @@ def add_slide(self, slide_layout): return rId, slide_part.slide @property - def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties of this presentation. + def core_properties(self) -> CorePropertiesPart: + """A |CoreProperties| object for the presentation. + + Provides read/write access to the Dublin Core properties of this presentation. """ return self.package.core_properties - def get_slide(self, slide_id): + def get_slide(self, slide_id: int) -> Slide | None: """Return optional related |Slide| object identified by `slide_id`. Returns |None| if no slide with `slide_id` is related to this presentation. @@ -46,7 +52,7 @@ def get_slide(self, slide_id): return None @lazyproperty - def notes_master(self): + def notes_master(self) -> NotesMaster: """ Return the |NotesMaster| object for this presentation. If the presentation does not have a notes master, one is created from @@ -56,12 +62,11 @@ def notes_master(self): return self.notes_master_part.notes_master @lazyproperty - def notes_master_part(self): - """ - Return the |NotesMasterPart| object for this presentation. If the - presentation does not have a notes master, one is created from - a default template. The same single instance is returned on each - call. + def notes_master_part(self) -> NotesMasterPart: + """Return the |NotesMasterPart| object for this presentation. + + If the presentation does not have a notes master, one is created from a default template. + The same single instance is returned on each call. """ try: return self.part_related_by(RT.NOTES_MASTER) @@ -78,27 +83,27 @@ def presentation(self): """ return Presentation(self._element, self) - def related_slide(self, rId): + def related_slide(self, rId: str) -> Slide: """Return |Slide| object for related |SlidePart| related by `rId`.""" return self.related_part(rId).slide - def related_slide_master(self, rId): + def related_slide_master(self, rId: str) -> SlideMaster: """Return |SlideMaster| object for |SlideMasterPart| related by `rId`.""" return self.related_part(rId).slide_master - def rename_slide_parts(self, rIds): + def rename_slide_parts(self, rIds: Iterable[str]): """Assign incrementing partnames to the slide parts identified by `rIds`. - Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their - id appears in the `rIds` sequence. The name portion is always ``slide``. The - number part forms a continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). - The extension is always ``.xml``. + Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their id appears + in the `rIds` sequence. The name portion is always `slide`. The number part forms a + continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always + `.xml`. """ for idx, rId in enumerate(rIds): slide_part = self.related_part(rId) slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1)) - def save(self, path_or_stream): + def save(self, path_or_stream: str | IO[bytes]): """Save this presentation package to `path_or_stream`. `path_or_stream` can be either a path to a filesystem location (a string) or a diff --git a/src/pptx/parts/slide.py b/src/pptx/parts/slide.py index 5d721bb41..6650564a5 100644 --- a/src/pptx/parts/slide.py +++ b/src/pptx/parts/slide.py @@ -1,9 +1,12 @@ -# encoding: utf-8 - """Slide and related objects.""" +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, cast + from pptx.enum.shapes import PROG_ID -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import XmlPart from pptx.opc.packuri import PackURI from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide @@ -13,6 +16,12 @@ from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.chart.data import ChartData + from pptx.enum.chart import XL_CHART_TYPE + from pptx.media import Video + from pptx.parts.image import Image, ImagePart + class BaseSlidePart(XmlPart): """Base class for slide parts. @@ -21,15 +30,17 @@ class BaseSlidePart(XmlPart): notes-master, and handout-master parts. """ - def get_image(self, rId): - """ - Return an |Image| object containing the image related to this slide - by *rId*. Raises |KeyError| if no image is related by that id, which - would generally indicate a corrupted .pptx file. + _element: CT_Slide + + def get_image(self, rId: str) -> Image: + """Return an |Image| object containing the image related to this slide by *rId*. + + Raises |KeyError| if no image is related by that id, which would generally indicate a + corrupted .pptx file. """ - return self.related_part(rId).image + return cast("ImagePart", self.related_part(rId)).image - def get_or_add_image_part(self, image_file): + def get_or_add_image_part(self, image_file: str | IO[bytes]): """Return `(image_part, rId)` pair corresponding to `image_file`. The returned |ImagePart| object contains the image in `image_file` and is @@ -41,10 +52,8 @@ def get_or_add_image_part(self, image_file): return image_part, rId @property - def name(self): - """ - Internal name of this slide. - """ + def name(self) -> str: + """Internal name of this slide.""" return self._element.cSld.name @@ -159,7 +168,7 @@ def new(cls, partname, package, slide_layout_part): slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT) return slide_part - def add_chart_part(self, chart_type, chart_data): + def add_chart_part(self, chart_type: XL_CHART_TYPE, chart_data: ChartData): """Return str rId of new |ChartPart| object containing chart of `chart_type`. The chart depicts `chart_data` and is related to the slide contained in this @@ -167,7 +176,9 @@ def add_chart_part(self, chart_type, chart_data): """ return self.relate_to(ChartPart.new(chart_type, chart_data, self._package), RT.CHART) - def add_embedded_ole_object_part(self, prog_id, ole_object_file): + def add_embedded_ole_object_part( + self, prog_id: PROG_ID | str, ole_object_file: str | IO[bytes] + ): """Return rId of newly-added OLE-object part formed from `ole_object_file`.""" relationship_type = RT.PACKAGE if isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT return self.relate_to( @@ -177,7 +188,7 @@ def add_embedded_ole_object_part(self, prog_id, ole_object_file): relationship_type, ) - def get_or_add_video_media_part(self, video): + def get_or_add_video_media_part(self, video: Video) -> tuple[str, str]: """Return rIds for media and video relationships to media part. A new |MediaPart| object is created if it does not already exist @@ -207,11 +218,11 @@ def has_notes_slide(self): return True @lazyproperty - def notes_slide(self): - """ - The |NotesSlide| instance associated with this slide. If the slide - does not have a notes slide, a new one is created. The same single - instance is returned on each call. + def notes_slide(self) -> NotesSlide: + """The |NotesSlide| instance associated with this slide. + + If the slide does not have a notes slide, a new one is created. The same single instance + is returned on each call. """ try: notes_slide_part = self.part_related_by(RT.NOTES_SLIDE) @@ -227,19 +238,14 @@ def slide(self): return Slide(self._element, self) @property - def slide_id(self): - """ - Return the slide identifier stored in the presentation part for this - slide part. - """ + def slide_id(self) -> int: + """Return the slide identifier stored in the presentation part for this slide part.""" presentation_part = self.package.presentation_part return presentation_part.slide_id(self) @property - def slide_layout(self): - """ - |SlideLayout| object the slide in this part inherits from. - """ + def slide_layout(self) -> SlideLayout: + """|SlideLayout| object the slide in this part inherits appearance from.""" slide_layout_part = self.part_related_by(RT.SLIDE_LAYOUT) return slide_layout_part.slide_layout @@ -268,10 +274,8 @@ def slide_layout(self): return SlideLayout(self._element, self) @property - def slide_master(self): - """ - Slide master from which this slide layout inherits properties. - """ + def slide_master(self) -> SlideMaster: + """Slide master from which this slide layout inherits properties.""" return self.part_related_by(RT.SLIDE_MASTER).slide_master @@ -281,11 +285,8 @@ class SlideMasterPart(BaseSlidePart): Corresponds to package files ppt/slideMasters/slideMaster[1-9][0-9]*.xml. """ - def related_slide_layout(self, rId): - """ - Return the |SlideLayout| object of the related |SlideLayoutPart| - corresponding to relationship key *rId*. - """ + def related_slide_layout(self, rId: str) -> SlideLayout: + """Return |SlideLayout| related to this slide-master by key `rId`.""" return self.related_part(rId).slide_layout @lazyproperty diff --git a/src/pptx/presentation.py b/src/pptx/presentation.py index eabcda72c..a41bfd59a 100644 --- a/src/pptx/presentation.py +++ b/src/pptx/presentation.py @@ -1,11 +1,19 @@ -# encoding: utf-8 - """Main presentation object.""" +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, cast + from pptx.shared import PartElementProxy from pptx.slide import SlideMasters, Slides from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.presentation import CT_Presentation, CT_SlideId + from pptx.parts.presentation import PresentationPart + from pptx.slide import NotesMaster, SlideLayouts + from pptx.util import Length + class Presentation(PartElementProxy): """PresentationML (PML) presentation. @@ -14,34 +22,37 @@ class Presentation(PartElementProxy): create a presentation. """ + _element: CT_Presentation + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + @property def core_properties(self): - """ - Instance of |CoreProperties| holding the read/write Dublin Core - document properties for this presentation. + """|CoreProperties| instance for this presentation. + + Provides read/write access to the Dublin Core document properties for the presentation. """ return self.part.core_properties @property - def notes_master(self): - """ - Instance of |NotesMaster| for this presentation. If the presentation - does not have a notes master, one is created from a default template + def notes_master(self) -> NotesMaster: + """Instance of |NotesMaster| for this presentation. + + If the presentation does not have a notes master, one is created from a default template and returned. The same single instance is returned on each call. """ return self.part.notes_master - def save(self, file): - """ - Save this presentation to *file*, where *file* can be either a path - to a file (a string) or a file-like object. + def save(self, file: str | IO[bytes]): + """Writes this presentation to `file`. + + `file` can be either a file-path or a file-like object open for writing bytes. """ self.part.save(file) @property - def slide_height(self): - """ - Height of slides in this presentation, in English Metric Units (EMU). + def slide_height(self) -> Length | None: + """Height of slides in this presentation, in English Metric Units (EMU). + Returns |None| if no slide width is defined. Read/write. """ sldSz = self._element.sldSz @@ -50,18 +61,17 @@ def slide_height(self): return sldSz.cy @slide_height.setter - def slide_height(self, height): + def slide_height(self, height: Length): sldSz = self._element.get_or_add_sldSz() sldSz.cy = height @property - def slide_layouts(self): - """ - Sequence of |SlideLayout| instances belonging to the first - |SlideMaster| of this presentation. A presentation can have more than - one slide master and each master will have its own set of layouts. - This property is a convenience for the common case where the - presentation has only a single slide master. + def slide_layouts(self) -> SlideLayouts: + """|SlideLayouts| collection belonging to the first |SlideMaster| of this presentation. + + A presentation can have more than one slide master and each master will have its own set + of layouts. This property is a convenience for the common case where the presentation has + only a single slide master. """ return self.slide_masters[0].slide_layouts @@ -75,10 +85,8 @@ def slide_master(self): return self.slide_masters[0] @lazyproperty - def slide_masters(self): - """ - Sequence of |SlideMaster| objects belonging to this presentation - """ + def slide_masters(self) -> SlideMasters: + """|SlideMasters| collection of slide-masters belonging to this presentation.""" return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self) @property @@ -93,15 +101,13 @@ def slide_width(self): return sldSz.cx @slide_width.setter - def slide_width(self, width): + def slide_width(self, width: Length): sldSz = self._element.get_or_add_sldSz() sldSz.cx = width @lazyproperty def slides(self): - """ - |Slides| object containing the slides in this presentation. - """ + """|Slides| object containing the slides in this presentation.""" sldIdLst = self._element.get_or_add_sldIdLst() - self.part.rename_slide_parts([sldId.rId for sldId in sldIdLst]) + self.part.rename_slide_parts([cast("CT_SlideId", sldId).rId for sldId in sldIdLst]) return Slides(sldIdLst, self) diff --git a/src/pptx/shapes/__init__.py b/src/pptx/shapes/__init__.py index c8e1f24d9..332109a31 100644 --- a/src/pptx/shapes/__init__.py +++ b/src/pptx/shapes/__init__.py @@ -1,25 +1,26 @@ -# encoding: utf-8 +"""Objects used across sub-package.""" -""" -Objects used across sub-package -""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.types import ProvidesPart class Subshape(object): - """ - Provides common services for drawing elements that occur below a shape - but may occasionally require an ancestor object to provide a service, - such as add or drop a relationship. Provides ``self._parent`` attribute - to subclasses. + """Provides access to the containing part for drawing elements that occur below a shape. + + Access to the part is required for example to add or drop a relationship. Provides + `self._parent` attribute to subclasses. """ - def __init__(self, parent): + def __init__(self, parent: ProvidesPart): super(Subshape, self).__init__() self._parent = parent @property - def part(self): - """ - The package part containing this object - """ + def part(self) -> XmlPart: + """The package part containing this object.""" return self._parent.part diff --git a/src/pptx/shapes/autoshape.py b/src/pptx/shapes/autoshape.py index ead5fecb5..c7f8cd93e 100644 --- a/src/pptx/shapes/autoshape.py +++ b/src/pptx/shapes/autoshape.py @@ -1,11 +1,10 @@ -# encoding: utf-8 - """Autoshape-related objects such as Shape and Adjustment.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from numbers import Number -import xml.sax.saxutils as saxutils +from typing import TYPE_CHECKING, Iterable +from xml.sax import saxutils from pptx.dml.fill import FillFormat from pptx.dml.line import LineFormat @@ -15,110 +14,104 @@ from pptx.text.text import TextFrame from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.shapes.autoshape import CT_GeomGuide, CT_PresetGeometry2D, CT_Shape + from pptx.spec import AdjustmentValue + from pptx.types import ProvidesPart -class Adjustment(object): - """ - An adjustment value for an autoshape. - An adjustment value corresponds to the position of an adjustment handle on - an auto shape. Adjustment handles are the small yellow diamond-shaped - handles that appear on certain auto shapes and allow the outline of the - shape to be adjusted. For example, a rounded rectangle has an adjustment - handle that allows the radius of its corner rounding to be adjusted. +class Adjustment: + """An adjustment value for an autoshape. - Values are |float| and generally range from 0.0 to 1.0, although the value - can be negative or greater than 1.0 in certain circumstances. + An adjustment value corresponds to the position of an adjustment handle on an auto shape. + Adjustment handles are the small yellow diamond-shaped handles that appear on certain auto + shapes and allow the outline of the shape to be adjusted. For example, a rounded rectangle has + an adjustment handle that allows the radius of its corner rounding to be adjusted. + + Values are |float| and generally range from 0.0 to 1.0, although the value can be negative or + greater than 1.0 in certain circumstances. """ - def __init__(self, name, def_val, actual=None): + def __init__(self, name: str, def_val: int, actual: int | None = None): super(Adjustment, self).__init__() self.name = name self.def_val = def_val self.actual = actual @property - def effective_value(self): - """ - Read/write |float| representing normalized adjustment value for this - adjustment. Actual values are a large-ish integer expressed in shape - coordinates, nominally between 0 and 100,000. The effective value is - normalized to a corresponding value nominally between 0.0 and 1.0. - Intuitively this represents the proportion of the width or height of - the shape at which the adjustment value is located from its starting - point. For simple shapes such as a rounded rectangle, this intuitive - correspondence holds. For more complicated shapes and at more extreme - shape proportions (e.g. width is much greater than height), the value - can become negative or greater than 1.0. - """ - raw_value = self.actual - if raw_value is None: - raw_value = self.def_val + def effective_value(self) -> float: + """Read/write |float| representing normalized adjustment value for this adjustment. + + Actual values are a large-ish integer expressed in shape coordinates, nominally between 0 + and 100,000. The effective value is normalized to a corresponding value nominally between + 0.0 and 1.0. Intuitively this represents the proportion of the width or height of the shape + at which the adjustment value is located from its starting point. For simple shapes such as + a rounded rectangle, this intuitive correspondence holds. For more complicated shapes and + at more extreme shape proportions (e.g. width is much greater than height), the value can + become negative or greater than 1.0. + """ + raw_value = self.actual if self.actual is not None else self.def_val return self._normalize(raw_value) @effective_value.setter - def effective_value(self, value): + def effective_value(self, value: float): if not isinstance(value, Number): - tmpl = "adjustment value must be numeric, got '%s'" - raise ValueError(tmpl % value) + raise ValueError(f"adjustment value must be numeric, got {repr(value)}") self.actual = self._denormalize(value) @staticmethod - def _denormalize(value): - """ - Return integer corresponding to normalized *raw_value* on unit basis - of 100,000. See Adjustment.normalize for additional details. + def _denormalize(value: float) -> int: + """Return integer corresponding to normalized `raw_value` on unit basis of 100,000. + + See Adjustment.normalize for additional details. """ return int(value * 100000.0) @staticmethod - def _normalize(raw_value): - """ - Return normalized value for *raw_value*. A normalized value is a - |float| between 0.0 and 1.0 for nominal raw values between 0 and - 100,000. Raw values less than 0 and greater than 100,000 are valid - and return values calculated on the same unit basis of 100,000. + def _normalize(raw_value: int) -> float: + """Return normalized value for `raw_value`. + + A normalized value is a |float| between 0.0 and 1.0 for nominal raw values between 0 and + 100,000. Raw values less than 0 and greater than 100,000 are valid and return values + calculated on the same unit basis of 100,000. """ return raw_value / 100000.0 @property - def val(self): - """ - Denormalized effective value (expressed in shape coordinates), - suitable for using in the XML. + def val(self) -> int: + """Denormalized effective value. + + Expressed in shape coordinates, this is suitable for using in the XML. """ return self.actual if self.actual is not None else self.def_val -class AdjustmentCollection(object): - """ - Sequence of |Adjustment| instances for an auto shape, each representing - an available adjustment for a shape of its type. Supports ``len()`` and - indexed access, e.g. ``shape.adjustments[1] = 0.15``. +class AdjustmentCollection: + """Sequence of |Adjustment| instances for an auto shape. + + Each represents an available adjustment for a shape of its type. Supports `len()` and indexed + access, e.g. `shape.adjustments[1] = 0.15`. """ - def __init__(self, prstGeom): + def __init__(self, prstGeom: CT_PresetGeometry2D): super(AdjustmentCollection, self).__init__() self._adjustments_ = self._initialized_adjustments(prstGeom) self._prstGeom = prstGeom - def __getitem__(self, key): + def __getitem__(self, idx: int) -> float: """Provides indexed access, (e.g. 'adjustments[9]').""" - return self._adjustments_[key].effective_value + return self._adjustments_[idx].effective_value - def __setitem__(self, key, value): - """ - Provides item assignment via an indexed expression, e.g. - ``adjustments[9] = 999.9``. Causes all adjustment values in - collection to be written to the XML. + def __setitem__(self, idx: int, value: float): + """Provides item assignment via an indexed expression, e.g. `adjustments[9] = 999.9`. + + Causes all adjustment values in collection to be written to the XML. """ - self._adjustments_[key].effective_value = value + self._adjustments_[idx].effective_value = value self._rewrite_guides() - def _initialized_adjustments(self, prstGeom): - """ - Return an initialized list of adjustment values based on the contents - of *prstGeom* - """ + def _initialized_adjustments(self, prstGeom: CT_PresetGeometry2D | None) -> list[Adjustment]: + """Return an initialized list of adjustment values based on the contents of `prstGeom`.""" if prstGeom is None: return [] davs = AutoShapeType.default_adjustment_values(prstGeom.prst) @@ -127,19 +120,21 @@ def _initialized_adjustments(self, prstGeom): return adjustments def _rewrite_guides(self): - """ - Write ```` elements to the XML, one for each adjustment value. + """Write `a:gd` elements to the XML, one for each adjustment value. + Any existing guide elements are overwritten. """ guides = [(adj.name, adj.val) for adj in self._adjustments_] self._prstGeom.rewrite_guides(guides) @staticmethod - def _update_adjustments_with_actuals(adjustments, guides): - """ - Update |Adjustment| instances in *adjustments* with actual values - held in *guides*, a list of ```` elements. Guides with a name - that does not match an adjustment object are skipped. + def _update_adjustments_with_actuals( + adjustments: Iterable[Adjustment], guides: Iterable[CT_GeomGuide] + ): + """Update |Adjustment| instances in `adjustments` with actual values held in `guides`. + + `guides` is a list of `a:gd` elements. Guides with a name that does not match an adjustment + object are skipped. """ adjustments_by_name = dict((adj.name, adj) for adj in adjustments) for gd in guides: @@ -153,11 +148,8 @@ def _update_adjustments_with_actuals(adjustments, guides): return @property - def _adjustments(self): - """ - Sequence containing direct references to the |Adjustment| objects - contained in collection. - """ + def _adjustments(self) -> tuple[Adjustment, ...]: + """Sequence of |Adjustment| objects contained in collection.""" return tuple(self._adjustments_) def __len__(self): @@ -165,103 +157,88 @@ def __len__(self): return len(self._adjustments_) -class AutoShapeType(object): - """ - Return an instance of |AutoShapeType| containing metadata for an auto - shape of type identified by *autoshape_type_id*. Instances are cached, so - no more than one instance for a particular auto shape type is in memory. +class AutoShapeType: + """Provides access to metadata for an auto-shape of type identified by `autoshape_type_id`. + + Instances are cached, so no more than one instance for a particular auto shape type is in + memory. Instances provide the following attributes: .. attribute:: autoshape_type_id Integer uniquely identifying this auto shape type. Corresponds to a - value in ``pptx.constants.MSO`` like ``MSO_SHAPE.ROUNDED_RECTANGLE``. + value in `pptx.constants.MSO` like `MSO_SHAPE.ROUNDED_RECTANGLE`. .. attribute:: basename - Base part of shape name for auto shapes of this type, e.g. ``Rounded - Rectangle`` becomes ``Rounded Rectangle 99`` when the distinguishing + Base part of shape name for auto shapes of this type, e.g. `Rounded + Rectangle` becomes `Rounded Rectangle 99` when the distinguishing integer is added to the shape name. .. attribute:: prst - String identifier for this auto shape type used in the ```` + String identifier for this auto shape type used in the `a:prstGeom` element. - .. attribute:: desc - - Informal string description of auto shape. - """ - _instances = {} + _instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {} - def __new__(cls, autoshape_type_id): - """ - Only create new instance on first call for content_type. After that, - use cached instance. + def __new__(cls, autoshape_type_id: MSO_AUTO_SHAPE_TYPE) -> AutoShapeType: + """Only create new instance on first call for content_type. + + After that, use cached instance. """ - # if there's not a matching instance in the cache, create one + # -- if there's not a matching instance in the cache, create one -- if autoshape_type_id not in cls._instances: inst = super(AutoShapeType, cls).__new__(cls) cls._instances[autoshape_type_id] = inst - # return the instance; note that __init__() gets called either way + # -- return the instance; note that __init__() gets called either way -- return cls._instances[autoshape_type_id] - def __init__(self, autoshape_type_id): - """Initialize attributes from constant values in pptx.spec""" - # skip loading if this instance is from the cache + def __init__(self, autoshape_type_id: MSO_AUTO_SHAPE_TYPE): + """Initialize attributes from constant values in `pptx.spec`.""" + # -- skip loading if this instance is from the cache -- if hasattr(self, "_loaded"): return - # raise on bad autoshape_type_id + # -- raise on bad autoshape_type_id -- if autoshape_type_id not in autoshape_types: raise KeyError( - "no autoshape type with id '%s' in pptx.spec.autoshape_types" - % autoshape_type_id + "no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id ) - # otherwise initialize new instance + # -- otherwise initialize new instance -- autoshape_type = autoshape_types[autoshape_type_id] self._autoshape_type_id = autoshape_type_id self._basename = autoshape_type["basename"] self._loaded = True @property - def autoshape_type_id(self): - """ - MSO_AUTO_SHAPE_TYPE enumeration value for this auto shape type - """ + def autoshape_type_id(self) -> MSO_AUTO_SHAPE_TYPE: + """MSO_AUTO_SHAPE_TYPE enumeration member identifying this auto shape type.""" return self._autoshape_type_id @property - def basename(self): + def basename(self) -> str: """Base of shape name for this auto shape type. - A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for - example at `p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less - the distinguishing integer. This value is escaped because at least one - autoshape-type name includes double quotes ('"No" Symbol'). + A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for example at + `p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less the distinguishing + integer. This value is escaped because at least one autoshape-type name includes double + quotes ('"No" Symbol'). """ return saxutils.escape(self._basename, {'"': """}) @classmethod - def default_adjustment_values(cls, prst): - """ - Return sequence of name, value tuples representing the adjustment - value defaults for the auto shape type identified by *prst*. - """ + def default_adjustment_values(cls, prst: MSO_AUTO_SHAPE_TYPE) -> tuple[AdjustmentValue, ...]: + """Sequence of (name, value) pair adjustment value defaults for `prst` autoshape-type.""" return autoshape_types[prst]["avLst"] - @property - def desc(self): - """Informal description of this auto shape type""" - return self._desc - @classmethod - def id_from_prst(cls, prst): - """ - Return auto shape id (e.g. ``MSO_SHAPE.RECTANGLE``) corresponding to - preset geometry keyword *prst*. + def id_from_prst(cls, prst: str) -> MSO_AUTO_SHAPE_TYPE: + """Select auto shape type with matching `prst`. + + e.g. `MSO_SHAPE.RECTANGLE` corresponding to preset geometry keyword `"rect"`. """ return MSO_AUTO_SHAPE_TYPE.from_xml(prst) @@ -269,8 +246,8 @@ def id_from_prst(cls, prst): def prst(self): """ Preset geometry identifier string for this auto shape. Used in the - ``prst`` attribute of ```` element to specify the geometry - to be used in rendering the shape, for example ``'roundRect'``. + `prst` attribute of `a:prstGeom` element to specify the geometry + to be used in rendering the shape, for example `'roundRect'`. """ return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id) @@ -278,28 +255,24 @@ def prst(self): class Shape(BaseShape): """A shape that can appear on a slide. - Corresponds to the ```` element that can appear in any of the slide-type parts + Corresponds to the `p:sp` element that can appear in any of the slide-type parts (slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster). """ - def __init__(self, sp, parent): + def __init__(self, sp: CT_Shape, parent: ProvidesPart): super(Shape, self).__init__(sp, parent) self._sp = sp @lazyproperty - def adjustments(self): - """ - Read-only reference to |AdjustmentCollection| instance for this - shape - """ + def adjustments(self) -> AdjustmentCollection: + """Read-only reference to |AdjustmentCollection| instance for this shape.""" return AdjustmentCollection(self._sp.prstGeom) @property def auto_shape_type(self): - """ - Enumeration value identifying the type of this auto shape, like - ``MSO_SHAPE.ROUNDED_RECTANGLE``. Raises |ValueError| if this shape is - not an auto shape. + """Enumeration value identifying the type of this auto shape. + + Like `MSO_SHAPE.ROUNDED_RECTANGLE`. Raises |ValueError| if this shape is not an auto shape. """ if not self._sp.is_autoshape: raise ValueError("shape is not an auto shape") @@ -307,49 +280,40 @@ def auto_shape_type(self): @lazyproperty def fill(self): - """ - |FillFormat| instance for this shape, providing access to fill - properties such as fill color. + """|FillFormat| instance for this shape. + + Provides access to fill properties such as fill color. """ return FillFormat.from_fill_parent(self._sp.spPr) def get_or_add_ln(self): - """ - Return the ```` element containing the line format properties - XML for this shape. - """ + """Return the `a:ln` element containing the line format properties XML for this shape.""" return self._sp.get_or_add_ln() @property - def has_text_frame(self): - """ - |True| if this shape can contain text. Always |True| for an - AutoShape. - """ + def has_text_frame(self) -> bool: + """|True| if this shape can contain text. Always |True| for an AutoShape.""" return True @lazyproperty def line(self): - """ - |LineFormat| instance for this shape, providing access to line - properties such as line color. + """|LineFormat| instance for this shape. + + Provides access to line properties such as line color. """ return LineFormat(self) @property def ln(self): - """ - The ```` element containing the line format properties such as - line color and width. |None| if no ```` element is present. + """The `a:ln` element containing the line format properties such as line color and width. + + |None| if no `a:ln` element is present. """ return self._sp.ln @property - def shape_type(self): - """ - Unique integer identifying the type of this shape, like - ``MSO_SHAPE_TYPE.TEXT_BOX``. - """ + def shape_type(self) -> MSO_SHAPE_TYPE: + """Unique integer identifying the type of this shape, like `MSO_SHAPE_TYPE.TEXT_BOX`.""" if self.is_placeholder: return MSO_SHAPE_TYPE.PLACEHOLDER if self._sp.has_custom_geometry: @@ -358,40 +322,34 @@ def shape_type(self): return MSO_SHAPE_TYPE.AUTO_SHAPE if self._sp.is_textbox: return MSO_SHAPE_TYPE.TEXT_BOX - msg = "Shape instance of unrecognized shape type" - raise NotImplementedError(msg) + raise NotImplementedError("Shape instance of unrecognized shape type") @property - def text(self): - """Read/write. Unicode (str in Python 3) representation of shape text. - - The returned string will contain a newline character (``"\\n"``) separating each - paragraph and a vertical-tab (``"\\v"``) character for each line break (soft - carriage return) in the shape's text. - - Assignment to *text* replaces all text previously contained in the shape, along - with any paragraph or font formatting applied to it. A newline character - (``"\\n"``) in the assigned text causes a new paragraph to be started. - A vertical-tab (``"\\v"``) character in the assigned text causes a line-break - (soft carriage-return) to be inserted. (The vertical-tab character appears in - clipboard text copied from PowerPoint as its encoding of line-breaks.) - - Either bytes (Python 2 str) or unicode (Python 3 str) can be assigned. Bytes can - be 7-bit ASCII or UTF-8 encoded 8-bit bytes. Bytes values are converted to - unicode assuming UTF-8 encoding (which also works for ASCII). + def text(self) -> str: + """Read/write. Text in shape as a single string. + + The returned string will contain a newline character (`"\\n"`) separating each paragraph + and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the + shape's text. + + Assignment to `text` replaces any text previously contained in the shape, along with any + paragraph or font formatting applied to it. A newline character (`"\\n"`) in the assigned + text causes a new paragraph to be started. A vertical-tab (`"\\v"`) character in the + assigned text causes a line-break (soft carriage-return) to be inserted. (The vertical-tab + character appears in clipboard text copied from PowerPoint as its str encoding of + line-breaks.) """ return self.text_frame.text @text.setter - def text(self, text): + def text(self, text: str): self.text_frame.text = text @property def text_frame(self): """|TextFrame| instance for this shape. - Contains the text of the shape and provides access to text formatting - properties. + Contains the text of the shape and provides access to text formatting properties. """ - txBody = self._element.get_or_add_txBody() + txBody = self._sp.get_or_add_txBody() return TextFrame(txBody, self) diff --git a/src/pptx/shapes/base.py b/src/pptx/shapes/base.py index c9472434d..751235023 100644 --- a/src/pptx/shapes/base.py +++ b/src/pptx/shapes/base.py @@ -1,14 +1,22 @@ -# encoding: utf-8 - """Base shape-related objects such as BaseShape.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, cast from pptx.action import ActionSetting from pptx.dml.effect import ShadowFormat from pptx.shared import ElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER + from pptx.oxml.shapes import ShapeElement + from pptx.oxml.shapes.shared import CT_Placeholder + from pptx.parts.slide import BaseSlidePart + from pptx.types import ProvidesPart + from pptx.util import Length + class BaseShape(object): """Base class for shape objects. @@ -16,158 +24,148 @@ class BaseShape(object): Subclasses include |Shape|, |Picture|, and |GraphicFrame|. """ - def __init__(self, shape_elm, parent): - super(BaseShape, self).__init__() + def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart): + super().__init__() self._element = shape_elm self._parent = parent - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """|True| if this shape object proxies the same element as *other*. - Equality for proxy objects is defined as referring to the same XML - element, whether or not they are the same proxy object instance. + Equality for proxy objects is defined as referring to the same XML element, whether or not + they are the same proxy object instance. """ if not isinstance(other, BaseShape): return False return self._element is other._element - def __ne__(self, other): + def __ne__(self, other: object) -> bool: if not isinstance(other, BaseShape): return True return self._element is not other._element @lazyproperty - def click_action(self): + def click_action(self) -> ActionSetting: """|ActionSetting| instance providing access to click behaviors. - Click behaviors are hyperlink-like behaviors including jumping to - a hyperlink (web page) or to another slide in the presentation. The - click action is that defined on the overall shape, not a run of text - within the shape. An |ActionSetting| object is always returned, even - when no click behavior is defined on the shape. + Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page) + or to another slide in the presentation. The click action is that defined on the overall + shape, not a run of text within the shape. An |ActionSetting| object is always returned, + even when no click behavior is defined on the shape. """ - cNvPr = self._element._nvXxPr.cNvPr + cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage] return ActionSetting(cNvPr, self) @property - def element(self): + def element(self) -> ShapeElement: """`lxml` element for this shape, e.g. a CT_Shape instance. - Note that manipulating this element improperly can produce an invalid - presentation file. Make sure you know what you're doing if you use - this to change the underlying XML. + Note that manipulating this element improperly can produce an invalid presentation file. + Make sure you know what you're doing if you use this to change the underlying XML. """ return self._element @property - def has_chart(self): - """ - |True| if this shape is a graphic frame containing a chart object. - |False| otherwise. When |True|, the chart object can be accessed - using the ``.chart`` property. + def has_chart(self) -> bool: + """|True| if this shape is a graphic frame containing a chart object. + + |False| otherwise. When |True|, the chart object can be accessed using the ``.chart`` + property. """ # This implementation is unconditionally False, the True version is # on GraphicFrame subclass. return False @property - def has_table(self): - """ - |True| if this shape is a graphic frame containing a table object. - |False| otherwise. When |True|, the table object can be accessed - using the ``.table`` property. + def has_table(self) -> bool: + """|True| if this shape is a graphic frame containing a table object. + + |False| otherwise. When |True|, the table object can be accessed using the ``.table`` + property. """ # This implementation is unconditionally False, the True version is # on GraphicFrame subclass. return False @property - def has_text_frame(self): - """ - |True| if this shape can contain text. - """ + def has_text_frame(self) -> bool: + """|True| if this shape can contain text.""" # overridden on Shape to return True. Only has text frame return False @property - def height(self): - """ - Read/write. Integer distance between top and bottom extents of shape - in EMUs - """ + def height(self) -> Length: + """Read/write. Integer distance between top and bottom extents of shape in EMUs.""" return self._element.cy @height.setter - def height(self, value): + def height(self, value: Length): self._element.cy = value @property - def is_placeholder(self): - """ - True if this shape is a placeholder. A shape is a placeholder if it - has a element. + def is_placeholder(self) -> bool: + """True if this shape is a placeholder. + + A shape is a placeholder if it has a element. """ return self._element.has_ph_elm @property - def left(self): - """ - Read/write. Integer distance of the left edge of this shape from the - left edge of the slide, in English Metric Units (EMU) + def left(self) -> Length: + """Integer distance of the left edge of this shape from the left edge of the slide. + + Read/write. Expressed in English Metric Units (EMU) """ return self._element.x @left.setter - def left(self, value): + def left(self, value: Length): self._element.x = value @property - def name(self): - """ - Name of this shape, e.g. 'Picture 7' - """ + def name(self) -> str: + """Name of this shape, e.g. 'Picture 7'.""" return self._element.shape_name @name.setter - def name(self, value): - self._element._nvXxPr.cNvPr.name = value + def name(self, value: str): + self._element._nvXxPr.cNvPr.name = value # pyright: ignore[reportPrivateUsage] @property - def part(self): + def part(self) -> BaseSlidePart: """The package part containing this shape. - A |BaseSlidePart| subclass in this case. Access to a slide part - should only be required if you are extending the behavior of |pp| API - objects. + A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if + you are extending the behavior of |pp| API objects. """ - return self._parent.part + return cast("BaseSlidePart", self._parent.part) @property - def placeholder_format(self): - """ - A |_PlaceholderFormat| object providing access to - placeholder-specific properties such as placeholder type. Raises - |ValueError| on access if the shape is not a placeholder. + def placeholder_format(self) -> _PlaceholderFormat: + """Provides access to placeholder-specific properties such as placeholder type. + + Raises |ValueError| on access if the shape is not a placeholder. """ - if not self.is_placeholder: + ph = self._element.ph + if ph is None: raise ValueError("shape is not a placeholder") - return _PlaceholderFormat(self._element.ph) + return _PlaceholderFormat(ph) @property - def rotation(self): - """ - Read/write float. Degrees of clockwise rotation. Negative values can - be assigned to indicate counter-clockwise rotation, e.g. assigning - -45.0 will change setting to 315.0. + def rotation(self) -> float: + """Degrees of clockwise rotation. + + Read/write float. Negative values can be assigned to indicate counter-clockwise rotation, + e.g. assigning -45.0 will change setting to 315.0. """ return self._element.rot @rotation.setter - def rotation(self, value): + def rotation(self, value: float): self._element.rot = value @lazyproperty - def shadow(self): + def shadow(self) -> ShadowFormat: """|ShadowFormat| object providing access to shadow for this shape. A |ShadowFormat| object is always returned, even when no shadow is @@ -177,7 +175,7 @@ def shadow(self): return ShadowFormat(self._element.spPr) @property - def shape_id(self): + def shape_id(self) -> int: """Read-only positive integer identifying this shape. The id of a shape is unique among all shapes on a slide. @@ -185,68 +183,62 @@ def shape_id(self): return self._element.shape_id @property - def shape_type(self): - """ - Unique integer identifying the type of this shape, like - ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses. + def shape_type(self) -> MSO_SHAPE_TYPE: + """A member of MSO_SHAPE_TYPE classifying this shape by type. + + Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses. """ - # # This one returns |None| unconditionally to account for shapes - # # that haven't been implemented yet, like group shape and chart. - # # Once those are done this should raise |NotImplementedError|. - # msg = 'shape_type property must be implemented by subclasses' - # raise NotImplementedError(msg) - return None + raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`") @property - def top(self): - """ - Read/write. Integer distance of the top edge of this shape from the - top edge of the slide, in English Metric Units (EMU) + def top(self) -> Length: + """Distance from the top edge of the slide to the top edge of this shape. + + Read/write. Expressed in English Metric Units (EMU) """ return self._element.y @top.setter - def top(self, value): + def top(self, value: Length): self._element.y = value @property - def width(self): - """ - Read/write. Integer distance between left and right extents of shape - in EMUs + def width(self) -> Length: + """Distance between left and right extents of this shape. + + Read/write. Expressed in English Metric Units (EMU). """ return self._element.cx @width.setter - def width(self, value): + def width(self, value: Length): self._element.cx = value class _PlaceholderFormat(ElementProxy): + """Provides properties specific to placeholders, such as the placeholder type. + + Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape, """ - Accessed via the :attr:`~.BaseShape.placeholder_format` property of - a placeholder shape, provides properties specific to placeholders, such - as the placeholder type. - """ + + def __init__(self, element: CT_Placeholder): + super().__init__(element) + self._ph = element @property - def element(self): - """ - The `p:ph` element proxied by this object. - """ - return super(_PlaceholderFormat, self).element + def element(self) -> CT_Placeholder: + """The `p:ph` element proxied by this object.""" + return self._ph @property - def idx(self): - """ - Integer placeholder 'idx' attribute. - """ - return self._element.idx + def idx(self) -> int: + """Integer placeholder 'idx' attribute.""" + return self._ph.idx @property - def type(self): - """ - Placeholder type, a member of the :ref:`PpPlaceholderType` - enumeration, e.g. PP_PLACEHOLDER.CHART + def type(self) -> PP_PLACEHOLDER: + """Placeholder type. + + A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART """ - return self._element.type + return self._ph.type diff --git a/src/pptx/shapes/connector.py b/src/pptx/shapes/connector.py index ecd8ec9a9..070b080d5 100644 --- a/src/pptx/shapes/connector.py +++ b/src/pptx/shapes/connector.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - """Connector (line) shape and related objects. A connector is a line shape having end-points that can be connected to other @@ -7,7 +5,7 @@ elbows, or can be curved. """ -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations from pptx.dml.line import LineFormat from pptx.enum.shapes import MSO_SHAPE_TYPE diff --git a/src/pptx/shapes/freeform.py b/src/pptx/shapes/freeform.py index 0168b2baf..e05b3484f 100644 --- a/src/pptx/shapes/freeform.py +++ b/src/pptx/shapes/freeform.py @@ -1,28 +1,50 @@ -# encoding: utf-8 - """Objects related to construction of freeform shapes.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Iterable, Iterator + +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from pptx.oxml.shapes.autoshape import ( + CT_Path2D, + CT_Path2DClose, + CT_Path2DLineTo, + CT_Path2DMoveTo, + CT_Shape, + ) + from pptx.shapes.shapetree import _BaseGroupShapes # pyright: ignore[reportPrivateUsage] + from pptx.util import Length -from pptx.compat import Sequence -from pptx.util import lazyproperty +CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo" +DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close" -class FreeformBuilder(Sequence): +class FreeformBuilder(Sequence[DrawingOperation]): """Allows a freeform shape to be specified and created. - The initial pen position is provided on construction. From there, drawing - proceeds using successive calls to draw line segments. The freeform shape - may be closed by calling the :meth:`close` method. + The initial pen position is provided on construction. From there, drawing proceeds using + successive calls to draw line segments. The freeform shape may be closed by calling the + :meth:`close` method. - A shape may have more than one contour, in which case overlapping areas - are "subtracted". A contour is a sequence of line segments beginning with - a "move-to" operation. A move-to operation is automatically inserted in - each new freeform; additional move-to ops can be inserted with the - `.move_to()` method. + A shape may have more than one contour, in which case overlapping areas are "subtracted". A + contour is a sequence of line segments beginning with a "move-to" operation. A move-to + operation is automatically inserted in each new freeform; additional move-to ops can be + inserted with the `.move_to()` method. """ - def __init__(self, shapes, start_x, start_y, x_scale, y_scale): + def __init__( + self, + shapes: _BaseGroupShapes, + start_x: Length, + start_y: Length, + x_scale: float, + y_scale: float, + ): super(FreeformBuilder, self).__init__() self._shapes = shapes self._start_x = start_x @@ -30,34 +52,41 @@ def __init__(self, shapes, start_x, start_y, x_scale, y_scale): self._x_scale = x_scale self._y_scale = y_scale - def __getitem__(self, idx): + def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride] + self, idx: int + ) -> DrawingOperation: return self._drawing_operations.__getitem__(idx) - def __iter__(self): + def __iter__(self) -> Iterator[DrawingOperation]: return self._drawing_operations.__iter__() def __len__(self): return self._drawing_operations.__len__() @classmethod - def new(cls, shapes, start_x, start_y, x_scale, y_scale): + def new( + cls, + shapes: _BaseGroupShapes, + start_x: float, + start_y: float, + x_scale: float, + y_scale: float, + ): """Return a new |FreeformBuilder| object. The initial pen location is specified (in local coordinates) by - (*start_x*, *start_y*). + (`start_x`, `start_y`). """ - return cls(shapes, int(round(start_x)), int(round(start_y)), x_scale, y_scale) + return cls(shapes, Emu(int(round(start_x))), Emu(int(round(start_y))), x_scale, y_scale) - def add_line_segments(self, vertices, close=True): - """Add a straight line segment to each point in *vertices*. + def add_line_segments(self, vertices: Iterable[tuple[float, float]], close: bool = True): + """Add a straight line segment to each point in `vertices`. - *vertices* must be an iterable of (x, y) pairs (2-tuples). Each x and - y value is rounded to the nearest integer before use. The optional - *close* parameter determines whether the resulting contour is - *closed* or left *open*. + `vertices` must be an iterable of (x, y) pairs (2-tuples). Each x and y value is rounded + to the nearest integer before use. The optional `close` parameter determines whether the + resulting contour is `closed` or left `open`. - Returns this |FreeformBuilder| object so it can be used in chained - calls. + Returns this |FreeformBuilder| object so it can be used in chained calls. """ for x, y in vertices: self._add_line_segment(x, y) @@ -65,109 +94,109 @@ def add_line_segments(self, vertices, close=True): self._add_close() return self - def convert_to_shape(self, origin_x=0, origin_y=0): + def convert_to_shape(self, origin_x: Length = Emu(0), origin_y: Length = Emu(0)): """Return new freeform shape positioned relative to specified offset. - *origin_x* and *origin_y* locate the origin of the local coordinate - system in slide coordinates (EMU), perhaps most conveniently by use - of a |Length| object. + `origin_x` and `origin_y` locate the origin of the local coordinate system in slide + coordinates (EMU), perhaps most conveniently by use of a |Length| object. - Note that this method may be called more than once to add multiple - shapes of the same geometry in different locations on the slide. + Note that this method may be called more than once to add multiple shapes of the same + geometry in different locations on the slide. """ sp = self._add_freeform_sp(origin_x, origin_y) path = self._start_path(sp) for drawing_operation in self: drawing_operation.apply_operation_to(path) - return self._shapes._shape_factory(sp) + return self._shapes._shape_factory(sp) # pyright: ignore[reportPrivateUsage] - def move_to(self, x, y): + def move_to(self, x: float, y: float): """Move pen to (x, y) (local coordinates) without drawing line. - Returns this |FreeformBuilder| object so it can be used in chained - calls. + Returns this |FreeformBuilder| object so it can be used in chained calls. """ self._drawing_operations.append(_MoveTo.new(self, x, y)) return self @property - def shape_offset_x(self): + def shape_offset_x(self) -> Length: """Return x distance of shape origin from local coordinate origin. - The returned integer represents the leftmost extent of the freeform - shape, in local coordinates. Note that the bounding box of the shape - need not start at the local origin. + The returned integer represents the leftmost extent of the freeform shape, in local + coordinates. Note that the bounding box of the shape need not start at the local origin. """ min_x = self._start_x for drawing_operation in self: - if hasattr(drawing_operation, "x"): - min_x = min(min_x, drawing_operation.x) - return min_x + if isinstance(drawing_operation, _Close): + continue + min_x = min(min_x, drawing_operation.x) + return Emu(min_x) @property - def shape_offset_y(self): + def shape_offset_y(self) -> Length: """Return y distance of shape origin from local coordinate origin. - The returned integer represents the topmost extent of the freeform - shape, in local coordinates. Note that the bounding box of the shape - need not start at the local origin. + The returned integer represents the topmost extent of the freeform shape, in local + coordinates. Note that the bounding box of the shape need not start at the local origin. """ min_y = self._start_y for drawing_operation in self: - if hasattr(drawing_operation, "y"): - min_y = min(min_y, drawing_operation.y) - return min_y + if isinstance(drawing_operation, _Close): + continue + min_y = min(min_y, drawing_operation.y) + return Emu(min_y) def _add_close(self): """Add a close |_Close| operation to the drawing sequence.""" self._drawing_operations.append(_Close.new()) - def _add_freeform_sp(self, origin_x, origin_y): + def _add_freeform_sp(self, origin_x: Length, origin_y: Length): """Add a freeform `p:sp` element having no drawing elements. - *origin_x* and *origin_y* are specified in slide coordinates, and - represent the location of the local coordinates origin on the slide. + `origin_x` and `origin_y` are specified in slide coordinates, and represent the location + of the local coordinates origin on the slide. """ - spTree = self._shapes._spTree + spTree = self._shapes._spTree # pyright: ignore[reportPrivateUsage] return spTree.add_freeform_sp( origin_x + self._left, origin_y + self._top, self._width, self._height ) - def _add_line_segment(self, x, y): + def _add_line_segment(self, x: float, y: float) -> None: """Add a |_LineSegment| operation to the drawing sequence.""" self._drawing_operations.append(_LineSegment.new(self, x, y)) @lazyproperty - def _drawing_operations(self): + def _drawing_operations(self) -> list[DrawingOperation]: """Return the sequence of drawing operation objects for freeform.""" return [] @property - def _dx(self): - """Return integer width of this shape's path in local units.""" + def _dx(self) -> Length: + """Return width of this shape's path in local units.""" min_x = max_x = self._start_x for drawing_operation in self: - if hasattr(drawing_operation, "x"): - min_x = min(min_x, drawing_operation.x) - max_x = max(max_x, drawing_operation.x) - return max_x - min_x + if isinstance(drawing_operation, _Close): + continue + min_x = min(min_x, drawing_operation.x) + max_x = max(max_x, drawing_operation.x) + return Emu(max_x - min_x) @property - def _dy(self): + def _dy(self) -> Length: """Return integer height of this shape's path in local units.""" min_y = max_y = self._start_y for drawing_operation in self: - if hasattr(drawing_operation, "y"): - min_y = min(min_y, drawing_operation.y) - max_y = max(max_y, drawing_operation.y) - return max_y - min_y + if isinstance(drawing_operation, _Close): + continue + min_y = min(min_y, drawing_operation.y) + max_y = max(max_y, drawing_operation.y) + return Emu(max_y - min_y) @property def _height(self): """Return vertical size of this shape's path in slide coordinates. - This value is based on the actual extents of the shape and does not - include any positioning offset. + This value is based on the actual extents of the shape and does not include any + positioning offset. """ return int(round(self._dy * self._y_scale)) @@ -175,26 +204,25 @@ def _height(self): def _left(self): """Return leftmost extent of this shape's path in slide coordinates. - Note that this value does not include any positioning offset; it - assumes the drawing (local) coordinate origin is at (0, 0) on the - slide. + Note that this value does not include any positioning offset; it assumes the drawing + (local) coordinate origin is at (0, 0) on the slide. """ return int(round(self.shape_offset_x * self._x_scale)) - def _local_to_shape(self, local_x, local_y): + def _local_to_shape(self, local_x: Length, local_y: Length) -> tuple[Length, Length]: """Translate local coordinates point to shape coordinates. - Shape coordinates have the same unit as local coordinates, but are - offset such that the origin of the shape coordinate system (0, 0) is - located at the top-left corner of the shape bounding box. + Shape coordinates have the same unit as local coordinates, but are offset such that the + origin of the shape coordinate system (0, 0) is located at the top-left corner of the + shape bounding box. """ - return (local_x - self.shape_offset_x, local_y - self.shape_offset_y) + return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y) - def _start_path(self, sp): - """Return a newly created `a:path` element added to *sp*. + def _start_path(self, sp: CT_Shape) -> CT_Path2D: + """Return a newly created `a:path` element added to `sp`. - The returned `a:path` element has an `a:moveTo` element representing - the shape starting point as its only child. + The returned `a:path` element has an `a:moveTo` element representing the shape starting + point as its only child. """ path = sp.add_path(w=self._dx, h=self._dy) path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y)) @@ -204,9 +232,9 @@ def _start_path(self, sp): def _top(self): """Return topmost extent of this shape's path in slide coordinates. - Note that this value does not include any positioning offset; it - assumes the drawing (local) coordinate origin is located at slide - coordinates (0, 0) (top-left corner of slide). + Note that this value does not include any positioning offset; it assumes the drawing + (local) coordinate origin is located at slide coordinates (0, 0) (top-left corner of + slide). """ return int(round(self.shape_offset_y * self._y_scale)) @@ -214,8 +242,8 @@ def _top(self): def _width(self): """Return width of this shape's path in slide coordinates. - This value is based on the actual extents of the shape path and does - not include any positioning offset. + This value is based on the actual extents of the shape path and does not include any + positioning offset. """ return int(round(self._dx * self._x_scale)) @@ -223,25 +251,24 @@ def _width(self): class _BaseDrawingOperation(object): """Base class for freeform drawing operations. - A drawing operation has at least one location (x, y) in local - coordinates. + A drawing operation has at least one location (x, y) in local coordinates. """ - def __init__(self, freeform_builder, x, y): + def __init__(self, freeform_builder: FreeformBuilder, x: Length, y: Length): super(_BaseDrawingOperation, self).__init__() self._freeform_builder = freeform_builder self._x = x self._y = y - def apply_operation_to(self, path): - """Add the XML element(s) implementing this operation to *path*. + def apply_operation_to(self, path: CT_Path2D) -> CT_DrawingOperation: + """Add the XML element(s) implementing this operation to `path`. Must be implemented by each subclass. """ raise NotImplementedError("must be implemented by each subclass") @property - def x(self): + def x(self) -> Length: """Return the horizontal (x) target location of this operation. The returned value is an integer in local coordinates. @@ -249,7 +276,7 @@ def x(self): return self._x @property - def y(self): + def y(self) -> Length: """Return the vertical (y) target location of this operation. The returned value is an integer in local coordinates. @@ -261,12 +288,12 @@ class _Close(object): """Specifies adding a `` element to the current contour.""" @classmethod - def new(cls): + def new(cls) -> _Close: """Return a new _Close object.""" return cls() - def apply_operation_to(self, path): - """Add `a:close` element to *path*.""" + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose: + """Add `a:close` element to `path`.""" return path.add_close() @@ -274,21 +301,21 @@ class _LineSegment(_BaseDrawingOperation): """Specifies a straight line segment ending at the specified point.""" @classmethod - def new(cls, freeform_builder, x, y): + def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _LineSegment: """Return a new _LineSegment object ending at point *(x, y)*. - Both *x* and *y* are rounded to the nearest integer before use. + Both `x` and `y` are rounded to the nearest integer before use. """ - return cls(freeform_builder, int(round(x)), int(round(y))) + return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y)))) - def apply_operation_to(self, path): - """Add `a:lnTo` element to *path* for this line segment. + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DLineTo: + """Add `a:lnTo` element to `path` for this line segment. Returns the `a:lnTo` element newly added to the path. """ return path.add_lnTo( - self._x - self._freeform_builder.shape_offset_x, - self._y - self._freeform_builder.shape_offset_y, + Emu(self._x - self._freeform_builder.shape_offset_x), + Emu(self._y - self._freeform_builder.shape_offset_y), ) @@ -296,16 +323,16 @@ class _MoveTo(_BaseDrawingOperation): """Specifies a new pen position.""" @classmethod - def new(cls, freeform_builder, x, y): - """Return a new _MoveTo object for move to point *(x, y)*. + def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _MoveTo: + """Return a new _MoveTo object for move to point `(x, y)`. - Both *x* and *y* are rounded to the nearest integer before use. + Both `x` and `y` are rounded to the nearest integer before use. """ - return cls(freeform_builder, int(round(x)), int(round(y))) + return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y)))) - def apply_operation_to(self, path): - """Add `a:moveTo` element to *path* for this line segment.""" + def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo: + """Add `a:moveTo` element to `path` for this line segment.""" return path.add_moveTo( - self._x - self._freeform_builder.shape_offset_x, - self._y - self._freeform_builder.shape_offset_y, + Emu(self._x - self._freeform_builder.shape_offset_x), + Emu(self._y - self._freeform_builder.shape_offset_y), ) diff --git a/src/pptx/shapes/graphfrm.py b/src/pptx/shapes/graphfrm.py index de317cc5c..c0ed2bbab 100644 --- a/src/pptx/shapes/graphfrm.py +++ b/src/pptx/shapes/graphfrm.py @@ -1,11 +1,13 @@ -# encoding: utf-8 - """Graphic Frame shape and related objects. A graphic frame is a common container for table, chart, smart art, and media objects. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.shapes.base import BaseShape from pptx.shared import ParentedElementProxy @@ -15,16 +17,29 @@ GRAPHIC_DATA_URI_TABLE, ) from pptx.table import Table +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.chart.chart import Chart + from pptx.dml.effect import ShadowFormat + from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectData, CT_GraphicalObjectFrame + from pptx.parts.chart import ChartPart + from pptx.parts.slide import BaseSlidePart + from pptx.types import ProvidesPart class GraphicFrame(BaseShape): """Container shape for table, chart, smart art, and media objects. - Corresponds to a ```` element in the shape tree. + Corresponds to a `p:graphicFrame` element in the shape tree. """ + def __init__(self, graphicFrame: CT_GraphicalObjectFrame, parent: ProvidesPart): + super().__init__(graphicFrame, parent) + self._graphicFrame = graphicFrame + @property - def chart(self): + def chart(self) -> Chart: """The |Chart| object containing the chart in this graphic frame. Raises |ValueError| if this graphic frame does not contain a chart. @@ -34,61 +49,62 @@ def chart(self): return self.chart_part.chart @property - def chart_part(self): + def chart_part(self) -> ChartPart: """The |ChartPart| object containing the chart in this graphic frame.""" - return self.part.related_part(self._element.chart_rId) + chart_rId = self._graphicFrame.chart_rId + if chart_rId is None: + raise ValueError("this graphic frame does not contain a chart") + return cast("ChartPart", self.part.related_part(chart_rId)) @property - def has_chart(self): + def has_chart(self) -> bool: """|True| if this graphic frame contains a chart object. |False| otherwise. - When |True|, the chart object can be accessed using the ``.chart`` property. + When |True|, the chart object can be accessed using the `.chart` property. """ - return self._element.graphicData_uri == GRAPHIC_DATA_URI_CHART + return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_CHART @property - def has_table(self): + def has_table(self) -> bool: """|True| if this graphic frame contains a table object, |False| otherwise. When |True|, the table object can be accessed using the `.table` property. """ - return self._element.graphicData_uri == GRAPHIC_DATA_URI_TABLE + return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_TABLE @property - def ole_format(self): - """Optional _OleFormat object for this graphic-frame shape. + def ole_format(self) -> _OleFormat: + """_OleFormat object for this graphic-frame shape. - Raises `ValueError` on a GraphicFrame instance that does not contain an OLE - object. + Raises `ValueError` on a GraphicFrame instance that does not contain an OLE object. An shape that contains an OLE object will have `.shape_type` of either `EMBEDDED_OLE_OBJECT` or `LINKED_OLE_OBJECT`. """ - if not self._element.has_oleobj: + if not self._graphicFrame.has_oleobj: raise ValueError("not an OLE-object shape") - return _OleFormat(self._element.graphicData, self._parent) + return _OleFormat(self._graphicFrame.graphicData, self._parent) - @property - def shadow(self): + @lazyproperty + def shadow(self) -> ShadowFormat: """Unconditionally raises |NotImplementedError|. - Access to the shadow effect for graphic-frame objects is - content-specific (i.e. different for charts, tables, etc.) and has - not yet been implemented. + Access to the shadow effect for graphic-frame objects is content-specific (i.e. different + for charts, tables, etc.) and has not yet been implemented. """ raise NotImplementedError("shadow property on GraphicFrame not yet supported") @property - def shape_type(self): + def shape_type(self) -> MSO_SHAPE_TYPE: """Optional member of `MSO_SHAPE_TYPE` identifying the type of this shape. - Possible values are ``MSO_SHAPE_TYPE.CHART``, ``MSO_SHAPE_TYPE.TABLE``, - ``MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT``, ``MSO_SHAPE_TYPE.LINKED_OLE_OBJECT``. + Possible values are `MSO_SHAPE_TYPE.CHART`, `MSO_SHAPE_TYPE.TABLE`, + `MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT`, `MSO_SHAPE_TYPE.LINKED_OLE_OBJECT`. - This value is `None` when none of these four types apply, for example when the - shape contains SmartArt. + This value is `None` when none of these four types apply, for example when the shape + contains SmartArt. """ - graphicData_uri = self._element.graphicData_uri + graphicData_uri = self._graphicFrame.graphicData_uri if graphicData_uri == GRAPHIC_DATA_URI_CHART: return MSO_SHAPE_TYPE.CHART elif graphicData_uri == GRAPHIC_DATA_URI_TABLE: @@ -96,50 +112,55 @@ def shape_type(self): elif graphicData_uri == GRAPHIC_DATA_URI_OLEOBJ: return ( MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT - if self._element.is_embedded_ole_obj + if self._graphicFrame.is_embedded_ole_obj else MSO_SHAPE_TYPE.LINKED_OLE_OBJECT ) else: - return None + return None # pyright: ignore[reportReturnType] @property - def table(self): - """ - The |Table| object contained in this graphic frame. Raises - |ValueError| if this graphic frame does not contain a table. + def table(self) -> Table: + """The |Table| object contained in this graphic frame. + + Raises |ValueError| if this graphic frame does not contain a table. """ if not self.has_table: raise ValueError("shape does not contain a table") - tbl = self._element.graphic.graphicData.tbl + tbl = self._graphicFrame.graphic.graphicData.tbl return Table(tbl, self) class _OleFormat(ParentedElementProxy): """Provides attributes on an embedded OLE object.""" - def __init__(self, graphicData, parent): - super(_OleFormat, self).__init__(graphicData, parent) + part: BaseSlidePart # pyright: ignore[reportIncompatibleMethodOverride] + + def __init__(self, graphicData: CT_GraphicalObjectData, parent: ProvidesPart): + super().__init__(graphicData, parent) self._graphicData = graphicData @property - def blob(self): + def blob(self) -> bytes | None: """Optional bytes of OLE object, suitable for loading or saving as a file. - This value is None if the embedded object does not represent a "file". + This value is `None` if the embedded object does not represent a "file". """ - return self.part.related_part(self._graphicData.blob_rId).blob + blob_rId = self._graphicData.blob_rId + if blob_rId is None: + return None + return self.part.related_part(blob_rId).blob @property - def prog_id(self): + def prog_id(self) -> str | None: """str "progId" attribute of this embedded OLE object. - The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the - embedded object, or perhaps more precisely, the application (aka. "server" in - OLE parlance) to be used to open this object. + The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the embedded + object, or perhaps more precisely, the application (aka. "server" in OLE parlance) to be + used to open this object. """ return self._graphicData.progId @property - def show_as_icon(self): + def show_as_icon(self) -> bool | None: """True when OLE object should appear as an icon (rather than preview).""" return self._graphicData.showAsIcon diff --git a/src/pptx/shapes/group.py b/src/pptx/shapes/group.py index 0de06f853..717375851 100644 --- a/src/pptx/shapes/group.py +++ b/src/pptx/shapes/group.py @@ -1,20 +1,30 @@ -# encoding: utf-8 - """GroupShape and related objects.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING from pptx.dml.effect import ShadowFormat from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.shapes.base import BaseShape from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.action import ActionSetting + from pptx.oxml.shapes.groupshape import CT_GroupShape + from pptx.shapes.shapetree import GroupShapes + from pptx.types import ProvidesPart + class GroupShape(BaseShape): """A shape that acts as a container for other shapes.""" - @property - def click_action(self): + def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart): + super().__init__(grpSp, parent) + self._grpSp = grpSp + + @lazyproperty + def click_action(self) -> ActionSetting: """Unconditionally raises `TypeError`. A group shape cannot have a click action or hover action. @@ -22,27 +32,25 @@ def click_action(self): raise TypeError("a group shape cannot have a click action") @property - def has_text_frame(self): + def has_text_frame(self) -> bool: """Unconditionally |False|. - A group shape does not have a textframe and cannot itself contain - text. This does not impact the ability of shapes contained by the - group to each have their own text. + A group shape does not have a textframe and cannot itself contain text. This does not + impact the ability of shapes contained by the group to each have their own text. """ return False @lazyproperty - def shadow(self): + def shadow(self) -> ShadowFormat: """|ShadowFormat| object representing shadow effect for this group. - A |ShadowFormat| object is always returned, even when no shadow is - explicitly defined on this group shape (i.e. when the group inherits - its shadow behavior). + A |ShadowFormat| object is always returned, even when no shadow is explicitly defined on + this group shape (i.e. when the group inherits its shadow behavior). """ - return ShadowFormat(self._element.grpSpPr) + return ShadowFormat(self._grpSp.grpSpPr) @property - def shape_type(self): + def shape_type(self) -> MSO_SHAPE_TYPE: """Member of :ref:`MsoShapeType` identifying the type of this shape. Unconditionally `MSO_SHAPE_TYPE.GROUP` in this case @@ -50,11 +58,11 @@ def shape_type(self): return MSO_SHAPE_TYPE.GROUP @lazyproperty - def shapes(self): + def shapes(self) -> GroupShapes: """|GroupShapes| object for this group. - The |GroupShapes| object provides access to the group's member shapes - and provides methods for adding new ones. + The |GroupShapes| object provides access to the group's member shapes and provides methods + for adding new ones. """ from pptx.shapes.shapetree import GroupShapes diff --git a/src/pptx/shapes/picture.py b/src/pptx/shapes/picture.py index 1cb660e6f..59182860d 100644 --- a/src/pptx/shapes/picture.py +++ b/src/pptx/shapes/picture.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Shapes based on the `p:pic` element, including Picture and Movie.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING from pptx.dml.line import LineFormat from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE @@ -10,84 +10,87 @@ from pptx.shared import ParentedElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.shapes.picture import CT_Picture + from pptx.oxml.shapes.shared import CT_LineProperties + from pptx.types import ProvidesPart + class _BasePicture(BaseShape): """Base class for shapes based on a `p:pic` element.""" - def __init__(self, pic, parent): + def __init__(self, pic: CT_Picture, parent: ProvidesPart): super(_BasePicture, self).__init__(pic, parent) self._pic = pic @property - def crop_bottom(self): + def crop_bottom(self) -> float: """|float| representing relative portion cropped from shape bottom. - Read/write. 1.0 represents 100%. For example, 25% is represented by - 0.25. Negative values are valid as are values greater than 1.0. + Read/write. 1.0 represents 100%. For example, 25% is represented by 0.25. Negative values + are valid as are values greater than 1.0. """ - return self._element.srcRect_b + return self._pic.srcRect_b @crop_bottom.setter - def crop_bottom(self, value): - self._element.srcRect_b = value + def crop_bottom(self, value: float): + self._pic.srcRect_b = value @property - def crop_left(self): + def crop_left(self) -> float: """|float| representing relative portion cropped from left of shape. - Read/write. 1.0 represents 100%. A negative value extends the side - beyond the image boundary. + Read/write. 1.0 represents 100%. A negative value extends the side beyond the image + boundary. """ - return self._element.srcRect_l + return self._pic.srcRect_l @crop_left.setter - def crop_left(self, value): - self._element.srcRect_l = value + def crop_left(self, value: float): + self._pic.srcRect_l = value @property - def crop_right(self): + def crop_right(self) -> float: """|float| representing relative portion cropped from right of shape. Read/write. 1.0 represents 100%. """ - return self._element.srcRect_r + return self._pic.srcRect_r @crop_right.setter - def crop_right(self, value): - self._element.srcRect_r = value + def crop_right(self, value: float): + self._pic.srcRect_r = value @property - def crop_top(self): + def crop_top(self) -> float: """|float| representing relative portion cropped from shape top. Read/write. 1.0 represents 100%. """ - return self._element.srcRect_t + return self._pic.srcRect_t @crop_top.setter - def crop_top(self, value): - self._element.srcRect_t = value + def crop_top(self, value: float): + self._pic.srcRect_t = value def get_or_add_ln(self): - """ - Return the `a:ln` element containing the line format properties XML - for this `p:pic`-based shape. + """Return the `a:ln` element for this `p:pic`-based image. + + The `a:ln` element contains the line format properties XML. """ return self._pic.get_or_add_ln() @lazyproperty - def line(self): - """ - An instance of |LineFormat|, providing access to the properties of - the outline bordering this shape, such as its color and width. - """ + def line(self) -> LineFormat: + """Provides access to properties of the picture outline, such as its color and width.""" return LineFormat(self) @property - def ln(self): - """ - The ```` element containing the line format properties such as - line color and width. |None| if no ```` element is present. + def ln(self) -> CT_LineProperties | None: + """The `a:ln` element for this `p:pic`. + + Contains the line format properties such as line color and width. |None| if no `a:ln` + element is present. """ return self._pic.ln @@ -95,26 +98,23 @@ def ln(self): class Movie(_BasePicture): """A movie shape, one that places a video on a slide. - Like |Picture|, a movie shape is based on the `p:pic` element. A movie is - composed of a video and a *poster frame*, the placeholder image that - represents the video before it is played. + Like |Picture|, a movie shape is based on the `p:pic` element. A movie is composed of a video + and a *poster frame*, the placeholder image that represents the video before it is played. """ @lazyproperty - def media_format(self): + def media_format(self) -> _MediaFormat: """The |_MediaFormat| object for this movie. - The |_MediaFormat| object provides access to formatting properties - for the movie. + The |_MediaFormat| object provides access to formatting properties for the movie. """ - return _MediaFormat(self._element, self) + return _MediaFormat(self._pic, self) @property - def media_type(self): + def media_type(self) -> PP_MEDIA_TYPE: """Member of :ref:`PpMediaType` describing this shape. - The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this - case. + The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this case. """ return PP_MEDIA_TYPE.MOVIE @@ -124,16 +124,16 @@ def poster_frame(self): Returns |None| if this movie has no poster frame (uncommon). """ - slide_part, rId = self.part, self._element.blip_rId + slide_part, rId = self.part, self._pic.blip_rId if rId is None: return None return slide_part.get_image(rId) @property - def shape_type(self): + def shape_type(self) -> MSO_SHAPE_TYPE: """Return member of :ref:`MsoShapeType` describing this shape. - The return value is unconditionally ``MSO_SHAPE_TYPE.MEDIA`` in this + The return value is unconditionally `MSO_SHAPE_TYPE.MEDIA` in this case. """ return MSO_SHAPE_TYPE.MEDIA @@ -146,27 +146,22 @@ class Picture(_BasePicture): """ @property - def auto_shape_type(self): + def auto_shape_type(self) -> MSO_SHAPE | None: """Member of MSO_SHAPE indicating masking shape. - A picture can be masked by any of the so-called "auto-shapes" - available in PowerPoint, such as an ellipse or triangle. When - a picture is masked by a shape, the shape assumes the same dimensions - as the picture and the portion of the picture outside the shape - boundaries does not appear. Note the default value for - a newly-inserted picture is `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which - performs no cropping because the extents of the rectangle exactly - correspond to the extents of the picture. - - The available shapes correspond to the members of - :ref:`MsoAutoShapeType`. - - The return value can also be |None|, indicating the picture either - has no geometry (not expected) or has custom geometry, like - a freeform shape. A picture with no geometry will have no visible - representation on the slide, although it can be selected. This is - because without geometry, there is no "inside-the-shape" for it to - appear in. + A picture can be masked by any of the so-called "auto-shapes" available in PowerPoint, + such as an ellipse or triangle. When a picture is masked by a shape, the shape assumes the + same dimensions as the picture and the portion of the picture outside the shape boundaries + does not appear. Note the default value for a newly-inserted picture is + `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which performs no cropping because the extents of the + rectangle exactly correspond to the extents of the picture. + + The available shapes correspond to the members of :ref:`MsoAutoShapeType`. + + The return value can also be |None|, indicating the picture either has no geometry (not + expected) or has custom geometry, like a freeform shape. A picture with no geometry will + have no visible representation on the slide, although it can be selected. This is because + without geometry, there is no "inside-the-shape" for it to appear in. """ prstGeom = self._pic.spPr.prstGeom if prstGeom is None: # ---generally means cropped with freeform--- @@ -174,32 +169,29 @@ def auto_shape_type(self): return prstGeom.prst @auto_shape_type.setter - def auto_shape_type(self, member): + def auto_shape_type(self, member: MSO_SHAPE): MSO_SHAPE.validate(member) spPr = self._pic.spPr prstGeom = spPr.prstGeom if prstGeom is None: - spPr._remove_custGeom() - prstGeom = spPr._add_prstGeom() + spPr._remove_custGeom() # pyright: ignore[reportPrivateUsage] + prstGeom = spPr._add_prstGeom() # pyright: ignore[reportPrivateUsage] prstGeom.prst = member @property def image(self): + """The |Image| object for this picture. + + Provides access to the properties and bytes of the image in this picture shape. """ - An |Image| object providing access to the properties and bytes of the - image in this picture shape. - """ - slide_part, rId = self.part, self._element.blip_rId + slide_part, rId = self.part, self._pic.blip_rId if rId is None: raise ValueError("no embedded image") return slide_part.get_image(rId) @property - def shape_type(self): - """ - Unique integer identifying the type of this shape, unconditionally - ``MSO_SHAPE_TYPE.PICTURE`` in this case. - """ + def shape_type(self) -> MSO_SHAPE_TYPE: + """Unconditionally `MSO_SHAPE_TYPE.PICTURE` in this case.""" return MSO_SHAPE_TYPE.PICTURE diff --git a/src/pptx/shapes/placeholder.py b/src/pptx/shapes/placeholder.py index 791d91e13..c44837bef 100644 --- a/src/pptx/shapes/placeholder.py +++ b/src/pptx/shapes/placeholder.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - """Placeholder-related objects. Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors @@ -7,6 +5,10 @@ non-trivial class inheritance structure. """ +from __future__ import annotations + +from typing import TYPE_CHECKING + from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame from pptx.oxml.shapes.picture import CT_Picture @@ -15,6 +17,9 @@ from pptx.shapes.picture import Picture from pptx.util import Emu +if TYPE_CHECKING: + from pptx.oxml.shapes.autoshape import CT_Shape + class _InheritsDimensions(object): """ @@ -208,13 +213,14 @@ def sz(self): class LayoutPlaceholder(_InheritsDimensions, Shape): - """ - Placeholder shape on a slide layout, providing differentiated behavior - for slide layout placeholders, in particular, inheriting shape properties - from the master placeholder having the same type, when a matching one - exists. + """Placeholder shape on a slide layout. + + Provides differentiated behavior for slide layout placeholders, in particular, inheriting + shape properties from the master placeholder having the same type, when a matching one exists. """ + element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride] + @property def _base_placeholder(self): """ @@ -241,9 +247,9 @@ def _base_placeholder(self): class MasterPlaceholder(BasePlaceholder): - """ - Placeholder shape on a slide master. - """ + """Placeholder shape on a slide master.""" + + element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride] class NotesSlidePlaceholder(_InheritsDimensions, Shape): @@ -299,9 +305,7 @@ def _new_chart_graphicFrame(self, rId, x, y, cx, cy): position and size and containing the chart identified by *rId*. """ id_, name = self.shape_id, self.name - return CT_GraphicalObjectFrame.new_chart_graphicFrame( - id_, name, rId, x, y, cx, cy - ) + return CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy) class PicturePlaceholder(_BaseSlidePlaceholder): diff --git a/src/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py index cebdfc91f..29623f1f5 100644 --- a/src/pptx/shapes/shapetree.py +++ b/src/pptx/shapes/shapetree.py @@ -1,14 +1,16 @@ -# encoding: utf-8 - """The shape tree, the structure that holds a slide's shapes.""" +from __future__ import annotations + +import io import os +from typing import IO, TYPE_CHECKING, Callable, Iterable, Iterator, cast -from pptx.compat import BytesIO from pptx.enum.shapes import PP_PLACEHOLDER, PROG_ID from pptx.media import SPEAKER_IMAGE_BYTES, Video from pptx.opc.constants import CONTENT_TYPE as CT from pptx.oxml.ns import qn +from pptx.oxml.shapes.autoshape import CT_Shape from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame from pptx.oxml.shapes.picture import CT_Picture from pptx.oxml.simpletypes import ST_Direction @@ -33,6 +35,20 @@ from pptx.shared import ParentedElementProxy from pptx.util import Emu, lazyproperty +if TYPE_CHECKING: + from pptx.chart.chart import Chart + from pptx.chart.data import ChartData + from pptx.enum.chart import XL_CHART_TYPE + from pptx.enum.shapes import MSO_CONNECTOR_TYPE, MSO_SHAPE + from pptx.oxml.shapes import ShapeElement + from pptx.oxml.shapes.connector import CT_Connector + from pptx.oxml.shapes.groupshape import CT_GroupShape + from pptx.parts.image import ImagePart + from pptx.parts.slide import SlidePart + from pptx.slide import Slide, SlideLayout + from pptx.types import ProvidesPart + from pptx.util import Length + # +-- _BaseShapes # | | # | +-- _BaseGroupShapes @@ -59,20 +75,18 @@ class _BaseShapes(ParentedElementProxy): - """ - Base class for a shape collection appearing in a slide-type object, - include Slide, SlideLayout, and SlideMaster, providing common methods. + """Base class for a shape collection appearing in a slide-type object. + + Subclasses include Slide, SlideLayout, and SlideMaster. Provides common methods. """ - def __init__(self, spTree, parent): + def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart): super(_BaseShapes, self).__init__(spTree, parent) self._spTree = spTree self._cached_max_shape_id = None - def __getitem__(self, idx): - """ - Return shape at *idx* in sequence, e.g. ``shapes[2]``. - """ + def __getitem__(self, idx: int) -> BaseShape: + """Return shape at `idx` in sequence, e.g. `shapes[2]`.""" shape_elms = list(self._iter_member_elms()) try: shape_elm = shape_elms[idx] @@ -80,36 +94,33 @@ def __getitem__(self, idx): raise IndexError("shape index out of range") return self._shape_factory(shape_elm) - def __iter__(self): - """ - Generate a reference to each shape in the collection, in sequence. - """ + def __iter__(self) -> Iterator[BaseShape]: + """Generate a reference to each shape in the collection, in sequence.""" for shape_elm in self._iter_member_elms(): yield self._shape_factory(shape_elm) - def __len__(self): - """ - Return count of shapes in this shape tree. A group shape contributes - 1 to the total, without regard to the number of shapes contained in - the group. + def __len__(self) -> int: + """Return count of shapes in this shape tree. + + A group shape contributes 1 to the total, without regard to the number of shapes contained + in the group. """ shape_elms = list(self._iter_member_elms()) return len(shape_elms) - def clone_placeholder(self, placeholder): - """Add a new placeholder shape based on *placeholder*.""" + def clone_placeholder(self, placeholder: LayoutPlaceholder) -> None: + """Add a new placeholder shape based on `placeholder`.""" sp = placeholder.element ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx) id_ = self._next_shape_id name = self._next_ph_name(ph_type, id_, orient) self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx) - def ph_basename(self, ph_type): - """ - Return the base name for a placeholder of *ph_type* in this shape - collection. There is some variance between slide types, for example - a notes slide uses a different name for the body placeholder, so this - method can be overriden by subclasses. + def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str: + """Return the base name for a placeholder of `ph_type` in this shape collection. + + There is some variance between slide types, for example a notes slide uses a different + name for the body placeholder, so this method can be overriden by subclasses. """ return { PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder", @@ -130,60 +141,51 @@ def ph_basename(self, ph_type): }[ph_type] @property - def turbo_add_enabled(self): + def turbo_add_enabled(self) -> bool: """True if "turbo-add" mode is enabled. Read/Write. - EXPERIMENTAL: This feature can radically improve performance when - adding large numbers (hundreds of shapes) to a slide. It works by - caching the last shape ID used and incrementing that value to assign - the next shape id. This avoids repeatedly searching all shape ids in - the slide each time a new ID is required. - - Performance is not noticeably improved for a slide with a relatively - small number of shapes, but because the search time rises with the - square of the shape count, this option can be useful for optimizing - generation of a slide composed of many shapes. - - Shape-id collisions can occur (causing a repair error on load) if - more than one |Slide| object is used to interact with the same slide - in the presentation. Note that the |Slides| collection creates a new - |Slide| object each time a slide is accessed - (e.g. `slide = prs.slides[0]`, so you must be careful to limit use to - a single |Slide| object. + EXPERIMENTAL: This feature can radically improve performance when adding large numbers + (hundreds of shapes) to a slide. It works by caching the last shape ID used and + incrementing that value to assign the next shape id. This avoids repeatedly searching all + shape ids in the slide each time a new ID is required. + + Performance is not noticeably improved for a slide with a relatively small number of + shapes, but because the search time rises with the square of the shape count, this option + can be useful for optimizing generation of a slide composed of many shapes. + + Shape-id collisions can occur (causing a repair error on load) if more than one |Slide| + object is used to interact with the same slide in the presentation. Note that the |Slides| + collection creates a new |Slide| object each time a slide is accessed (e.g. `slide = + prs.slides[0]`, so you must be careful to limit use to a single |Slide| object. """ return self._cached_max_shape_id is not None @turbo_add_enabled.setter - def turbo_add_enabled(self, value): + def turbo_add_enabled(self, value: bool): enable = bool(value) self._cached_max_shape_id = self._spTree.max_shape_id if enable else None @staticmethod - def _is_member_elm(shape_elm): - """ - Return true if *shape_elm* represents a member of this collection, - False otherwise. - """ + def _is_member_elm(shape_elm: ShapeElement) -> bool: + """Return true if `shape_elm` represents a member of this collection, False otherwise.""" return True - def _iter_member_elms(self): - """ - Generate each child of the ```` element that corresponds to - a shape, in the sequence they appear in the XML. + def _iter_member_elms(self) -> Iterator[ShapeElement]: + """Generate each child of the `p:spTree` element that corresponds to a shape. + + Items appear in XML document order. """ for shape_elm in self._spTree.iter_shape_elms(): if self._is_member_elm(shape_elm): yield shape_elm - def _next_ph_name(self, ph_type, id, orient): - """ - Next unique placeholder name for placeholder shape of type *ph_type*, - with id number *id* and orientation *orient*. Usually will be standard - placeholder root name suffixed with id-1, e.g. - _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> - 'Table Placeholder 3'. The number is incremented as necessary to make - the name unique within the collection. If *orient* is ``'vert'``, the - placeholder name is prefixed with ``'Vertical '``. + def _next_ph_name(self, ph_type: PP_PLACEHOLDER, id: int, orient: str) -> str: + """Next unique placeholder name for placeholder shape of type `ph_type`. + + Usually will be standard placeholder root name suffixed with id-1, e.g. + _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> 'Table Placeholder 3'. The number is + incremented as necessary to make the name unique within the collection. If `orient` is + `'vert'`, the placeholder name is prefixed with `'Vertical '`. """ basename = self.ph_basename(ph_type) @@ -203,12 +205,11 @@ def _next_ph_name(self, ph_type, id, orient): return name @property - def _next_shape_id(self): + def _next_shape_id(self) -> int: """Return a unique shape id suitable for use with a new shape. - The returned id is 1 greater than the maximum shape id used so far. - In practice, the minimum id is 2 because the spTree element is always - assigned id="1". + The returned id is 1 greater than the maximum shape id used so far. In practice, the + minimum id is 2 because the spTree element is always assigned id="1". """ # ---presence of cached-max-shape-id indicates turbo mode is on--- if self._cached_max_shape_id is not None: @@ -217,108 +218,120 @@ def _next_shape_id(self): return self._spTree.max_shape_id + 1 - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" return BaseShapeFactory(shape_elm, self) class _BaseGroupShapes(_BaseShapes): """Base class for shape-trees that can add shapes.""" - def __init__(self, grpSp, parent): + part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride] + _element: CT_GroupShape + + def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart): super(_BaseGroupShapes, self).__init__(grpSp, parent) self._grpSp = grpSp - def add_chart(self, chart_type, x, y, cx, cy, chart_data): - """Add a new chart of *chart_type* to the slide. - - The chart is positioned at (*x*, *y*), has size (*cx*, *cy*), and - depicts *chart_data*. *chart_type* is one of the :ref:`XlChartType` - enumeration values. *chart_data* is a |ChartData| object populated - with the categories and series values for the chart. - - Note that a |GraphicFrame| shape object is returned, not the |Chart| - object contained in that graphic frame shape. The chart object may be - accessed using the :attr:`chart` property of the returned - |GraphicFrame| object. + def add_chart( + self, + chart_type: XL_CHART_TYPE, + x: Length, + y: Length, + cx: Length, + cy: Length, + chart_data: ChartData, + ) -> Chart: + """Add a new chart of `chart_type` to the slide. + + The chart is positioned at (`x`, `y`), has size (`cx`, `cy`), and depicts `chart_data`. + `chart_type` is one of the :ref:`XlChartType` enumeration values. `chart_data` is a + |ChartData| object populated with the categories and series values for the chart. + + Note that a |GraphicFrame| shape object is returned, not the |Chart| object contained in + that graphic frame shape. The chart object may be accessed using the :attr:`chart` + property of the returned |GraphicFrame| object. """ rId = self.part.add_chart_part(chart_type, chart_data) graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy) self._recalculate_extents() - return self._shape_factory(graphicFrame) + return cast("Chart", self._shape_factory(graphicFrame)) - def add_connector(self, connector_type, begin_x, begin_y, end_x, end_y): + def add_connector( + self, + connector_type: MSO_CONNECTOR_TYPE, + begin_x: Length, + begin_y: Length, + end_x: Length, + end_y: Length, + ) -> Connector: """Add a newly created connector shape to the end of this shape tree. - *connector_type* is a member of the :ref:`MsoConnectorType` - enumeration and the end-point values are specified as EMU values. The - returned connector is of type *connector_type* and has begin and end - points as specified. + `connector_type` is a member of the :ref:`MsoConnectorType` enumeration and the end-point + values are specified as EMU values. The returned connector is of type `connector_type` and + has begin and end points as specified. """ cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y) self._recalculate_extents() - return self._shape_factory(cxnSp) + return cast(Connector, self._shape_factory(cxnSp)) - def add_group_shape(self, shapes=[]): + def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape: """Return a |GroupShape| object newly appended to this shape tree. - The group shape is empty and must be populated with shapes using - methods on its shape tree, available on its `.shapes` property. The - position and extents of the group shape are determined by the shapes - it contains; its position and extents are recalculated each time + The group shape is empty and must be populated with shapes using methods on its shape + tree, available on its `.shapes` property. The position and extents of the group shape are + determined by the shapes it contains; its position and extents are recalculated each time a shape is added to it. """ + shapes = tuple(shapes) grpSp = self._element.add_grpSp() for shape in shapes: - grpSp.insert_element_before(shape._element, "p:extLst") + grpSp.insert_element_before( + shape._element, "p:extLst" # pyright: ignore[reportPrivateUsage] + ) if shapes: grpSp.recalculate_extents() - return self._shape_factory(grpSp) + return cast(GroupShape, self._shape_factory(grpSp)) def add_ole_object( self, - object_file, - prog_id, - left, - top, - width=None, - height=None, - icon_file=None, - icon_width=None, - icon_height=None, - ): + object_file: str | IO[bytes], + prog_id: str, + left: Length, + top: Length, + width: Length | None = None, + height: Length | None = None, + icon_file: str | IO[bytes] | None = None, + icon_width: Length | None = None, + icon_height: Length | None = None, + ) -> GraphicFrame: """Return newly-created GraphicFrame shape embedding `object_file`. - The returned graphic-frame shape contains `object_file` as an embedded OLE - object. It is displayed as an icon at `left`, `top` with size `width`, `height`. - `width` and `height` may be omitted when `prog_id` is a member of `PROG_ID`, in - which case the default icon size is used. This is advised for best appearance - where applicable because it avoids an icon with a "stretched" appearance. + The returned graphic-frame shape contains `object_file` as an embedded OLE object. It is + displayed as an icon at `left`, `top` with size `width`, `height`. `width` and `height` + may be omitted when `prog_id` is a member of `PROG_ID`, in which case the default icon + size is used. This is advised for best appearance where applicable because it avoids an + icon with a "stretched" appearance. `object_file` may either be a str path to a file or file-like object (such as - `io.BytesIO`) containing the bytes of the object to be embedded (such as an - Excel file). - - `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value - like `"Adobe.Exchange.7"` determined by inspecting the XML generated by - PowerPoint for an object of the desired type. - - `icon_file` may either be a str path to an image file or a file-like object - containing the image. The image provided will be displayed in lieu of the OLE - object; double-clicking on the image opens the object (subject to - operating-system limitations). The image file can be any supported image file. - Those produced by PowerPoint itself are generally EMF and can be harvested from - a PPTX package that embeds such an object. PNG and JPG also work fine. - - `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that - describe the size of the icon image within the shape. These should be omitted - unless a custom `icon_file` is provided. The dimensions must be discovered by - inspecting the XML. Automatic resizing of the OLE-object shape can occur when - the icon is double-clicked if these values are not as set by PowerPoint. This - behavior may only manifest in the Windows version of PowerPoint. + `io.BytesIO`) containing the bytes of the object to be embedded (such as an Excel file). + + `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value like + `"Adobe.Exchange.7"` determined by inspecting the XML generated by PowerPoint for an + object of the desired type. + + `icon_file` may either be a str path to an image file or a file-like object containing the + image. The image provided will be displayed in lieu of the OLE object; double-clicking on + the image opens the object (subject to operating-system limitations). The image file can + be any supported image file. Those produced by PowerPoint itself are generally EMF and can + be harvested from a PPTX package that embeds such an object. PNG and JPG also work fine. + + `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that describe + the size of the icon image within the shape. These should be omitted unless a custom + `icon_file` is provided. The dimensions must be discovered by inspecting the XML. + Automatic resizing of the OLE-object shape can occur when the icon is double-clicked if + these values are not as set by PowerPoint. This behavior may only manifest in the Windows + version of PowerPoint. """ graphicFrame = _OleObjectElementCreator.graphicFrame( self, @@ -335,85 +348,91 @@ def add_ole_object( ) self._spTree.append(graphicFrame) self._recalculate_extents() - return self._shape_factory(graphicFrame) - - def add_picture(self, image_file, left, top, width=None, height=None): - """Add picture shape displaying image in *image_file*. - - *image_file* can be either a path to a file (a string) or a file-like - object. The picture is positioned with its top-left corner at (*top*, - *left*). If *width* and *height* are both |None|, the native size of - the image is used. If only one of *width* or *height* is used, the - unspecified dimension is calculated to preserve the aspect ratio of - the image. If both are specified, the picture is stretched to fit, - without regard to its native aspect ratio. + return cast(GraphicFrame, self._shape_factory(graphicFrame)) + + def add_picture( + self, + image_file: str | IO[bytes], + left: Length, + top: Length, + width: Length | None = None, + height: Length | None = None, + ) -> Picture: + """Add picture shape displaying image in `image_file`. + + `image_file` can be either a path to a file (a string) or a file-like object. The picture + is positioned with its top-left corner at (`top`, `left`). If `width` and `height` are + both |None|, the native size of the image is used. If only one of `width` or `height` is + used, the unspecified dimension is calculated to preserve the aspect ratio of the image. + If both are specified, the picture is stretched to fit, without regard to its native + aspect ratio. """ image_part, rId = self.part.get_or_add_image_part(image_file) pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height) self._recalculate_extents() - return self._shape_factory(pic) + return cast(Picture, self._shape_factory(pic)) - def add_shape(self, autoshape_type_id, left, top, width, height): + def add_shape( + self, autoshape_type_id: MSO_SHAPE, left: Length, top: Length, width: Length, height: Length + ) -> Shape: """Return new |Shape| object appended to this shape tree. - *autoshape_type_id* is a member of :ref:`MsoAutoShapeType` e.g. - ``MSO_SHAPE.RECTANGLE`` specifying the type of shape to be added. The - remaining arguments specify the new shape's position and size. + `autoshape_type_id` is a member of :ref:`MsoAutoShapeType` e.g. `MSO_SHAPE.RECTANGLE` + specifying the type of shape to be added. The remaining arguments specify the new shape's + position and size. """ autoshape_type = AutoShapeType(autoshape_type_id) sp = self._add_sp(autoshape_type, left, top, width, height) self._recalculate_extents() - return self._shape_factory(sp) + return cast(Shape, self._shape_factory(sp)) - def add_textbox(self, left, top, width, height): + def add_textbox(self, left: Length, top: Length, width: Length, height: Length) -> Shape: """Return newly added text box shape appended to this shape tree. - The text box is of the specified size, located at the specified - position on the slide. + The text box is of the specified size, located at the specified position on the slide. """ sp = self._add_textbox_sp(left, top, width, height) self._recalculate_extents() - return self._shape_factory(sp) + return cast(Shape, self._shape_factory(sp)) - def build_freeform(self, start_x=0, start_y=0, scale=1.0): + def build_freeform( + self, start_x: float = 0, start_y: float = 0, scale: tuple[float, float] | float = 1.0 + ) -> FreeformBuilder: """Return |FreeformBuilder| object to specify a freeform shape. - The optional *start_x* and *start_y* arguments specify the starting - pen position in local coordinates. They will be rounded to the - nearest integer before use and each default to zero. - - The optional *scale* argument specifies the size of local coordinates - proportional to slide coordinates (EMU). If the vertical scale is - different than the horizontal scale (local coordinate units are - "rectangular"), a pair of numeric values can be provided as the - *scale* argument, e.g. `scale=(1.0, 2.0)`. In this case the first - number is interpreted as the horizontal (X) scale and the second as - the vertical (Y) scale. - - A convenient method for calculating scale is to divide a |Length| - object by an equivalent count of local coordinate units, e.g. - `scale = Inches(1)/1000` for 1000 local units per inch. + The optional `start_x` and `start_y` arguments specify the starting pen position in local + coordinates. They will be rounded to the nearest integer before use and each default to + zero. + + The optional `scale` argument specifies the size of local coordinates proportional to + slide coordinates (EMU). If the vertical scale is different than the horizontal scale + (local coordinate units are "rectangular"), a pair of numeric values can be provided as + the `scale` argument, e.g. `scale=(1.0, 2.0)`. In this case the first number is + interpreted as the horizontal (X) scale and the second as the vertical (Y) scale. + + A convenient method for calculating scale is to divide a |Length| object by an equivalent + count of local coordinate units, e.g. `scale = Inches(1)/1000` for 1000 local units per + inch. """ - try: - x_scale, y_scale = scale - except TypeError: - x_scale = y_scale = scale + x_scale, y_scale = scale if isinstance(scale, tuple) else (scale, scale) return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale) - def index(self, shape): - """Return the index of *shape* in this sequence. + def index(self, shape: BaseShape) -> int: + """Return the index of `shape` in this sequence. - Raises |ValueError| if *shape* is not in the collection. + Raises |ValueError| if `shape` is not in the collection. """ shape_elms = list(self._element.iter_shape_elms()) return shape_elms.index(shape.element) - def _add_chart_graphicFrame(self, rId, x, y, cx, cy): + def _add_chart_graphicFrame( + self, rId: str, x: Length, y: Length, cx: Length, cy: Length + ) -> CT_GraphicalObjectFrame: """Return new `p:graphicFrame` element appended to this shape tree. - The `p:graphicFrame` element has the specified position and size and - refers to the chart part identified by *rId*. + The `p:graphicFrame` element has the specified position and size and refers to the chart + part identified by `rId`. """ shape_id = self._next_shape_id name = "Chart %d" % (shape_id - 1) @@ -423,12 +442,18 @@ def _add_chart_graphicFrame(self, rId, x, y, cx, cy): self._spTree.append(graphicFrame) return graphicFrame - def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, end_y): + def _add_cxnSp( + self, + connector_type: MSO_CONNECTOR_TYPE, + begin_x: Length, + begin_y: Length, + end_x: Length, + end_y: Length, + ) -> CT_Connector: """Return a newly-added `p:cxnSp` element as specified. - The `p:cxnSp` element is for a connector of *connector_type* - beginning at (*begin_x*, *begin_y*) and extending to - (*end_x*, *end_y*). + The `p:cxnSp` element is for a connector of `connector_type` beginning at (`begin_x`, + `begin_y`) and extending to (`end_x`, `end_y`). """ id_ = self._next_shape_id name = "Connector %d" % (id_ - 1) @@ -439,13 +464,20 @@ def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, end_y): return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV) - def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy): + def _add_pic_from_image_part( + self, + image_part: ImagePart, + rId: str, + x: Length, + y: Length, + cx: Length | None, + cy: Length | None, + ) -> CT_Picture: """Return a newly appended `p:pic` element as specified. - The `p:pic` element displays the image in *image_part* with size and - position specified by *x*, *y*, *cx*, and *cy*. The element is - appended to the shape tree, causing it to be displayed first in - z-order on the slide. + The `p:pic` element displays the image in `image_part` with size and position specified by + `x`, `y`, `cx`, and `cy`. The element is appended to the shape tree, causing it to be + displayed first in z-order on the slide. """ id_ = self._next_shape_id scaled_cx, scaled_cy = image_part.scale(cx, cy) @@ -454,32 +486,33 @@ def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy): pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy) return pic - def _add_sp(self, autoshape_type, x, y, cx, cy): + def _add_sp( + self, autoshape_type: AutoShapeType, x: Length, y: Length, cx: Length, cy: Length + ) -> CT_Shape: """Return newly-added `p:sp` element as specified. - `p:sp` element is of *autoshape_type* at position (*x*, *y*) and of - size (*cx*, *cy*). + `p:sp` element is of `autoshape_type` at position (`x`, `y`) and of size (`cx`, `cy`). """ id_ = self._next_shape_id name = "%s %d" % (autoshape_type.basename, id_ - 1) sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy) return sp - def _add_textbox_sp(self, x, y, cx, cy): + def _add_textbox_sp(self, x: Length, y: Length, cx: Length, cy: Length) -> CT_Shape: """Return newly-appended textbox `p:sp` element. - Element has position (*x*, *y*) and size (*cx*, *cy*). + Element has position (`x`, `y`) and size (`cx`, `cy`). """ id_ = self._next_shape_id name = "TextBox %d" % (id_ - 1) sp = self._spTree.add_textbox(id_, name, x, y, cx, cy) return sp - def _recalculate_extents(self): + def _recalculate_extents(self) -> None: """Adjust position and size to incorporate all contained shapes. - This would typically be called when a contained shape is added, - removed, or its position or size updated. + This would typically be called when a contained shape is added, removed, or its position + or size updated. """ # ---default behavior is to do nothing, GroupShapes overrides to # produce the distinctive behavior of groups and subgroups.--- @@ -489,15 +522,15 @@ def _recalculate_extents(self): class GroupShapes(_BaseGroupShapes): """The sequence of child shapes belonging to a group shape. - Note that this collection can itself contain a group shape, making this - part of a recursive, tree data structure (acyclic graph). + Note that this collection can itself contain a group shape, making this part of a recursive, + tree data structure (acyclic graph). """ - def _recalculate_extents(self): + def _recalculate_extents(self) -> None: """Adjust position and size to incorporate all contained shapes. - This would typically be called when a contained shape is added, - removed, or its position or size updated. + This would typically be called when a contained shape is added, removed, or its position + or size updated. """ self._grpSp.recalculate_extents() @@ -505,38 +538,38 @@ def _recalculate_extents(self): class SlideShapes(_BaseGroupShapes): """Sequence of shapes appearing on a slide. - The first shape in the sequence is the backmost in z-order and the last - shape is topmost. Supports indexed access, len(), index(), and iteration. + The first shape in the sequence is the backmost in z-order and the last shape is topmost. + Supports indexed access, len(), index(), and iteration. """ + parent: Slide # pyright: ignore[reportIncompatibleMethodOverride] + def add_movie( self, - movie_file, - left, - top, - width, - height, - poster_frame_image=None, - mime_type=CT.VIDEO, - ): - """Return newly added movie shape displaying video in *movie_file*. + movie_file: str | IO[bytes], + left: Length, + top: Length, + width: Length, + height: Length, + poster_frame_image: str | IO[bytes] | None = None, + mime_type: str = CT.VIDEO, + ) -> GraphicFrame: + """Return newly added movie shape displaying video in `movie_file`. **EXPERIMENTAL.** This method has important limitations: - * The size must be specified; no auto-scaling such as that provided - by :meth:`add_picture` is performed. - * The MIME type of the video file should be specified, e.g. - 'video/mp4'. The provided video file is not interrogated for its - type. The MIME type `video/unknown` is used by default (and works - fine in tests as of this writing). - * A poster frame image must be provided, it cannot be automatically - extracted from the video file. If no poster frame is provided, the - default "media loudspeaker" image will be used. - - Return a newly added movie shape to the slide, positioned at (*left*, - *top*), having size (*width*, *height*), and containing *movie_file*. - Before the video is started, *poster_frame_image* is displayed as - a placeholder for the video. + * The size must be specified; no auto-scaling such as that provided by :meth:`add_picture` + is performed. + * The MIME type of the video file should be specified, e.g. 'video/mp4'. The provided + video file is not interrogated for its type. The MIME type `video/unknown` is used by + default (and works fine in tests as of this writing). + * A poster frame image must be provided, it cannot be automatically extracted from the + video file. If no poster frame is provided, the default "media loudspeaker" image will + be used. + + Return a newly added movie shape to the slide, positioned at (`left`, `top`), having size + (`width`, `height`), and containing `movie_file`. Before the video is started, + `poster_frame_image` is displayed as a placeholder for the video. """ movie_pic = _MoviePicElementCreator.new_movie_pic( self, @@ -551,120 +584,106 @@ def add_movie( ) self._spTree.append(movie_pic) self._add_video_timing(movie_pic) - return self._shape_factory(movie_pic) + return cast(GraphicFrame, self._shape_factory(movie_pic)) - def add_table(self, rows, cols, left, top, width, height): - """ - Add a |GraphicFrame| object containing a table with the specified - number of *rows* and *cols* and the specified position and size. - *width* is evenly distributed between the columns of the new table. - Likewise, *height* is evenly distributed between the rows. Note that - the ``.table`` property on the returned |GraphicFrame| shape must be - used to access the enclosed |Table| object. + def add_table( + self, rows: int, cols: int, left: Length, top: Length, width: Length, height: Length + ) -> GraphicFrame: + """Add a |GraphicFrame| object containing a table. + + The table has the specified number of `rows` and `cols` and the specified position and + size. `width` is evenly distributed between the columns of the new table. Likewise, + `height` is evenly distributed between the rows. Note that the `.table` property on the + returned |GraphicFrame| shape must be used to access the enclosed |Table| object. """ graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height) - graphic_frame = self._shape_factory(graphicFrame) - return graphic_frame + return cast(GraphicFrame, self._shape_factory(graphicFrame)) - def clone_layout_placeholders(self, slide_layout): - """ - Add placeholder shapes based on those in *slide_layout*. Z-order of - placeholders is preserved. Latent placeholders (date, slide number, - and footer) are not cloned. + def clone_layout_placeholders(self, slide_layout: SlideLayout) -> None: + """Add placeholder shapes based on those in `slide_layout`. + + Z-order of placeholders is preserved. Latent placeholders (date, slide number, and footer) + are not cloned. """ for placeholder in slide_layout.iter_cloneable_placeholders(): self.clone_placeholder(placeholder) @property - def placeholders(self): - """ - Instance of |SlidePlaceholders| containing sequence of placeholder - shapes in this slide. - """ + def placeholders(self) -> SlidePlaceholders: + """Sequence of placeholder shapes in this slide.""" return self.parent.placeholders @property - def title(self): - """ - The title placeholder shape on the slide or |None| if the slide has - no title placeholder. + def title(self) -> Shape | None: + """The title placeholder shape on the slide. + + |None| if the slide has no title placeholder. """ for elm in self._spTree.iter_ph_elms(): if elm.ph_idx == 0: - return self._shape_factory(elm) + return cast(Shape, self._shape_factory(elm)) return None - def _add_graphicFrame_containing_table(self, rows, cols, x, y, cx, cy): - """ - Return a newly added ```` element containing a table - as specified by the parameters. - """ + def _add_graphicFrame_containing_table( + self, rows: int, cols: int, x: Length, y: Length, cx: Length, cy: Length + ) -> CT_GraphicalObjectFrame: + """Return a newly added `p:graphicFrame` element containing a table as specified.""" _id = self._next_shape_id name = "Table %d" % (_id - 1) graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy) return graphicFrame - def _add_video_timing(self, pic): + def _add_video_timing(self, pic: CT_Picture) -> None: """Add a `p:video` element under `p:sld/p:timing`. - The element will refer to the specified *pic* element by its shape - id, and cause the video play controls to appear for that video. + The element will refer to the specified `pic` element by its shape id, and cause the video + play controls to appear for that video. """ sld = self._spTree.xpath("/p:sld")[0] childTnLst = sld.get_or_add_childTnLst() childTnLst.add_video(pic.shape_id) - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" return SlideShapeFactory(shape_elm, self) class LayoutShapes(_BaseShapes): - """ - Sequence of shapes appearing on a slide layout. The first shape in the - sequence is the backmost in z-order and the last shape is topmost. + """Sequence of shapes appearing on a slide layout. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. Supports indexed access, len(), index(), and iteration. """ - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" return _LayoutShapeFactory(shape_elm, self) class MasterShapes(_BaseShapes): - """ - Sequence of shapes appearing on a slide master. The first shape in the - sequence is the backmost in z-order and the last shape is topmost. + """Sequence of shapes appearing on a slide master. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. Supports indexed access, len(), and iteration. """ - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" return _MasterShapeFactory(shape_elm, self) class NotesSlideShapes(_BaseShapes): - """ - Sequence of shapes appearing on a notes slide. The first shape in the - sequence is the backmost in z-order and the last shape is topmost. + """Sequence of shapes appearing on a notes slide. + + The first shape in the sequence is the backmost in z-order and the last shape is topmost. Supports indexed access, len(), index(), and iteration. """ - def ph_basename(self, ph_type): - """ - Return the base name for a placeholder of *ph_type* in this shape - collection. A notes slide uses a different name for the body - placeholder and has some unique placeholder types, so this - method overrides the default in the base class. + def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str: + """Return the base name for a placeholder of `ph_type` in this shape collection. + + A notes slide uses a different name for the body placeholder and has some unique + placeholder types, so this method overrides the default in the base class. """ return { PP_PLACEHOLDER.BODY: "Notes Placeholder", @@ -675,105 +694,96 @@ def ph_basename(self, ph_type): PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder", }[ph_type] - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm* appearing on a notes slide. - """ + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return appropriate shape object for `shape_elm` appearing on a notes slide.""" return _NotesSlideShapeFactory(shape_elm, self) class BasePlaceholders(_BaseShapes): - """ - Base class for placeholder collections that differentiate behaviors for - a master, layout, and slide. By default, placeholder shapes are - constructed using |BaseShapeFactory|. Subclasses should override + """Base class for placeholder collections. + + Subclasses differentiate behaviors for a master, layout, and slide. By default, placeholder + shapes are constructed using |BaseShapeFactory|. Subclasses should override :method:`_shape_factory` to use custom placeholder classes. """ @staticmethod - def _is_member_elm(shape_elm): - """ - True if *shape_elm* is a placeholder shape, False otherwise. - """ + def _is_member_elm(shape_elm: ShapeElement) -> bool: + """True if `shape_elm` is a placeholder shape, False otherwise.""" return shape_elm.has_ph_elm class LayoutPlaceholders(BasePlaceholders): - """ - Sequence of |LayoutPlaceholder| instances representing the placeholder - shapes on a slide layout. - """ + """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout.""" - def get(self, idx, default=None): - """ - Return the first placeholder shape with matching *idx* value, or - *default* if not found. - """ + __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride] + [], Iterator[LayoutPlaceholder] + ] + + def get(self, idx: int, default: LayoutPlaceholder | None = None) -> LayoutPlaceholder | None: + """The first placeholder shape with matching `idx` value, or `default` if not found.""" for placeholder in self: if placeholder.element.ph_idx == idx: return placeholder return default - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ + def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" return _LayoutShapeFactory(shape_elm, self) class MasterPlaceholders(BasePlaceholders): - """ - Sequence of _MasterPlaceholder instances representing the placeholder - shapes on a slide master. - """ + """Sequence of MasterPlaceholder representing the placeholder shapes on a slide master.""" - def get(self, ph_type, default=None): - """ - Return the first placeholder shape with type *ph_type* (e.g. 'body'), - or *default* if no such placeholder shape is present in the - collection. + __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride] + [], Iterator[MasterPlaceholder] + ] + + def get(self, ph_type: PP_PLACEHOLDER, default: MasterPlaceholder | None = None): + """Return the first placeholder shape with type `ph_type` (e.g. 'body'). + + Returns `default` if no such placeholder shape is present in the collection. """ for placeholder in self: if placeholder.ph_type == ph_type: return placeholder return default - def _shape_factory(self, shape_elm): - """ - Return an instance of the appropriate shape proxy class for - *shape_elm*. - """ - return _MasterShapeFactory(shape_elm, self) + def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride] + self, placeholder_elm: CT_Shape + ) -> MasterPlaceholder: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" + return cast(MasterPlaceholder, _MasterShapeFactory(placeholder_elm, self)) class NotesSlidePlaceholders(MasterPlaceholders): - """ - Sequence of placeholder shapes on a notes slide. - """ + """Sequence of placeholder shapes on a notes slide.""" - def _shape_factory(self, placeholder_elm): - """ - Return an instance of the appropriate placeholder proxy class for - *placeholder_elm*. - """ - return _NotesSlideShapeFactory(placeholder_elm, self) + __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride] + [], Iterator[NotesSlidePlaceholder] + ] + + def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride] + self, placeholder_elm: CT_Shape + ) -> NotesSlidePlaceholder: + """Return an instance of the appropriate placeholder proxy class for `placeholder_elm`.""" + return cast(NotesSlidePlaceholder, _NotesSlideShapeFactory(placeholder_elm, self)) class SlidePlaceholders(ParentedElementProxy): - """ - Collection of placeholder shapes on a slide. Supports iteration, - :func:`len`, and dictionary-style lookup on the `idx` value of the + """Collection of placeholder shapes on a slide. + + Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the placeholders it contains. """ - def __getitem__(self, idx): - """ - Access placeholder shape having *idx*. Note that while this looks - like list access, idx is actually a dictionary key and will raise - |KeyError| if no placeholder with that idx value is in the - collection. + _element: CT_GroupShape + + def __getitem__(self, idx: int): + """Access placeholder shape having `idx`. + + Note that while this looks like list access, idx is actually a dictionary key and will + raise |KeyError| if no placeholder with that idx value is in the collection. """ for e in self._element.iter_ph_elms(): if e.ph_idx == idx: @@ -781,26 +791,20 @@ def __getitem__(self, idx): raise KeyError("no placeholder on this slide with idx == %d" % idx) def __iter__(self): - """ - Generate placeholder shapes in `idx` order. - """ + """Generate placeholder shapes in `idx` order.""" ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx) return (SlideShapeFactory(e, self) for e in ph_elms) - def __len__(self): - """ - Return count of placeholder shapes. - """ + def __len__(self) -> int: + """Return count of placeholder shapes.""" return len(list(self._element.iter_ph_elms())) -def BaseShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm*. - """ +def BaseShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return an instance of the appropriate shape proxy class for `shape_elm`.""" tag = shape_elm.tag - if tag == qn("p:pic"): + if isinstance(shape_elm, CT_Picture): videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile") if videoFiles: return Movie(shape_elm, parent) @@ -813,46 +817,32 @@ def BaseShapeFactory(shape_elm, parent): qn("p:graphicFrame"): GraphicFrame, }.get(tag, BaseShape) - return shape_cls(shape_elm, parent) + return shape_cls(shape_elm, parent) # pyright: ignore[reportArgumentType] -def _LayoutShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide layout. - """ - tag_name = shape_elm.tag - if tag_name == qn("p:sp") and shape_elm.has_ph_elm: +def _LayoutShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a slide layout.""" + if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm: return LayoutPlaceholder(shape_elm, parent) return BaseShapeFactory(shape_elm, parent) -def _MasterShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide master. - """ - tag_name = shape_elm.tag - if tag_name == qn("p:sp") and shape_elm.has_ph_elm: +def _MasterShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a slide master.""" + if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm: return MasterPlaceholder(shape_elm, parent) return BaseShapeFactory(shape_elm, parent) -def _NotesSlideShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a notes slide. - """ - tag_name = shape_elm.tag - if tag_name == qn("p:sp") and shape_elm.has_ph_elm: +def _NotesSlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a notes slide.""" + if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm: return NotesSlidePlaceholder(shape_elm, parent) return BaseShapeFactory(shape_elm, parent) -def _SlidePlaceholderFactory(shape_elm, parent): - """ - Return a placeholder shape of the appropriate type for *shape_elm*. - """ +def _SlidePlaceholderFactory(shape_elm: ShapeElement, parent: ProvidesPart): + """Return a placeholder shape of the appropriate type for `shape_elm`.""" tag = shape_elm.tag if tag == qn("p:sp"): Constructor = { @@ -867,14 +857,11 @@ def _SlidePlaceholderFactory(shape_elm, parent): Constructor = PlaceholderPicture else: Constructor = BaseShapeFactory - return Constructor(shape_elm, parent) + return Constructor(shape_elm, parent) # pyright: ignore[reportArgumentType] -def SlideShapeFactory(shape_elm, parent): - """ - Return an instance of the appropriate shape proxy class for *shape_elm* - on a slide. - """ +def SlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape: + """Return appropriate shape object for `shape_elm` on a slide.""" if shape_elm.has_ph_elm: return _SlidePlaceholderFactory(shape_elm, parent) return BaseShapeFactory(shape_elm, parent) @@ -883,14 +870,24 @@ def SlideShapeFactory(shape_elm, parent): class _MoviePicElementCreator(object): """Functional service object for creating a new movie p:pic element. - It's entire external interface is its :meth:`new_movie_pic` class method - that returns a new `p:pic` element containing the specified video. This - class is not intended to be constructed or an instance of it retained by - the caller; it is a "one-shot" object, really a function wrapped in - a object such that its helper methods can be organized here. + It's entire external interface is its :meth:`new_movie_pic` class method that returns a new + `p:pic` element containing the specified video. This class is not intended to be constructed + or an instance of it retained by the caller; it is a "one-shot" object, really a function + wrapped in a object such that its helper methods can be organized here. """ - def __init__(self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file, mime_type): + def __init__( + self, + shapes: SlideShapes, + shape_id: int, + movie_file: str | IO[bytes], + x: Length, + y: Length, + cx: Length, + cy: Length, + poster_frame_file: str | IO[bytes] | None, + mime_type: str | None, + ): super(_MoviePicElementCreator, self).__init__() self._shapes = shapes self._shape_id = shape_id @@ -901,28 +898,35 @@ def __init__(self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file @classmethod def new_movie_pic( - cls, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type - ): - """Return a new `p:pic` element containing video in *movie_file*. - - If *mime_type* is None, 'video/unknown' is used. If - *poster_frame_file* is None, the default "media loudspeaker" image is - used. + cls, + shapes: SlideShapes, + shape_id: int, + movie_file: str | IO[bytes], + x: Length, + y: Length, + cx: Length, + cy: Length, + poster_frame_image: str | IO[bytes] | None, + mime_type: str | None, + ) -> CT_Picture: + """Return a new `p:pic` element containing video in `movie_file`. + + If `mime_type` is None, 'video/unknown' is used. If `poster_frame_file` is None, the + default "media loudspeaker" image is used. """ return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic - return @property - def _media_rId(self): + def _media_rId(self) -> str: """Return the rId of RT.MEDIA relationship to video part. - For historical reasons, there are two relationships to the same part; - one is the video rId and the other is the media rId. + For historical reasons, there are two relationships to the same part; one is the video rId + and the other is the media rId. """ return self._video_part_rIds[0] @lazyproperty - def _pic(self): + def _pic(self) -> CT_Picture: """Return the new `p:pic` element referencing the video.""" return CT_Picture.new_video_pic( self._shape_id, @@ -937,29 +941,27 @@ def _pic(self): ) @lazyproperty - def _poster_frame_image_file(self): + def _poster_frame_image_file(self) -> str | IO[bytes]: """Return the image file for video placeholder image. - If no poster frame file is provided, the default "media loudspeaker" - image is used. + If no poster frame file is provided, the default "media loudspeaker" image is used. """ poster_frame_file = self._poster_frame_file if poster_frame_file is None: - return BytesIO(SPEAKER_IMAGE_BYTES) + return io.BytesIO(SPEAKER_IMAGE_BYTES) return poster_frame_file @lazyproperty - def _poster_frame_rId(self): + def _poster_frame_rId(self) -> str: """Return the rId of relationship to poster frame image. - The poster frame is the image used to represent the video before it's - played. + The poster frame is the image used to represent the video before it's played. """ _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file) return poster_frame_rId @property - def _shape_name(self): + def _shape_name(self) -> str: """Return the appropriate shape name for the p:pic shape. A movie shape is named with the base filename of the video. @@ -967,31 +969,30 @@ def _shape_name(self): return self._video.filename @property - def _slide_part(self): + def _slide_part(self) -> SlidePart: """Return SlidePart object for slide containing this movie.""" return self._shapes.part @lazyproperty - def _video(self): + def _video(self) -> Video: """Return a |Video| object containing the movie file.""" return Video.from_path_or_file_like(self._movie_file, self._mime_type) @lazyproperty - def _video_part_rIds(self): + def _video_part_rIds(self) -> tuple[str, str]: """Return the rIds for relationships to media part for video. - This is where the media part and its relationships to the slide are - actually created. + This is where the media part and its relationships to the slide are actually created. """ media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video) return media_rId, video_rId @property - def _video_rId(self): + def _video_rId(self) -> str: """Return the rId of RT.VIDEO relationship to video part. - For historical reasons, there are two relationships to the same part; - one is the video rId and the other is the media rId. + For historical reasons, there are two relationships to the same part; one is the video rId + and the other is the media rId. """ return self._video_part_rIds[1] @@ -999,26 +1000,26 @@ def _video_rId(self): class _OleObjectElementCreator(object): """Functional service object for creating a new OLE-object p:graphicFrame element. - It's entire external interface is its :meth:`graphicFrame` class method that returns - a new `p:graphicFrame` element containing the specified embedded OLE-object shape. - This class is not intended to be constructed or an instance of it retained by the - caller; it is a "one-shot" object, really a function wrapped in a object such that - its helper methods can be organized here. + It's entire external interface is its :meth:`graphicFrame` class method that returns a new + `p:graphicFrame` element containing the specified embedded OLE-object shape. This class is not + intended to be constructed or an instance of it retained by the caller; it is a "one-shot" + object, really a function wrapped in a object such that its helper methods can be organized + here. """ def __init__( self, - shapes, - shape_id, - ole_object_file, - prog_id, - x, - y, - cx, - cy, - icon_file, - icon_width, - icon_height, + shapes: _BaseGroupShapes, + shape_id: int, + ole_object_file: str | IO[bytes], + prog_id: PROG_ID | str, + x: Length, + y: Length, + cx: Length | None, + cy: Length | None, + icon_file: str | IO[bytes] | None, + icon_width: Length | None, + icon_height: Length | None, ): self._shapes = shapes self._shape_id = shape_id @@ -1035,18 +1036,18 @@ def __init__( @classmethod def graphicFrame( cls, - shapes, - shape_id, - ole_object_file, - prog_id, - x, - y, - cx, - cy, - icon_file, - icon_width, - icon_height, - ): + shapes: _BaseGroupShapes, + shape_id: int, + ole_object_file: str | IO[bytes], + prog_id: PROG_ID | str, + x: Length, + y: Length, + cx: Length | None, + cy: Length | None, + icon_file: str | IO[bytes] | None, + icon_width: Length | None, + icon_height: Length | None, + ) -> CT_GraphicalObjectFrame: """Return new `p:graphicFrame` element containing embedded `ole_object_file`.""" return cls( shapes, @@ -1063,7 +1064,7 @@ def graphicFrame( )._graphicFrame @lazyproperty - def _graphicFrame(self): + def _graphicFrame(self) -> CT_GraphicalObjectFrame: """Newly-created `p:graphicFrame` element referencing embedded OLE-object.""" return CT_GraphicalObjectFrame.new_ole_object_graphicFrame( self._shape_id, @@ -1080,7 +1081,7 @@ def _graphicFrame(self): ) @lazyproperty - def _cx(self): + def _cx(self) -> Length: """Emu object specifying width of "show-as-icon" image for OLE shape.""" # --- a user-specified width overrides any default --- if self._cx_arg is not None: @@ -1093,7 +1094,7 @@ def _cx(self): ) @lazyproperty - def _cy(self): + def _cy(self) -> Length: """Emu object specifying height of "show-as-icon" image for OLE shape.""" # --- a user-specified width overrides any default --- if self._cy_arg is not None: @@ -1106,20 +1107,19 @@ def _cy(self): ) @lazyproperty - def _icon_height(self): + def _icon_height(self) -> Length: """Vertical size of enclosed EMF icon within the OLE graphic-frame. - This must be specified when a custom icon is used, to avoid stretching of the - image and possible undesired resizing by PowerPoint when the OLE shape is - double-clicked to open it. + This must be specified when a custom icon is used, to avoid stretching of the image and + possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it. - The correct size can be determined by creating an example PPTX using PowerPoint - and then inspecting the XML of the OLE graphics-frame (p:oleObj.imgH). + The correct size can be determined by creating an example PPTX using PowerPoint and then + inspecting the XML of the OLE graphics-frame (p:oleObj.imgH). """ return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600) @lazyproperty - def _icon_image_file(self): + def _icon_image_file(self) -> str | IO[bytes]: """Reference to image file containing icon to show in lieu of this object. This can be either a str path or a file-like object (io.BytesIO typically). @@ -1140,38 +1140,35 @@ def _icon_image_file(self): return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename)) @lazyproperty - def _icon_rId(self): + def _icon_rId(self) -> str: """str rId like "rId7" of rel to icon (image) representing OLE-object part.""" _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file) return rId @lazyproperty - def _icon_width(self): + def _icon_width(self) -> Length: """Width of enclosed EMF icon within the OLE graphic-frame. - This must be specified when a custom icon is used, to avoid stretching of the - image and possible undesired resizing by PowerPoint when the OLE shape is - double-clicked to open it. + This must be specified when a custom icon is used, to avoid stretching of the image and + possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it. """ return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200) @lazyproperty - def _ole_object_rId(self): + def _ole_object_rId(self) -> str: """str rId like "rId6" of relationship to embedded ole_object part. - This is where the ole_object part and its relationship to the slide are actually - created. + This is where the ole_object part and its relationship to the slide are actually created. """ return self._slide_part.add_embedded_ole_object_part( self._prog_id_arg, self._ole_object_file ) @lazyproperty - def _progId(self): + def _progId(self) -> str: """str like "Excel.Sheet.12" identifying program used to open object. - This value appears in the `progId` attribute of the `p:oleObj` element for the - object. + This value appears in the `progId` attribute of the `p:oleObj` element for the object. """ prog_id_arg = self._prog_id_arg @@ -1180,7 +1177,7 @@ def _progId(self): return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg @lazyproperty - def _shape_name(self): + def _shape_name(self) -> str: """str name like "Object 1" for the embedded ole_object shape. The name is formed from the prefix "Object " and the shape-id decremented by 1. @@ -1188,6 +1185,6 @@ def _shape_name(self): return "Object %d" % (self._shape_id - 1) @lazyproperty - def _slide_part(self): + def _slide_part(self) -> SlidePart: """SlidePart object for this slide.""" return self._shapes.part diff --git a/src/pptx/shared.py b/src/pptx/shared.py index 32b529d69..da2a17182 100644 --- a/src/pptx/shared.py +++ b/src/pptx/shared.py @@ -1,87 +1,82 @@ -# encoding: utf-8 - """Objects shared by pptx modules.""" -from __future__ import unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.oxml.xmlchemy import BaseOxmlElement + from pptx.types import ProvidesPart class ElementProxy(object): - """ - Base class for lxml element proxy classes. An element proxy class is one - whose primary responsibilities are fulfilled by manipulating the - attributes and child elements of an XML element. They are the most common - type of class in python-pptx other than custom element (oxml) classes. + """Base class for lxml element proxy classes. + + An element proxy class is one whose primary responsibilities are fulfilled by manipulating the + attributes and child elements of an XML element. They are the most common type of class in + python-pptx other than custom element (oxml) classes. """ - def __init__(self, element): + def __init__(self, element: BaseOxmlElement): self._element = element - def __eq__(self, other): - """ - Return |True| if this proxy object refers to the same oxml element as - does *other*. ElementProxy objects are value objects and should - maintain no mutable local state. Equality for proxy objects is - defined as referring to the same XML element, whether or not they are - the same proxy object instance. + def __eq__(self, other: object) -> bool: + """Return |True| if this proxy object refers to the same oxml element as does *other*. + + ElementProxy objects are value objects and should maintain no mutable local state. + Equality for proxy objects is defined as referring to the same XML element, whether or not + they are the same proxy object instance. """ if not isinstance(other, ElementProxy): return False return self._element is other._element - def __ne__(self, other): + def __ne__(self, other: object) -> bool: if not isinstance(other, ElementProxy): return True return self._element is not other._element @property def element(self): - """ - The lxml element proxied by this object. - """ + """The lxml element proxied by this object.""" return self._element class ParentedElementProxy(ElementProxy): - """ - Provides common services for document elements that occur below a part - but may occasionally require an ancestor object to provide a service, - such as add or drop a relationship. Provides the :attr:`_parent` - attribute to subclasses and the public :attr:`parent` read-only property. + """Provides access to ancestor objects and part. + + An ancestor may occasionally be required to provide a service, such as add or drop a + relationship. Provides the :attr:`_parent` attribute to subclasses and the public + :attr:`parent` read-only property. """ - def __init__(self, element, parent): + def __init__(self, element: BaseOxmlElement, parent: ProvidesPart): super(ParentedElementProxy, self).__init__(element) self._parent = parent @property def parent(self): - """ - The ancestor proxy object to this one. For example, the parent of - a shape is generally the |SlideShapes| object that contains it. + """The ancestor proxy object to this one. + + For example, the parent of a shape is generally the |SlideShapes| object that contains it. """ return self._parent @property - def part(self): - """ - The package part containing this object - """ + def part(self) -> XmlPart: + """The package part containing this object.""" return self._parent.part class PartElementProxy(ElementProxy): - """ - Provides common members for proxy objects that wrap the root element of - a part such as `p:sld`. - """ + """Provides common members for proxy-objects that wrap a part's root element, e.g. `p:sld`.""" - def __init__(self, element, part): + def __init__(self, element: BaseOxmlElement, part: XmlPart): super(PartElementProxy, self).__init__(element) self._part = part @property - def part(self): - """ - The package part containing this object - """ + def part(self) -> XmlPart: + """The package part containing this object.""" return self._part diff --git a/src/pptx/slide.py b/src/pptx/slide.py index 9b93666c6..3b1b65d8e 100644 --- a/src/pptx/slide.py +++ b/src/pptx/slide.py @@ -1,7 +1,9 @@ -# encoding: utf-8 - """Slide-related objects, including masters, layouts, and notes.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator, cast + from pptx.dml.fill import FillFormat from pptx.enum.shapes import PP_PLACEHOLDER from pptx.shapes.shapetree import ( @@ -17,12 +19,30 @@ from pptx.shared import ElementProxy, ParentedElementProxy, PartElementProxy from pptx.util import lazyproperty +if TYPE_CHECKING: + from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList + from pptx.oxml.slide import ( + CT_CommonSlideData, + CT_NotesSlide, + CT_Slide, + CT_SlideLayoutIdList, + CT_SlideMaster, + ) + from pptx.parts.presentation import PresentationPart + from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart + from pptx.presentation import Presentation + from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder + from pptx.shapes.shapetree import NotesSlidePlaceholder + from pptx.text.text import TextFrame + class _BaseSlide(PartElementProxy): """Base class for slide objects, including masters, layouts and notes.""" + _element: CT_Slide + @lazyproperty - def background(self): + def background(self) -> _Background: """|_Background| object providing slide background properties. This property returns a |_Background| object whether or not the @@ -34,31 +54,31 @@ def background(self): return _Background(self._element.cSld) @property - def name(self): - """ - String representing the internal name of this slide. Returns an empty - string (`''`) if no name is assigned. Assigning an empty string or - |None| to this property causes any name to be removed. + def name(self) -> str: + """String representing the internal name of this slide. + + Returns an empty string (`''`) if no name is assigned. Assigning an empty string or |None| + to this property causes any name to be removed. """ return self._element.cSld.name @name.setter - def name(self, value): + def name(self, value: str | None): new_value = "" if value is None else value self._element.cSld.name = new_value class _BaseMaster(_BaseSlide): - """ - Base class for master objects such as |SlideMaster| and |NotesMaster|. + """Base class for master objects such as |SlideMaster| and |NotesMaster|. + Provides access to placeholders and regular shapes. """ @lazyproperty - def placeholders(self): - """ - Instance of |MasterPlaceholders| containing sequence of placeholder - shapes in this master, sorted in *idx* order. + def placeholders(self) -> MasterPlaceholders: + """|MasterPlaceholders| collection of placeholder shapes in this master. + + Sequence sorted in `idx` order. """ return MasterPlaceholders(self._element.spTree, self) @@ -72,9 +92,9 @@ def shapes(self): class NotesMaster(_BaseMaster): - """ - Proxy for the notes master XML document. Provides access to shapes, the - most commonly used of which are placeholders. + """Proxy for the notes master XML document. + + Provides access to shapes, the most commonly used of which are placeholders. """ @@ -85,19 +105,21 @@ class NotesSlide(_BaseSlide): page. """ - def clone_master_placeholders(self, notes_master): - """Selectively add placeholder shape elements from *notes_master*. + element: CT_NotesSlide # pyright: ignore[reportIncompatibleMethodOverride] + + def clone_master_placeholders(self, notes_master: NotesMaster) -> None: + """Selectively add placeholder shape elements from `notes_master`. - Selected placeholder shape elements from *notes_master* are added to the shapes + Selected placeholder shape elements from `notes_master` are added to the shapes collection of this notes slide. Z-order of placeholders is preserved. Certain placeholders (header, date, footer) are not cloned. """ - def iter_cloneable_placeholders(notes_master): - """ - Generate a reference to each placeholder in *notes_master* that - should be cloned to a notes slide when the a new notes slide is - created. + def iter_cloneable_placeholders() -> Iterator[MasterPlaceholder]: + """Generate a reference to each cloneable placeholder in `notes_master`. + + These are the placeholders that should be cloned to a notes slide when the a new notes + slide is created. """ cloneable = ( PP_PLACEHOLDER.SLIDE_IMAGE, @@ -109,17 +131,16 @@ def iter_cloneable_placeholders(notes_master): yield placeholder shapes = self.shapes - for placeholder in iter_cloneable_placeholders(notes_master): - shapes.clone_placeholder(placeholder) + for placeholder in iter_cloneable_placeholders(): + shapes.clone_placeholder(cast("LayoutPlaceholder", placeholder)) @property - def notes_placeholder(self): - """ - Return the notes placeholder on this notes slide, the shape that - contains the actual notes text. Return |None| if no notes placeholder - is present; while this is probably uncommon, it can happen if the - notes master does not have a body placeholder, or if the notes - placeholder has been deleted from the notes slide. + def notes_placeholder(self) -> NotesSlidePlaceholder | None: + """the notes placeholder on this notes slide, the shape that contains the actual notes text. + + Return |None| if no notes placeholder is present; while this is probably uncommon, it can + happen if the notes master does not have a body placeholder, or if the notes placeholder + has been deleted from the notes slide. """ for placeholder in self.placeholders: if placeholder.placeholder_format.type == PP_PLACEHOLDER.BODY: @@ -127,12 +148,11 @@ def notes_placeholder(self): return None @property - def notes_text_frame(self): - """ - Return the text frame of the notes placeholder on this notes slide, - or |None| if there is no notes placeholder. This is a shortcut to - accommodate the common case of simply adding "notes" text to the - notes "page". + def notes_text_frame(self) -> TextFrame | None: + """The text frame of the notes placeholder on this notes slide. + + |None| if there is no notes placeholder. This is a shortcut to accommodate the common case + of simply adding "notes" text to the notes "page". """ notes_placeholder = self.notes_placeholder if notes_placeholder is None: @@ -140,38 +160,23 @@ def notes_text_frame(self): return notes_placeholder.text_frame @lazyproperty - def placeholders(self): - """ - An instance of |NotesSlidePlaceholders| containing the sequence of - placeholder shapes in this notes slide. + def placeholders(self) -> NotesSlidePlaceholders: + """Instance of |NotesSlidePlaceholders| for this notes-slide. + + Contains the sequence of placeholder shapes in this notes slide. """ return NotesSlidePlaceholders(self.element.spTree, self) @lazyproperty - def shapes(self): - """ - An instance of |NotesSlideShapes| containing the sequence of shape - objects appearing on this notes slide. - """ + def shapes(self) -> NotesSlideShapes: + """Sequence of shape objects appearing on this notes slide.""" return NotesSlideShapes(self._element.spTree, self) class Slide(_BaseSlide): """Slide object. Provides access to shapes and slide-level properties.""" - @property - def background(self): - """|_Background| object providing slide background properties. - - This property returns a |_Background| object whether or not the slide - overrides the default background or inherits it. Determining which of - those conditions applies for this slide is accomplished using the - :attr:`follow_master_background` property. - - The same |_Background| object is returned on every call for the same - slide object. - """ - return super(Slide, self).background + part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride] @property def follow_master_background(self): @@ -188,115 +193,99 @@ def follow_master_background(self): return self._element.bg is None @property - def has_notes_slide(self): - """ - Return True if this slide has a notes slide, False otherwise. A notes - slide is created by :attr:`.notes_slide` when one doesn't exist; use - this property to test for a notes slide without the possible side - effect of creating one. + def has_notes_slide(self) -> bool: + """`True` if this slide has a notes slide, `False` otherwise. + + A notes slide is created by :attr:`.notes_slide` when one doesn't exist; use this property + to test for a notes slide without the possible side effect of creating one. """ return self.part.has_notes_slide @property - def notes_slide(self): - """ - Return the |NotesSlide| instance for this slide. If the slide does - not have a notes slide, one is created. The same single instance is + def notes_slide(self) -> NotesSlide: + """The |NotesSlide| instance for this slide. + + If the slide does not have a notes slide, one is created. The same single instance is returned on each call. """ return self.part.notes_slide @lazyproperty - def placeholders(self): - """ - Instance of |SlidePlaceholders| containing sequence of placeholder - shapes in this slide. - """ + def placeholders(self) -> SlidePlaceholders: + """Sequence of placeholder shapes in this slide.""" return SlidePlaceholders(self._element.spTree, self) @lazyproperty - def shapes(self): - """ - Instance of |SlideShapes| containing sequence of shape objects - appearing on this slide. - """ + def shapes(self) -> SlideShapes: + """Sequence of shape objects appearing on this slide.""" return SlideShapes(self._element.spTree, self) @property - def slide_id(self): - """ - The integer value that uniquely identifies this slide within this - presentation. The slide id does not change if the position of this - slide in the slide sequence is changed by adding, rearranging, or - deleting slides. + def slide_id(self) -> int: + """Integer value that uniquely identifies this slide within this presentation. + + The slide id does not change if the position of this slide in the slide sequence is changed + by adding, rearranging, or deleting slides. """ return self.part.slide_id @property - def slide_layout(self): - """ - |SlideLayout| object this slide inherits appearance from. - """ + def slide_layout(self) -> SlideLayout: + """|SlideLayout| object this slide inherits appearance from.""" return self.part.slide_layout class Slides(ParentedElementProxy): + """Sequence of slides belonging to an instance of |Presentation|. + + Has list semantics for access to individual slides. Supports indexed access, len(), and + iteration. """ - Sequence of slides belonging to an instance of |Presentation|, having - list semantics for access to individual slides. Supports indexed access, - len(), and iteration. - """ - def __init__(self, sldIdLst, prs): + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + + def __init__(self, sldIdLst: CT_SlideIdList, prs: Presentation): super(Slides, self).__init__(sldIdLst, prs) self._sldIdLst = sldIdLst - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. 'slides[0]'). - """ + def __getitem__(self, idx: int) -> Slide: + """Provide indexed access, (e.g. 'slides[0]').""" try: - sldId = self._sldIdLst[idx] + sldId = self._sldIdLst.sldId_lst[idx] except IndexError: raise IndexError("slide index out of range") return self.part.related_slide(sldId.rId) - def __iter__(self): - """ - Support iteration (e.g. 'for slide in slides:'). - """ - for sldId in self._sldIdLst: + def __iter__(self) -> Iterator[Slide]: + """Support iteration, e.g. `for slide in slides:`.""" + for sldId in self._sldIdLst.sldId_lst: yield self.part.related_slide(sldId.rId) - def __len__(self): - """ - Support len() built-in function (e.g. 'len(slides) == 4'). - """ + def __len__(self) -> int: + """Support len() built-in function, e.g. `len(slides) == 4`.""" return len(self._sldIdLst) - def add_slide(self, slide_layout): - """ - Return a newly added slide that inherits layout from *slide_layout*. - """ + def add_slide(self, slide_layout: SlideLayout) -> Slide: + """Return a newly added slide that inherits layout from `slide_layout`.""" rId, slide = self.part.add_slide(slide_layout) slide.shapes.clone_layout_placeholders(slide_layout) self._sldIdLst.add_sldId(rId) return slide - def get(self, slide_id, default=None): - """ - Return the slide identified by integer *slide_id* in this - presentation, or *default* if not found. + def get(self, slide_id: int, default: Slide | None = None) -> Slide | None: + """Return the slide identified by int `slide_id` in this presentation. + + Returns `default` if not found. """ slide = self.part.get_slide(slide_id) if slide is None: return default return slide - def index(self, slide): - """ - Map *slide* to an integer representing its zero-based position in - this slide collection. Raises |ValueError| on *slide* not present. + def index(self, slide: Slide) -> int: + """Map `slide` to its zero-based position in this slide sequence. + + Raises |ValueError| on *slide* not present. """ for idx, this_slide in enumerate(self): if this_slide == slide: @@ -305,16 +294,17 @@ def index(self, slide): class SlideLayout(_BaseSlide): - """ - Slide layout object. Provides access to placeholders, regular shapes, and - slide layout-level properties. + """Slide layout object. + + Provides access to placeholders, regular shapes, and slide layout-level properties. """ - def iter_cloneable_placeholders(self): - """ - Generate a reference to each layout placeholder on this slide layout - that should be cloned to a slide when the layout is applied to that - slide. + part: SlideLayoutPart # pyright: ignore[reportIncompatibleMethodOverride] + + def iter_cloneable_placeholders(self) -> Iterator[LayoutPlaceholder]: + """Generate layout-placeholders on this slide-layout that should be cloned to a new slide. + + Used when creating a new slide from this slide-layout. """ latent_ph_types = ( PP_PLACEHOLDER.DATE, @@ -326,26 +316,21 @@ def iter_cloneable_placeholders(self): yield ph @lazyproperty - def placeholders(self): - """ - Instance of |LayoutPlaceholders| containing sequence of placeholder - shapes in this slide layout, sorted in *idx* order. + def placeholders(self) -> LayoutPlaceholders: + """Sequence of placeholder shapes in this slide layout. + + Placeholders appear in `idx` order. """ return LayoutPlaceholders(self._element.spTree, self) @lazyproperty - def shapes(self): - """ - Instance of |LayoutShapes| containing the sequence of shapes - appearing on this slide layout. - """ + def shapes(self) -> LayoutShapes: + """Sequence of shapes appearing on this slide layout.""" return LayoutShapes(self._element.spTree, self) @property - def slide_master(self): - """ - Slide master from which this slide layout inherits properties. - """ + def slide_master(self) -> SlideMaster: + """Slide master from which this slide-layout inherits properties.""" return self.part.slide_master @property @@ -362,56 +347,51 @@ class SlideLayouts(ParentedElementProxy): Supports indexed access, len(), iteration, index() and remove(). """ - def __init__(self, sldLayoutIdLst, parent): + part: SlideMasterPart # pyright: ignore[reportIncompatibleMethodOverride] + + def __init__(self, sldLayoutIdLst: CT_SlideLayoutIdList, parent: SlideMaster): super(SlideLayouts, self).__init__(sldLayoutIdLst, parent) self._sldLayoutIdLst = sldLayoutIdLst - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. ``slide_layouts[2]``). - """ + def __getitem__(self, idx: int) -> SlideLayout: + """Provides indexed access, e.g. `slide_layouts[2]`.""" try: - sldLayoutId = self._sldLayoutIdLst[idx] + sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[idx] except IndexError: raise IndexError("slide layout index out of range") return self.part.related_slide_layout(sldLayoutId.rId) - def __iter__(self): - """ - Generate a reference to each of the |SlideLayout| instances in the - collection, in sequence. - """ - for sldLayoutId in self._sldLayoutIdLst: + def __iter__(self) -> Iterator[SlideLayout]: + """Generate each |SlideLayout| in the collection, in sequence.""" + for sldLayoutId in self._sldLayoutIdLst.sldLayoutId_lst: yield self.part.related_slide_layout(sldLayoutId.rId) - def __len__(self): - """ - Support len() built-in function (e.g. 'len(slides) == 4'). - """ + def __len__(self) -> int: + """Support len() built-in function, e.g. `len(slides) == 4`.""" return len(self._sldLayoutIdLst) - def get_by_name(self, name, default=None): - """Return SlideLayout object having *name* or *default* if not found.""" + def get_by_name(self, name: str, default: SlideLayout | None = None) -> SlideLayout | None: + """Return SlideLayout object having `name`, or `default` if not found.""" for slide_layout in self: if slide_layout.name == name: return slide_layout return default - def index(self, slide_layout): - """Return zero-based index of *slide_layout* in this collection. + def index(self, slide_layout: SlideLayout) -> int: + """Return zero-based index of `slide_layout` in this collection. - Raises ValueError if *slide_layout* is not present in this collection. + Raises `ValueError` if `slide_layout` is not present in this collection. """ for idx, this_layout in enumerate(self): if slide_layout == this_layout: return idx raise ValueError("layout not in this SlideLayouts collection") - def remove(self, slide_layout): - """Remove *slide_layout* from the collection. + def remove(self, slide_layout: SlideLayout) -> None: + """Remove `slide_layout` from the collection. - Raises ValueError when *slide_layout* is in use; a slide layout which is the - basis for one or more slides cannot be removed. + Raises ValueError when `slide_layout` is in use; a slide layout which is the basis for one + or more slides cannot be removed. """ # ---raise if layout is in use--- if slide_layout.used_by_slides: @@ -432,14 +412,16 @@ def remove(self, slide_layout): class SlideMaster(_BaseMaster): - """ - Slide master object. Provides access to slide layouts. Access to - placeholders, regular shapes, and slide master-level properties is - inherited from |_BaseMaster|. + """Slide master object. + + Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level + properties is inherited from |_BaseMaster|. """ + _element: CT_SlideMaster # pyright: ignore[reportIncompatibleVariableOverride] + @lazyproperty - def slide_layouts(self): + def slide_layouts(self) -> SlideLayouts: """|SlideLayouts| object providing access to this slide-master's layouts.""" return SlideLayouts(self._element.get_or_add_sldLayoutIdLst(), self) @@ -450,32 +432,27 @@ class SlideMasters(ParentedElementProxy): Has list access semantics, supporting indexed access, len(), and iteration. """ - def __init__(self, sldMasterIdLst, parent): + part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride] + + def __init__(self, sldMasterIdLst: CT_SlideMasterIdList, parent: Presentation): super(SlideMasters, self).__init__(sldMasterIdLst, parent) self._sldMasterIdLst = sldMasterIdLst - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. ``slide_masters[2]``). - """ + def __getitem__(self, idx: int) -> SlideMaster: + """Provides indexed access, e.g. `slide_masters[2]`.""" try: - sldMasterId = self._sldMasterIdLst[idx] + sldMasterId = self._sldMasterIdLst.sldMasterId_lst[idx] except IndexError: raise IndexError("slide master index out of range") return self.part.related_slide_master(sldMasterId.rId) def __iter__(self): - """ - Generate a reference to each of the |SlideMaster| instances in the - collection, in sequence. - """ - for smi in self._sldMasterIdLst: + """Generate each |SlideMaster| instance in the collection, in sequence.""" + for smi in self._sldMasterIdLst.sldMasterId_lst: yield self.part.related_slide_master(smi.rId) def __len__(self): - """ - Support len() built-in function (e.g. 'len(slide_masters) == 4'). - """ + """Support len() built-in function, e.g. `len(slide_masters) == 4`.""" return len(self._sldMasterIdLst) @@ -487,7 +464,7 @@ class _Background(ElementProxy): has a |_Background| object. """ - def __init__(self, cSld): + def __init__(self, cSld: CT_CommonSlideData): super(_Background, self).__init__(cSld) self._cSld = cSld diff --git a/src/pptx/spec.py b/src/pptx/spec.py index 835fde6d0..1e7bffb36 100644 --- a/src/pptx/spec.py +++ b/src/pptx/spec.py @@ -1,23 +1,34 @@ -# encoding: utf-8 - """Mappings from the ISO/IEC 29500 spec. Some of these are inferred from PowerPoint application behavior """ -from pptx.enum.shapes import MSO_SHAPE +from __future__ import annotations +from typing import TYPE_CHECKING, TypedDict + +from pptx.enum.shapes import MSO_SHAPE GRAPHIC_DATA_URI_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart" GRAPHIC_DATA_URI_OLEOBJ = "http://schemas.openxmlformats.org/presentationml/2006/ole" GRAPHIC_DATA_URI_TABLE = "http://schemas.openxmlformats.org/drawingml/2006/table" +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +AdjustmentValue: TypeAlias = tuple[str, int] + + +class ShapeSpec(TypedDict): + basename: str + avLst: tuple[AdjustmentValue, ...] + # ============================================================================ # AutoShape type specs # ============================================================================ -autoshape_types = { +autoshape_types: dict[MSO_SHAPE, ShapeSpec] = { MSO_SHAPE.ACTION_BUTTON_BACK_OR_PREVIOUS: { "basename": "Action Button: Back or Previous", "avLst": (), diff --git a/src/pptx/table.py b/src/pptx/table.py index 63872eab8..3bdf54ba6 100644 --- a/src/pptx/table.py +++ b/src/pptx/table.py @@ -1,13 +1,22 @@ -# encoding: utf-8 - """Table-related objects such as Table and Cell.""" -from pptx.compat import is_integer +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator + from pptx.dml.fill import FillFormat from pptx.oxml.table import TcRange from pptx.shapes import Subshape from pptx.text.text import TextFrame -from pptx.util import lazyproperty +from pptx.util import Emu, lazyproperty + +if TYPE_CHECKING: + from pptx.enum.text import MSO_VERTICAL_ANCHOR + from pptx.oxml.table import CT_Table, CT_TableCell, CT_TableCol, CT_TableRow + from pptx.parts.slide import BaseSlidePart + from pptx.shapes.graphfrm import GraphicFrame + from pptx.types import ProvidesPart + from pptx.util import Length class Table(object): @@ -17,66 +26,68 @@ class Table(object): :meth:`.Slide.shapes.add_table` to add a table to a slide. """ - def __init__(self, tbl, graphic_frame): + def __init__(self, tbl: CT_Table, graphic_frame: GraphicFrame): super(Table, self).__init__() self._tbl = tbl self._graphic_frame = graphic_frame - def cell(self, row_idx, col_idx): - """Return cell at *row_idx*, *col_idx*. + def cell(self, row_idx: int, col_idx: int) -> _Cell: + """Return cell at `row_idx`, `col_idx`. - Return value is an instance of |_Cell|. *row_idx* and *col_idx* are - zero-based, e.g. cell(0, 0) is the top, left cell in the table. + Return value is an instance of |_Cell|. `row_idx` and `col_idx` are zero-based, e.g. + cell(0, 0) is the top, left cell in the table. """ return _Cell(self._tbl.tc(row_idx, col_idx), self) @lazyproperty - def columns(self): + def columns(self) -> _ColumnCollection: """|_ColumnCollection| instance for this table. - Provides access to |_Column| objects representing the table's columns. |_Column| - objects are accessed using list notation, e.g. ``col = tbl.columns[0]``. + Provides access to |_Column| objects representing the table's columns. |_Column| objects + are accessed using list notation, e.g. `col = tbl.columns[0]`. """ return _ColumnCollection(self._tbl, self) @property - def first_col(self): - """ - Read/write boolean property which, when true, indicates the first - column should be formatted differently, as for a side-heading column - at the far left of the table. + def first_col(self) -> bool: + """When `True`, indicates first column should have distinct formatting. + + Read/write. Distinct formatting is used, for example, when the first column contains row + headings (is a side-heading column). """ return self._tbl.firstCol @first_col.setter - def first_col(self, value): + def first_col(self, value: bool): self._tbl.firstCol = value @property - def first_row(self): - """ - Read/write boolean property which, when true, indicates the first - row should be formatted differently, e.g. for column headings. + def first_row(self) -> bool: + """When `True`, indicates first row should have distinct formatting. + + Read/write. Distinct formatting is used, for example, when the first row contains column + headings. """ return self._tbl.firstRow @first_row.setter - def first_row(self, value): + def first_row(self, value: bool): self._tbl.firstRow = value @property - def horz_banding(self): - """ - Read/write boolean property which, when true, indicates the rows of - the table should appear with alternating shading. + def horz_banding(self) -> bool: + """When `True`, indicates rows should have alternating shading. + + Read/write. Used to allow rows to be traversed more easily without losing track of which + row is being read. """ return self._tbl.bandRow @horz_banding.setter - def horz_banding(self, value): + def horz_banding(self, value: bool): self._tbl.bandRow = value - def iter_cells(self): + def iter_cells(self) -> Iterator[_Cell]: """Generate _Cell object for each cell in this table. Each grid cell is generated in left-to-right, top-to-bottom order. @@ -84,185 +95,177 @@ def iter_cells(self): return (_Cell(tc, self) for tc in self._tbl.iter_tcs()) @property - def last_col(self): - """ - Read/write boolean property which, when true, indicates the last - column should be formatted differently, as for a row totals column at - the far right of the table. + def last_col(self) -> bool: + """When `True`, indicates the rightmost column should have distinct formatting. + + Read/write. Used, for example, when a row totals column appears at the far right of the + table. """ return self._tbl.lastCol @last_col.setter - def last_col(self, value): + def last_col(self, value: bool): self._tbl.lastCol = value @property - def last_row(self): - """ - Read/write boolean property which, when true, indicates the last - row should be formatted differently, as for a totals row at the - bottom of the table. + def last_row(self) -> bool: + """When `True`, indicates the bottom row should have distinct formatting. + + Read/write. Used, for example, when a totals row appears as the bottom row. """ return self._tbl.lastRow @last_row.setter - def last_row(self, value): + def last_row(self, value: bool): self._tbl.lastRow = value - def notify_height_changed(self): - """ - Called by a row when its height changes, triggering the graphic frame - to recalculate its total height (as the sum of the row heights). + def notify_height_changed(self) -> None: + """Called by a row when its height changes. + + Triggers the graphic frame to recalculate its total height (as the sum of the row + heights). """ - new_table_height = sum([row.height for row in self.rows]) + new_table_height = Emu(sum([row.height for row in self.rows])) self._graphic_frame.height = new_table_height - def notify_width_changed(self): - """ - Called by a column when its width changes, triggering the graphic - frame to recalculate its total width (as the sum of the column + def notify_width_changed(self) -> None: + """Called by a column when its width changes. + + Triggers the graphic frame to recalculate its total width (as the sum of the column widths). """ - new_table_width = sum([col.width for col in self.columns]) + new_table_width = Emu(sum([col.width for col in self.columns])) self._graphic_frame.width = new_table_width @property - def part(self): - """ - The package part containing this table. - """ + def part(self) -> BaseSlidePart: + """The package part containing this table.""" return self._graphic_frame.part @lazyproperty def rows(self): """|_RowCollection| instance for this table. - Provides access to |_Row| objects representing the table's rows. |_Row| objects - are accessed using list notation, e.g. ``col = tbl.rows[0]``. + Provides access to |_Row| objects representing the table's rows. |_Row| objects are + accessed using list notation, e.g. `col = tbl.rows[0]`. """ return _RowCollection(self._tbl, self) @property - def vert_banding(self): - """ - Read/write boolean property which, when true, indicates the columns - of the table should appear with alternating shading. + def vert_banding(self) -> bool: + """When `True`, indicates columns should have alternating shading. + + Read/write. Used to allow columns to be traversed more easily without losing track of + which column is being read. """ return self._tbl.bandCol @vert_banding.setter - def vert_banding(self, value): + def vert_banding(self, value: bool): self._tbl.bandCol = value class _Cell(Subshape): """Table cell""" - def __init__(self, tc, parent): + def __init__(self, tc: CT_TableCell, parent: ProvidesPart): super(_Cell, self).__init__(parent) self._tc = tc - def __eq__(self, other): - """|True| if this object proxies the same element as *other*. + def __eq__(self, other: object) -> bool: + """|True| if this object proxies the same element as `other`. - Equality for proxy objects is defined as referring to the same XML - element, whether or not they are the same proxy object instance. + Equality for proxy objects is defined as referring to the same XML element, whether or not + they are the same proxy object instance. """ if not isinstance(other, type(self)): return False return self._tc is other._tc - def __ne__(self, other): + def __ne__(self, other: object) -> bool: if not isinstance(other, type(self)): return True return self._tc is not other._tc @lazyproperty - def fill(self): - """ - |FillFormat| instance for this cell, providing access to fill - properties such as foreground color. + def fill(self) -> FillFormat: + """|FillFormat| instance for this cell. + + Provides access to fill properties such as foreground color. """ tcPr = self._tc.get_or_add_tcPr() return FillFormat.from_fill_parent(tcPr) @property - def is_merge_origin(self): + def is_merge_origin(self) -> bool: """True if this cell is the top-left grid cell in a merged cell.""" return self._tc.is_merge_origin @property - def is_spanned(self): + def is_spanned(self) -> bool: """True if this cell is spanned by a merge-origin cell. - A merge-origin cell "spans" the other grid cells in its merge range, - consuming their area and "shadowing" the spanned grid cells. + A merge-origin cell "spans" the other grid cells in its merge range, consuming their area + and "shadowing" the spanned grid cells. - Note this value is |False| for a merge-origin cell. A merge-origin - cell spans other grid cells, but is not itself a spanned cell. + Note this value is |False| for a merge-origin cell. A merge-origin cell spans other grid + cells, but is not itself a spanned cell. """ return self._tc.is_spanned @property - def margin_left(self): - """ - Read/write integer value of left margin of cell as a |Length| value - object. If assigned |None|, the default value is used, 0.1 inches for - left and right margins and 0.05 inches for top and bottom. + def margin_left(self) -> Length: + """Left margin of cells. + + Read/write. If assigned |None|, the default value is used, 0.1 inches for left and right + margins and 0.05 inches for top and bottom. """ return self._tc.marL @margin_left.setter - def margin_left(self, margin_left): + def margin_left(self, margin_left: Length | None): self._validate_margin_value(margin_left) self._tc.marL = margin_left @property - def margin_right(self): - """ - Right margin of cell. - """ + def margin_right(self) -> Length: + """Right margin of cell.""" return self._tc.marR @margin_right.setter - def margin_right(self, margin_right): + def margin_right(self, margin_right: Length | None): self._validate_margin_value(margin_right) self._tc.marR = margin_right @property - def margin_top(self): - """ - Top margin of cell. - """ + def margin_top(self) -> Length: + """Top margin of cell.""" return self._tc.marT @margin_top.setter - def margin_top(self, margin_top): + def margin_top(self, margin_top: Length | None): self._validate_margin_value(margin_top) self._tc.marT = margin_top @property - def margin_bottom(self): - """ - Bottom margin of cell. - """ + def margin_bottom(self) -> Length: + """Bottom margin of cell.""" return self._tc.marB @margin_bottom.setter - def margin_bottom(self, margin_bottom): + def margin_bottom(self, margin_bottom: Length | None): self._validate_margin_value(margin_bottom) self._tc.marB = margin_bottom - def merge(self, other_cell): - """Create merged cell from this cell to *other_cell*. + def merge(self, other_cell: _Cell) -> None: + """Create merged cell from this cell to `other_cell`. - This cell and *other_cell* specify opposite corners of the merged - cell range. Either diagonal of the cell region may be specified in - either order, e.g. self=bottom-right, other_cell=top-left, etc. + This cell and `other_cell` specify opposite corners of the merged cell range. Either + diagonal of the cell region may be specified in either order, e.g. self=bottom-right, + other_cell=top-left, etc. - Raises |ValueError| if the specified range already contains merged - cells anywhere within its extents or if *other_cell* is not in the - same table as *self*. + Raises |ValueError| if the specified range already contains merged cells anywhere within + its extents or if `other_cell` is not in the same table as `self`. """ tc_range = TcRange(self._tc, other_cell._tc) @@ -285,43 +288,38 @@ def merge(self, other_cell): tc.vMerge = True @property - def span_height(self): + def span_height(self) -> int: """int count of rows spanned by this cell. - The value of this property may be misleading (often 1) on cells where - `.is_merge_origin` is not |True|, since only a merge-origin cell - contains complete span information. This property is only intended - for use on cells known to be a merge origin by testing + The value of this property may be misleading (often 1) on cells where `.is_merge_origin` + is not |True|, since only a merge-origin cell contains complete span information. This + property is only intended for use on cells known to be a merge origin by testing `.is_merge_origin`. """ return self._tc.rowSpan @property - def span_width(self): + def span_width(self) -> int: """int count of columns spanned by this cell. - The value of this property may be misleading (often 1) on cells where - `.is_merge_origin` is not |True|, since only a merge-origin cell - contains complete span information. This property is only intended - for use on cells known to be a merge origin by testing + The value of this property may be misleading (often 1) on cells where `.is_merge_origin` + is not |True|, since only a merge-origin cell contains complete span information. This + property is only intended for use on cells known to be a merge origin by testing `.is_merge_origin`. """ return self._tc.gridSpan - def split(self): + def split(self) -> None: """Remove merge from this (merge-origin) cell. - The merged cell represented by this object will be "unmerged", - yielding a separate unmerged cell for each grid cell previously - spanned by this merge. + The merged cell represented by this object will be "unmerged", yielding a separate + unmerged cell for each grid cell previously spanned by this merge. - Raises |ValueError| when this cell is not a merge-origin cell. Test - with `.is_merge_origin` before calling. + Raises |ValueError| when this cell is not a merge-origin cell. Test with + `.is_merge_origin` before calling. """ if not self.is_merge_origin: - raise ValueError( - "not a merge-origin cell; only a merge-origin cell can be sp" "lit" - ) + raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit") tc_range = TcRange.from_merge_origin(self._tc) @@ -330,64 +328,52 @@ def split(self): tc.hMerge = tc.vMerge = False @property - def text(self): - """Unicode (str in Python 3) representation of cell contents. - - The returned string will contain a newline character (``"\\n"``) separating each - paragraph and a vertical-tab (``"\\v"``) character for each line break (soft - carriage return) in the cell's text. - - Assignment to *text* replaces all text currently contained in the cell. A - newline character (``"\\n"``) in the assigned text causes a new paragraph to be - started. A vertical-tab (``"\\v"``) character in the assigned text causes - a line-break (soft carriage-return) to be inserted. (The vertical-tab character - appears in clipboard text copied from PowerPoint as its encoding of - line-breaks.) - - Either bytes (Python 2 str) or unicode (Python 3 str) can be assigned. Bytes can - be 7-bit ASCII or UTF-8 encoded 8-bit bytes. Bytes values are converted to - unicode assuming UTF-8 encoding (which correctly decodes ASCII). + def text(self) -> str: + """Textual content of cell as a single string. + + The returned string will contain a newline character (`"\\n"`) separating each paragraph + and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the + cell's text. + + Assignment to `text` replaces all text currently contained in the cell. A newline + character (`"\\n"`) in the assigned text causes a new paragraph to be started. A + vertical-tab (`"\\v"`) character in the assigned text causes a line-break (soft + carriage-return) to be inserted. (The vertical-tab character appears in clipboard text + copied from PowerPoint as its encoding of line-breaks.) """ return self.text_frame.text @text.setter - def text(self, text): + def text(self, text: str): self.text_frame.text = text @property - def text_frame(self): - """ - |TextFrame| instance containing the text that appears in the cell. - """ + def text_frame(self) -> TextFrame: + """|TextFrame| containing the text that appears in the cell.""" txBody = self._tc.get_or_add_txBody() return TextFrame(txBody, self) @property - def vertical_anchor(self): + def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None: """Vertical alignment of this cell. - This value is a member of the :ref:`MsoVerticalAnchor` enumeration or - |None|. A value of |None| indicates the cell has no explicitly - applied vertical anchor setting and its effective value is inherited - from its style-hierarchy ancestors. + This value is a member of the :ref:`MsoVerticalAnchor` enumeration or |None|. A value of + |None| indicates the cell has no explicitly applied vertical anchor setting and its + effective value is inherited from its style-hierarchy ancestors. - Assigning |None| to this property causes any explicitly applied - vertical anchor setting to be cleared and inheritance of its - effective value to be restored. + Assigning |None| to this property causes any explicitly applied vertical anchor setting to + be cleared and inheritance of its effective value to be restored. """ return self._tc.anchor @vertical_anchor.setter - def vertical_anchor(self, mso_anchor_idx): + def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None): self._tc.anchor = mso_anchor_idx @staticmethod - def _validate_margin_value(margin_value): - """ - Raise ValueError if *margin_value* is not a positive integer value or - |None|. - """ - if not is_integer(margin_value) and margin_value is not None: + def _validate_margin_value(margin_value: Length | None) -> None: + """Raise ValueError if `margin_value` is not a positive integer value or |None|.""" + if not isinstance(margin_value, int) and margin_value is not None: tmpl = "margin value must be integer or None, got '%s'" raise TypeError(tmpl % margin_value) @@ -395,19 +381,18 @@ def _validate_margin_value(margin_value): class _Column(Subshape): """Table column""" - def __init__(self, gridCol, parent): + def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection): super(_Column, self).__init__(parent) + self._parent = parent self._gridCol = gridCol @property - def width(self): - """ - Width of column in EMU. - """ + def width(self) -> Length: + """Width of column in EMU.""" return self._gridCol.w @width.setter - def width(self, width): + def width(self, width: Length): self._gridCol.w = width self._parent.notify_width_changed() @@ -415,27 +400,26 @@ def width(self, width): class _Row(Subshape): """Table row""" - def __init__(self, tr, parent): + def __init__(self, tr: CT_TableRow, parent: _RowCollection): super(_Row, self).__init__(parent) + self._parent = parent self._tr = tr @property def cells(self): - """ - Read-only reference to collection of cells in row. An individual cell - is referenced using list notation, e.g. ``cell = row.cells[0]``. + """Read-only reference to collection of cells in row. + + An individual cell is referenced using list notation, e.g. `cell = row.cells[0]`. """ return _CellCollection(self._tr, self) @property - def height(self): - """ - Height of row in EMU. - """ + def height(self) -> Length: + """Height of row in EMU.""" return self._tr.h @height.setter - def height(self, height): + def height(self, height: Length): self._tr.h = height self._parent.notify_height_changed() @@ -443,22 +427,23 @@ def height(self, height): class _CellCollection(Subshape): """Horizontal sequence of row cells""" - def __init__(self, tr, parent): + def __init__(self, tr: CT_TableRow, parent: _Row): super(_CellCollection, self).__init__(parent) + self._parent = parent self._tr = tr - def __getitem__(self, idx): + def __getitem__(self, idx: int) -> _Cell: """Provides indexed access, (e.g. 'cells[0]').""" if idx < 0 or idx >= len(self._tr.tc_lst): msg = "cell index [%d] out of range" % idx raise IndexError(msg) return _Cell(self._tr.tc_lst[idx], self) - def __iter__(self): + def __iter__(self) -> Iterator[_Cell]: """Provides iterability.""" return (_Cell(tc, self) for tc in self._tr.tc_lst) - def __len__(self): + def __len__(self) -> int: """Supports len() function (e.g. 'len(cells) == 1').""" return len(self._tr.tc_lst) @@ -466,56 +451,46 @@ def __len__(self): class _ColumnCollection(Subshape): """Sequence of table columns.""" - def __init__(self, tbl, parent): + def __init__(self, tbl: CT_Table, parent: Table): super(_ColumnCollection, self).__init__(parent) + self._parent = parent self._tbl = tbl - def __getitem__(self, idx): - """ - Provides indexed access, (e.g. 'columns[0]'). - """ + def __getitem__(self, idx: int): + """Provides indexed access, (e.g. 'columns[0]').""" if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst): msg = "column index [%d] out of range" % idx raise IndexError(msg) return _Column(self._tbl.tblGrid.gridCol_lst[idx], self) def __len__(self): - """ - Supports len() function (e.g. 'len(columns) == 1'). - """ + """Supports len() function (e.g. 'len(columns) == 1').""" return len(self._tbl.tblGrid.gridCol_lst) def notify_width_changed(self): - """ - Called by a column when its width changes. Pass along to parent. - """ + """Called by a column when its width changes. Pass along to parent.""" self._parent.notify_width_changed() class _RowCollection(Subshape): """Sequence of table rows""" - def __init__(self, tbl, parent): + def __init__(self, tbl: CT_Table, parent: Table): super(_RowCollection, self).__init__(parent) + self._parent = parent self._tbl = tbl - def __getitem__(self, idx): - """ - Provides indexed access, (e.g. 'rows[0]'). - """ + def __getitem__(self, idx: int) -> _Row: + """Provides indexed access, (e.g. 'rows[0]').""" if idx < 0 or idx >= len(self): msg = "row index [%d] out of range" % idx raise IndexError(msg) return _Row(self._tbl.tr_lst[idx], self) def __len__(self): - """ - Supports len() function (e.g. 'len(rows) == 1'). - """ + """Supports len() function (e.g. 'len(rows) == 1').""" return len(self._tbl.tr_lst) def notify_height_changed(self): - """ - Called by a row when its height changes. Pass along to parent. - """ + """Called by a row when its height changes. Pass along to parent.""" self._parent.notify_height_changed() diff --git a/src/pptx/text/fonts.py b/src/pptx/text/fonts.py index ebc5b7d49..5ae054a83 100644 --- a/src/pptx/text/fonts.py +++ b/src/pptx/text/fonts.py @@ -1,27 +1,24 @@ -# encoding: utf-8 - """Objects related to system font file lookup.""" +from __future__ import annotations + import os import sys - from struct import calcsize, unpack_from -from ..util import lazyproperty +from pptx.util import lazyproperty class FontFiles(object): - """ - A class-based singleton serving as a lazy cache for system font details. - """ + """A class-based singleton serving as a lazy cache for system font details.""" _font_files = None @classmethod - def find(cls, family_name, is_bold, is_italic): - """ - Return the absolute path to the installed OpenType font having - *family_name* and the styles *is_bold* and *is_italic*. + def find(cls, family_name: str, is_bold: bool, is_italic: bool) -> str: + """Return the absolute path to an installed OpenType font. + + File is matched by `family_name` and the styles `is_bold` and `is_italic`. """ if cls._font_files is None: cls._font_files = cls._installed_fonts() @@ -327,9 +324,7 @@ def _iter_names(self): table_bytes = self._table_bytes for idx in range(count): - platform_id, name_id, name = self._read_name( - table_bytes, idx, strings_offset - ) + platform_id, name_id, name = self._read_name(table_bytes, idx, strings_offset) if name is None: continue yield ((platform_id, name_id), name) @@ -360,12 +355,8 @@ def _read_name(self, bufr, idx, strings_offset): `idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual name strings begin. The returned name is a unicode string. """ - platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header( - bufr, idx - ) - name = self._read_name_text( - bufr, platform_id, enc_id, strings_offset, str_offset, length - ) + platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(bufr, idx) + name = self._read_name_text(bufr, platform_id, enc_id, strings_offset, str_offset, length) return platform_id, name_id, name def _read_name_text( diff --git a/src/pptx/text/layout.py b/src/pptx/text/layout.py index c230a0ec6..d2b439939 100644 --- a/src/pptx/text/layout.py +++ b/src/pptx/text/layout.py @@ -1,21 +1,26 @@ -# encoding: utf-8 - """Objects related to layout of rendered text, such as TextFitter.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from PIL import ImageFont +if TYPE_CHECKING: + from pptx.util import Length + class TextFitter(tuple): - """ - Value object that knows how to fit text into given rectangular extents. - """ + """Value object that knows how to fit text into given rectangular extents.""" def __new__(cls, line_source, extents, font_file): width, height = extents return tuple.__new__(cls, (line_source, width, height, font_file)) @classmethod - def best_fit_font_size(cls, text, extents, max_size, font_file): + def best_fit_font_size( + cls, text: str, extents: tuple[Length, Length], max_size: int, font_file: str + ) -> int: """Return whole-number best fit point size less than or equal to `max_size`. The return value is the largest whole-number point size less than or equal to @@ -294,9 +299,7 @@ class _Fonts(object): @classmethod def font(cls, font_path, point_size): if (font_path, point_size) not in cls.fonts: - cls.fonts[(font_path, point_size)] = ImageFont.truetype( - font_path, point_size - ) + cls.fonts[(font_path, point_size)] = ImageFont.truetype(font_path, point_size) return cls.fonts[(font_path, point_size)] diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index ba941230a..e139410c2 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -1,30 +1,49 @@ -# encoding: utf-8 - """Text-related objects such as TextFrame and Paragraph.""" -from pptx.compat import to_unicode +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator, cast + from pptx.dml.fill import FillFormat from pptx.enum.dml import MSO_FILL from pptx.enum.lang import MSO_LANGUAGE_ID -from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE +from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.oxml.simpletypes import ST_TextWrappingType from pptx.shapes import Subshape from pptx.text.fonts import FontFiles from pptx.text.layout import TextFitter -from pptx.util import Centipoints, Emu, lazyproperty, Pt +from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty + +if TYPE_CHECKING: + from pptx.dml.color import ColorFormat + from pptx.enum.text import ( + MSO_TEXT_UNDERLINE_TYPE, + MSO_VERTICAL_ANCHOR, + PP_PARAGRAPH_ALIGNMENT, + ) + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.text import ( + CT_RegularTextRun, + CT_TextBody, + CT_TextCharacterProperties, + CT_TextParagraph, + CT_TextParagraphProperties, + ) + from pptx.types import ProvidesExtents, ProvidesPart class TextFrame(Subshape): """The part of a shape that contains its text. - Not all shapes have a text frame. Corresponds to the ```` element that can - appear as a child element of ````. Not intended to be constructed directly. + Not all shapes have a text frame. Corresponds to the `p:txBody` element that can + appear as a child element of `p:sp`. Not intended to be constructed directly. """ - def __init__(self, txBody, parent): + def __init__(self, txBody: CT_TextBody, parent: ProvidesPart): super(TextFrame, self).__init__(parent) self._element = self._txBody = txBody + self._parent = parent def add_paragraph(self): """ @@ -35,18 +54,18 @@ def add_paragraph(self): return _Paragraph(p, self) @property - def auto_size(self): - """ - The type of automatic resizing that should be used to fit the text of - this shape within its bounding box when the text would otherwise - extend beyond the shape boundaries. May be |None|, - ``MSO_AUTO_SIZE.NONE``, ``MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT``, or - ``MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE``. + def auto_size(self) -> MSO_AUTO_SIZE | None: + """Resizing strategy used to fit text within this shape. + + Determins the type of automatic resizing used to fit the text of this shape within its + bounding box when the text would otherwise extend beyond the shape boundaries. May be + |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or + `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`. """ return self._bodyPr.autofit @auto_size.setter - def auto_size(self, value): + def auto_size(self, value: MSO_AUTO_SIZE | None): self._bodyPr.autofit = value def clear(self): @@ -58,145 +77,126 @@ def clear(self): def fit_text( self, - font_family="Calibri", - max_size=18, - bold=False, - italic=False, - font_file=None, + font_family: str = "Calibri", + max_size: int = 18, + bold: bool = False, + italic: bool = False, + font_file: str | None = None, ): """Fit text-frame text entirely within bounds of its shape. - Make the text in this text frame fit entirely within the bounds of - its shape by setting word wrap on and applying the "best-fit" font - size to all the text it contains. :attr:`TextFrame.auto_size` is set - to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not be set larger - than *max_size* points. If the path to a matching TrueType font is - provided as *font_file*, that font file will be used for the font - metrics. If *font_file* is |None|, best efforts are made to locate - a font file with matchhing *font_family*, *bold*, and *italic* - installed on the current system (usually succeeds if the font is - installed). + Make the text in this text frame fit entirely within the bounds of its shape by setting + word wrap on and applying the "best-fit" font size to all the text it contains. + + :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not + be set larger than `max_size` points. If the path to a matching TrueType font is provided + as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|, + best efforts are made to locate a font file with matchhing `font_family`, `bold`, and + `italic` installed on the current system (usually succeeds if the font is installed). """ # ---no-op when empty as fit behavior not defined for that case--- if self.text == "": return # pragma: no cover - font_size = self._best_fit_font_size( - font_family, max_size, bold, italic, font_file - ) + font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file) self._apply_fit(font_family, font_size, bold, italic) @property - def margin_bottom(self): - """ - |Length| value representing the inset of text from the bottom text - frame border. :meth:`pptx.util.Inches` provides a convenient way of - setting the value, e.g. ``text_frame.margin_bottom = Inches(0.05)``. + def margin_bottom(self) -> Length: + """|Length| value representing the inset of text from the bottom text frame border. + + :meth:`pptx.util.Inches` provides a convenient way of setting the value, e.g. + `text_frame.margin_bottom = Inches(0.05)`. """ return self._bodyPr.bIns @margin_bottom.setter - def margin_bottom(self, emu): + def margin_bottom(self, emu: Length): self._bodyPr.bIns = emu @property - def margin_left(self): - """ - Inset of text from left text frame border as |Length| value. - """ + def margin_left(self) -> Length: + """Inset of text from left text frame border as |Length| value.""" return self._bodyPr.lIns @margin_left.setter - def margin_left(self, emu): + def margin_left(self, emu: Length): self._bodyPr.lIns = emu @property - def margin_right(self): - """ - Inset of text from right text frame border as |Length| value. - """ + def margin_right(self) -> Length: + """Inset of text from right text frame border as |Length| value.""" return self._bodyPr.rIns @margin_right.setter - def margin_right(self, emu): + def margin_right(self, emu: Length): self._bodyPr.rIns = emu @property - def margin_top(self): - """ - Inset of text from top text frame border as |Length| value. - """ + def margin_top(self) -> Length: + """Inset of text from top text frame border as |Length| value.""" return self._bodyPr.tIns @margin_top.setter - def margin_top(self, emu): + def margin_top(self, emu: Length): self._bodyPr.tIns = emu @property - def paragraphs(self): - """ - Immutable sequence of |_Paragraph| instances corresponding to the - paragraphs in this text frame. A text frame always contains at least - one paragraph. + def paragraphs(self) -> tuple[_Paragraph, ...]: + """Sequence of paragraphs in this text frame. + + A text frame always contains at least one paragraph. """ return tuple([_Paragraph(p, self) for p in self._txBody.p_lst]) @property - def text(self): - """Unicode/str containing all text in this text-frame. + def text(self) -> str: + """All text in this text-frame as a single string. - Read/write. The return value is a str (unicode) containing all text in this - text-frame. A line-feed character (``"\\n"``) separates the text for each - paragraph. A vertical-tab character (``"\\v"``) appears for each line break - (aka. soft carriage-return) encountered. + Read/write. The return value contains all text in this text-frame. A line-feed character + (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears + for each line break (aka. soft carriage-return) encountered. - The vertical-tab character is how PowerPoint represents a soft carriage return - in clipboard text, which is why that encoding was chosen. + The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard + text, which is why that encoding was chosen. - Assignment replaces all text in the text frame. The assigned value can be - a 7-bit ASCII string, a UTF-8 encoded 8-bit string, or unicode. A bytes value - (such as a Python 2 ``str``) is converted to unicode assuming UTF-8 encoding. - A new paragraph is added for each line-feed character (``"\\n"``) encountered. - A line-break (soft carriage-return) is inserted for each vertical-tab character - (``"\\v"``) encountered. + Assignment replaces all text in the text frame. A new paragraph is added for each line-feed + character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each + vertical-tab character (`"\\v"`) encountered. - Any control character other than newline, tab, or vertical-tab are escaped as - plain-text like "_x001B_" (for ESC (ASCII 32) in this example). + Any control character other than newline, tab, or vertical-tab are escaped as plain-text + like "_x001B_" (for ESC (ASCII 32) in this example). """ return "\n".join(paragraph.text for paragraph in self.paragraphs) @text.setter - def text(self, text): + def text(self, text: str): txBody = self._txBody txBody.clear_content() - for p_text in to_unicode(text).split("\n"): + for p_text in text.split("\n"): p = txBody.add_p() p.append_text(p_text) @property - def vertical_anchor(self): - """ - Read/write member of :ref:`MsoVerticalAnchor` enumeration or |None|, - representing the vertical alignment of text in this text frame. - |None| indicates the effective value should be inherited from this - object's style hierarchy. + def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None: + """Represents the vertical alignment of text in this text frame. + + |None| indicates the effective value should be inherited from this object's style hierarchy. """ return self._txBody.bodyPr.anchor @vertical_anchor.setter - def vertical_anchor(self, value): + def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None): bodyPr = self._txBody.bodyPr bodyPr.anchor = value @property - def word_wrap(self): - """ - Read-write setting determining whether lines of text in this shape - are wrapped to fit within the shape's width. Valid values are True, - False, or None. True and False turn word wrap on and off, - respectively. Assigning None to word wrap causes any word wrap - setting to be removed from the text frame, causing it to inherit this - setting from its style hierarchy. + def word_wrap(self) -> bool | None: + """`True` when lines of text in this shape are wrapped to fit within the shape's width. + + Read-write. Valid values are True, False, or None. True and False turn word wrap on and + off, respectively. Assigning None to word wrap causes any word wrap setting to be removed + from the text frame, causing it to inherit this setting from its style hierarchy. """ return { ST_TextWrappingType.SQUARE: True, @@ -205,7 +205,7 @@ def word_wrap(self): }[self._txBody.bodyPr.wrap] @word_wrap.setter - def word_wrap(self, value): + def word_wrap(self, value: bool | None): if value not in (True, False, None): raise ValueError( # pragma: no cover "assigned value must be True, False, or None, got %s" % value @@ -216,7 +216,7 @@ def word_wrap(self, value): None: None, }[value] - def _apply_fit(self, font_family, font_size, is_bold, is_italic): + def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool): """Arrange text in this text frame to fit inside its extents. This is accomplished by setting auto size off, wrap on, and setting the font of @@ -226,49 +226,49 @@ def _apply_fit(self, font_family, font_size, is_bold, is_italic): self.word_wrap = True self._set_font(font_family, font_size, is_bold, is_italic) - def _best_fit_font_size(self, family, max_size, bold, italic, font_file): - """ - Return the largest integer point size not greater than *max_size* - that allows all the text in this text frame to fit inside its extents - when rendered using the font described by *family*, *bold*, and - *italic*. If *font_file* is specified, it is used to calculate the - fit, whether or not it matches *family*, *bold*, and *italic*. + def _best_fit_font_size( + self, family: str, max_size: int, bold: bool, italic: bool, font_file: str | None + ) -> int: + """Return font-size in points that best fits text in this text-frame. + + The best-fit font size is the largest integer point size not greater than `max_size` that + allows all the text in this text frame to fit inside its extents when rendered using the + font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used + to calculate the fit, whether or not it matches `family`, `bold`, and `italic`. """ if font_file is None: font_file = FontFiles.find(family, bold, italic) - return TextFitter.best_fit_font_size( - self.text, self._extents, max_size, font_file - ) + return TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file) @property def _bodyPr(self): return self._txBody.bodyPr @property - def _extents(self): - """ - A (cx, cy) 2-tuple representing the effective rendering area for text - within this text frame when margins are taken into account. + def _extents(self) -> tuple[Length, Length]: + """(cx, cy) 2-tuple representing the effective rendering area of this text-frame. + + Margins are taken into account. """ + parent = cast("ProvidesExtents", self._parent) return ( - self._parent.width - self.margin_left - self.margin_right, - self._parent.height - self.margin_top - self.margin_bottom, + Length(parent.width - self.margin_left - self.margin_right), + Length(parent.height - self.margin_top - self.margin_bottom), ) - def _set_font(self, family, size, bold, italic): - """ - Set the font properties of all the text in this text frame to - *family*, *size*, *bold*, and *italic*. - """ + def _set_font(self, family: str, size: int, bold: bool, italic: bool): + """Set the font properties of all the text in this text frame.""" - def iter_rPrs(txBody): + def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]: for p in txBody.p_lst: for elm in p.content_children: yield elm.get_or_add_rPr() # generate a:endParaRPr for each element yield p.get_or_add_endParaRPr() - def set_rPr_font(rPr, name, size, bold, italic): + def set_rPr_font( + rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool + ): f = Font(rPr) f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic @@ -278,70 +278,63 @@ def set_rPr_font(rPr, name, size, bold, italic): class Font(object): - """ - Character properties object, providing font size, font name, bold, - italic, etc. Corresponds to ```` child element of a run. Also - appears as ```` and ```` in paragraph and - ```` in list style elements. + """Character properties object, providing font size, font name, bold, italic, etc. + + Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and + `a:endParaRPr` in paragraph and `a:defRPr` in list style elements. """ - def __init__(self, rPr): + def __init__(self, rPr: CT_TextCharacterProperties): super(Font, self).__init__() self._element = self._rPr = rPr @property - def bold(self): - """ - Get or set boolean bold value of |Font|, e.g. - ``paragraph.font.bold = True``. If set to |None|, the bold setting is - cleared and is inherited from an enclosing shape's setting, or a - setting in a style or master. Returns None if no bold attribute is - present, meaning the effective bold value is inherited from a master - or the theme. + def bold(self) -> bool | None: + """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`. + + If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's + setting, or a setting in a style or master. Returns None if no bold attribute is present, + meaning the effective bold value is inherited from a master or the theme. """ return self._rPr.b @bold.setter - def bold(self, value): + def bold(self, value: bool | None): self._rPr.b = value @lazyproperty - def color(self): - """ - The |ColorFormat| instance that provides access to the color settings - for this font. - """ + def color(self) -> ColorFormat: + """The |ColorFormat| instance that provides access to the color settings for this font.""" if self.fill.type != MSO_FILL.SOLID: self.fill.solid() return self.fill.fore_color @lazyproperty - def fill(self): - """ - |FillFormat| instance for this font, providing access to fill - properties such as fill color. + def fill(self) -> FillFormat: + """|FillFormat| instance for this font. + + Provides access to fill properties such as fill color. """ return FillFormat.from_fill_parent(self._rPr) @property - def italic(self): - """ - Get or set boolean italic value of |Font| instance, with the same - behaviors as bold with respect to None values. + def italic(self) -> bool | None: + """Get or set boolean italic value of |Font| instance. + + Has the same behaviors as bold with respect to None values. """ return self._rPr.i @italic.setter - def italic(self, value): + def italic(self, value: bool | None): self._rPr.i = value @property - def language_id(self): - """ - Get or set the language id of this |Font| instance. The language id - is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None| - removes any language setting, the same behavior as assigning - `MSO_LANGUAGE_ID.NONE`. + def language_id(self) -> MSO_LANGUAGE_ID | None: + """Get or set the language id of this |Font| instance. + + The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None| + removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`. """ lang = self._rPr.lang if lang is None: @@ -349,19 +342,18 @@ def language_id(self): return self._rPr.lang @language_id.setter - def language_id(self, value): + def language_id(self, value: MSO_LANGUAGE_ID | None): if value == MSO_LANGUAGE_ID.NONE: value = None self._rPr.lang = value @property - def name(self): - """ - Get or set the typeface name for this |Font| instance, causing the - text it controls to appear in the named font, if a matching font is - found. Returns |None| if the typeface is currently inherited from the - theme. Setting it to |None| removes any override of the theme - typeface. + def name(self) -> str | None: + """Get or set the typeface name for this |Font| instance. + + Causes the text it controls to appear in the named font, if a matching font is found. + Returns |None| if the typeface is currently inherited from the theme. Setting it to |None| + removes any override of the theme typeface. """ latin = self._rPr.latin if latin is None: @@ -369,28 +361,26 @@ def name(self): return latin.typeface @name.setter - def name(self, value): + def name(self, value: str | None): if value is None: - self._rPr._remove_latin() + self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage] else: latin = self._rPr.get_or_add_latin() latin.typeface = value @property - def size(self): - """ - Read/write |Length| value or |None|, indicating the font height in - English Metric Units (EMU). |None| indicates the font size should be - inherited from its style hierarchy, such as a placeholder or document - defaults (usually 18pt). |Length| is a subclass of |int| having - properties for convenient conversion into points or other length - units. Likewise, the :class:`pptx.util.Pt` class allows convenient - specification of point values:: - - >> font.size = Pt(24) - >> font.size + def size(self) -> Length | None: + """Indicates the font height in English Metric Units (EMU). + + Read/write. |None| indicates the font size should be inherited from its style hierarchy, + such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int| + having properties for convenient conversion into points or other length units. Likewise, + the :class:`pptx.util.Pt` class allows convenient specification of point values:: + + >>> font.size = Pt(24) + >>> font.size 304800 - >> font.size.pt + >>> font.size.pt 24.0 """ sz = self._rPr.sz @@ -399,7 +389,7 @@ def size(self): return Centipoints(sz) @size.setter - def size(self, emu): + def size(self, emu: Length | None): if emu is None: self._rPr.sz = None else: @@ -407,16 +397,14 @@ def size(self, emu): self._rPr.sz = sz @property - def underline(self): - """ - Read/write. |True|, |False|, |None|, or a member of the - :ref:`MsoTextUnderlineType` enumeration indicating the underline - setting for this font. |None| is the default and indicates the - underline setting should be inherited from the style hierarchy, such - as from a placeholder. |True| indicates single underline. |False| - indicates no underline. Other settings such as double and wavy - underlining are indicated with members of the - :ref:`MsoTextUnderlineType` enumeration. + def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None: + """Indicaties the underline setting for this font. + + Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType` + enumeration. |None| is the default and indicates the underline setting should be inherited + from the style hierarchy, such as from a placeholder. |True| indicates single underline. + |False| indicates no underline. Other settings such as double and wavy underlining are + indicated with members of the :ref:`MsoTextUnderlineType` enumeration. """ u = self._rPr.u if u is MSO_UNDERLINE.NONE: @@ -426,7 +414,7 @@ def underline(self): return u @underline.setter - def underline(self, value): + def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None): if value is True: value = MSO_UNDERLINE.SINGLE_LINE elif value is False: @@ -435,51 +423,51 @@ def underline(self, value): class _Hyperlink(Subshape): - """ - Text run hyperlink object. Corresponds to ```` child - element of the run's properties element (````). + """Text run hyperlink object. + + Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`). """ - def __init__(self, rPr, parent): + def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart): super(_Hyperlink, self).__init__(parent) self._rPr = rPr @property - def address(self): - """ - Read/write. The URL of the hyperlink. URL can be on http, https, - mailto, or file scheme; others may work. + def address(self) -> str | None: + """The URL of the hyperlink. + + Read/write. URL can be on http, https, mailto, or file scheme; others may work. """ if self._hlinkClick is None: return None return self.part.target_ref(self._hlinkClick.rId) @address.setter - def address(self, url): + def address(self, url: str | None): # implements all three of add, change, and remove hyperlink if self._hlinkClick is not None: self._remove_hlinkClick() if url: self._add_hlinkClick(url) - def _add_hlinkClick(self, url): + def _add_hlinkClick(self, url: str): rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True) self._rPr.add_hlinkClick(rId) @property - def _hlinkClick(self): + def _hlinkClick(self) -> CT_Hyperlink | None: return self._rPr.hlinkClick def _remove_hlinkClick(self): assert self._hlinkClick is not None self.part.drop_rel(self._hlinkClick.rId) - self._rPr._remove_hlinkClick() + self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage] class _Paragraph(Subshape): """Paragraph object. Not intended to be constructed directly.""" - def __init__(self, p, parent): + def __init__(self, p: CT_TextParagraph, parent: ProvidesPart): super(_Paragraph, self).__init__(parent) self._element = self._p = p @@ -487,73 +475,67 @@ def add_line_break(self): """Add line break at end of this paragraph.""" self._p.add_br() - def add_run(self): - """ - Return a new run appended to the runs in this paragraph. - """ + def add_run(self) -> _Run: + """Return a new run appended to the runs in this paragraph.""" r = self._p.add_r() return _Run(r, self) @property - def alignment(self): - """ - Horizontal alignment of this paragraph, represented by either - a member of the enumeration :ref:`PpParagraphAlignment` or |None|. - The value |None| indicates the paragraph should 'inherit' its - effective value from its style hierarchy. Assigning |None| removes - any explicit setting, causing its inherited value to be used. + def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None: + """Horizontal alignment of this paragraph. + + The value |None| indicates the paragraph should 'inherit' its effective value from its + style hierarchy. Assigning |None| removes any explicit setting, causing its inherited + value to be used. """ return self._pPr.algn @alignment.setter - def alignment(self, value): + def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None): self._pPr.algn = value def clear(self): - """ - Remove all content from this paragraph. Paragraph properties are - preserved. Content includes runs, line breaks, and fields. + """Remove all content from this paragraph. + + Paragraph properties are preserved. Content includes runs, line breaks, and fields. """ for elm in self._element.content_children: self._element.remove(elm) return self @property - def font(self): - """ - |Font| object containing default character properties for the runs in - this paragraph. These character properties override default properties - inherited from parent objects such as the text frame the paragraph is - contained in and they may be overridden by character properties set at - the run level. + def font(self) -> Font: + """|Font| object containing default character properties for the runs in this paragraph. + + These character properties override default properties inherited from parent objects such + as the text frame the paragraph is contained in and they may be overridden by character + properties set at the run level. """ return Font(self._defRPr) @property - def level(self): - """ - Read-write integer indentation level of this paragraph, having a - range of 0-8 inclusive. 0 represents a top-level paragraph and is the - default value. Indentation level is most commonly encountered in a - bulleted list, as is found on a word bullet slide. + def level(self) -> int: + """Indentation level of this paragraph. + + Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the + default value. Indentation level is most commonly encountered in a bulleted list, as is + found on a word bullet slide. """ return self._pPr.lvl @level.setter - def level(self, level): + def level(self, level: int): self._pPr.lvl = level @property - def line_spacing(self): - """ - Numeric or |Length| value specifying the space between baselines in - successive lines of this paragraph. A value of |None| indicates no - explicit value is assigned and its effective value is inherited from - the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`, - indicates spacing is applied in multiples of line heights. A |Length| - value such as ``Pt(12)`` indicates spacing is a fixed height. The - |Pt| value class is a convenient way to apply line spacing in units - of points. + def line_spacing(self) -> int | float | Length | None: + """The space between baselines in successive lines of this paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`, + indicates spacing is applied in multiples of line heights. A |Length| value such as + `Pt(12)` indicates spacing is a fixed height. The |Pt| value class is a convenient way to + apply line spacing in units of points. """ pPr = self._p.pPr if pPr is None: @@ -561,27 +543,23 @@ def line_spacing(self): return pPr.line_spacing @line_spacing.setter - def line_spacing(self, value): + def line_spacing(self, value: int | float | Length | None): pPr = self._p.get_or_add_pPr() pPr.line_spacing = value @property - def runs(self): - """ - Immutable sequence of |_Run| objects corresponding to the runs in - this paragraph. - """ + def runs(self) -> tuple[_Run, ...]: + """Sequence of runs in this paragraph.""" return tuple(_Run(r, self) for r in self._element.r_lst) @property - def space_after(self): - """ - |Length| value specifying the spacing to appear between this - paragraph and the subsequent paragraph. A value of |None| indicates - no explicit value is assigned and its effective value is inherited - from the paragraph's style hierarchy. |Length| objects provide - convenience properties, such as ``.pt`` and ``.inches``, that allow - easy conversion to various length units. + def space_after(self) -> Length | None: + """The spacing to appear between this paragraph and the subsequent paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. |Length| objects provide convenience + properties, such as `.pt` and `.inches`, that allow easy conversion to various length + units. """ pPr = self._p.pPr if pPr is None: @@ -589,19 +567,17 @@ def space_after(self): return pPr.space_after @space_after.setter - def space_after(self, value): + def space_after(self, value: Length | None): pPr = self._p.get_or_add_pPr() pPr.space_after = value @property - def space_before(self): - """ - |Length| value specifying the spacing to appear between this - paragraph and the prior paragraph. A value of |None| indicates no - explicit value is assigned and its effective value is inherited from - the paragraph's style hierarchy. |Length| objects provide convenience - properties, such as ``.pt`` and ``.cm``, that allow easy conversion - to various length units. + def space_before(self) -> Length | None: + """The spacing to appear between this paragraph and the prior paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. |Length| objects provide convenience + properties, such as `.pt` and `.cm`, that allow easy conversion to various length units. """ pPr = self._p.pPr if pPr is None: @@ -609,88 +585,78 @@ def space_before(self): return pPr.space_before @space_before.setter - def space_before(self, value): + def space_before(self, value: Length | None): pPr = self._p.get_or_add_pPr() pPr.space_before = value @property - def text(self): - """str (unicode) representation of paragraph contents. - - Read/write. This value is formed by concatenating the text in each run and field - making up the paragraph, adding a vertical-tab character (``"\\v"``) for each - line-break element (``, soft carriage-return) encountered. + def text(self) -> str: + """Text of paragraph as a single string. - While the encoding of line-breaks as a vertical tab might be surprising at - first, doing so is consistent with PowerPoint's clipboard copy behavior and - allows a line-break to be distinguished from a paragraph boundary within the str - return value. + Read/write. This value is formed by concatenating the text in each run and field making up + the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element + (``, soft carriage-return) encountered. - Assignment causes all content in the paragraph to be replaced. Each vertical-tab - character (``"\\v"``) in the assigned str is translated to a line-break, as is - each line-feed character (``"\\n"``). Contrast behavior of line-feed character - in `TextFrame.text` setter. If line-feed characters are intended to produce new - paragraphs, use `TextFrame.text` instead. Any other control characters in the - assigned string are escaped as a hex representation like "_x001B_" (for ESC - (ASCII 27) in this example). + While the encoding of line-breaks as a vertical tab might be surprising at first, doing so + is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be + distinguished from a paragraph boundary within the str return value. - The assigned value can be a 7-bit ASCII byte string (Python 2 str), a UTF-8 - encoded 8-bit byte string (Python 2 str), or unicode. Bytes values are converted - to unicode assuming UTF-8 encoding. + Assignment causes all content in the paragraph to be replaced. Each vertical-tab character + (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed + character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter. + If line-feed characters are intended to produce new paragraphs, use `TextFrame.text` + instead. Any other control characters in the assigned string are escaped as a hex + representation like "_x001B_" (for ESC (ASCII 27) in this example). """ return "".join(elm.text for elm in self._element.content_children) @text.setter - def text(self, text): + def text(self, text: str): self.clear() - self._element.append_text(to_unicode(text)) + self._element.append_text(text) @property - def _defRPr(self): - """ - The |CT_TextCharacterProperties| instance ( element) that - defines the default run properties for runs in this paragraph. Causes - the element to be added if not present. + def _defRPr(self) -> CT_TextCharacterProperties: + """The element that defines the default run properties for runs in this paragraph. + + Causes the element to be added if not present. """ return self._pPr.get_or_add_defRPr() @property - def _pPr(self): - """ - The |CT_TextParagraphProperties| instance for this paragraph, the - element containing its paragraph properties. Causes the - element to be added if not present. + def _pPr(self) -> CT_TextParagraphProperties: + """Contains the properties for this paragraph. + + Causes the element to be added if not present. """ return self._p.get_or_add_pPr() class _Run(Subshape): - """Text run object. Corresponds to ```` child element in a paragraph.""" + """Text run object. Corresponds to `a:r` child element in a paragraph.""" - def __init__(self, r, parent): + def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart): super(_Run, self).__init__(parent) self._r = r @property def font(self): - """ - |Font| instance containing run-level character properties for the - text in this run. Character properties can be and perhaps most often - are inherited from parent objects such as the paragraph and slide - layout the run is contained in. Only those specifically overridden at - the run level are contained in the font object. + """|Font| instance containing run-level character properties for the text in this run. + + Character properties can be and perhaps most often are inherited from parent objects such + as the paragraph and slide layout the run is contained in. Only those specifically + overridden at the run level are contained in the font object. """ rPr = self._r.get_or_add_rPr() return Font(rPr) @lazyproperty - def hyperlink(self): - """ - |_Hyperlink| instance acting as proxy for any ```` - element under the run properties element. Created on demand, the - hyperlink object is available whether an ```` element - is present or not, and creates or deletes that element as appropriate - in response to actions on its methods and attributes. + def hyperlink(self) -> _Hyperlink: + """Proxy for any `a:hlinkClick` element under the run properties element. + + Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is + present or not, and creates or deletes that element as appropriate in response to actions + on its methods and attributes. """ rPr = self._r.get_or_add_rPr() return _Hyperlink(rPr, self) @@ -711,5 +677,5 @@ def text(self): return self._r.text @text.setter - def text(self, str): - self._r.text = to_unicode(str) + def text(self, text: str): + self._r.text = text diff --git a/src/pptx/types.py b/src/pptx/types.py new file mode 100644 index 000000000..46d86661b --- /dev/null +++ b/src/pptx/types.py @@ -0,0 +1,36 @@ +"""Abstract types used by `python-pptx`.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from typing_extensions import Protocol + +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.util import Length + + +class ProvidesExtents(Protocol): + """An object that has width and height.""" + + @property + def height(self) -> Length: + """Distance between top and bottom extents of shape in EMUs.""" + ... + + @property + def width(self) -> Length: + """Distance between left and right extents of shape in EMUs.""" + ... + + +class ProvidesPart(Protocol): + """An object that provides access to its XmlPart. + + This type is for objects that need access to their part, possibly because they need access to + the package or related parts. + """ + + @property + def part(self) -> XmlPart: ... diff --git a/src/pptx/util.py b/src/pptx/util.py index 5e5d92ecd..bbe8ac204 100644 --- a/src/pptx/util.py +++ b/src/pptx/util.py @@ -1,16 +1,15 @@ -# encoding: utf-8 - """Utility functions and classes.""" -from __future__ import division +from __future__ import annotations import functools +from typing import Any, Callable, Generic, TypeVar, cast class Length(int): - """ - Base class for length classes Inches, Emu, Cm, Mm, Pt, and Px. Provides - properties for converting length values to convenient units. + """Base class for length classes Inches, Emu, Cm, Mm, Pt, and Px. + + Provides properties for converting length values to convenient units. """ _EMUS_PER_INCH = 914400 @@ -19,149 +18,124 @@ class Length(int): _EMUS_PER_MM = 36000 _EMUS_PER_PT = 12700 - def __new__(cls, emu): + def __new__(cls, emu: int): return int.__new__(cls, emu) @property - def inches(self): - """ - Floating point length in inches - """ + def inches(self) -> float: + """Floating point length in inches.""" return self / float(self._EMUS_PER_INCH) @property - def centipoints(self): - """ - Integer length in hundredths of a point (1/7200 inch). Used - internally because PowerPoint stores font size in centipoints. + def centipoints(self) -> int: + """Integer length in hundredths of a point (1/7200 inch). + + Used internally because PowerPoint stores font size in centipoints. """ return self // self._EMUS_PER_CENTIPOINT @property - def cm(self): - """ - Floating point length in centimeters - """ + def cm(self) -> float: + """Floating point length in centimeters.""" return self / float(self._EMUS_PER_CM) @property - def emu(self): - """ - Integer length in English Metric Units - """ + def emu(self) -> int: + """Integer length in English Metric Units.""" return self @property - def mm(self): - """ - Floating point length in millimeters - """ + def mm(self) -> float: + """Floating point length in millimeters.""" return self / float(self._EMUS_PER_MM) @property - def pt(self): - """ - Floating point length in points - """ + def pt(self) -> float: + """Floating point length in points.""" return self / float(self._EMUS_PER_PT) class Inches(Length): - """ - Convenience constructor for length in inches - """ + """Convenience constructor for length in inches.""" - def __new__(cls, inches): + def __new__(cls, inches: float): emu = int(inches * Length._EMUS_PER_INCH) return Length.__new__(cls, emu) class Centipoints(Length): - """ - Convenience constructor for length in hundredths of a point - """ + """Convenience constructor for length in hundredths of a point.""" - def __new__(cls, centipoints): + def __new__(cls, centipoints: int): emu = int(centipoints * Length._EMUS_PER_CENTIPOINT) return Length.__new__(cls, emu) class Cm(Length): - """ - Convenience constructor for length in centimeters - """ + """Convenience constructor for length in centimeters.""" - def __new__(cls, cm): + def __new__(cls, cm: float): emu = int(cm * Length._EMUS_PER_CM) return Length.__new__(cls, emu) class Emu(Length): - """ - Convenience constructor for length in english metric units - """ + """Convenience constructor for length in english metric units.""" - def __new__(cls, emu): + def __new__(cls, emu: int): return Length.__new__(cls, int(emu)) class Mm(Length): - """ - Convenience constructor for length in millimeters - """ + """Convenience constructor for length in millimeters.""" - def __new__(cls, mm): + def __new__(cls, mm: float): emu = int(mm * Length._EMUS_PER_MM) return Length.__new__(cls, emu) class Pt(Length): - """ - Convenience value class for specifying a length in points - """ + """Convenience value class for specifying a length in points.""" - def __new__(cls, points): + def __new__(cls, points: float): emu = int(points * Length._EMUS_PER_PT) return Length.__new__(cls, emu) -class lazyproperty(object): +_T = TypeVar("_T") + + +class lazyproperty(Generic[_T]): """Decorator like @property, but evaluated only on first access. - Like @property, this can only be used to decorate methods having only - a `self` parameter, and is accessed like an attribute on an instance, - i.e. trailing parentheses are not used. Unlike @property, the decorated - method is only evaluated on first access; the resulting value is cached - and that same value returned on second and later access without - re-evaluation of the method. - - Like @property, this class produces a *data descriptor* object, which is - stored in the __dict__ of the *class* under the name of the decorated - method ('fget' nominally). The cached value is stored in the __dict__ of - the *instance* under that same name. - - Because it is a data descriptor (as opposed to a *non-data descriptor*), - its `__get__()` method is executed on each access of the decorated - attribute; the __dict__ item of the same name is "shadowed" by the - descriptor. - - While this may represent a performance improvement over a property, its - greater benefit may be its other characteristics. One common use is to - construct collaborator objects, removing that "real work" from the - constructor, while still only executing once. It also de-couples client - code from any sequencing considerations; if it's accessed from more than - one location, it's assured it will be ready whenever needed. + Like @property, this can only be used to decorate methods having only a `self` parameter, and + is accessed like an attribute on an instance, i.e. trailing parentheses are not used. Unlike + @property, the decorated method is only evaluated on first access; the resulting value is + cached and that same value returned on second and later access without re-evaluation of the + method. + + Like @property, this class produces a *data descriptor* object, which is stored in the __dict__ + of the *class* under the name of the decorated method ('fget' nominally). The cached value is + stored in the __dict__ of the *instance* under that same name. + + Because it is a data descriptor (as opposed to a *non-data descriptor*), its `__get__()` method + is executed on each access of the decorated attribute; the __dict__ item of the same name is + "shadowed" by the descriptor. + + While this may represent a performance improvement over a property, its greater benefit may be + its other characteristics. One common use is to construct collaborator objects, removing that + "real work" from the constructor, while still only executing once. It also de-couples client + code from any sequencing considerations; if it's accessed from more than one location, it's + assured it will be ready whenever needed. Loosely based on: https://stackoverflow.com/a/6849299/1902513. - A lazyproperty is read-only. There is no counterpart to the optional - "setter" (or deleter) behavior of an @property. This is critically - important to maintaining its immutability and idempotence guarantees. - Attempting to assign to a lazyproperty raises AttributeError + A lazyproperty is read-only. There is no counterpart to the optional "setter" (or deleter) + behavior of an @property. This is critically important to maintaining its immutability and + idempotence guarantees. Attempting to assign to a lazyproperty raises AttributeError unconditionally. - The parameter names in the methods below correspond to this usage - example:: + The parameter names in the methods below correspond to this usage example:: class Obj(object) @@ -171,68 +145,70 @@ def fget(self): obj = Obj() - Not suitable for wrapping a function (as opposed to a method) because it - is not callable. + Not suitable for wrapping a function (as opposed to a method) because it is not callable. """ - def __init__(self, fget): + def __init__(self, fget: Callable[..., _T]) -> None: """*fget* is the decorated method (a "getter" function). - A lazyproperty is read-only, so there is only an *fget* function (a - regular @property can also have an fset and fdel function). This name - was chosen for consistency with Python's `property` class which uses - this name for the corresponding parameter. + A lazyproperty is read-only, so there is only an *fget* function (a regular + @property can also have an fset and fdel function). This name was chosen for + consistency with Python's `property` class which uses this name for the + corresponding parameter. """ - # ---maintain a reference to the wrapped getter method + # --- maintain a reference to the wrapped getter method self._fget = fget - # ---adopt fget's __name__, __doc__, and other attributes - functools.update_wrapper(self, fget) + # --- and store the name of that decorated method + self._name = fget.__name__ + # --- adopt fget's __name__, __doc__, and other attributes + functools.update_wrapper(self, fget) # pyright: ignore - def __get__(self, obj, type=None): + def __get__(self, obj: Any, type: Any = None) -> _T: """Called on each access of 'fget' attribute on class or instance. - *self* is this instance of a lazyproperty descriptor "wrapping" the - property method it decorates (`fget`, nominally). + *self* is this instance of a lazyproperty descriptor "wrapping" the property + method it decorates (`fget`, nominally). - *obj* is the "host" object instance when the attribute is accessed - from an object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None - when accessed on the class, e.g. `Obj.fget`. + *obj* is the "host" object instance when the attribute is accessed from an + object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on + the class, e.g. `Obj.fget`. - *type* is the class hosting the decorated getter method (`fget`) on - both class and instance attribute access. + *type* is the class hosting the decorated getter method (`fget`) on both class + and instance attribute access. """ - # ---when accessed on class, e.g. Obj.fget, just return this - # ---descriptor instance (patched above to look like fget). + # --- when accessed on class, e.g. Obj.fget, just return this descriptor + # --- instance (patched above to look like fget). if obj is None: - return self + return self # type: ignore - # ---when accessed on instance, start by checking instance __dict__ - value = obj.__dict__.get(self.__name__) + # --- when accessed on instance, start by checking instance __dict__ for + # --- item with key matching the wrapped function's name + value = obj.__dict__.get(self._name) if value is None: - # ---on first access, __dict__ item will absent. Evaluate fget() - # ---and store that value in the (otherwise unused) host-object - # ---__dict__ value of same name ('fget' nominally) + # --- on first access, the __dict__ item will be absent. Evaluate fget() + # --- and store that value in the (otherwise unused) host-object + # --- __dict__ value of same name ('fget' nominally) value = self._fget(obj) - obj.__dict__[self.__name__] = value - return value + obj.__dict__[self._name] = value + return cast(_T, value) - def __set__(self, obj, value): + def __set__(self, obj: Any, value: Any) -> None: """Raises unconditionally, to preserve read-only behavior. - This decorator is intended to implement immutable (and idempotent) - object attributes. For that reason, assignment to this property must - be explicitly prevented. - - If this __set__ method was not present, this descriptor would become - a *non-data descriptor*. That would be nice because the cached value - would be accessed directly once set (__dict__ attrs have precedence - over non-data descriptors on instance attribute lookup). The problem - is, there would be nothing to stop assignment to the cached value, - which would overwrite the result of `fget()` and break both the - immutability and idempotence guarantees of this decorator. - - The performance with this __set__() method in place was roughly 0.4 - usec per access when measured on a 2.8GHz development machine; so - quite snappy and probably not a rich target for optimization efforts. + This decorator is intended to implement immutable (and idempotent) object + attributes. For that reason, assignment to this property must be explicitly + prevented. + + If this __set__ method was not present, this descriptor would become a + *non-data descriptor*. That would be nice because the cached value would be + accessed directly once set (__dict__ attrs have precedence over non-data + descriptors on instance attribute lookup). The problem is, there would be + nothing to stop assignment to the cached value, which would overwrite the result + of `fget()` and break both the immutability and idempotence guarantees of this + decorator. + + The performance with this __set__() method in place was roughly 0.4 usec per + access when measured on a 2.8GHz development machine; so quite snappy and + probably not a rich target for optimization efforts. """ - raise AttributeError("can't set attribute") # pragma: no cover + raise AttributeError("can't set attribute") diff --git a/tests/chart/test_axis.py b/tests/chart/test_axis.py index aa0ce302f..9dbb50f51 100644 --- a/tests/chart/test_axis.py +++ b/tests/chart/test_axis.py @@ -1,25 +1,29 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Unit-test suite for pptx.chart.axis module.""" +"""Unit-test suite for `pptx.chart.axis` module.""" + +from __future__ import annotations import pytest from pptx.chart.axis import ( AxisTitle, - _BaseAxis, CategoryAxis, DateAxis, MajorGridlines, TickLabels, ValueAxis, + _BaseAxis, ) from pptx.dml.chtfmt import ChartFormat from pptx.enum.chart import ( XL_AXIS_CROSSES, XL_CATEGORY_TYPE, - XL_TICK_LABEL_POSITION as XL_TICK_LBL_POS, XL_TICK_MARK, ) +from pptx.enum.chart import ( + XL_TICK_LABEL_POSITION as XL_TICK_LBL_POS, +) from pptx.text.text import Font from ..unitutil.cxml import element, xml @@ -116,9 +120,7 @@ def it_knows_whether_it_renders_in_reverse_order(self, reverse_order_get_fixture xAx, expected_value = reverse_order_get_fixture assert _BaseAxis(xAx).reverse_order == expected_value - def it_can_change_whether_it_renders_in_reverse_order( - self, reverse_order_set_fixture - ): + def it_can_change_whether_it_renders_in_reverse_order(self, reverse_order_set_fixture): xAx, new_value, expected_xml = reverse_order_set_fixture axis = _BaseAxis(xAx) @@ -655,9 +657,7 @@ def visible_set_fixture(self, request): @pytest.fixture def AxisTitle_(self, request, axis_title_): - return class_mock( - request, "pptx.chart.axis.AxisTitle", return_value=axis_title_ - ) + return class_mock(request, "pptx.chart.axis.AxisTitle", return_value=axis_title_) @pytest.fixture def axis_title_(self, request): @@ -673,9 +673,7 @@ def format_(self, request): @pytest.fixture def MajorGridlines_(self, request, major_gridlines_): - return class_mock( - request, "pptx.chart.axis.MajorGridlines", return_value=major_gridlines_ - ) + return class_mock(request, "pptx.chart.axis.MajorGridlines", return_value=major_gridlines_) @pytest.fixture def major_gridlines_(self, request): @@ -683,9 +681,7 @@ def major_gridlines_(self, request): @pytest.fixture def TickLabels_(self, request, tick_labels_): - return class_mock( - request, "pptx.chart.axis.TickLabels", return_value=tick_labels_ - ) + return class_mock(request, "pptx.chart.axis.TickLabels", return_value=tick_labels_) @pytest.fixture def tick_labels_(self, request): @@ -740,20 +736,17 @@ def has_tf_get_fixture(self, request): ( "c:title{a:b=c}", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx/c:strRef", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ("c:title/c:tx/c:rich", True, "c:title/c:tx/c:rich"), ("c:title", False, "c:title"), @@ -822,9 +815,7 @@ def it_provides_access_to_its_format(self, format_fixture): gridlines, expected_xml, ChartFormat_, format_ = format_fixture format = gridlines.format assert gridlines._xAx.xml == expected_xml - ChartFormat_.assert_called_once_with( - gridlines._xAx.xpath("c:majorGridlines")[0] - ) + ChartFormat_.assert_called_once_with(gridlines._xAx.xpath("c:majorGridlines")[0]) assert format is format_ # fixtures ------------------------------------------------------- @@ -873,9 +864,7 @@ def it_can_change_its_number_format(self, number_format_set_fixture): tick_labels.number_format = new_value assert tick_labels._element.xml == expected_xml - def it_knows_whether_its_number_format_is_linked( - self, number_format_is_linked_get_fixture - ): + def it_knows_whether_its_number_format_is_linked(self, number_format_is_linked_get_fixture): tick_labels, expected_value = number_format_is_linked_get_fixture assert tick_labels.number_format_is_linked is expected_value diff --git a/tests/chart/test_category.py b/tests/chart/test_category.py index 28bbeb096..9319d664b 100644 --- a/tests/chart/test_category.py +++ b/tests/chart/test_category.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.chart.category` module.""" -""" -Unit test suite for the pptx.chart.category module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -28,7 +24,12 @@ def it_supports_indexed_access(self, getitem_fixture): assert category is category_ def it_can_iterate_over_the_categories_it_contains(self, iter_fixture): - categories, expected_categories, Category_, calls, = iter_fixture + ( + categories, + expected_categories, + Category_, + calls, + ) = iter_fixture assert [c for c in categories] == expected_categories assert Category_.call_args_list == calls @@ -117,9 +118,7 @@ def iter_fixture(self, Category_, category_): calls = [call(None, 0), call(pt, 1)] return categories, expected_categories, Category_, calls - @pytest.fixture( - params=[("c:barChart", 0), ("c:barChart/c:ser/c:cat/c:ptCount{val=4}", 4)] - ) + @pytest.fixture(params=[("c:barChart", 0), ("c:barChart/c:ser/c:cat/c:ptCount{val=4}", 4)]) def len_fixture(self, request): xChart_cxml, expected_len = request.param categories = Categories(element(xChart_cxml)) @@ -147,9 +146,7 @@ def levels_fixture(self, request, CategoryLevel_, category_level_): @pytest.fixture def Category_(self, request, category_): - return class_mock( - request, "pptx.chart.category.Category", return_value=category_ - ) + return class_mock(request, "pptx.chart.category.Category", return_value=category_) @pytest.fixture def category_(self, request): @@ -245,9 +242,7 @@ def len_fixture(self, request): @pytest.fixture def Category_(self, request, category_): - return class_mock( - request, "pptx.chart.category.Category", return_value=category_ - ) + return class_mock(request, "pptx.chart.category.Category", return_value=category_) @pytest.fixture def category_(self, request): diff --git a/tests/chart/test_chart.py b/tests/chart/test_chart.py index 8b89c902a..667253347 100644 --- a/tests/chart/test_chart.py +++ b/tests/chart/test_chart.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.chart.chart` module.""" +from __future__ import annotations + import pytest from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis @@ -36,9 +38,7 @@ def it_provides_access_to_its_font(self, font_fixture, Font_, font_): font = chart.font assert chartSpace.xml == expected_xml - Font_.assert_called_once_with( - chartSpace.xpath("./c:txPr/a:p/a:pPr/a:defRPr")[0] - ) + Font_.assert_called_once_with(chartSpace.xpath("./c:txPr/a:p/a:pPr/a:defRPr")[0]) assert font is font_ def it_knows_whether_it_has_a_title(self, has_title_get_fixture): @@ -171,8 +171,7 @@ def cat_ax_raise_fixture(self): params=[ ( "c:chartSpace{a:b=c}", - "c:chartSpace{a:b=c}/c:txPr/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:chartSpace{a:b=c}/c:txPr/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ("c:chartSpace/c:txPr/a:p", "c:chartSpace/c:txPr/a:p/a:pPr/a:defRPr"), ( @@ -198,9 +197,7 @@ def has_legend_get_fixture(self, request): chart = Chart(element(chartSpace_cxml), None) return chart, expected_value - @pytest.fixture( - params=[("c:chartSpace/c:chart", True, "c:chartSpace/c:chart/c:legend")] - ) + @pytest.fixture(params=[("c:chartSpace/c:chart", True, "c:chartSpace/c:chart/c:legend")]) def has_legend_set_fixture(self, request): chartSpace_cxml, new_value, expected_chartSpace_cxml = request.param chart = Chart(element(chartSpace_cxml), None) @@ -285,9 +282,7 @@ def series_fixture(self, SeriesCollection_, series_collection_): chart = Chart(chartSpace, None) return chart, SeriesCollection_, plotArea, series_collection_ - @pytest.fixture( - params=[("c:chartSpace/c:style{val=42}", 42), ("c:chartSpace", None)] - ) + @pytest.fixture(params=[("c:chartSpace/c:style{val=42}", 42), ("c:chartSpace", None)]) def style_get_fixture(self, request): chartSpace_cxml, expected_value = request.param chart = Chart(element(chartSpace_cxml), None) @@ -341,9 +336,7 @@ def val_ax_raise_fixture(self): @pytest.fixture def CategoryAxis_(self, request, category_axis_): - return class_mock( - request, "pptx.chart.chart.CategoryAxis", return_value=category_axis_ - ) + return class_mock(request, "pptx.chart.chart.CategoryAxis", return_value=category_axis_) @pytest.fixture def category_axis_(self, request): @@ -355,9 +348,7 @@ def chart_data_(self, request): @pytest.fixture def ChartTitle_(self, request, chart_title_): - return class_mock( - request, "pptx.chart.chart.ChartTitle", return_value=chart_title_ - ) + return class_mock(request, "pptx.chart.chart.ChartTitle", return_value=chart_title_) @pytest.fixture def chart_title_(self, request): @@ -430,9 +421,7 @@ def series_rewriter_(self, request): @pytest.fixture def ValueAxis_(self, request, value_axis_): - return class_mock( - request, "pptx.chart.chart.ValueAxis", return_value=value_axis_ - ) + return class_mock(request, "pptx.chart.chart.ValueAxis", return_value=value_axis_) @pytest.fixture def value_axis_(self, request): @@ -497,20 +486,17 @@ def has_tf_get_fixture(self, request): ( "c:title{a:b=c}", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ( "c:title{a:b=c}/c:tx/c:strRef", True, - "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" - ")", + "c:title{a:b=c}/c:tx/c:rich/(a:bodyPr,a:lstStyle,a:p/a:pPr/a:defRPr" ")", ), ("c:title/c:tx/c:rich", True, "c:title/c:tx/c:rich"), ("c:title", False, "c:title"), @@ -594,9 +580,7 @@ def chart_(self, request): @pytest.fixture def PlotFactory_(self, request, plot_): - return function_mock( - request, "pptx.chart.chart.PlotFactory", return_value=plot_ - ) + return function_mock(request, "pptx.chart.chart.PlotFactory", return_value=plot_) @pytest.fixture def plot_(self, request): diff --git a/tests/chart/test_data.py b/tests/chart/test_data.py index 10b325bf9..9b6097020 100644 --- a/tests/chart/test_data.py +++ b/tests/chart/test_data.py @@ -1,19 +1,14 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.data module -""" +"""Test suite for `pptx.chart.data` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from datetime import date, datetime import pytest from pptx.chart.data import ( - _BaseChartData, - _BaseDataPoint, - _BaseSeriesData, BubbleChartData, BubbleDataPoint, BubbleSeriesData, @@ -26,11 +21,14 @@ XyChartData, XyDataPoint, XySeriesData, + _BaseChartData, + _BaseDataPoint, + _BaseSeriesData, ) from pptx.chart.xlsx import CategoryWorkbookWriter from pptx.enum.chart import XL_CHART_TYPE -from ..unitutil.mock import call, class_mock, instance_mock, property_mock +from ..unitutil.mock import Mock, call, class_mock, instance_mock, property_mock class DescribeChartData(object): @@ -39,12 +37,16 @@ def it_is_a_CategoryChartData_object(self): class Describe_BaseChartData(object): - def it_can_generate_chart_part_XML_for_its_data(self, xml_bytes_fixture): - chart_data, chart_type_, ChartXmlWriter_, expected_bytes = xml_bytes_fixture - xml_bytes = chart_data.xml_bytes(chart_type_) + """Unit-test suite for `pptx.chart.data._BaseChartData`.""" - ChartXmlWriter_.assert_called_once_with(chart_type_, chart_data) - assert xml_bytes == expected_bytes + def it_can_generate_chart_part_XML_for_its_data(self, ChartXmlWriter_: Mock): + ChartXmlWriter_.return_value.xml = "ƒøØßår" + chart_data = _BaseChartData() + + xml_bytes = chart_data.xml_bytes(XL_CHART_TYPE.PIE) + + ChartXmlWriter_.assert_called_once_with(XL_CHART_TYPE.PIE, chart_data) + assert xml_bytes == "ƒøØßår".encode("utf-8") def it_knows_its_number_format(self, number_format_fixture): chart_data, expected_value = number_format_fixture @@ -59,12 +61,6 @@ def number_format_fixture(self, request): chart_data = _BaseChartData(*argv) return chart_data, expected_value - @pytest.fixture - def xml_bytes_fixture(self, chart_type_, ChartXmlWriter_): - chart_data = _BaseChartData() - expected_bytes = "ƒøØßår".encode("utf-8") - return chart_data, chart_type_, ChartXmlWriter_, expected_bytes - # fixture components --------------------------------------------- @pytest.fixture @@ -73,10 +69,6 @@ def ChartXmlWriter_(self, request): ChartXmlWriter_.return_value.xml = "ƒøØßår" return ChartXmlWriter_ - @pytest.fixture - def chart_type_(self): - return XL_CHART_TYPE.PIE - class Describe_BaseSeriesData(object): def it_knows_its_name(self, name_fixture): diff --git a/tests/chart/test_datalabel.py b/tests/chart/test_datalabel.py index 19eddcc6f..ad02efc10 100644 --- a/tests/chart/test_datalabel.py +++ b/tests/chart/test_datalabel.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Unit test suite for the pptx.chart.datalabel module""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -279,9 +277,7 @@ def it_can_change_its_number_format(self, number_format_set_fixture): data_labels.number_format = new_value assert data_labels._element.xml == expected_xml - def it_knows_whether_its_number_format_is_linked( - self, number_format_is_linked_get_fixture - ): + def it_knows_whether_its_number_format_is_linked(self, number_format_is_linked_get_fixture): data_labels, expected_value = number_format_is_linked_get_fixture assert data_labels.number_format_is_linked is expected_value diff --git a/tests/chart/test_legend.py b/tests/chart/test_legend.py index 1624dc6d6..d77cd9f37 100644 --- a/tests/chart/test_legend.py +++ b/tests/chart/test_legend.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.chart.legend` module.""" -""" -Test suite for pptx.chart.legend module -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -32,15 +28,11 @@ def it_can_change_its_horizontal_offset(self, horz_offset_set_fixture): legend.horz_offset = new_value assert legend._element.xml == expected_xml - def it_knows_whether_it_should_overlap_the_chart( - self, include_in_layout_get_fixture - ): + def it_knows_whether_it_should_overlap_the_chart(self, include_in_layout_get_fixture): legend, expected_value = include_in_layout_get_fixture assert legend.include_in_layout == expected_value - def it_can_change_whether_it_overlaps_the_chart( - self, include_in_layout_set_fixture - ): + def it_can_change_whether_it_overlaps_the_chart(self, include_in_layout_set_fixture): legend, new_value, expected_xml = include_in_layout_set_fixture legend.include_in_layout = new_value assert legend._element.xml == expected_xml @@ -80,8 +72,7 @@ def font_fixture(self, request): ("c:legend/c:layout/c:manualLayout/c:xMode{val=factor}", 0.0), ("c:legend/c:layout/c:manualLayout/(c:xMode,c:x{val=0.42})", 0.42), ( - "c:legend/c:layout/c:manualLayout/(c:xMode{val=factor},c:x{val=0.42" - "})", + "c:legend/c:layout/c:manualLayout/(c:xMode{val=factor},c:x{val=0.42" "})", 0.42, ), ] diff --git a/tests/chart/test_marker.py b/tests/chart/test_marker.py index 4bbe22cb4..b9a8f3c5d 100644 --- a/tests/chart/test_marker.py +++ b/tests/chart/test_marker.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.chart.marker` module.""" -""" -Unit test suite for the pptx.chart.marker module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -131,9 +127,7 @@ def style_set_fixture(self, request): @pytest.fixture def ChartFormat_(self, request, chart_format_): - return class_mock( - request, "pptx.chart.marker.ChartFormat", return_value=chart_format_ - ) + return class_mock(request, "pptx.chart.marker.ChartFormat", return_value=chart_format_) @pytest.fixture def chart_format_(self, request): diff --git a/tests/chart/test_plot.py b/tests/chart/test_plot.py index 3a9e9f136..7e0f75e2d 100644 --- a/tests/chart/test_plot.py +++ b/tests/chart/test_plot.py @@ -1,19 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.plot module -""" +"""Unit-test suite for `pptx.chart.plot` module.""" -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest from pptx.chart.category import Categories from pptx.chart.chart import Chart from pptx.chart.plot import ( - _BasePlot, - AreaPlot, Area3DPlot, + AreaPlot, BarPlot, BubblePlot, DataLabels, @@ -24,6 +21,7 @@ PlotTypeInspector, RadarPlot, XyPlot, + _BasePlot, ) from pptx.chart.series import SeriesCollection from pptx.enum.chart import XL_CHART_TYPE as XL @@ -46,15 +44,11 @@ def it_can_change_whether_it_has_data_labels(self, has_data_labels_set_fixture): plot.has_data_labels = new_value assert plot._element.xml == expected_xml - def it_knows_whether_it_varies_color_by_category( - self, vary_by_categories_get_fixture - ): + def it_knows_whether_it_varies_color_by_category(self, vary_by_categories_get_fixture): plot, expected_value = vary_by_categories_get_fixture assert plot.vary_by_categories == expected_value - def it_can_change_whether_it_varies_color_by_category( - self, vary_by_categories_set_fixture - ): + def it_can_change_whether_it_varies_color_by_category(self, vary_by_categories_set_fixture): plot, new_value, expected_xml = vary_by_categories_set_fixture plot.vary_by_categories = new_value assert plot._element.xml == expected_xml @@ -176,9 +170,7 @@ def vary_by_categories_set_fixture(self, request): @pytest.fixture def Categories_(self, request, categories_): - return class_mock( - request, "pptx.chart.plot.Categories", return_value=categories_ - ) + return class_mock(request, "pptx.chart.plot.Categories", return_value=categories_) @pytest.fixture def categories_(self, request): @@ -190,9 +182,7 @@ def chart_(self, request): @pytest.fixture def DataLabels_(self, request, data_labels_): - return class_mock( - request, "pptx.chart.plot.DataLabels", return_value=data_labels_ - ) + return class_mock(request, "pptx.chart.plot.DataLabels", return_value=data_labels_) @pytest.fixture def data_labels_(self, request): @@ -430,21 +420,18 @@ def it_can_determine_the_chart_type_of_a_plot(self, chart_type_fixture): ("c:lineChart/c:grouping{val=percentStacked}", XL.LINE_MARKERS_STACKED_100), ("c:lineChart/c:ser/c:marker/c:symbol{val=none}", XL.LINE), ( - "c:lineChart/(c:grouping{val=stacked},c:ser/c:marker/c:symbol{val=n" - "one})", + "c:lineChart/(c:grouping{val=stacked},c:ser/c:marker/c:symbol{val=n" "one})", XL.LINE_STACKED, ), ( - "c:lineChart/(c:grouping{val=percentStacked},c:ser/c:marker/c:symbo" - "l{val=none})", + "c:lineChart/(c:grouping{val=percentStacked},c:ser/c:marker/c:symbo" "l{val=none})", XL.LINE_STACKED_100, ), ("c:pieChart", XL.PIE), ("c:pieChart/c:ser/c:explosion{val=25}", XL.PIE_EXPLODED), ("c:scatterChart/c:scatterStyle", XL.XY_SCATTER), ( - "c:scatterChart/(c:scatterStyle{val=lineMarker},c:ser/c:spPr/a:ln/a" - ":noFill)", + "c:scatterChart/(c:scatterStyle{val=lineMarker},c:ser/c:spPr/a:ln/a" ":noFill)", XL.XY_SCATTER, ), ("c:scatterChart/c:scatterStyle{val=lineMarker}", XL.XY_SCATTER_LINES), @@ -473,8 +460,7 @@ def it_can_determine_the_chart_type_of_a_plot(self, chart_type_fixture): ("c:radarChart/c:radarStyle{val=marker}", XL.RADAR_MARKERS), ("c:radarChart/c:radarStyle{val=filled}", XL.RADAR_FILLED), ( - "c:radarChart/(c:radarStyle{val=marker},c:ser/c:marker/c:symbol{val" - "=none})", + "c:radarChart/(c:radarStyle{val=marker},c:ser/c:marker/c:symbol{val" "=none})", XL.RADAR, ), ] diff --git a/tests/chart/test_point.py b/tests/chart/test_point.py index cba2eb0bc..8e00d9675 100644 --- a/tests/chart/test_point.py +++ b/tests/chart/test_point.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.chart.point` module.""" -""" -Unit test suite for the pptx.chart.point module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -166,9 +162,7 @@ def marker_fixture(self, Marker_, marker_): @pytest.fixture def ChartFormat_(self, request, chart_format_): - return class_mock( - request, "pptx.chart.point.ChartFormat", return_value=chart_format_ - ) + return class_mock(request, "pptx.chart.point.ChartFormat", return_value=chart_format_) @pytest.fixture def chart_format_(self, request): @@ -176,9 +170,7 @@ def chart_format_(self, request): @pytest.fixture def DataLabel_(self, request, data_label_): - return class_mock( - request, "pptx.chart.point.DataLabel", return_value=data_label_ - ) + return class_mock(request, "pptx.chart.point.DataLabel", return_value=data_label_) @pytest.fixture def data_label_(self, request): diff --git a/tests/chart/test_series.py b/tests/chart/test_series.py index 35fa2425d..9a60351e1 100644 --- a/tests/chart/test_series.py +++ b/tests/chart/test_series.py @@ -1,8 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.chart.series module.""" +"""Unit-test suite for `pptx.chart.series` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -12,16 +12,16 @@ from pptx.chart.series import ( AreaSeries, BarSeries, - _BaseCategorySeries, - _BaseSeries, BubbleSeries, LineSeries, - _MarkerMixin, PieSeries, RadarSeries, SeriesCollection, - _SeriesFactory, XySeries, + _BaseCategorySeries, + _BaseSeries, + _MarkerMixin, + _SeriesFactory, ) from pptx.dml.chtfmt import ChartFormat @@ -73,9 +73,7 @@ def name_fixture(self, request): @pytest.fixture def ChartFormat_(self, request, chart_format_): - return class_mock( - request, "pptx.chart.series.ChartFormat", return_value=chart_format_ - ) + return class_mock(request, "pptx.chart.series.ChartFormat", return_value=chart_format_) @pytest.fixture def chart_format_(self, request): @@ -87,9 +85,7 @@ def it_is_a_BaseSeries_subclass(self, subclass_fixture): base_category_series = subclass_fixture assert isinstance(base_category_series, _BaseSeries) - def it_provides_access_to_its_data_labels( - self, data_labels_fixture, DataLabels_, data_labels_ - ): + def it_provides_access_to_its_data_labels(self, data_labels_fixture, DataLabels_, data_labels_): ser, expected_dLbls_xml = data_labels_fixture DataLabels_.return_value = data_labels_ series = _BaseCategorySeries(ser) @@ -148,8 +144,7 @@ def subclass_fixture(self): ("c:ser/c:val/c:numRef/c:numCache", ()), ("c:ser/c:val/c:numRef/c:numCache/c:ptCount{val=0}", ()), ( - 'c:ser/c:val/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v"' - '1.1")', + 'c:ser/c:val/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v"' '1.1")', (1.1,), ), ( @@ -178,9 +173,7 @@ def values_get_fixture(self, request): @pytest.fixture def CategoryPoints_(self, request, points_): - return class_mock( - request, "pptx.chart.series.CategoryPoints", return_value=points_ - ) + return class_mock(request, "pptx.chart.series.CategoryPoints", return_value=points_) @pytest.fixture def DataLabels_(self, request): @@ -238,15 +231,11 @@ def it_is_a_BaseCategorySeries_subclass(self, subclass_fixture): bar_series = subclass_fixture assert isinstance(bar_series, _BaseCategorySeries) - def it_knows_whether_it_should_invert_if_negative( - self, invert_if_negative_get_fixture - ): + def it_knows_whether_it_should_invert_if_negative(self, invert_if_negative_get_fixture): bar_series, expected_value = invert_if_negative_get_fixture assert bar_series.invert_if_negative == expected_value - def it_can_change_whether_it_inverts_if_negative( - self, invert_if_negative_set_fixture - ): + def it_can_change_whether_it_inverts_if_negative(self, invert_if_negative_set_fixture): bar_series, new_value, expected_xml = invert_if_negative_set_fixture bar_series.invert_if_negative = new_value assert bar_series._element.xml == expected_xml @@ -312,9 +301,7 @@ def points_fixture(self, BubblePoints_, points_): @pytest.fixture def BubblePoints_(self, request, points_): - return class_mock( - request, "pptx.chart.series.BubblePoints", return_value=points_ - ) + return class_mock(request, "pptx.chart.series.BubblePoints", return_value=points_) @pytest.fixture def points_(self, request): @@ -433,8 +420,7 @@ def subclass_fixture(self): ("c:ser/c:yVal/c:numRef", ()), ("c:ser/c:val/c:numRef/c:numCache", ()), ( - "c:ser/c:yVal/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v" - '"1.1")', + "c:ser/c:yVal/c:numRef/c:numCache/(c:ptCount{val=1},c:pt{idx=0}/c:v" '"1.1")', (1.1,), ), ( @@ -483,8 +469,7 @@ def it_supports_len(self, len_fixture): params=[ ("c:barChart/c:ser/c:order{val=42}", 0, 0), ( - "c:barChart/(c:ser/c:order{val=9},c:ser/c:order{val=6},c:ser/c:orde" - "r{val=3})", + "c:barChart/(c:ser/c:order{val=9},c:ser/c:order{val=6},c:ser/c:orde" "r{val=3})", 2, 0, ), @@ -509,8 +494,7 @@ def getitem_fixture(self, request, _SeriesFactory_, series_): ("c:barChart", 0), ("c:barChart/c:ser/c:order{val=4}", 1), ( - "c:barChart/(c:ser/c:order{val=4},c:ser/c:order{val=1},c:ser/c:orde" - "r{val=6})", + "c:barChart/(c:ser/c:order{val=4},c:ser/c:order{val=1},c:ser/c:orde" "r{val=6})", 3, ), ("c:plotArea/c:barChart", 0), @@ -531,9 +515,7 @@ def len_fixture(self, request): @pytest.fixture def _SeriesFactory_(self, request, series_): - return function_mock( - request, "pptx.chart.series._SeriesFactory", return_value=series_ - ) + return function_mock(request, "pptx.chart.series._SeriesFactory", return_value=series_) @pytest.fixture def series_(self, request): diff --git a/tests/chart/test_xlsx.py b/tests/chart/test_xlsx.py index ec96c9e02..dde9d4d53 100644 --- a/tests/chart/test_xlsx.py +++ b/tests/chart/test_xlsx.py @@ -1,9 +1,12 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.chart.xlsx` module.""" -import pytest +from __future__ import annotations + +import io +import pytest from xlsxwriter import Workbook from xlsxwriter.worksheet import Worksheet @@ -15,12 +18,11 @@ XyChartData, ) from pptx.chart.xlsx import ( - _BaseWorkbookWriter, BubbleWorkbookWriter, CategoryWorkbookWriter, XyWorkbookWriter, + _BaseWorkbookWriter, ) -from pptx.compat import BytesIO from ..unitutil.mock import ANY, call, class_mock, instance_mock, method_mock @@ -31,9 +33,7 @@ class Describe_BaseWorkbookWriter(object): def it_can_generate_a_chart_data_Excel_blob( self, request, xlsx_file_, workbook_, worksheet_, BytesIO_ ): - _populate_worksheet_ = method_mock( - request, _BaseWorkbookWriter, "_populate_worksheet" - ) + _populate_worksheet_ = method_mock(request, _BaseWorkbookWriter, "_populate_worksheet") _open_worksheet_ = method_mock(request, _BaseWorkbookWriter, "_open_worksheet") # --- to make context manager behavior work --- _open_worksheet_.return_value.__enter__.return_value = (workbook_, worksheet_) @@ -44,9 +44,7 @@ def it_can_generate_a_chart_data_Excel_blob( xlsx_blob = workbook_writer.xlsx_blob _open_worksheet_.assert_called_once_with(workbook_writer, xlsx_file_) - _populate_worksheet_.assert_called_once_with( - workbook_writer, workbook_, worksheet_ - ) + _populate_worksheet_.assert_called_once_with(workbook_writer, workbook_, worksheet_) assert xlsx_blob == b"xlsx-blob" def it_can_open_a_worksheet_in_a_context(self, open_fixture): @@ -81,7 +79,7 @@ def populate_fixture(self): @pytest.fixture def BytesIO_(self, request): - return class_mock(request, "pptx.chart.xlsx.BytesIO") + return class_mock(request, "pptx.chart.xlsx.io.BytesIO") @pytest.fixture def Workbook_(self, request, workbook_): @@ -97,7 +95,7 @@ def worksheet_(self, request): @pytest.fixture def xlsx_file_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) class DescribeCategoryWorkbookWriter(object): @@ -207,9 +205,7 @@ def col_ref_fixture(self, request): return column_number, expected_value @pytest.fixture - def populate_fixture( - self, workbook_, worksheet_, _write_categories_, _write_series_ - ): + def populate_fixture(self, workbook_, worksheet_, _write_categories_, _write_series_): workbook_writer = CategoryWorkbookWriter(None) return workbook_writer, workbook_, worksheet_ @@ -293,9 +289,7 @@ def write_cats_fixture( return workbook_writer, workbook_, worksheet_, number_format, calls @pytest.fixture - def write_sers_fixture( - self, request, chart_data_, workbook_, worksheet_, categories_ - ): + def write_sers_fixture(self, request, chart_data_, workbook_, worksheet_, categories_): workbook_writer = CategoryWorkbookWriter(chart_data_) num_format = workbook_.add_format.return_value calls = [call.write(0, 1, "S1"), call.write_column(1, 1, (42, 24), num_format)] @@ -330,21 +324,15 @@ def worksheet_(self, request): @pytest.fixture def _write_cat_column_(self, request): - return method_mock( - request, CategoryWorkbookWriter, "_write_cat_column", autospec=True - ) + return method_mock(request, CategoryWorkbookWriter, "_write_cat_column", autospec=True) @pytest.fixture def _write_categories_(self, request): - return method_mock( - request, CategoryWorkbookWriter, "_write_categories", autospec=True - ) + return method_mock(request, CategoryWorkbookWriter, "_write_categories", autospec=True) @pytest.fixture def _write_series_(self, request): - return method_mock( - request, CategoryWorkbookWriter, "_write_series", autospec=True - ) + return method_mock(request, CategoryWorkbookWriter, "_write_series", autospec=True) class DescribeBubbleWorkbookWriter(object): diff --git a/tests/chart/test_xmlwriter.py b/tests/chart/test_xmlwriter.py index 19e7e6473..bb7354983 100644 --- a/tests/chart/test_xmlwriter.py +++ b/tests/chart/test_xmlwriter.py @@ -1,10 +1,8 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.chart.xmlwriter module -""" +"""Unit-test suite for `pptx.chart.xmlwriter` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from datetime import date from itertools import islice @@ -12,14 +10,16 @@ import pytest from pptx.chart.data import ( - _BaseChartData, - _BaseSeriesData, BubbleChartData, CategoryChartData, CategorySeriesData, XyChartData, + _BaseChartData, + _BaseSeriesData, ) from pptx.chart.xmlwriter import ( + ChartXmlWriter, + SeriesXmlRewriterFactory, _AreaChartXmlWriter, _BarChartXmlWriter, _BaseSeriesXmlRewriter, @@ -28,12 +28,10 @@ _BubbleSeriesXmlWriter, _CategorySeriesXmlRewriter, _CategorySeriesXmlWriter, - ChartXmlWriter, _DoughnutChartXmlWriter, _LineChartXmlWriter, _PieChartXmlWriter, _RadarChartXmlWriter, - SeriesXmlRewriterFactory, _XyChartXmlWriter, _XySeriesXmlRewriter, _XySeriesXmlWriter, @@ -292,9 +290,7 @@ class Describe_PieChartXmlWriter(object): ("PIE_EXPLODED", 3, 1, "3x1-pie-exploded"), ), ) - def it_can_generate_xml_for_a_pie_chart( - self, enum_member, cat_count, ser_count, snippet_name - ): + def it_can_generate_xml_for_a_pie_chart(self, enum_member, cat_count, ser_count, snippet_name): chart_type = getattr(XL_CHART_TYPE, enum_member) chart_data = make_category_chart_data(cat_count, str, ser_count) xml_writer = _PieChartXmlWriter(chart_type, chart_data) @@ -306,9 +302,7 @@ class Describe_RadarChartXmlWriter(object): """Unit-test suite for `pptx.chart.xmlwriter._RadarChartXmlWriter`.""" def it_can_generate_xml_for_a_radar_chart(self): - series_data_seq = make_category_chart_data( - cat_count=5, cat_type=str, ser_count=2 - ) + series_data_seq = make_category_chart_data(cat_count=5, cat_type=str, ser_count=2) xml_writer = _RadarChartXmlWriter(XL_CHART_TYPE.RADAR, series_data_seq) assert xml_writer.xml == snippet_text("2x5-radar") @@ -456,9 +450,7 @@ class Describe_BaseSeriesXmlRewriter(object): def it_can_replace_series_data(self, replace_fixture): rewriter, chartSpace, plotArea, ser_count, calls = replace_fixture rewriter.replace_series_data(chartSpace) - rewriter._adjust_ser_count.assert_called_once_with( - rewriter, plotArea, ser_count - ) + rewriter._adjust_ser_count.assert_called_once_with(rewriter, plotArea, ser_count) assert rewriter._rewrite_ser_data.call_args_list == calls def it_adjusts_the_ser_count_to_help(self, adjust_fixture): @@ -519,9 +511,7 @@ def clone_fixture(self, request): return rewriter, plotArea, count, expected_xml @pytest.fixture - def replace_fixture( - self, request, chart_data_, _adjust_ser_count_, _rewrite_ser_data_ - ): + def replace_fixture(self, request, chart_data_, _adjust_ser_count_, _rewrite_ser_data_): rewriter = _BaseSeriesXmlRewriter(chart_data_) chartSpace = element( "c:chartSpace/c:chart/c:plotArea/c:barChart/(c:ser/c:order{val=0" @@ -572,15 +562,11 @@ def trim_fixture(self, request): @pytest.fixture def _add_cloned_sers_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_add_cloned_sers", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_add_cloned_sers", autospec=True) @pytest.fixture def _adjust_ser_count_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_adjust_ser_count", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_adjust_ser_count", autospec=True) @pytest.fixture def chart_data_(self, request): @@ -588,15 +574,11 @@ def chart_data_(self, request): @pytest.fixture def _rewrite_ser_data_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_rewrite_ser_data", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_rewrite_ser_data", autospec=True) @pytest.fixture def _trim_ser_count_by_(self, request): - return method_mock( - request, _BaseSeriesXmlRewriter, "_trim_ser_count_by", autospec=True - ) + return method_mock(request, _BaseSeriesXmlRewriter, "_trim_ser_count_by", autospec=True) class Describe_BubbleSeriesXmlRewriter(object): diff --git a/tests/dml/test_chtfmt.py b/tests/dml/test_chtfmt.py index 42b90f498..f87752180 100644 --- a/tests/dml/test_chtfmt.py +++ b/tests/dml/test_chtfmt.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.dml.chtfmt` module.""" -""" -Unit test suite for the pptx.dml.chtfmt module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index f0c536340..95a1f7c5d 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.text` module.""" -""" -Test suite for pptx.text module. -""" - -from __future__ import absolute_import +from __future__ import annotations import pytest @@ -182,9 +178,7 @@ def set_brightness_fixture_(self, request): "-0.3 to -0.4": (an_srgbClr, 70000, None, -0.4, 60000, None), "-0.4 to 0": (a_sysClr, 60000, None, 0, None, None), } - xClr_bldr_fn, mod_in, off_in, brightness, mod_out, off_out = mapping[ - request.param - ] + xClr_bldr_fn, mod_in, off_in, brightness, mod_out, off_out = mapping[request.param] xClr_bldr = xClr_bldr_fn() if mod_in is not None: @@ -222,10 +216,7 @@ def set_rgb_fixture_(self, request): color_format = ColorFormat.from_colorchoice_parent(solidFill) rgb_color = RGBColor(0x12, 0x34, 0x56) expected_xml = ( - a_solidFill() - .with_nsdecls() - .with_child(an_srgbClr().with_val("123456")) - .xml() + a_solidFill().with_nsdecls().with_child(an_srgbClr().with_val("123456")).xml() ) return color_format, rgb_color, expected_xml @@ -248,10 +239,7 @@ def set_theme_color_fixture_(self, request): color_format = ColorFormat.from_colorchoice_parent(solidFill) theme_color = MSO_THEME_COLOR.ACCENT_6 expected_xml = ( - a_solidFill() - .with_nsdecls() - .with_child(a_schemeClr().with_val("accent6")) - .xml() + a_solidFill().with_nsdecls().with_child(a_schemeClr().with_val("accent6")).xml() ) return color_format, theme_color, expected_xml diff --git a/tests/dml/test_effect.py b/tests/dml/test_effect.py index 53e2106de..1907e561d 100644 --- a/tests/dml/test_effect.py +++ b/tests/dml/test_effect.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.dml.effect` module.""" -"""Test suite for pptx.dml.effect module.""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/dml/test_fill.py b/tests/dml/test_fill.py index 2c2af4e03..defbaf980 100644 --- a/tests/dml/test_fill.py +++ b/tests/dml/test_fill.py @@ -1,14 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.dml.fill` module.""" +from __future__ import annotations + import pytest from pptx.dml.color import ColorFormat from pptx.dml.fill import ( + FillFormat, _BlipFill, _Fill, - FillFormat, _GradFill, _GradientStop, _GradientStops, @@ -94,9 +96,7 @@ def it_can_change_the_angle_of_a_linear_gradient(self, grad_fill_, type_prop_): assert grad_fill_.gradient_angle == 42.24 - def it_provides_access_to_the_gradient_stops( - self, type_prop_, grad_fill_, gradient_stops_ - ): + def it_provides_access_to_the_gradient_stops(self, type_prop_, grad_fill_, gradient_stops_): type_prop_.return_value = MSO_FILL.GRADIENT grad_fill_.gradient_stops = gradient_stops_ fill = FillFormat(None, grad_fill_) @@ -618,9 +618,7 @@ def pattern_set_fixture(self, request): @pytest.fixture def ColorFormat_from_colorchoice_parent_(self, request): - return method_mock( - request, ColorFormat, "from_colorchoice_parent", autospec=False - ) + return method_mock(request, ColorFormat, "from_colorchoice_parent", autospec=False) @pytest.fixture def color_(self, request): @@ -662,9 +660,7 @@ def fore_color_fixture(self, ColorFormat_from_colorchoice_parent_, color_): @pytest.fixture def ColorFormat_from_colorchoice_parent_(self, request): - return method_mock( - request, ColorFormat, "from_colorchoice_parent", autospec=False - ) + return method_mock(request, ColorFormat, "from_colorchoice_parent", autospec=False) @pytest.fixture def color_(self, request): diff --git a/tests/dml/test_line.py b/tests/dml/test_line.py index b33e6e094..158e55589 100644 --- a/tests/dml/test_line.py +++ b/tests/dml/test_line.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Test suite for `pptx.dml.line` module.""" -""" -Test suite for pptx.dml.line module -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations import pytest @@ -25,10 +21,39 @@ def it_knows_its_dash_style(self, dash_style_get_fixture): line, expected_value = dash_style_get_fixture assert line.dash_style == expected_value - def it_can_change_its_dash_style(self, dash_style_set_fixture): - line, dash_style, spPr, expected_xml = dash_style_set_fixture + @pytest.mark.parametrize( + ("spPr_cxml", "dash_style", "expected_cxml"), + [ + ("p:spPr{a:b=c}", MSO_LINE.DASH, "p:spPr{a:b=c}/a:ln/a:prstDash{val=dash}"), + ("p:spPr/a:ln", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=sysDot}"), + ( + "p:spPr/a:ln/a:prstDash", + MSO_LINE.SOLID, + "p:spPr/a:ln/a:prstDash{val=solid}", + ), + ( + "p:spPr/a:ln/a:custDash", + MSO_LINE.DASH_DOT, + "p:spPr/a:ln/a:prstDash{val=dashDot}", + ), + ( + "p:spPr/a:ln/a:prstDash{val=dash}", + MSO_LINE.LONG_DASH, + "p:spPr/a:ln/a:prstDash{val=lgDash}", + ), + ("p:spPr/a:ln/a:prstDash{val=dash}", None, "p:spPr/a:ln"), + ("p:spPr/a:ln/a:custDash", None, "p:spPr/a:ln"), + ], + ) + def it_can_change_its_dash_style( + self, spPr_cxml: str, dash_style: MSO_LINE, expected_cxml: str + ): + spPr = element(spPr_cxml) + line = LineFormat(spPr) + line.dash_style = dash_style - assert spPr.xml == expected_xml + + assert spPr.xml == xml(expected_cxml) def it_knows_its_width(self, width_get_fixture): line, expected_line_width = width_get_fixture @@ -75,36 +100,6 @@ def dash_style_get_fixture(self, request): line = LineFormat(spPr) return line, expected_value - @pytest.fixture( - params=[ - ("p:spPr{a:b=c}", MSO_LINE.DASH, "p:spPr{a:b=c}/a:ln/a:prstDash{val=dash}"), - ("p:spPr/a:ln", MSO_LINE.ROUND_DOT, "p:spPr/a:ln/a:prstDash{val=sysDot}"), - ( - "p:spPr/a:ln/a:prstDash", - MSO_LINE.SOLID, - "p:spPr/a:ln/a:prstDash{val=solid}", - ), - ( - "p:spPr/a:ln/a:custDash", - MSO_LINE.DASH_DOT, - "p:spPr/a:ln/a:prstDash{val=dashDot}", - ), - ( - "p:spPr/a:ln/a:prstDash{val=dash}", - MSO_LINE.LONG_DASH, - "p:spPr/a:ln/a:prstDash{val=lgDash}", - ), - ("p:spPr/a:ln/a:prstDash{val=dash}", None, "p:spPr/a:ln"), - ("p:spPr/a:ln/a:custDash", None, "p:spPr/a:ln"), - ] - ) - def dash_style_set_fixture(self, request): - spPr_cxml, dash_style, expected_cxml = request.param - spPr = element(spPr_cxml) - line = LineFormat(spPr) - expected_xml = xml(expected_cxml) - return line, dash_style, spPr, expected_xml - @pytest.fixture def fill_fixture(self, line, FillFormat_, ln_, fill_): return line, FillFormat_, ln_, fill_ diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index f68c6857a..5dee9408b 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -1,8 +1,8 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.opc.oxml` module.""" -from __future__ import unicode_literals +from __future__ import annotations + +from typing import cast import pytest @@ -13,141 +13,179 @@ CT_Relationship, CT_Relationships, CT_Types, + nsmap, oxml_tostring, serialize_part_xml, ) +from pptx.opc.packuri import PackURI from pptx.oxml import parse_xml +from pptx.oxml.xmlchemy import BaseOxmlElement -from .unitdata.rels import ( - a_Default, - an_Override, - a_Relationship, - a_Relationships, - a_Types, -) +from ..unitutil.cxml import element -class DescribeCT_Default(object): +class DescribeCT_Default: """Unit-test suite for `pptx.opc.oxml.CT_Default` objects.""" def it_provides_read_access_to_xml_values(self): - default = a_Default().element + default = cast(CT_Default, element("ct:Default{Extension=xml,ContentType=application/xml}")) assert default.extension == "xml" assert default.contentType == "application/xml" -class DescribeCT_Override(object): +class DescribeCT_Override: """Unit-test suite for `pptx.opc.oxml.CT_Override` objects.""" def it_provides_read_access_to_xml_values(self): - override = an_Override().element + override = cast( + CT_Override, element("ct:Override{PartName=/part/name.xml,ContentType=text/plain}") + ) assert override.partName == "/part/name.xml" - assert override.contentType == "app/vnd.type" + assert override.contentType == "text/plain" -class DescribeCT_Relationship(object): +class DescribeCT_Relationship: """Unit-test suite for `pptx.opc.oxml.CT_Relationship` objects.""" def it_provides_read_access_to_xml_values(self): - rel = a_Relationship().element + rel = cast( + CT_Relationship, + element("pr:Relationship{Id=rId9,Type=ReLtYpE,Target=docProps/core.xml}"), + ) assert rel.rId == "rId9" assert rel.reltype == "ReLtYpE" assert rel.target_ref == "docProps/core.xml" assert rel.targetMode == RTM.INTERNAL - def it_can_construct_from_attribute_values(self): - cases = ( - ("rId9", "ReLtYpE", "foo/bar.xml", None), - ("rId9", "ReLtYpE", "bar/foo.xml", RTM.INTERNAL), - ("rId9", "ReLtYpE", "http://some/link", RTM.EXTERNAL), + def it_constructs_an_internal_relationship_when_no_target_mode_is_provided(self): + rel = CT_Relationship.new("rId9", "ReLtYpE", "foo/bar.xml") + + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "foo/bar.xml" + assert rel.targetMode == RTM.INTERNAL + assert rel.xml == ( + f'' ) - for rId, reltype, target, target_mode in cases: - if target_mode is None: - rel = CT_Relationship.new(rId, reltype, target) - else: - rel = CT_Relationship.new(rId, reltype, target, target_mode) - builder = a_Relationship().with_target(target) - if target_mode == RTM.EXTERNAL: - builder = builder.with_target_mode(RTM.EXTERNAL) - expected_rel_xml = builder.xml - assert rel.xml == expected_rel_xml - - -class DescribeCT_Relationships(object): + + def and_it_constructs_an_internal_relationship_when_target_mode_INTERNAL_is_specified(self): + rel = CT_Relationship.new("rId9", "ReLtYpE", "foo/bar.xml", RTM.INTERNAL) + + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "foo/bar.xml" + assert rel.targetMode == RTM.INTERNAL + assert rel.xml == ( + f'' + ) + + def and_it_constructs_an_external_relationship_when_target_mode_EXTERNAL_is_specified(self): + rel = CT_Relationship.new("rId9", "ReLtYpE", "http://some/link", RTM.EXTERNAL) + + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "http://some/link" + assert rel.targetMode == RTM.EXTERNAL + assert rel.xml == ( + f'' + ) + + +class DescribeCT_Relationships: """Unit-test suite for `pptx.opc.oxml.CT_Relationships` objects.""" def it_can_construct_a_new_relationships_element(self): rels = CT_Relationships.new() - expected_xml = ( - "\n" - '' + assert rels.xml == ( + '' ) - assert rels.xml.decode("utf-8") == expected_xml def it_can_build_rels_element_incrementally(self): - # setup ------------------------ rels = CT_Relationships.new() - # exercise --------------------- + rels.add_rel("rId1", "http://reltype1", "docProps/core.xml") rels.add_rel("rId2", "http://linktype", "http://some/link", True) rels.add_rel("rId3", "http://reltype2", "../slides/slide1.xml") - # verify ----------------------- - expected_rels_xml = a_Relationships().xml - actual_xml = oxml_tostring(rels, encoding="unicode", pretty_print=True) - assert actual_xml == expected_rels_xml + + assert oxml_tostring(rels, encoding="unicode", pretty_print=True) == ( + '\n' + ' \n' + ' \n' + ' \n' + "\n" + ) def it_can_generate_rels_file_xml(self): - expected_xml = ( + assert CT_Relationships.new().xml_file_bytes == ( "\n" ''.encode("utf-8") ) - assert CT_Relationships.new().xml == expected_xml -class DescribeCT_Types(object): +class DescribeCT_Types: """Unit-test suite for `pptx.opc.oxml.CT_Types` objects.""" - def it_provides_access_to_default_child_elements(self): - types = a_Types().element + def it_provides_access_to_default_child_elements(self, types: CT_Types): assert len(types.default_lst) == 2 for default in types.default_lst: assert isinstance(default, CT_Default) - def it_provides_access_to_override_child_elements(self): - types = a_Types().element + def it_provides_access_to_override_child_elements(self, types: CT_Types): assert len(types.override_lst) == 3 for override in types.override_lst: assert isinstance(override, CT_Override) def it_should_have_empty_list_on_no_matching_elements(self): - types = a_Types().empty().element + types = cast(CT_Types, element("ct:Types")) assert types.default_lst == [] assert types.override_lst == [] def it_can_construct_a_new_types_element(self): types = CT_Types.new() - expected_xml = a_Types().empty().xml - assert types.xml == expected_xml + assert types.xml == ( + '\n' + ) def it_can_build_types_element_incrementally(self): types = CT_Types.new() types.add_default("xml", "application/xml") types.add_default("jpeg", "image/jpeg") - types.add_override("/docProps/core.xml", "app/vnd.type1") - types.add_override("/ppt/presentation.xml", "app/vnd.type2") - types.add_override("/docProps/thumbnail.jpeg", "image/jpeg") - expected_types_xml = a_Types().xml - assert types.xml == expected_types_xml + types.add_override(PackURI("/docProps/core.xml"), "app/vnd.type1") + types.add_override(PackURI("/ppt/presentation.xml"), "app/vnd.type2") + types.add_override(PackURI("/docProps/thumbnail.jpeg"), "image/jpeg") + assert types.xml == ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + "\n" + ) + + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def types(self) -> CT_Types: + return cast( + CT_Types, + element( + "ct:Types/(ct:Default{Extension=xml,ContentType=application/xml}" + ",ct:Default{Extension=jpeg,ContentType=image/jpeg}" + ",ct:Override{PartName=/docProps/core.xml,ContentType=app/vnd.type1}" + ",ct:Override{PartName=/ppt/presentation.xml,ContentType=app/vnd.type2}" + ",ct:Override{PartName=/docProps/thunbnail.jpeg,ContentType=image/jpeg})" + ), + ) -class Describe_serialize_part_xml(object): +class Describe_serialize_part_xml: """Unit-test suite for `pptx.opc.oxml.serialize_part_xml` function.""" - def it_produces_properly_formatted_xml_for_an_opc_part( - self, part_elm, expected_part_xml - ): + def it_produces_properly_formatted_xml_for_an_opc_part(self): """ Tested aspects: --------------- @@ -156,27 +194,18 @@ def it_produces_properly_formatted_xml_for_an_opc_part( * [X] it preserves unused namespaces * [X] it returns bytes ready to save to file (not unicode) """ + part_elm = cast( + BaseOxmlElement, + parse_xml( + '\n fØØ' + "bÅr\n\n" + ), + ) xml = serialize_part_xml(part_elm) - assert xml == expected_part_xml # xml contains 134 chars, of which 3 are double-byte; it will have # len of 134 if it's unicode and 137 if it's bytes assert len(xml) == 137 - - # fixtures ----------------------------------- - - @pytest.fixture - def part_elm(self): - return parse_xml( - '\n fØØ' - "bÅr\n\n" - ) - - @pytest.fixture - def expected_part_xml(self): - unicode_xml = ( + assert xml == ( "\n" - 'fØØbÅr<' - "/f:bar>" - ) - xml_bytes = unicode_xml.encode("utf-8") - return xml_bytes + 'fØØbÅr' + ).encode("utf-8") diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index d8bf20703..8c0e95809 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -1,18 +1,19 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.opc.package` module.""" +from __future__ import annotations + import collections import io import itertools +from typing import Any import pytest -from pptx.opc.constants import ( - CONTENT_TYPE as CT, - RELATIONSHIP_TARGET_MODE as RTM, - RELATIONSHIP_TYPE as RT, -) +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.oxml import CT_Relationship, CT_Relationships from pptx.opc.package import ( OpcPackage, @@ -30,9 +31,11 @@ from pptx.parts.presentation import PresentationPart from ..unitutil.cxml import element -from ..unitutil.file import absjoin, snippet_bytes, testfile_bytes, test_file_dir +from ..unitutil.file import absjoin, snippet_bytes, test_file_dir, testfile_bytes from ..unitutil.mock import ( ANY, + FixtureRequest, + Mock, call, class_mock, function_mock, @@ -43,7 +46,7 @@ ) -class Describe_RelatableMixin(object): +class Describe_RelatableMixin: """Unit-test suite for `pptx.opc.package._RelatableMixin`. This mixin is used for both OpcPackage and Part because both a package and a part @@ -60,9 +63,7 @@ def it_can_find_a_part_related_by_reltype(self, _rels_prop_, relationships_, par relationships_.part_with_reltype.assert_called_once_with(RT.CHART) assert related_part is part_ - def it_can_establish_a_relationship_to_another_part( - self, _rels_prop_, relationships_, part_ - ): + def it_can_establish_a_relationship_to_another_part(self, _rels_prop_, relationships_, part_): relationships_.get_or_add.return_value = "rId42" _rels_prop_.return_value = relationships_ mixin = _RelatableMixin() @@ -81,9 +82,7 @@ def and_it_can_establish_a_relationship_to_an_external_link( rId = mixin.relate_to("http://url", RT.HYPERLINK, is_external=True) - relationships_.get_or_add_ext_rel.assert_called_once_with( - RT.HYPERLINK, "http://url" - ) + relationships_.get_or_add_ext_rel.assert_called_once_with(RT.HYPERLINK, "http://url") assert rId == "rId24" def it_can_find_a_related_part_by_rId( @@ -131,7 +130,7 @@ def _rels_prop_(self, request): return property_mock(request, _RelatableMixin, "_rels") -class DescribeOpcPackage(object): +class DescribeOpcPackage: """Unit-test suite for `pptx.opc.package.OpcPackage` objects.""" def it_can_open_a_pkg_file(self, request): @@ -153,13 +152,9 @@ def it_can_drop_a_relationship(self, _rels_prop_, relationships_): relationships_.pop.assert_called_once_with("rId42") def it_can_iterate_over_its_parts(self, request): - part_, part_2_ = [ - instance_mock(request, Part, name="part_%d" % i) for i in range(2) - ] + part_, part_2_ = [instance_mock(request, Part, name="part_%d" % i) for i in range(2)] rels_iter = ( - instance_mock( - request, _Relationship, is_external=is_external, target_part=target - ) + instance_mock(request, _Relationship, is_external=is_external, target_part=target) for is_external, target in ( (True, "http://some/url/"), (False, part_), @@ -187,9 +182,7 @@ def it_can_iterate_over_its_relationships(self, request, _rels_prop_): +--------> | part_1 | +--------+ """ - part_0_, part_1_ = [ - instance_mock(request, Part, name="part_%d" % i) for i in range(2) - ] + part_0_, part_1_ = [instance_mock(request, Part, name="part_%d" % i) for i in range(2)] all_rels = tuple( instance_mock( request, @@ -299,7 +292,7 @@ def _rels_prop_(self, request): return property_mock(request, OpcPackage, "_rels") -class Describe_PackageLoader(object): +class Describe_PackageLoader: """Unit-test suite for `pptx.opc.package._PackageLoader` objects.""" def it_provides_a_load_interface_classmethod(self, request, package_): @@ -328,10 +321,7 @@ def it_loads_the_package_to_help(self, request, _xml_rels_prop_): rels_ = dict( itertools.chain( (("/", instance_mock(request, _Relationships)),), - ( - ("partname_%d" % n, instance_mock(request, _Relationships)) - for n in range(1, 4) - ), + (("partname_%d" % n, instance_mock(request, _Relationships)) for n in range(1, 4)), ) ) _xml_rels_prop_.return_value = rels_ @@ -340,9 +330,7 @@ def it_loads_the_package_to_help(self, request, _xml_rels_prop_): pkg_xml_rels, parts = package_loader._load() for part_ in parts_.values(): - part_.load_rels_from_xml.assert_called_once_with( - rels_[part_.partname], parts_ - ) + part_.load_rels_from_xml.assert_called_once_with(rels_[part_.partname], parts_) assert pkg_xml_rels is rels_["/"] assert parts is parts_ @@ -397,7 +385,7 @@ def _xml_rels_prop_(self, request): return property_mock(request, _PackageLoader, "_xml_rels") -class DescribePart(object): +class DescribePart: """Unit-test suite for `pptx.opc.package.Part` objects.""" def it_can_be_constructed_by_PartFactory(self, request, package_): @@ -420,19 +408,6 @@ def it_can_change_its_blob(self): def it_knows_its_content_type(self): assert Part(None, CT.PML_SLIDE, None).content_type == CT.PML_SLIDE - @pytest.mark.parametrize("ref_count, calls", ((2, []), (1, [call("rId42")]))) - def it_can_drop_a_relationship(self, request, relationships_, ref_count, calls): - _rel_ref_count_ = method_mock( - request, Part, "_rel_ref_count", return_value=ref_count - ) - property_mock(request, Part, "_rels", return_value=relationships_) - part = Part(None, None, None) - - part.drop_rel("rId42") - - _rel_ref_count_.assert_called_once_with(part, "rId42") - assert relationships_.pop.call_args_list == calls - def it_knows_the_package_it_belongs_to(self, package_): assert Part(None, None, package_).package is package_ @@ -444,9 +419,7 @@ def it_can_change_its_partname(self): part.partname = PackURI("/new/part/name") assert part.partname == PackURI("/new/part/name") - def it_provides_access_to_its_relationships_for_traversal( - self, request, relationships_ - ): + def it_provides_access_to_its_relationships_for_traversal(self, request, relationships_): property_mock(request, Part, "_rels", return_value=relationships_) assert Part(None, None, None).rels is relationships_ @@ -484,16 +457,14 @@ def relationships_(self, request): return instance_mock(request, _Relationships) -class DescribeXmlPart(object): +class DescribeXmlPart: """Unit-test suite for `pptx.opc.package.XmlPart` objects.""" def it_can_be_constructed_by_PartFactory(self, request): partname = PackURI("/ppt/slides/slide1.xml") element_ = element("p:sld") package_ = instance_mock(request, OpcPackage) - parse_xml_ = function_mock( - request, "pptx.opc.package.parse_xml", return_value=element_ - ) + parse_xml_ = function_mock(request, "pptx.opc.package.parse_xml", return_value=element_) _init_ = initializer_mock(request, XmlPart) part = XmlPart.load(partname, CT.PML_SLIDE, package_, b"blob") @@ -504,9 +475,7 @@ def it_can_be_constructed_by_PartFactory(self, request): def it_can_serialize_to_xml(self, request): element_ = element("p:sld") - serialize_part_xml_ = function_mock( - request, "pptx.opc.package.serialize_part_xml" - ) + serialize_part_xml_ = function_mock(request, "pptx.opc.package.serialize_part_xml") xml_part = XmlPart(None, None, None, element_) blob = xml_part.blob @@ -514,17 +483,34 @@ def it_can_serialize_to_xml(self, request): serialize_part_xml_.assert_called_once_with(element_) assert blob is serialize_part_xml_.return_value + @pytest.mark.parametrize(("ref_count", "calls"), [(2, []), (1, [call("rId42")])]) + def it_can_drop_a_relationship( + self, request: FixtureRequest, relationships_: Mock, ref_count: int, calls: list[Any] + ): + _rel_ref_count_ = method_mock(request, XmlPart, "_rel_ref_count", return_value=ref_count) + property_mock(request, XmlPart, "_rels", return_value=relationships_) + part = XmlPart(None, None, None, None) + + part.drop_rel("rId42") + + _rel_ref_count_.assert_called_once_with(part, "rId42") + assert relationships_.pop.call_args_list == calls + def it_knows_it_is_the_part_for_its_child_objects(self): xml_part = XmlPart(None, None, None, None) assert xml_part.part is xml_part + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def relationships_(self, request): + return instance_mock(request, _Relationships) + -class DescribePartFactory(object): +class DescribePartFactory: """Unit-test suite for `pptx.opc.package.PartFactory` objects.""" - def it_constructs_custom_part_type_for_registered_content_types( - self, request, package_, part_ - ): + def it_constructs_custom_part_type_for_registered_content_types(self, request, package_, part_): SlidePart_ = class_mock(request, "pptx.opc.package.XmlPart") SlidePart_.load.return_value = part_ partname = PackURI("/ppt/slides/slide7.xml") @@ -532,9 +518,7 @@ def it_constructs_custom_part_type_for_registered_content_types( part = PartFactory(partname, CT.PML_SLIDE, package_, b"blob") - SlidePart_.load.assert_called_once_with( - partname, CT.PML_SLIDE, package_, b"blob" - ) + SlidePart_.load.assert_called_once_with(partname, CT.PML_SLIDE, package_, b"blob") assert part is part_ def it_constructs_part_using_default_class_when_no_custom_registered( @@ -546,9 +530,7 @@ def it_constructs_part_using_default_class_when_no_custom_registered( part = PartFactory(partname, CT.OFC_VML_DRAWING, package_, b"blob") - Part_.load.assert_called_once_with( - partname, CT.OFC_VML_DRAWING, package_, b"blob" - ) + Part_.load.assert_called_once_with(partname, CT.OFC_VML_DRAWING, package_, b"blob") assert part is part_ # fixtures components ---------------------------------- @@ -562,7 +544,7 @@ def part_(self, request): return instance_mock(request, Part) -class Describe_ContentTypeMap(object): +class Describe_ContentTypeMap: """Unit-test suite for `pptx.opc.package._ContentTypeMap` objects.""" def it_can_construct_from_content_types_xml(self, request): @@ -617,8 +599,7 @@ def it_raises_KeyError_on_partname_not_found(self, content_type_map): with pytest.raises(KeyError) as e: content_type_map[PackURI("/!blat/rhumba.1x&")] assert str(e.value) == ( - "\"no content-type for partname '/!blat/rhumba.1x&' " - 'in [Content_Types].xml"' + "\"no content-type for partname '/!blat/rhumba.1x&' " 'in [Content_Types].xml"' ) def it_raises_TypeError_on_key_not_instance_of_PackURI(self, content_type_map): @@ -630,12 +611,10 @@ def it_raises_TypeError_on_key_not_instance_of_PackURI(self, content_type_map): @pytest.fixture(scope="class") def content_type_map(self): - return _ContentTypeMap.from_xml( - testfile_bytes("expanded_pptx", "[Content_Types].xml") - ) + return _ContentTypeMap.from_xml(testfile_bytes("expanded_pptx", "[Content_Types].xml")) -class Describe_Relationships(object): +class Describe_Relationships: """Unit-test suite for `pptx.opc.package._Relationships` objects.""" @pytest.mark.parametrize("rId, expected_value", (("rId1", True), ("rId2", False))) @@ -655,9 +634,7 @@ def but_it_raises_KeyError_when_no_relationship_has_rId(self, _rels_prop_): _Relationships(None)["rId6"] assert str(e.value) == "\"no relationship with key 'rId6'\"" - def it_can_iterate_the_rIds_of_the_relationships_it_contains( - self, request, _rels_prop_ - ): + def it_can_iterate_the_rIds_of_the_relationships_it_contains(self, request, _rels_prop_): rels_ = set(instance_mock(request, _Relationship) for n in range(5)) _rels_prop_.return_value = {"rId%d" % (i + 1): r for i, r in enumerate(rels_)} relationships = _Relationships(None) @@ -671,9 +648,7 @@ def it_has_a_len(self, _rels_prop_): _rels_prop_.return_value = {"a": 0, "b": 1} assert len(_Relationships(None)) == 2 - def it_can_add_a_relationship_to_a_target_part( - self, part_, _get_matching_, _add_relationship_ - ): + def it_can_add_a_relationship_to_a_target_part(self, part_, _get_matching_, _add_relationship_): _get_matching_.return_value = None _add_relationship_.return_value = "rId7" relationships = _Relationships(None) @@ -684,9 +659,7 @@ def it_can_add_a_relationship_to_a_target_part( _add_relationship_.assert_called_once_with(relationships, RT.IMAGE, part_) assert rId == "rId7" - def but_it_returns_an_existing_relationship_if_it_matches( - self, part_, _get_matching_ - ): + def but_it_returns_an_existing_relationship_if_it_matches(self, part_, _get_matching_): _get_matching_.return_value = "rId3" relationships = _Relationships(None) @@ -695,9 +668,7 @@ def but_it_returns_an_existing_relationship_if_it_matches( _get_matching_.assert_called_once_with(relationships, RT.IMAGE, part_) assert rId == "rId3" - def it_can_add_an_external_relationship_to_a_URI( - self, _get_matching_, _add_relationship_ - ): + def it_can_add_an_external_relationship_to_a_URI(self, _get_matching_, _add_relationship_): _get_matching_.return_value = None _add_relationship_.return_value = "rId2" relationships = _Relationships(None) @@ -712,9 +683,7 @@ def it_can_add_an_external_relationship_to_a_URI( ) assert rId == "rId2" - def but_it_returns_an_existing_external_relationship_if_it_matches( - self, part_, _get_matching_ - ): + def but_it_returns_an_existing_external_relationship_if_it_matches(self, part_, _get_matching_): _get_matching_.return_value = "rId10" relationships = _Relationships(None) @@ -727,8 +696,7 @@ def but_it_returns_an_existing_external_relationship_if_it_matches( def it_can_load_from_the_xml_in_a_rels_part(self, request, _Relationship_, part_): rels_ = tuple( - instance_mock(request, _Relationship, rId="rId%d" % (i + 1)) - for i in range(5) + instance_mock(request, _Relationship, rId="rId%d" % (i + 1)) for i in range(5) ) _Relationship_.from_xml.side_effect = iter(rels_) parts = {"/ppt/slideLayouts/slideLayout1.xml": part_} @@ -743,9 +711,7 @@ def it_can_load_from_the_xml_in_a_rels_part(self, request, _Relationship_, part_ ] assert relationships._rels == {"rId1": rels_[0], "rId2": rels_[1]} - def it_can_find_a_part_with_reltype( - self, _rels_by_reltype_prop_, relationship_, part_ - ): + def it_can_find_a_part_with_reltype(self, _rels_by_reltype_prop_, relationship_, part_): relationship_.target_part = part_ _rels_by_reltype_prop_.return_value = collections.defaultdict( list, ((RT.SLIDE_LAYOUT, [relationship_]),) @@ -852,9 +818,7 @@ def and_it_can_add_an_external_relationship_to_help( _rels_prop_.return_value = {} relationships = _Relationships("/ppt") - rId = relationships._add_relationship( - RT.HYPERLINK, "http://url", is_external=True - ) + rId = relationships._add_relationship(RT.HYPERLINK, "http://url", is_external=True) _Relationship_.assert_called_once_with( "/ppt", "rId9", RT.HYPERLINK, target_mode=RTM.EXTERNAL, target="http://url" @@ -894,18 +858,14 @@ def it_can_get_a_matching_relationship_to_help( ) ] } - target = ( - target_ref if is_external else part_1 if target_ref == "part_1" else part_2 - ) + target = target_ref if is_external else part_1 if target_ref == "part_1" else part_2 relationships = _Relationships(None) matching = relationships._get_matching(RT.SLIDE, target, is_external) assert matching == expected_value - def but_it_returns_None_when_there_is_no_matching_relationship( - self, _rels_by_reltype_prop_ - ): + def but_it_returns_None_when_there_is_no_matching_relationship(self, _rels_by_reltype_prop_): _rels_by_reltype_prop_.return_value = collections.defaultdict(list) relationships = _Relationships(None) @@ -979,7 +939,7 @@ def _rels_prop_(self, request): return property_mock(request, _Relationships, "_rels") -class Describe_Relationship(object): +class Describe_Relationship: """Unit-test suite for `pptx.opc.package._Relationship` objects.""" def it_can_construct_from_xml(self, request, part_): @@ -996,9 +956,7 @@ def it_can_construct_from_xml(self, request, part_): relationship = _Relationship.from_xml("/ppt", rel_elm, parts) - _init_.assert_called_once_with( - relationship, "/ppt", "rId42", RT.SLIDE, RTM.INTERNAL, part_ - ) + _init_.assert_called_once_with(relationship, "/ppt", "rId42", RT.SLIDE, RTM.INTERNAL, part_) assert isinstance(relationship, _Relationship) @pytest.mark.parametrize( @@ -1026,8 +984,7 @@ def but_it_raises_ValueError_on_target_part_for_external_rel(self): with pytest.raises(ValueError) as e: relationship.target_part assert str(e.value) == ( - "`.target_part` property on _Relationship is undefined when " - "target-mode is external" + "`.target_part` property on _Relationship is undefined when " "target-mode is external" ) def it_knows_its_target_partname(self, part_): diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index f77ea68f5..5b7e64a2f 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for the `pptx.opc.packuri` module.""" +from __future__ import annotations + import pytest from pptx.opc.packuri import PackURI @@ -11,9 +11,7 @@ class DescribePackURI(object): """Unit-test suite for the `pptx.opc.packuri.PackURI` objects.""" def it_can_construct_from_relative_ref(self): - pack_uri = PackURI.from_rel_ref( - "/ppt/slides", "../slideLayouts/slideLayout1.xml" - ) + pack_uri = PackURI.from_rel_ref("/ppt/slides", "../slideLayouts/slideLayout1.xml") assert pack_uri == "/ppt/slideLayouts/slideLayout1.xml" def it_should_raise_on_construct_with_bad_pack_uri_str(self): @@ -21,53 +19,53 @@ def it_should_raise_on_construct_with_bad_pack_uri_str(self): PackURI("foobar") @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", "/"), ("/ppt/presentation.xml", "/ppt"), ("/ppt/slides/slide1.xml", "/ppt/slides"), - ), + ], ) - def it_knows_its_base_URI(self, uri, expected_value): + def it_knows_its_base_URI(self, uri: str, expected_value: str): assert PackURI(uri).baseURI == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", ""), ("/ppt/presentation.xml", "xml"), ("/ppt/media/image.PnG", "PnG"), - ), + ], ) - def it_knows_its_extension(self, uri, expected_value): + def it_knows_its_extension(self, uri: str, expected_value: str): assert PackURI(uri).ext == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", ""), ("/ppt/presentation.xml", "presentation.xml"), ("/ppt/media/image.png", "image.png"), - ), + ], ) - def it_knows_its_filename(self, uri, expected_value): + def it_knows_its_filename(self, uri: str, expected_value: str): assert PackURI(uri).filename == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", None), ("/ppt/presentation.xml", None), ("/ppt/,foo,grob!.xml", None), ("/ppt/media/image42.png", 42), - ), + ], ) - def it_knows_the_filename_index(self, uri, expected_value): + def it_knows_the_filename_index(self, uri: str, expected_value: str): assert PackURI(uri).idx == expected_value @pytest.mark.parametrize( - "uri, base_uri, expected_value", - ( + ("uri", "base_uri", "expected_value"), + [ ("/ppt/presentation.xml", "/", "ppt/presentation.xml"), ( "/ppt/slideMasters/slideMaster1.xml", @@ -79,18 +77,18 @@ def it_knows_the_filename_index(self, uri, expected_value): "/ppt/slides", "../slideLayouts/slideLayout1.xml", ), - ), + ], ) - def it_can_compute_its_relative_reference(self, uri, base_uri, expected_value): + def it_can_compute_its_relative_reference(self, uri: str, base_uri: str, expected_value: str): assert PackURI(uri).relative_ref(base_uri) == expected_value @pytest.mark.parametrize( - "uri, expected_value", - ( + ("uri", "expected_value"), + [ ("/", "/_rels/.rels"), ("/ppt/presentation.xml", "/ppt/_rels/presentation.xml.rels"), ("/ppt/slides/slide42.xml", "/ppt/slides/_rels/slide42.xml.rels"), - ), + ], ) - def it_knows_the_uri_of_its_rels_part(self, uri, expected_value): + def it_knows_the_uri_of_its_rels_part(self, uri: str, expected_value: str): assert PackURI(uri).rels_uri == expected_value diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index 31c965904..d5b867c4e 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -1,13 +1,16 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.opc.serialized` module.""" +from __future__ import annotations + import hashlib -import pytest +import io import zipfile -from pptx.compat import BytesIO -from pptx.exceptions import PackageNotFoundError +import pytest + +from pptx.exc import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import Part, _Relationships from pptx.opc.packuri import CONTENT_TYPES_URI, PackURI @@ -25,6 +28,8 @@ from ..unitutil.file import absjoin, snippet_text, test_file_dir from ..unitutil.mock import ( ANY, + FixtureRequest, + Mock, call, class_mock, function_mock, @@ -34,41 +39,40 @@ property_mock, ) - test_pptx_path = absjoin(test_file_dir, "test.pptx") dir_pkg_path = absjoin(test_file_dir, "expanded_pptx") zip_pkg_path = test_pptx_path -class DescribePackageReader(object): +class DescribePackageReader: """Unit-test suite for `pptx.opc.serialized.PackageReader` objects.""" - def it_knows_whether_it_contains_a_partname(self, _blob_reader_prop_): - _blob_reader_prop_.return_value = set(("/ppt", "/docProps")) - package_reader = PackageReader(None) + def it_knows_whether_it_contains_a_partname(self, _blob_reader_prop_: Mock): + _blob_reader_prop_.return_value = {"/ppt", "/docProps"} + package_reader = PackageReader("") assert "/ppt" in package_reader assert "/xyz" not in package_reader - def it_can_get_a_blob_by_partname(self, _blob_reader_prop_): + def it_can_get_a_blob_by_partname(self, _blob_reader_prop_: Mock): _blob_reader_prop_.return_value = {"/ppt/slides/slide1.xml": b"blob"} - package_reader = PackageReader(None) + package_reader = PackageReader("") - assert package_reader["/ppt/slides/slide1.xml"] == b"blob" + assert package_reader[PackURI("/ppt/slides/slide1.xml")] == b"blob" - def it_can_get_the_rels_xml_for_a_partname(self, _blob_reader_prop_): + def it_can_get_the_rels_xml_for_a_partname(self, _blob_reader_prop_: Mock): _blob_reader_prop_.return_value = {"/ppt/_rels/presentation.xml.rels": b"blob"} - package_reader = PackageReader(None) + package_reader = PackageReader("") assert package_reader.rels_xml_for(PackURI("/ppt/presentation.xml")) == b"blob" - def but_it_returns_None_when_the_part_has_no_rels(self, _blob_reader_prop_): + def but_it_returns_None_when_the_part_has_no_rels(self, _blob_reader_prop_: Mock): _blob_reader_prop_.return_value = {"/ppt/_rels/presentation.xml.rels": b"blob"} - package_reader = PackageReader(None) + package_reader = PackageReader("") assert package_reader.rels_xml_for(PackURI("/ppt/slides.slide1.xml")) is None - def it_constructs_its_blob_reader_to_help(self, request): + def it_constructs_its_blob_reader_to_help(self, request: FixtureRequest): phys_pkg_reader_ = instance_mock(request, _PhysPkgReader) _PhysPkgReader_ = class_mock(request, "pptx.opc.serialized._PhysPkgReader") _PhysPkgReader_.factory.return_value = phys_pkg_reader_ @@ -82,25 +86,27 @@ def it_constructs_its_blob_reader_to_help(self, request): # fixture components ----------------------------------- @pytest.fixture - def _blob_reader_prop_(self, request): + def _blob_reader_prop_(self, request: FixtureRequest): return property_mock(request, PackageReader, "_blob_reader") -class DescribePackageWriter(object): +class DescribePackageWriter: """Unit-test suite for `pptx.opc.serialized.PackageWriter` objects.""" - def it_provides_a_write_interface_classmethod(self, request, relationships_): + def it_provides_a_write_interface_classmethod( + self, request: FixtureRequest, relationships_: Mock, part_: Mock + ): _init_ = initializer_mock(request, PackageWriter) _write_ = method_mock(request, PackageWriter, "_write") - PackageWriter.write("prs.pptx", relationships_, ("part_1", "part_2")) + PackageWriter.write("prs.pptx", relationships_, (part_, part_)) - _init_.assert_called_once_with( - ANY, "prs.pptx", relationships_, ("part_1", "part_2") - ) + _init_.assert_called_once_with(ANY, "prs.pptx", relationships_, (part_, part_)) _write_.assert_called_once_with(ANY) - def it_can_write_a_package(self, request, phys_writer_): + def it_can_write_a_package( + self, request: FixtureRequest, phys_writer_: Mock, relationships_: Mock + ): _PhysPkgWriter_ = class_mock(request, "pptx.opc.serialized._PhysPkgWriter") phys_writer_.__enter__.return_value = phys_writer_ _PhysPkgWriter_.factory.return_value = phys_writer_ @@ -109,35 +115,35 @@ def it_can_write_a_package(self, request, phys_writer_): ) _write_pkg_rels_ = method_mock(request, PackageWriter, "_write_pkg_rels") _write_parts_ = method_mock(request, PackageWriter, "_write_parts") - package_writer = PackageWriter("prs.pptx", None, None) + package_writer = PackageWriter("prs.pptx", relationships_, []) package_writer._write() _PhysPkgWriter_.factory.assert_called_once_with("prs.pptx") - _write_content_types_stream_.assert_called_once_with( - package_writer, phys_writer_ - ) + _write_content_types_stream_.assert_called_once_with(package_writer, phys_writer_) _write_pkg_rels_.assert_called_once_with(package_writer, phys_writer_) _write_parts_.assert_called_once_with(package_writer, phys_writer_) - def it_can_write_a_content_types_stream(self, request, phys_writer_): - _ContentTypesItem_ = class_mock( - request, "pptx.opc.serialized._ContentTypesItem" - ) + def it_can_write_a_content_types_stream( + self, request: FixtureRequest, phys_writer_: Mock, relationships_: Mock, part_: Mock + ): + _ContentTypesItem_ = class_mock(request, "pptx.opc.serialized._ContentTypesItem") _ContentTypesItem_.xml_for.return_value = "part_xml" serialize_part_xml_ = function_mock( request, "pptx.opc.serialized.serialize_part_xml", return_value=b"xml" ) - package_writer = PackageWriter(None, None, ("part_1", "part_2")) + package_writer = PackageWriter("", relationships_, (part_, part_)) package_writer._write_content_types_stream(phys_writer_) - _ContentTypesItem_.xml_for.assert_called_once_with(("part_1", "part_2")) + _ContentTypesItem_.xml_for.assert_called_once_with((part_, part_)) serialize_part_xml_.assert_called_once_with("part_xml") phys_writer_.write.assert_called_once_with(CONTENT_TYPES_URI, b"xml") - def it_can_write_a_sequence_of_parts(self, request, phys_writer_): - parts_ = ( + def it_can_write_a_sequence_of_parts( + self, request: FixtureRequest, relationships_: Mock, phys_writer_: Mock + ): + parts_ = [ instance_mock( request, Part, @@ -146,8 +152,8 @@ def it_can_write_a_sequence_of_parts(self, request, phys_writer_): rels=instance_mock(request, _Relationships, xml="rels_xml_%s" % x), ) for x in ("a", "b", "c") - ) - package_writer = PackageWriter(None, None, parts_) + ] + package_writer = PackageWriter("", relationships_, parts_) package_writer._write_parts(phys_writer_) @@ -160,40 +166,44 @@ def it_can_write_a_sequence_of_parts(self, request, phys_writer_): call("/ppt/_rels/c.xml.rels", "rels_xml_c"), ] - def it_can_write_a_pkg_rels_item(self, request, phys_writer_, relationships_): + def it_can_write_a_pkg_rels_item(self, phys_writer_: Mock, relationships_: Mock): relationships_.xml = b"pkg-rels-xml" - package_writer = PackageWriter(None, relationships_, None) + package_writer = PackageWriter("", relationships_, []) package_writer._write_pkg_rels(phys_writer_) phys_writer_.write.assert_called_once_with("/_rels/.rels", b"pkg-rels-xml") - # fixture components ----------------------------------- + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) @pytest.fixture - def phys_writer_(self, request): + def phys_writer_(self, request: FixtureRequest): return instance_mock(request, _ZipPkgWriter) @pytest.fixture - def relationships_(self, request): + def relationships_(self, request: FixtureRequest): return instance_mock(request, _Relationships) -class Describe_PhysPkgReader(object): +class Describe_PhysPkgReader: """Unit-test suite for `pptx.opc.serialized._PhysPkgReader` objects.""" def it_constructs_ZipPkgReader_when_pkg_is_file_like( - self, _ZipPkgReader_, zip_pkg_reader_ + self, _ZipPkgReader_: Mock, zip_pkg_reader_: Mock ): _ZipPkgReader_.return_value = zip_pkg_reader_ - file_like_pkg = BytesIO(b"pkg-bytes") + file_like_pkg = io.BytesIO(b"pkg-bytes") phys_reader = _PhysPkgReader.factory(file_like_pkg) _ZipPkgReader_.assert_called_once_with(file_like_pkg) assert phys_reader is zip_pkg_reader_ - def and_it_constructs_DirPkgReader_when_pkg_is_a_dir(self, request): + def and_it_constructs_DirPkgReader_when_pkg_is_a_dir(self, request: FixtureRequest): dir_pkg_reader_ = instance_mock(request, _DirPkgReader) _DirPkgReader_ = class_mock( request, "pptx.opc.serialized._DirPkgReader", return_value=dir_pkg_reader_ @@ -205,7 +215,7 @@ def and_it_constructs_DirPkgReader_when_pkg_is_a_dir(self, request): assert phys_reader is dir_pkg_reader_ def and_it_constructs_ZipPkgReader_when_pkg_is_a_zip_file_path( - self, _ZipPkgReader_, zip_pkg_reader_ + self, _ZipPkgReader_: Mock, zip_pkg_reader_: Mock ): _ZipPkgReader_.return_value = zip_pkg_reader_ pkg_file_path = test_pptx_path @@ -223,29 +233,27 @@ def but_it_raises_when_pkg_path_is_not_a_package(self): # --- fixture components ------------------------------- @pytest.fixture - def zip_pkg_reader_(self, request): + def zip_pkg_reader_(self, request: FixtureRequest): return instance_mock(request, _ZipPkgReader) @pytest.fixture - def _ZipPkgReader_(self, request): + def _ZipPkgReader_(self, request: FixtureRequest): return class_mock(request, "pptx.opc.serialized._ZipPkgReader") -class Describe_DirPkgReader(object): +class Describe_DirPkgReader: """Unit-test suite for `pptx.opc.serialized._DirPkgReader` objects.""" - def it_knows_whether_it_contains_a_partname(self, dir_pkg_reader): + def it_knows_whether_it_contains_a_partname(self, dir_pkg_reader: _DirPkgReader): assert PackURI("/ppt/presentation.xml") in dir_pkg_reader assert PackURI("/ppt/foobar.xml") not in dir_pkg_reader - def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_pkg_reader): + def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_pkg_reader: _DirPkgReader): blob = dir_pkg_reader[PackURI("/ppt/presentation.xml")] - assert ( - hashlib.sha1(blob).hexdigest() == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" - ) + assert hashlib.sha1(blob).hexdigest() == "51b78f4dabc0af2419d4e044ab73028c4bef53aa" def but_it_raises_KeyError_when_requested_member_is_not_present( - self, dir_pkg_reader + self, dir_pkg_reader: _DirPkgReader ): with pytest.raises(KeyError) as e: dir_pkg_reader[PackURI("/ppt/foobar.xml")] @@ -254,31 +262,29 @@ def but_it_raises_KeyError_when_requested_member_is_not_present( # --- fixture components ------------------------------- @pytest.fixture(scope="class") - def dir_pkg_reader(self, request): + def dir_pkg_reader(self): return _DirPkgReader(dir_pkg_path) -class Describe_ZipPkgReader(object): +class Describe_ZipPkgReader: """Unit-test suite for `pptx.opc.serialized._ZipPkgReader` objects.""" - def it_knows_whether_it_contains_a_partname(self, zip_pkg_reader): + def it_knows_whether_it_contains_a_partname(self, zip_pkg_reader: _ZipPkgReader): assert PackURI("/ppt/presentation.xml") in zip_pkg_reader assert PackURI("/ppt/foobar.xml") not in zip_pkg_reader - def it_can_get_a_blob_by_partname(self, zip_pkg_reader): + def it_can_get_a_blob_by_partname(self, zip_pkg_reader: _ZipPkgReader): blob = zip_pkg_reader[PackURI("/ppt/presentation.xml")] - assert hashlib.sha1(blob).hexdigest() == ( - "efa7bee0ac72464903a67a6744c1169035d52a54" - ) + assert hashlib.sha1(blob).hexdigest() == ("efa7bee0ac72464903a67a6744c1169035d52a54") def but_it_raises_KeyError_when_requested_member_is_not_present( - self, zip_pkg_reader + self, zip_pkg_reader: _ZipPkgReader ): with pytest.raises(KeyError) as e: zip_pkg_reader[PackURI("/ppt/foobar.xml")] assert str(e.value) == "\"no member '/ppt/foobar.xml' in package\"" - def it_loads_the_package_blobs_on_first_access_to_help(self, zip_pkg_reader): + def it_loads_the_package_blobs_on_first_access_to_help(self, zip_pkg_reader: _ZipPkgReader): blobs = zip_pkg_reader._blobs assert len(blobs) == 38 assert "/ppt/presentation.xml" in blobs @@ -287,14 +293,14 @@ def it_loads_the_package_blobs_on_first_access_to_help(self, zip_pkg_reader): # --- fixture components ------------------------------- @pytest.fixture(scope="class") - def zip_pkg_reader(self, request): + def zip_pkg_reader(self): return _ZipPkgReader(zip_pkg_path) -class Describe_PhysPkgWriter(object): +class Describe_PhysPkgWriter: """Unit-test suite for `pptx.opc.serialized._PhysPkgWriter` objects.""" - def it_constructs_ZipPkgWriter_unconditionally(self, request): + def it_constructs_ZipPkgWriter_unconditionally(self, request: FixtureRequest): zip_pkg_writer_ = instance_mock(request, _ZipPkgWriter) _ZipPkgWriter_ = class_mock( request, "pptx.opc.serialized._ZipPkgWriter", return_value=zip_pkg_writer_ @@ -306,22 +312,22 @@ def it_constructs_ZipPkgWriter_unconditionally(self, request): assert phys_writer is zip_pkg_writer_ -class Describe_ZipPkgWriter(object): +class Describe_ZipPkgWriter: """Unit-test suite for `pptx.opc.serialized._ZipPkgWriter` objects.""" def it_has_an__enter__method_for_context_management(self): - pkg_writer = _ZipPkgWriter(None) + pkg_writer = _ZipPkgWriter("") assert pkg_writer.__enter__() is pkg_writer - def and_it_closes_the_zip_archive_on_context__exit__(self, _zipf_prop_): - _ZipPkgWriter(None).__exit__(None, None, None) + def and_it_closes_the_zip_archive_on_context__exit__(self, _zipf_prop_: Mock): + _ZipPkgWriter("").__exit__() _zipf_prop_.return_value.close.assert_called_once_with() - def it_can_write_a_blob(self, _zipf_prop_): + def it_can_write_a_blob(self, _zipf_prop_: Mock): """Integrates with zipfile.ZipFile.""" pack_uri = PackURI("/part/name.xml") - _zipf_prop_.return_value = zipf = zipfile.ZipFile(BytesIO(), "w") - pkg_writer = _ZipPkgWriter(None) + _zipf_prop_.return_value = zipf = zipfile.ZipFile(io.BytesIO(), "w") + pkg_writer = _ZipPkgWriter("") pkg_writer.write(pack_uri, b"blob") @@ -329,37 +335,35 @@ def it_can_write_a_blob(self, _zipf_prop_): assert len(members) == 1 assert members[pack_uri] == b"blob" - def it_provides_access_to_the_open_zip_file_to_help(self, request): + def it_provides_access_to_the_open_zip_file_to_help(self, request: FixtureRequest): ZipFile_ = class_mock(request, "pptx.opc.serialized.zipfile.ZipFile") pkg_writer = _ZipPkgWriter("prs.pptx") zipf = pkg_writer._zipf - ZipFile_.assert_called_once_with( - "prs.pptx", "w", compression=zipfile.ZIP_DEFLATED - ) + ZipFile_.assert_called_once_with("prs.pptx", "w", compression=zipfile.ZIP_DEFLATED) assert zipf is ZipFile_.return_value # fixtures --------------------------------------------- @pytest.fixture - def _zipf_prop_(self, request): + def _zipf_prop_(self, request: FixtureRequest): return property_mock(request, _ZipPkgWriter, "_zipf") -class Describe_ContentTypesItem(object): +class Describe_ContentTypesItem: """Unit-test suite for `pptx.opc.serialized._ContentTypesItem` objects.""" - def it_provides_an_interface_classmethod(self, request): + def it_provides_an_interface_classmethod(self, request: FixtureRequest, part_: Mock): _init_ = initializer_mock(request, _ContentTypesItem) property_mock(request, _ContentTypesItem, "_xml", return_value=b"xml") - xml = _ContentTypesItem.xml_for(("part", "zuh")) + xml = _ContentTypesItem.xml_for((part_, part_)) - _init_.assert_called_once_with(ANY, ("part", "zuh")) + _init_.assert_called_once_with(ANY, (part_, part_)) assert xml == b"xml" - def it_can_compose_content_types_xml(self, request): + def it_can_compose_content_types_xml(self, request: FixtureRequest): defaults = {"png": CT.PNG, "xml": CT.XML, "rels": CT.OPC_RELATIONSHIPS} overrides = { "/docProps/core.xml": "app/vnd.core", @@ -373,22 +377,20 @@ def it_can_compose_content_types_xml(self, request): return_value=(defaults, overrides), ) - content_types = _ContentTypesItem(None)._xml + content_types = _ContentTypesItem([])._xml assert content_types.xml == snippet_text("content-types-xml").strip() - def it_computes_defaults_and_overrides_to_help(self, request): - parts = ( - instance_mock( - request, Part, partname=PackURI(partname), content_type=content_type - ) + def it_computes_defaults_and_overrides_to_help(self, request: FixtureRequest): + parts = [ + instance_mock(request, Part, partname=PackURI(partname), content_type=content_type) for partname, content_type in ( ("/media/image1.png", CT.PNG), ("/ppt/slides/slide1.xml", CT.PML_SLIDE), ("/foo/bar.xml", CT.XML), ("/docProps/core.xml", CT.OPC_CORE_PROPERTIES), ) - ) + ] content_types = _ContentTypesItem(parts) defaults, overrides = content_types._defaults_and_overrides @@ -398,3 +400,9 @@ def it_computes_defaults_and_overrides_to_help(self, request): "/ppt/slides/slide1.xml": CT.PML_SLIDE, "/docProps/core.xml": CT.OPC_CORE_PROPERTIES, } + + # -- fixtures ---------------------------------------------------- + + @pytest.fixture + def part_(self, request: FixtureRequest): + return instance_mock(request, Part) diff --git a/tests/opc/unitdata/__init__.py b/tests/opc/unitdata/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py deleted file mode 100644 index 2fd0aa947..000000000 --- a/tests/opc/unitdata/rels.py +++ /dev/null @@ -1,261 +0,0 @@ -# encoding: utf-8 - -"""Test data for relationship-related unit tests.""" - -from pptx.opc.constants import NAMESPACE as NS -from pptx.oxml import parse_xml - - -class BaseBuilder(object): - """ - Provides common behavior for all data builders. - """ - - @property - def element(self): - """Return element based on XML generated by builder""" - return parse_xml(self.xml) - - def with_indent(self, indent): - """Add integer *indent* spaces at beginning of element XML""" - self._indent = indent - return self - - -class CT_DefaultBuilder(BaseBuilder): - """ - Test data builder for CT_Default (Default) XML element that appears in - `[Content_Types].xml`. - """ - - def __init__(self): - """Establish instance variables with default values""" - self._content_type = "application/xml" - self._extension = "xml" - self._indent = 0 - self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES - - def with_content_type(self, content_type): - """Set ContentType attribute to *content_type*""" - self._content_type = content_type - return self - - def with_extension(self, extension): - """Set Extension attribute to *extension*""" - self._extension = extension - return self - - def without_namespace(self): - """Don't include an 'xmlns=' attribute""" - self._namespace = "" - return self - - @property - def xml(self): - """Return Default element""" - tmpl = '%s\n' - indent = " " * self._indent - return tmpl % (indent, self._namespace, self._extension, self._content_type) - - -class CT_OverrideBuilder(BaseBuilder): - """ - Test data builder for CT_Override (Override) XML element that appears in - `[Content_Types].xml`. - """ - - def __init__(self): - """Establish instance variables with default values""" - self._content_type = "app/vnd.type" - self._indent = 0 - self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES - self._partname = "/part/name.xml" - - def with_content_type(self, content_type): - """Set ContentType attribute to *content_type*""" - self._content_type = content_type - return self - - def with_partname(self, partname): - """Set PartName attribute to *partname*""" - self._partname = partname - return self - - def without_namespace(self): - """Don't include an 'xmlns=' attribute""" - self._namespace = "" - return self - - @property - def xml(self): - """Return Override element""" - tmpl = '%s\n' - indent = " " * self._indent - return tmpl % (indent, self._namespace, self._partname, self._content_type) - - -class CT_RelationshipBuilder(BaseBuilder): - """ - Test data builder for CT_Relationship (Relationship) XML element that - appears in .rels files - """ - - def __init__(self): - """Establish instance variables with default values""" - self._rId = "rId9" - self._reltype = "ReLtYpE" - self._target = "docProps/core.xml" - self._target_mode = None - self._indent = 0 - self._namespace = ' xmlns="%s"' % NS.OPC_RELATIONSHIPS - - def with_rId(self, rId): - """Set Id attribute to *rId*""" - self._rId = rId - return self - - def with_reltype(self, reltype): - """Set Type attribute to *reltype*""" - self._reltype = reltype - return self - - def with_target(self, target): - """Set XXX attribute to *target*""" - self._target = target - return self - - def with_target_mode(self, target_mode): - """Set TargetMode attribute to *target_mode*""" - self._target_mode = None if target_mode == "Internal" else target_mode - return self - - def without_namespace(self): - """Don't include an 'xmlns=' attribute""" - self._namespace = "" - return self - - @property - def target_mode(self): - if self._target_mode is None: - return "" - return ' TargetMode="%s"' % self._target_mode - - @property - def xml(self): - """Return Relationship element""" - tmpl = '%s\n' - indent = " " * self._indent - return tmpl % ( - indent, - self._namespace, - self._rId, - self._reltype, - self._target, - self.target_mode, - ) - - -class CT_RelationshipsBuilder(BaseBuilder): - """ - Test data builder for CT_Relationships (Relationships) XML element, the - root element in .rels files. - """ - - def __init__(self): - """Establish instance variables with default values""" - self._rels = ( - ("rId1", "http://reltype1", "docProps/core.xml", "Internal"), - ("rId2", "http://linktype", "http://some/link", "External"), - ("rId3", "http://reltype2", "../slides/slide1.xml", "Internal"), - ) - - @property - def xml(self): - """ - Return XML string based on settings accumulated via method calls. - """ - xml = '\n' % NS.OPC_RELATIONSHIPS - for rId, reltype, target, target_mode in self._rels: - xml += ( - a_Relationship() - .with_rId(rId) - .with_reltype(reltype) - .with_target(target) - .with_target_mode(target_mode) - .with_indent(2) - .without_namespace() - .xml - ) - xml += "\n" - return xml - - -class CT_TypesBuilder(BaseBuilder): - """ - Test data builder for CT_Types () XML element, the root element in - [Content_Types].xml files - """ - - def __init__(self): - """Establish instance variables with default values""" - self._defaults = (("xml", "application/xml"), ("jpeg", "image/jpeg")) - self._empty = False - self._overrides = ( - ("/docProps/core.xml", "app/vnd.type1"), - ("/ppt/presentation.xml", "app/vnd.type2"), - ("/docProps/thumbnail.jpeg", "image/jpeg"), - ) - - def empty(self): - self._empty = True - return self - - @property - def xml(self): - """ - Return XML string based on settings accumulated via method calls - """ - if self._empty: - return '\n' % NS.OPC_CONTENT_TYPES - - xml = '\n' % NS.OPC_CONTENT_TYPES - for extension, content_type in self._defaults: - xml += ( - a_Default() - .with_extension(extension) - .with_content_type(content_type) - .with_indent(2) - .without_namespace() - .xml - ) - for partname, content_type in self._overrides: - xml += ( - an_Override() - .with_partname(partname) - .with_content_type(content_type) - .with_indent(2) - .without_namespace() - .xml - ) - xml += "\n" - return xml - - -def a_Default(): - return CT_DefaultBuilder() - - -def a_Relationship(): - return CT_RelationshipBuilder() - - -def a_Relationships(): - return CT_RelationshipsBuilder() - - -def a_Types(): - return CT_TypesBuilder() - - -def an_Override(): - return CT_OverrideBuilder() diff --git a/tests/oxml/shapes/test_autoshape.py b/tests/oxml/shapes/test_autoshape.py index 020246d58..a03bc7f22 100644 --- a/tests/oxml/shapes/test_autoshape.py +++ b/tests/oxml/shapes/test_autoshape.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.autoshape` module.""" -""" -Test suite for pptx.oxml.autoshape module. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -13,8 +9,8 @@ from pptx.oxml.shapes.autoshape import CT_Shape from pptx.oxml.shapes.shared import ST_Direction, ST_PlaceholderSize -from ..unitdata.shape import a_gd, a_prstGeom, an_avLst from ...unitutil.cxml import element +from ..unitdata.shape import a_gd, a_prstGeom, an_avLst class DescribeCT_PresetGeometry2D(object): @@ -77,9 +73,7 @@ def prstGeom_bldr(self, prst, gd_vals): for name, fmla in gd_vals: gd_bldr = a_gd().with_name(name).with_fmla(fmla) avLst_bldr.with_child(gd_bldr) - prstGeom_bldr = ( - a_prstGeom().with_nsdecls().with_prst(prst).with_child(avLst_bldr) - ) + prstGeom_bldr = a_prstGeom().with_nsdecls().with_prst(prst).with_child(avLst_bldr) return prstGeom_bldr @@ -103,8 +97,7 @@ def it_knows_how_to_create_a_new_autoshape_sp(self): 'schemeClr val="lt1"/>\n \n \n \n \n ' '\n \n \n \n \n\n" - % (nsdecls("a", "p"), id_, name, left, top, width, height, prst) + ">\n\n" % (nsdecls("a", "p"), id_, name, left, top, width, height, prst) ) # exercise --------------------- sp = CT_Shape.new_autoshape_sp(id_, name, prst, left, top, width, height) diff --git a/tests/oxml/shapes/test_graphfrm.py b/tests/oxml/shapes/test_graphfrm.py index 887d95290..1f1124ec5 100644 --- a/tests/oxml/shapes/test_graphfrm.py +++ b/tests/oxml/shapes/test_graphfrm.py @@ -1,14 +1,13 @@ -# encoding: utf-8 - """Unit-test suite for pptx.oxml.graphfrm module.""" +from __future__ import annotations + import pytest from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame from ...unitutil.cxml import xml - CHART_URI = "http://schemas.openxmlformats.org/drawingml/2006/chart" TABLE_URI = "http://schemas.openxmlformats.org/drawingml/2006/table" @@ -23,9 +22,7 @@ def it_can_construct_a_new_graphicFrame(self, new_graphicFrame_fixture): def it_can_construct_a_new_chart_graphicFrame(self, new_chart_graphicFrame_fixture): id_, name, rId, x, y, cx, cy, expected_xml = new_chart_graphicFrame_fixture - graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame( - id_, name, rId, x, y, cx, cy - ) + graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy) assert graphicFrame.xml == expected_xml def it_can_construct_a_new_table_graphicFrame(self, new_table_graphicFrame_fixture): diff --git a/tests/oxml/shapes/test_groupshape.py b/tests/oxml/shapes/test_groupshape.py index 66025261f..6884b06cd 100644 --- a/tests/oxml/shapes/test_groupshape.py +++ b/tests/oxml/shapes/test_groupshape.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.shapes.groupshape` module.""" +from __future__ import annotations + import pytest from pptx.oxml.shapes.autoshape import CT_Shape @@ -23,12 +23,8 @@ def it_can_add_a_graphicFrame_element_containing_a_table(self, add_table_fixt): graphicFrame = spTree.add_table(id_, name, rows, cols, x, y, cx, cy) - new_table_graphicFrame_.assert_called_once_with( - id_, name, rows, cols, x, y, cx, cy - ) - insert_element_before_.assert_called_once_with( - spTree, graphicFrame_, "p:extLst" - ) + new_table_graphicFrame_.assert_called_once_with(id_, name, rows, cols, x, y, cx, cy) + insert_element_before_.assert_called_once_with(spTree, graphicFrame_, "p:extLst") assert graphicFrame is graphicFrame_ def it_can_add_a_grpSp_element(self, add_grpSp_fixture): @@ -55,9 +51,7 @@ def it_can_add_an_sp_element_for_a_placeholder(self, add_placeholder_fixt): sp = spTree.add_placeholder(id_, name, ph_type, orient, sz, idx) - CT_Shape_.new_placeholder_sp.assert_called_once_with( - id_, name, ph_type, orient, sz, idx - ) + CT_Shape_.new_placeholder_sp.assert_called_once_with(id_, name, ph_type, orient, sz, idx) insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ @@ -67,9 +61,7 @@ def it_can_add_an_sp_element_for_an_autoshape(self, add_autoshape_fixt): sp = spTree.add_autoshape(id_, name, prst, x, y, cx, cy) - CT_Shape_.new_autoshape_sp.assert_called_once_with( - id_, name, prst, x, y, cx, cy - ) + CT_Shape_.new_autoshape_sp.assert_called_once_with(id_, name, prst, x, y, cx, cy) insert_element_before_.assert_called_once_with(spTree, sp_, "p:extLst") assert sp is sp_ diff --git a/tests/oxml/shapes/test_picture.py b/tests/oxml/shapes/test_picture.py index 9f16599ba..546d6b0fd 100644 --- a/tests/oxml/shapes/test_picture.py +++ b/tests/oxml/shapes/test_picture.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.shapes.picture` module.""" +from __future__ import annotations + import pytest from pptx.oxml.ns import nsdecls diff --git a/tests/oxml/test___init__.py b/tests/oxml/test___init__.py index 176d8ace4..d4d163d09 100644 --- a/tests/oxml/test___init__.py +++ b/tests/oxml/test___init__.py @@ -1,13 +1,8 @@ -# encoding: utf-8 +"""Test suite for pptx.oxml.__init__.py module, primarily XML parser-related.""" -""" -Test suite for pptx.oxml.__init__.py module, primarily XML parser-related. -""" - -from __future__ import print_function, unicode_literals +from __future__ import annotations import pytest - from lxml import etree from pptx.oxml import oxml_parser, parse_xml, register_element_cls diff --git a/tests/oxml/test_dml.py b/tests/oxml/test_dml.py index 8befa16c2..cc205b701 100644 --- a/tests/oxml/test_dml.py +++ b/tests/oxml/test_dml.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.dml` module.""" -""" -Test suite for pptx.oxml.dml module. -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest diff --git a/tests/oxml/test_ns.py b/tests/oxml/test_ns.py index d4c4cc65d..0c4896f76 100644 --- a/tests/oxml/test_ns.py +++ b/tests/oxml/test_ns.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Test suite for pptx.oxml.ns.py module.""" -""" -Test suite for pptx.oxml.ns.py module. -""" - -from __future__ import print_function, unicode_literals +from __future__ import annotations import pytest @@ -44,16 +40,12 @@ def it_formats_namespace_declarations_from_a_list_of_prefixes(self, nsdecls_str) class DescribeNsuri(object): - def it_finds_the_namespace_uri_corresponding_to_a_namespace_prefix( - self, namespace_uri_a - ): + def it_finds_the_namespace_uri_corresponding_to_a_namespace_prefix(self, namespace_uri_a): assert nsuri("a") == namespace_uri_a class DescribeQn(object): - def it_calculates_the_clark_name_for_an_ns_prefixed_tag_string( - self, nsptag_str, clark_name - ): + def it_calculates_the_clark_name_for_an_ns_prefixed_tag_string(self, nsptag_str, clark_name): assert qn(nsptag_str) == clark_name diff --git a/tests/oxml/test_presentation.py b/tests/oxml/test_presentation.py index fc09cb444..1607ab5cc 100644 --- a/tests/oxml/test_presentation.py +++ b/tests/oxml/test_presentation.py @@ -1,46 +1,40 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for pptx.oxml.presentation module -""" +"""Unit-test suite for `pptx.oxml.presentation` module.""" -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations + +from typing import cast import pytest +from pptx.oxml.presentation import CT_SlideIdList + from ..unitutil.cxml import element, xml class DescribeCT_SlideIdList(object): - def it_can_add_a_sldId_element_as_a_child(self, add_fixture): - sldIdLst, expected_xml = add_fixture - sldIdLst.add_sldId("rId1") - assert sldIdLst.xml == expected_xml + """Unit-test suite for `pptx.oxml.presentation.CT_SlideIdLst` objects.""" - def it_knows_the_next_available_slide_id(self, next_id_fixture): - sldIdLst, expected_id = next_id_fixture - assert sldIdLst._next_id == expected_id + def it_can_add_a_sldId_element_as_a_child(self): + sldIdLst = cast(CT_SlideIdList, element("p:sldIdLst/p:sldId{r:id=rId4,id=256}")) - # fixtures ------------------------------------------------------- + sldIdLst.add_sldId("rId1") - @pytest.fixture - def add_fixture(self): - sldIdLst = element("p:sldIdLst/p:sldId{r:id=rId4,id=256}") - expected_xml = xml( + assert sldIdLst.xml == xml( "p:sldIdLst/(p:sldId{r:id=rId4,id=256},p:sldId{r:id=rId1,id=257})" ) - return sldIdLst, expected_xml - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("sldIdLst_cxml", "expected_value"), + [ ("p:sldIdLst", 256), ("p:sldIdLst/p:sldId{id=42}", 256), ("p:sldIdLst/p:sldId{id=256}", 257), ("p:sldIdLst/(p:sldId{id=256},p:sldId{id=712})", 713), ("p:sldIdLst/(p:sldId{id=280},p:sldId{id=257})", 281), - ] + ], ) - def next_id_fixture(self, request): - sldIdLst_cxml, expected_value = request.param - sldIdLst = element(sldIdLst_cxml) - return sldIdLst, expected_value + def it_knows_the_next_available_slide_id(self, sldIdLst_cxml: str, expected_value: int): + sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml)) + assert sldIdLst._next_id == expected_value diff --git a/tests/oxml/test_simpletypes.py b/tests/oxml/test_simpletypes.py index e1a98b2ef..261edf550 100644 --- a/tests/oxml/test_simpletypes.py +++ b/tests/oxml/test_simpletypes.py @@ -1,14 +1,20 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.simpletypes` module. -`simpletypes` contains simple type class definitions. A simple type in this context -corresponds to an `` e.g. `ST_Foobar` definition in the XML schema and -provides data validation and type conversion services for use by xmlchemy. A simple-type -generally corresponds to an element attribute whereas a complex type corresponds to an -XML element (which itself can have multiple attributes and have child elements). +The `simpletypes` module contains classes that each define a scalar-type that appears as an XML +attribute. + +The term "simple-type", as distinct from "complex-type", is an XML Schema distinction. An XML +attribute value must be a single string, and corresponds to a scalar value, like `bool`, `int`, or +`str`. Complex-types describe _elements_, which can have multiple attributes as well as child +elements. + +A simple type corresponds to an `` definition in the XML schema e.g. `ST_Foobar`. +The `BaseSimpleType` subclass provides data validation and type conversion services for use by +`xmlchemy`. """ +from __future__ import annotations + import pytest from pptx.oxml.simpletypes import ( @@ -19,13 +25,13 @@ ST_Percentage, ) -from ..unitutil.mock import method_mock, instance_mock +from ..unitutil.mock import instance_mock, method_mock class DescribeBaseSimpleType(object): """Unit-test suite for `pptx.oxml.simpletypes.BaseSimpleType` objects.""" - def it_can_convert_attr_value_to_python_type( + def it_can_convert_an_XML_attribute_value_to_a_python_type( self, str_value_, py_value_, convert_from_xml_ ): py_value = ST_SimpleType.from_xml(str_value_) diff --git a/tests/oxml/test_slide.py b/tests/oxml/test_slide.py index d1b48ebc4..63b321da7 100644 --- a/tests/oxml/test_slide.py +++ b/tests/oxml/test_slide.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.oxml.slide` module.""" +from __future__ import annotations + from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide from ..unitutil.file import snippet_text diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 02ce4b302..c64196f9b 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Unit-test suite for pptx.oxml.table module""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -36,8 +34,7 @@ def it_can_create_a_new_tbl_element_tree(self): "dyPr/>\n \n \n " "\n \n \n \n \n " " \n \n \n " - "\n \n \n \n\n" - % nsdecls("a") + "\n \n \n \n\n" % nsdecls("a") ) tbl = CT_Table.new_tbl(2, 3, 334, 445) assert tbl.xml == expected_xml @@ -154,8 +151,7 @@ def dimensions_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", [0, 1], []), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", [2, 1], [1, 3]), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", [0, 8], [1, 2, 4, 5, 7, 8], ), @@ -174,8 +170,7 @@ def except_left_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", [0, 1], [1]), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", [2, 1], [2, 3]), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", [0, 8], [3, 4, 5, 6, 7, 8], ), @@ -194,9 +189,7 @@ def in_same_table_fixture(self, request): tbl = element("a:tbl/a:tr/(a:tc,a:tc)") other_tbl = element("a:tbl/a:tr/(a:tc,a:tc)") tc = tbl.xpath("//a:tc")[0] - other_tc = ( - tbl.xpath("//a:tc")[1] if expected_value else other_tbl.xpath("//a:tc")[1] - ) + other_tc = tbl.xpath("//a:tc")[1] if expected_value else other_tbl.xpath("//a:tc")[1] return tc, other_tc, expected_value @pytest.fixture( @@ -205,8 +198,7 @@ def in_same_table_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", (0, 1), (0, 1)), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", (2, 1), (0, 2)), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", (4, 8), (4, 7), ), @@ -225,8 +217,7 @@ def left_col_fixture(self, request): ('a:tbl/a:tr/(a:tc/a:txBody/a:p,a:tc/a:txBody/a:p/a:r/a:t"b")', "b"), ('a:tbl/a:tr/(a:tc/a:txBody/a:p/a:r/a:t"a",a:tc/a:txBody/a:p)', "a"), ( - 'a:tbl/a:tr/(a:tc/a:txBody/a:p/a:r/a:t"a",a:tc/a:txBody/a:p/a:r/a:t' - '"b")', + 'a:tbl/a:tr/(a:tc/a:txBody/a:p/a:r/a:t"a",a:tc/a:txBody/a:p/a:r/a:t' '"b")', "a\nb", ), ( @@ -250,8 +241,7 @@ def move_fixture(self, request): ("a:tbl/(a:tr/a:tc,a:tr/a:tc)", (0, 1), (0,)), ("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))", (2, 1), (0, 1)), ( - "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" - ",a:tc))", + "a:tbl/(a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc,a:tc),a:tr/(a:tc,a:tc" ",a:tc))", (4, 8), (4, 5), ), diff --git a/tests/oxml/test_theme.py b/tests/oxml/test_theme.py index 9bff00568..87d051726 100644 --- a/tests/oxml/test_theme.py +++ b/tests/oxml/test_theme.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.oxml.theme` module.""" -""" -Test suite for pptx.oxml.theme module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index 6fd88f831..abb38b7f8 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -1,12 +1,10 @@ -# encoding: utf-8 +"""Unit-test suite for the `pptx.oxml.xmlchemy` module. -""" -Test suite for the pptx.oxml.xmlchemy module, focused on the metaclass and -element and attribute definition classes. A major part of the fixture is -provided by the metaclass-built test classes at the end of the file. +Focused on the metaclass and element and attribute definition classes. A major part of the fixture +is provided by the metaclass-built test classes at the end of the file. """ -from __future__ import absolute_import, print_function +from __future__ import annotations import pytest @@ -48,22 +46,16 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, choice, expected_xml = insert_fixture parent._insert_choice(choice) assert parent.xml == expected_xml - assert parent._insert_choice.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_choice.__doc__.startswith("Return the passed ```` ") def it_adds_an_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture choice = parent._add_choice() assert parent.xml == expected_xml assert isinstance(choice, CT_Choice) - assert parent._add_choice.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_choice.__doc__.startswith("Add a new ```` child element ") - def it_adds_a_get_or_change_to_method_for_the_child_element( - self, get_or_change_to_fixture - ): + def it_adds_a_get_or_change_to_method_for_the_child_element(self, get_or_change_to_fixture): parent, expected_xml = get_or_change_to_fixture choice = parent.get_or_change_to_choice() assert isinstance(choice, CT_Choice) @@ -77,9 +69,7 @@ def add_fixture(self): expected_xml = self.parent_bldr("choice").xml() return parent, expected_xml - @pytest.fixture( - params=[("choice2", "choice"), (None, "choice"), ("choice", "choice")] - ) + @pytest.fixture(params=[("choice2", "choice"), (None, "choice"), ("choice", "choice")]) def get_or_change_to_fixture(self, request): before_member_tag, after_member_tag = request.param parent = self.parent_bldr(before_member_tag).element @@ -96,10 +86,7 @@ def getter_fixture(self, request): @pytest.fixture def insert_fixture(self): parent = ( - a_parent() - .with_nsdecls() - .with_child(an_oomChild()) - .with_child(an_oooChild()) + a_parent().with_nsdecls().with_child(an_oomChild()).with_child(an_oooChild()) ).element choice = a_choice().with_nsdecls().element expected_xml = ( @@ -156,27 +143,21 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, oomChild, expected_xml = insert_fixture parent._insert_oomChild(oomChild) assert parent.xml == expected_xml - assert parent._insert_oomChild.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_oomChild.__doc__.startswith("Return the passed ```` ") def it_adds_a_private_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture oomChild = parent._add_oomChild() assert parent.xml == expected_xml assert isinstance(oomChild, CT_OomChild) - assert parent._add_oomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_oomChild.__doc__.startswith("Add a new ```` child element ") def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture oomChild = parent.add_oomChild() assert parent.xml == expected_xml assert isinstance(oomChild, CT_OomChild) - assert parent._add_oomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_oomChild.__doc__.startswith("Add a new ```` child element ") # fixtures ------------------------------------------------------- @@ -238,9 +219,7 @@ def it_adds_a_setter_property_for_the_attr(self, setter_fixture): assert parent.xml == expected_xml def it_adds_a_docstring_for_the_property(self): - assert CT_Parent.optAttr.__doc__.startswith( - "ST_IntegerType type-converted value of " - ) + assert CT_Parent.optAttr.__doc__.startswith("ST_IntegerType type-converted value of ") # fixtures ------------------------------------------------------- @@ -271,9 +250,7 @@ def it_adds_a_setter_property_for_the_attr(self, setter_fixture): assert parent.xml == expected_xml def it_adds_a_docstring_for_the_property(self): - assert CT_Parent.reqAttr.__doc__.startswith( - "ST_IntegerType type-converted value of " - ) + assert CT_Parent.reqAttr.__doc__.startswith("ST_IntegerType type-converted value of ") def it_raises_on_get_when_attribute_not_present(self): parent = a_parent().with_nsdecls().element @@ -320,18 +297,14 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, zomChild, expected_xml = insert_fixture parent._insert_zomChild(zomChild) assert parent.xml == expected_xml - assert parent._insert_zomChild.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_zomChild.__doc__.startswith("Return the passed ```` ") def it_adds_an_add_method_for_the_child_element(self, add_fixture): parent, expected_xml = add_fixture zomChild = parent._add_zomChild() assert parent.xml == expected_xml assert isinstance(zomChild, CT_ZomChild) - assert parent._add_zomChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_zomChild.__doc__.startswith("Add a new ```` child element ") def it_removes_the_property_root_name_used_for_declaration(self): assert not hasattr(CT_Parent, "zomChild") @@ -393,17 +366,13 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture): zooChild = parent._add_zooChild() assert parent.xml == expected_xml assert isinstance(zooChild, CT_ZooChild) - assert parent._add_zooChild.__doc__.startswith( - "Add a new ```` child element " - ) + assert parent._add_zooChild.__doc__.startswith("Add a new ```` child element ") def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent, zooChild, expected_xml = insert_fixture parent._insert_zooChild(zooChild) assert parent.xml == expected_xml - assert parent._insert_zooChild.__doc__.startswith( - "Return the passed ```` " - ) + assert parent._insert_zooChild.__doc__.startswith("Return the passed ```` ") def it_adds_a_get_or_add_method_for_the_child_element(self, get_or_add_fixture): parent, expected_xml = get_or_add_fixture @@ -522,9 +491,7 @@ class CT_Parent(BaseOxmlElement): (Choice("p:choice"), Choice("p:choice2")), successors=("p:oomChild", "p:oooChild"), ) - oomChild = OneOrMore( - "p:oomChild", successors=("p:oooChild", "p:zomChild", "p:zooChild") - ) + oomChild = OneOrMore("p:oomChild", successors=("p:oooChild", "p:zomChild", "p:zooChild")) oooChild = OneAndOnlyOne("p:oooChild") zomChild = ZeroOrMore("p:zomChild", successors=("p:zooChild",)) zooChild = ZeroOrOne("p:zooChild", successors=()) diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index 5573116f2..8c716ab81 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""XML test data builders for pptx.oxml.dml unit tests.""" -""" -XML test data builders for pptx.oxml.dml unit tests -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/shape.py b/tests/oxml/unitdata/shape.py index 560657e8a..a5a39360a 100644 --- a/tests/oxml/unitdata/shape.py +++ b/tests/oxml/unitdata/shape.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Test data for autoshape-related unit tests.""" +from __future__ import annotations + from ...unitdata import BaseBuilder diff --git a/tests/oxml/unitdata/text.py b/tests/oxml/unitdata/text.py index 23753fdc8..b86ff45d7 100644 --- a/tests/oxml/unitdata/text.py +++ b/tests/oxml/unitdata/text.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""XML test data builders for `pptx.oxml.text` unit tests.""" -""" -XML test data builders for pptx.oxml.text unit tests -""" - -from __future__ import absolute_import, print_function +from __future__ import annotations from ...unitdata import BaseBuilder diff --git a/tests/parts/test_chart.py b/tests/parts/test_chart.py index ca7fe7771..b0a41f581 100644 --- a/tests/parts/test_chart.py +++ b/tests/parts/test_chart.py @@ -1,13 +1,14 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.chart` module.""" +from __future__ import annotations + import pytest from pptx.chart.chart import Chart from pptx.chart.data import ChartData from pptx.enum.chart import XL_CHART_TYPE as XCT -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import OpcPackage from pptx.opc.packuri import PackURI from pptx.oxml.chart.chart import CT_ChartSpace @@ -28,9 +29,7 @@ def it_can_construct_from_chart_type_and_data(self, request): package_.next_partname.return_value = PackURI("/ppt/charts/chart42.xml") chart_part_ = instance_mock(request, ChartPart) # --- load() must have autospec turned off to work in Python 2.7 mock --- - load_ = method_mock( - request, ChartPart, "load", autospec=False, return_value=chart_part_ - ) + load_ = method_mock(request, ChartPart, "load", autospec=False, return_value=chart_part_) chart_part = ChartPart.new(XCT.RADAR, chart_data_, package_) @@ -39,9 +38,7 @@ def it_can_construct_from_chart_type_and_data(self, request): load_.assert_called_once_with( "/ppt/charts/chart42.xml", CT.DML_CHART, package_, b"chart-blob" ) - chart_part_.chart_workbook.update_from_xlsx_blob.assert_called_once_with( - b"xlsx-blob" - ) + chart_part_.chart_workbook.update_from_xlsx_blob.assert_called_once_with(b"xlsx-blob") assert chart_part is chart_part_ def it_provides_access_to_the_chart_object(self, request, chartSpace_): @@ -129,9 +126,7 @@ def it_adds_an_xlsx_part_on_update_if_needed( EmbeddedXlsxPart_.new.assert_called_once_with(b"xlsx-blob", package_) xlsx_part_prop_.assert_called_with(xlsx_part_) - def but_it_replaces_the_xlsx_blob_when_the_part_exists( - self, xlsx_part_prop_, xlsx_part_ - ): + def but_it_replaces_the_xlsx_blob_when_the_part_exists(self, xlsx_part_prop_, xlsx_part_): xlsx_part_prop_.return_value = xlsx_part_ chart_data = ChartWorkbook(None, None) chart_data.update_from_xlsx_blob(b"xlsx-blob") diff --git a/tests/parts/test_coreprops.py b/tests/parts/test_coreprops.py index 3f20ca933..0983218e4 100644 --- a/tests/parts/test_coreprops.py +++ b/tests/parts/test_coreprops.py @@ -1,3 +1,5 @@ +# pyright: reportPrivateUsage=false + """Unit-test suite for `pptx.parts.coreprops` module.""" from __future__ import annotations @@ -14,68 +16,71 @@ class DescribeCorePropertiesPart(object): """Unit-test suite for `pptx.parts.coreprops.CorePropertiesPart` objects.""" - def it_knows_the_string_property_values(self, str_prop_get_fixture): - core_properties, prop_name, expected_value = str_prop_get_fixture - actual_value = getattr(core_properties, prop_name) - assert actual_value == expected_value - - def it_can_change_the_string_property_values(self, str_prop_set_fixture): - core_properties, prop_name, value, expected_xml = str_prop_set_fixture - setattr(core_properties, prop_name, value) - assert core_properties._element.xml == expected_xml - - def it_knows_the_date_property_values(self, date_prop_get_fixture): - core_properties, prop_name, expected_datetime = date_prop_get_fixture - actual_datetime = getattr(core_properties, prop_name) - assert actual_datetime == expected_datetime + @pytest.mark.parametrize( + ("prop_name", "expected_value"), + [ + ("author", "python-pptx"), + ("category", ""), + ("comments", ""), + ("content_status", "DRAFT"), + ("identifier", "GXS 10.2.1ab"), + ("keywords", "foo bar baz"), + ("language", "US-EN"), + ("last_modified_by", "Steve Canny"), + ("subject", "Spam"), + ("title", "Presentation"), + ("version", "1.2.88"), + ], + ) + def it_knows_the_string_property_values( + self, core_properties: CorePropertiesPart, prop_name: str, expected_value: str + ): + assert getattr(core_properties, prop_name) == expected_value + + @pytest.mark.parametrize( + ("prop_name", "tagname", "value"), + [ + ("author", "dc:creator", "scanny"), + ("category", "cp:category", "silly stories"), + ("comments", "dc:description", "Bar foo to you"), + ("content_status", "cp:contentStatus", "FINAL"), + ("identifier", "dc:identifier", "GT 5.2.xab"), + ("keywords", "cp:keywords", "dog cat moo"), + ("language", "dc:language", "GB-EN"), + ("last_modified_by", "cp:lastModifiedBy", "Billy Bob"), + ("subject", "dc:subject", "Eggs"), + ("title", "dc:title", "Dissertation"), + ("version", "cp:version", "81.2.8"), + ], + ) + def it_can_change_the_string_property_values(self, prop_name: str, tagname: str, value: str): + coreProperties = self.coreProperties_xml(None, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore - def it_can_change_the_date_property_values(self, date_prop_set_fixture): - core_properties, prop_name, value, expected_xml = date_prop_set_fixture setattr(core_properties, prop_name, value) - assert core_properties._element.xml == expected_xml - - def it_knows_the_revision_number(self, revision_get_fixture): - core_properties, expected_revision = revision_get_fixture - assert core_properties.revision == expected_revision - - def it_can_change_the_revision_number(self, revision_set_fixture): - core_properties, revision, expected_xml = revision_set_fixture - core_properties.revision = revision - assert core_properties._element.xml == expected_xml - - def it_can_construct_a_default_core_props(self): - core_props = CorePropertiesPart.default(None) - # verify ----------------------- - assert isinstance(core_props, CorePropertiesPart) - assert core_props.content_type is CT.OPC_CORE_PROPERTIES - assert core_props.partname == "/docProps/core.xml" - assert isinstance(core_props._element, CT_CoreProperties) - assert core_props.title == "PowerPoint Presentation" - assert core_props.last_modified_by == "python-pptx" - assert core_props.revision == 1 - # core_props.modified only stores time with seconds resolution, so - # comparison needs to be a little loose (within two seconds) - modified_timedelta = ( - dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) - core_props.modified - ) - max_expected_timedelta = dt.timedelta(seconds=2) - assert modified_timedelta < max_expected_timedelta - # fixtures ------------------------------------------------------- + assert core_properties._element.xml == self.coreProperties_xml(tagname, value) - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("prop_name", "expected_value"), + [ ("created", dt.datetime(2012, 11, 17, 16, 37, 40)), ("last_printed", dt.datetime(2014, 6, 4, 4, 28)), ("modified", None), - ] + ], ) - def date_prop_get_fixture(self, request, core_properties): - prop_name, expected_datetime = request.param - return core_properties, prop_name, expected_datetime + def it_knows_the_date_property_values( + self, + core_properties: CorePropertiesPart, + prop_name: str, + expected_value: dt.datetime | None, + ): + actual_datetime = getattr(core_properties, prop_name) + assert actual_datetime == expected_value - @pytest.fixture( - params=[ + @pytest.mark.parametrize( + ("prop_name", "tagname", "value", "str_val", "attrs"), + [ ( "created", "dcterms:created", @@ -97,75 +102,59 @@ def date_prop_get_fixture(self, request, core_properties): "2005-04-03T02:01:00Z", ' xsi:type="dcterms:W3CDTF"', ), - ] - ) - def date_prop_set_fixture(self, request): - prop_name, tagname, value, str_val, attrs = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, None, coreProperties) - expected_xml = self.coreProperties(tagname, str_val, attrs) - return core_properties, prop_name, value, expected_xml - - @pytest.fixture( - params=[ - ("author", "python-pptx"), - ("category", ""), - ("comments", ""), - ("content_status", "DRAFT"), - ("identifier", "GXS 10.2.1ab"), - ("keywords", "foo bar baz"), - ("language", "US-EN"), - ("last_modified_by", "Steve Canny"), - ("subject", "Spam"), - ("title", "Presentation"), - ("version", "1.2.88"), - ] + ], ) - def str_prop_get_fixture(self, request, core_properties): - prop_name, expected_value = request.param - return core_properties, prop_name, expected_value + def it_can_change_the_date_property_values( + self, prop_name: str, tagname: str, value: dt.datetime, str_val: str, attrs: str + ): + coreProperties = self.coreProperties_xml(None, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore - @pytest.fixture( - params=[ - ("author", "dc:creator", "scanny"), - ("category", "cp:category", "silly stories"), - ("comments", "dc:description", "Bar foo to you"), - ("content_status", "cp:contentStatus", "FINAL"), - ("identifier", "dc:identifier", "GT 5.2.xab"), - ("keywords", "cp:keywords", "dog cat moo"), - ("language", "dc:language", "GB-EN"), - ("last_modified_by", "cp:lastModifiedBy", "Billy Bob"), - ("subject", "dc:subject", "Eggs"), - ("title", "dc:title", "Dissertation"), - ("version", "cp:version", "81.2.8"), - ] + setattr(core_properties, prop_name, value) + + assert core_properties._element.xml == self.coreProperties_xml(tagname, str_val, attrs) + + @pytest.mark.parametrize( + ("str_val", "expected_value"), + [("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)], ) - def str_prop_set_fixture(self, request): - prop_name, tagname, value = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, None, coreProperties) - expected_xml = self.coreProperties(tagname, value) - return core_properties, prop_name, value, expected_xml - - @pytest.fixture(params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)]) - def revision_get_fixture(self, request): - str_val, expected_revision = request.param + def it_knows_the_revision_number(self, str_val: str | None, expected_value: int): tagname = "" if str_val is None else "cp:revision" - coreProperties = self.coreProperties(tagname, str_val) - core_properties = CorePropertiesPart.load(None, None, None, coreProperties) - return core_properties, expected_revision + coreProperties = self.coreProperties_xml(tagname, str_val) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore + + assert core_properties.revision == expected_value + + def it_can_change_the_revision_number(self): + coreProperties = self.coreProperties_xml(None, None) + core_properties = CorePropertiesPart.load(None, None, None, coreProperties) # type: ignore + + core_properties.revision = 42 - @pytest.fixture(params=[(42, "42")]) - def revision_set_fixture(self, request): - value, str_val = request.param - coreProperties = self.coreProperties(None, None) - core_properties = CorePropertiesPart.load(None, None, None, coreProperties) - expected_xml = self.coreProperties("cp:revision", str_val) - return core_properties, value, expected_xml + assert core_properties._element.xml == self.coreProperties_xml("cp:revision", "42") + + def it_can_construct_a_default_core_props(self): + core_props = CorePropertiesPart.default(None) # type: ignore + # verify ----------------------- + assert isinstance(core_props, CorePropertiesPart) + assert core_props.content_type is CT.OPC_CORE_PROPERTIES + assert core_props.partname == "/docProps/core.xml" + assert isinstance(core_props._element, CT_CoreProperties) + assert core_props.title == "PowerPoint Presentation" + assert core_props.last_modified_by == "python-pptx" + assert core_props.revision == 1 + assert core_props.modified is not None + # core_props.modified only stores time with seconds resolution, so + # comparison needs to be a little loose (within two seconds) + modified_timedelta = ( + dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) - core_props.modified + ) + max_expected_timedelta = dt.timedelta(seconds=2) + assert modified_timedelta < max_expected_timedelta - # fixture components --------------------------------------------- + # -- fixtures ---------------------------------------------------- - def coreProperties(self, tagname, str_val, attrs=""): + def coreProperties_xml(self, tagname: str | None, str_val: str | None, attrs: str = "") -> str: tmpl = ( '1.2.88\n" b"\n" ) - return CorePropertiesPart.load(None, None, None, xml) + return CorePropertiesPart.load(None, None, None, xml) # type: ignore diff --git a/tests/parts/test_embeddedpackage.py b/tests/parts/test_embeddedpackage.py index 1f368d557..ae2aca82f 100644 --- a/tests/parts/test_embeddedpackage.py +++ b/tests/parts/test_embeddedpackage.py @@ -1,35 +1,35 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.embeddedpackage` module.""" +from __future__ import annotations + import pytest from pptx.enum.shapes import PROG_ID from pptx.opc.constants import CONTENT_TYPE as CT from pptx.opc.package import OpcPackage, PackURI from pptx.parts.embeddedpackage import ( - EmbeddedPackagePart, EmbeddedDocxPart, + EmbeddedPackagePart, EmbeddedPptxPart, EmbeddedXlsxPart, ) -from ..unitutil.mock import ANY, class_mock, initializer_mock, instance_mock +from ..unitutil.mock import ANY, FixtureRequest, class_mock, initializer_mock, instance_mock class DescribeEmbeddedPackagePart(object): """Unit-test suite for `pptx.parts.embeddedpackage.EmbeddedPackagePart` objects.""" @pytest.mark.parametrize( - "prog_id, EmbeddedPartCls", - ( + ("prog_id", "EmbeddedPartCls"), + [ (PROG_ID.DOCX, EmbeddedDocxPart), (PROG_ID.PPTX, EmbeddedPptxPart), (PROG_ID.XLSX, EmbeddedXlsxPart), - ), + ], ) def it_provides_a_factory_that_creates_a_package_part_for_MS_Office_files( - self, request, prog_id, EmbeddedPartCls + self, request: FixtureRequest, prog_id: PROG_ID, EmbeddedPartCls: type ): object_blob_ = b"0123456789" package_ = instance_mock(request, OpcPackage) @@ -44,7 +44,7 @@ def it_provides_a_factory_that_creates_a_package_part_for_MS_Office_files( EmbeddedPartCls_.new.assert_called_once_with(object_blob_, package_) assert ole_object_part is embedded_object_part_ - def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request): + def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request: FixtureRequest): progId = "Foo.Bar.42" object_blob_ = b"0123456789" package_ = instance_mock(request, OpcPackage) @@ -54,15 +54,11 @@ def but_it_creates_a_generic_object_part_for_non_MS_Office_files(self, request): ole_object_part = EmbeddedPackagePart.factory(progId, object_blob_, package_) - package_.next_partname.assert_called_once_with( - "/ppt/embeddings/oleObject%d.bin" - ) - _init_.assert_called_once_with( - ANY, partname_, CT.OFC_OLE_OBJECT, package_, object_blob_ - ) + package_.next_partname.assert_called_once_with("/ppt/embeddings/oleObject%d.bin") + _init_.assert_called_once_with(ANY, partname_, CT.OFC_OLE_OBJECT, package_, object_blob_) assert isinstance(ole_object_part, EmbeddedPackagePart) - def it_provides_a_contructor_classmethod_for_subclasses(self, request): + def it_provides_a_contructor_classmethod_for_subclasses(self, request: FixtureRequest): blob_ = b"0123456789" package_ = instance_mock(request, OpcPackage) _init_ = initializer_mock(request, EmbeddedXlsxPart, autospec=True) @@ -71,9 +67,7 @@ def it_provides_a_contructor_classmethod_for_subclasses(self, request): xlsx_part = EmbeddedXlsxPart.new(blob_, package_) - package_.next_partname.assert_called_once_with( - EmbeddedXlsxPart.partname_template - ) + package_.next_partname.assert_called_once_with(EmbeddedXlsxPart.partname_template) _init_.assert_called_once_with( xlsx_part, partname_, EmbeddedXlsxPart.content_type, package_, blob_ ) diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 8e1f68274..386e3fce9 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -1,10 +1,11 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.image` module.""" +from __future__ import annotations + +import io + import pytest -from pptx.compat import BytesIO from pptx.package import Package from pptx.parts.image import Image, ImagePart from pptx.util import Emu @@ -18,7 +19,6 @@ property_mock, ) - images_pptx_path = absjoin(test_file_dir, "with_images.pptx") test_image_path = absjoin(test_file_dir, "python-icon.jpeg") @@ -67,9 +67,7 @@ def it_provides_access_to_its_image(self, request, image_): (3337, 9999, 3337, 9999), ), ) - def it_can_scale_its_dimensions( - self, width, height, expected_width, expected_height - ): + def it_can_scale_its_dimensions(self, width, height, expected_width, expected_height): with open(test_image_path, "rb") as f: blob = f.read() image_part = ImagePart(None, None, None, blob) @@ -211,7 +209,7 @@ def filename_fixture(self, request): def from_stream_fixture(self, from_blob_, image_): with open(test_image_path, "rb") as f: blob = f.read() - image_file = BytesIO(blob) + image_file = io.BytesIO(blob) from_blob_.return_value = image_ return image_file, blob, image_ diff --git a/tests/parts/test_media.py b/tests/parts/test_media.py index f183d7c47..f7095f35d 100644 --- a/tests/parts/test_media.py +++ b/tests/parts/test_media.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit test suite for `pptx.parts.media` module.""" +from __future__ import annotations + from pptx.media import Video from pptx.package import Package from pptx.parts.media import MediaPart diff --git a/tests/parts/test_presentation.py b/tests/parts/test_presentation.py index 7089e73de..edde4c44c 100644 --- a/tests/parts/test_presentation.py +++ b/tests/parts/test_presentation.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.presentation` module.""" +from __future__ import annotations + import pytest from pptx.opc.constants import RELATIONSHIP_TYPE as RT @@ -62,9 +62,7 @@ def but_it_adds_a_notes_master_part_when_needed( The notes master present case is just above. """ - NotesMasterPart_ = class_mock( - request, "pptx.parts.presentation.NotesMasterPart" - ) + NotesMasterPart_ = class_mock(request, "pptx.parts.presentation.NotesMasterPart") NotesMasterPart_.create_default.return_value = notes_master_part_ part_related_by_.side_effect = KeyError prs_part = PresentationPart(None, None, package_, None) @@ -72,9 +70,7 @@ def but_it_adds_a_notes_master_part_when_needed( notes_master_part = prs_part.notes_master_part NotesMasterPart_.create_default.assert_called_once_with(package_) - relate_to_.assert_called_once_with( - prs_part, notes_master_part_, RT.NOTES_MASTER - ) + relate_to_.assert_called_once_with(prs_part, notes_master_part_, RT.NOTES_MASTER) assert notes_master_part is notes_master_part_ def it_provides_access_to_its_notes_master(self, request, notes_master_part_): @@ -100,12 +96,8 @@ def it_provides_access_to_a_related_slide(self, request, slide_, related_part_): related_part_.assert_called_once_with(prs_part, "rId42") assert slide is slide_ - def it_provides_access_to_a_related_master( - self, request, slide_master_, related_part_ - ): - slide_master_part_ = instance_mock( - request, SlideMasterPart, slide_master=slide_master_ - ) + def it_provides_access_to_a_related_master(self, request, slide_master_, related_part_): + slide_master_part_ = instance_mock(request, SlideMasterPart, slide_master=slide_master_) related_part_.return_value = slide_master_part_ prs_part = PresentationPart(None, None, None, None) @@ -131,14 +123,10 @@ def it_can_save_the_package_to_a_file(self, package_): PresentationPart(None, None, package_, None).save("prs.pptx") package_.save.assert_called_once_with("prs.pptx") - def it_can_add_a_new_slide( - self, request, package_, slide_part_, slide_, relate_to_ - ): + def it_can_add_a_new_slide(self, request, package_, slide_part_, slide_, relate_to_): slide_layout_ = instance_mock(request, SlideLayout) partname = PackURI("/ppt/slides/slide9.xml") - property_mock( - request, PresentationPart, "_next_slide_partname", return_value=partname - ) + property_mock(request, PresentationPart, "_next_slide_partname", return_value=partname) SlidePart_ = class_mock(request, "pptx.parts.presentation.SlidePart") SlidePart_.new.return_value = slide_part_ relate_to_.return_value = "rId42" @@ -181,9 +169,7 @@ def it_raises_on_slide_id_not_found(self, slide_part_, related_part_): prs_part.slide_id(slide_part_) @pytest.mark.parametrize("is_present", (True, False)) - def it_finds_a_slide_by_slide_id( - self, is_present, slide_, slide_part_, related_part_ - ): + def it_finds_a_slide_by_slide_id(self, is_present, slide_, slide_part_, related_part_): prs_elm = element( "p:presentation/p:sldIdLst/(p:sldId{r:id=a,id=256},p:sldId{r:id=" "b,id=257},p:sldId{r:id=c,id=258})" diff --git a/tests/parts/test_slide.py b/tests/parts/test_slide.py index 58929d124..9eb2f11b0 100644 --- a/tests/parts/test_slide.py +++ b/tests/parts/test_slide.py @@ -1,14 +1,15 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.parts.slide` module.""" +from __future__ import annotations + import pytest from pptx.chart.data import ChartData from pptx.enum.chart import XL_CHART_TYPE as XCT from pptx.enum.shapes import PROG_ID from pptx.media import Video -from pptx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import Part from pptx.opc.packuri import PackURI from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide @@ -44,9 +45,7 @@ class DescribeBaseSlidePart(object): """Unit-test suite for `pptx.parts.slide.BaseSlidePart` objects.""" def it_knows_its_name(self): - slide_part = BaseSlidePart( - None, None, None, element("p:sld/p:cSld{name=Foobar}") - ) + slide_part = BaseSlidePart(None, None, None, element("p:sld/p:cSld{name=Foobar}")) assert slide_part.name == "Foobar" def it_can_get_a_related_image_by_rId(self, request, image_part_): @@ -65,9 +64,7 @@ def it_can_get_a_related_image_by_rId(self, request, image_part_): def it_can_add_an_image_part(self, request, image_part_): package_ = instance_mock(request, Package) package_.get_or_add_image_part.return_value = image_part_ - relate_to_ = method_mock( - request, BaseSlidePart, "relate_to", return_value="rId6" - ) + relate_to_ = method_mock(request, BaseSlidePart, "relate_to", return_value="rId6") slide_part = BaseSlidePart(None, None, package_, None) image_part, rId = slide_part.get_or_add_image_part("foobar.png") @@ -87,9 +84,7 @@ def image_part_(self, request): class DescribeNotesMasterPart(object): """Unit-test suite for `pptx.parts.slide.NotesMasterPart` objects.""" - def it_can_create_a_notes_master_part( - self, request, package_, notes_master_part_, theme_part_ - ): + def it_can_create_a_notes_master_part(self, request, package_, notes_master_part_, theme_part_): method_mock( request, NotesMasterPart, @@ -124,9 +119,7 @@ def it_provides_access_to_its_notes_master(self, request): NotesMaster_.assert_called_once_with(notesMaster, notes_master_part) assert notes_master is notes_master_ - def it_creates_a_new_notes_master_part_to_help( - self, request, package_, notes_master_part_ - ): + def it_creates_a_new_notes_master_part_to_help(self, request, package_, notes_master_part_): NotesMasterPart_ = class_mock( request, "pptx.parts.slide.NotesMasterPart", return_value=notes_master_part_ ) @@ -151,9 +144,7 @@ def it_creates_a_new_notes_master_part_to_help( assert notes_master_part is notes_master_part_ def it_creates_a_new_theme_part_to_help(self, request, package_, theme_part_): - XmlPart_ = class_mock( - request, "pptx.parts.slide.XmlPart", return_value=theme_part_ - ) + XmlPart_ = class_mock(request, "pptx.parts.slide.XmlPart", return_value=theme_part_) theme_elm = element("p:theme") method_mock( request, @@ -216,15 +207,11 @@ def it_can_create_a_notes_slide_part( notes_slide_part = NotesSlidePart.new(package_, slide_part_) - _add_notes_slide_part_.assert_called_once_with( - package_, slide_part_, notes_master_part_ - ) + _add_notes_slide_part_.assert_called_once_with(package_, slide_part_, notes_master_part_) notes_slide_.clone_master_placeholders.assert_called_once_with(notes_master_) assert notes_slide_part is notes_slide_part_ - def it_provides_access_to_the_notes_master( - self, request, notes_master_, notes_master_part_ - ): + def it_provides_access_to_the_notes_master(self, request, notes_master_, notes_master_part_): part_related_by_ = method_mock( request, NotesSlidePart, "part_related_by", return_value=notes_master_part_ ) @@ -237,9 +224,7 @@ def it_provides_access_to_the_notes_master( assert notes_master is notes_master_ def it_provides_access_to_its_notes_slide(self, request, notes_slide_): - NotesSlide_ = class_mock( - request, "pptx.parts.slide.NotesSlide", return_value=notes_slide_ - ) + NotesSlide_ = class_mock(request, "pptx.parts.slide.NotesSlide", return_value=notes_slide_) notes = element("p:notes") notes_slide_part = NotesSlidePart(None, None, None, notes) @@ -255,20 +240,14 @@ def it_adds_a_notes_slide_part_to_help( request, "pptx.parts.slide.NotesSlidePart", return_value=notes_slide_part_ ) notes = element("p:notes") - new_ = method_mock( - request, CT_NotesSlide, "new", autospec=False, return_value=notes - ) - package_.next_partname.return_value = PackURI( - "/ppt/notesSlides/notesSlide42.xml" - ) + new_ = method_mock(request, CT_NotesSlide, "new", autospec=False, return_value=notes) + package_.next_partname.return_value = PackURI("/ppt/notesSlides/notesSlide42.xml") notes_slide_part = NotesSlidePart._add_notes_slide_part( package_, slide_part_, notes_master_part_ ) - package_.next_partname.assert_called_once_with( - "/ppt/notesSlides/notesSlide%d.xml" - ) + package_.next_partname.assert_called_once_with("/ppt/notesSlides/notesSlide%d.xml") new_.assert_called_once_with() NotesSlidePart_.assert_called_once_with( PackURI("/ppt/notesSlides/notesSlide42.xml"), @@ -354,9 +333,7 @@ def it_can_add_an_embedded_ole_object_part( request, SlidePart, "_blob_from_file", return_value=b"012345" ) embedded_package_part_ = instance_mock(request, EmbeddedPackagePart) - EmbeddedPackagePart_ = class_mock( - request, "pptx.parts.slide.EmbeddedPackagePart" - ) + EmbeddedPackagePart_ = class_mock(request, "pptx.parts.slide.EmbeddedPackagePart") EmbeddedPackagePart_.factory.return_value = embedded_package_part_ relate_to_.return_value = "rId9" slide_part = SlidePart(None, None, package_, None) @@ -364,9 +341,7 @@ def it_can_add_an_embedded_ole_object_part( _rId = slide_part.add_embedded_ole_object_part(prog_id, "workbook.xlsx") _blob_from_file_.assert_called_once_with(slide_part, "workbook.xlsx") - EmbeddedPackagePart_.factory.assert_called_once_with( - prog_id, b"012345", package_ - ) + EmbeddedPackagePart_.factory.assert_called_once_with(prog_id, b"012345", package_) relate_to_.assert_called_once_with(slide_part, embedded_package_part_, rel_type) assert _rId == "rId9" @@ -394,9 +369,7 @@ def it_can_create_a_new_slide_part(self, request, package_, relate_to_): slide_part = SlidePart.new(partname, package_, slide_layout_part_) - _init_.assert_called_once_with( - slide_part, partname, CT.PML_SLIDE, package_, sld - ) + _init_.assert_called_once_with(slide_part, partname, CT.PML_SLIDE, package_, sld) slide_part.relate_to.assert_called_once_with( slide_part, slide_layout_part_, RT.SLIDE_LAYOUT ) @@ -559,9 +532,7 @@ class DescribeSlideLayoutPart(object): def it_provides_access_to_its_slide_master(self, request): slide_master_ = instance_mock(request, SlideMaster) - slide_master_part_ = instance_mock( - request, SlideMasterPart, slide_master=slide_master_ - ) + slide_master_part_ = instance_mock(request, SlideMasterPart, slide_master=slide_master_) part_related_by_ = method_mock( request, SlideLayoutPart, "part_related_by", return_value=slide_master_part_ ) @@ -604,9 +575,7 @@ def it_provides_access_to_its_slide_master(self, request): def it_provides_access_to_a_related_slide_layout(self, request): slide_layout_ = instance_mock(request, SlideLayout) - slide_layout_part_ = instance_mock( - request, SlideLayoutPart, slide_layout=slide_layout_ - ) + slide_layout_part_ = instance_mock(request, SlideLayoutPart, slide_layout=slide_layout_) related_part_ = method_mock( request, SlideMasterPart, "related_part", return_value=slide_layout_part_ ) diff --git a/tests/shapes/test_autoshape.py b/tests/shapes/test_autoshape.py index 9e6173caf..efb38e6b9 100644 --- a/tests/shapes/test_autoshape.py +++ b/tests/shapes/test_autoshape.py @@ -1,8 +1,10 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for pptx.shapes.autoshape module.""" +"""Unit-test suite for `pptx.shapes.autoshape` module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, cast import pytest @@ -26,45 +28,76 @@ from ..unitutil.cxml import element, xml from ..unitutil.mock import class_mock, instance_mock, property_mock +if TYPE_CHECKING: + from pptx.spec import AdjustmentValue -class DescribeAdjustment(object): - def it_knows_its_effective_value(self, effective_val_fixture_): - adjustment, expected_effective_value = effective_val_fixture_ - assert adjustment.effective_value == expected_effective_value - # fixture -------------------------------------------------------- +class DescribeAdjustment(object): + """Unit-test suite for `pptx.shapes.autoshape.Adjustment`.""" - def _effective_adj_val_cases(): - return [ - # no actual, effective should be determined by default value + @pytest.mark.parametrize( + ("def_val", "actual", "expected_value"), + [ + # -- no actual, effective should be determined by default value -- (50000, None, 0.5), - # actual matches default + # -- actual matches default -- (50000, 50000, 0.5), - # actual is different than default + # -- actual is different than default -- (50000, 12500, 0.125), - # actual is zero + # -- actual is zero -- (50000, 0, 0.0), - # negative default + # -- negative default -- (-20833, None, -0.20833), - # negative actual + # -- negative actual -- (-20833, -5678901, -56.78901), - ] - - @pytest.fixture(params=_effective_adj_val_cases()) - def effective_val_fixture_(self, request): - name = None - def_val, actual, expected_effective_value = request.param - adjustment = Adjustment(name, def_val, actual) - return adjustment, expected_effective_value + ], + ) + def it_knows_its_effective_value(self, def_val: int, actual: int | None, expected_value: float): + assert Adjustment("foobar", def_val, actual).effective_value == expected_value class DescribeAdjustmentCollection(object): - def it_should_load_default_adjustment_values(self, prstGeom_cases_): - prstGeom, prst, expected = prstGeom_cases_ + """Unit-test suite for `pptx.shapes.autoshape.AdjustmentCollection`.""" + + @pytest.mark.parametrize( + ("prst", "expected_values"), + [ + # -- rect has no adjustments -- + ("rect", ()), + # -- chevron has one simple one + ("chevron", (("adj", 50000),)), + # -- one with several and some negative -- + ( + "accentBorderCallout1", + (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)), + ), + # -- another one with some negative -- + ( + "wedgeRoundRectCallout", + (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)), + ), + # -- one with values outside normal range -- + ( + "circularArrow", + ( + ("adj1", 12500), + ("adj2", 1142319), + ("adj3", 20457681), + ("adj4", 10800000), + ("adj5", 12500), + ), + ), + ], + ) + def it_should_load_default_adjustment_values( + self, prst: str, expected_values: tuple[str, tuple[tuple[str, int], ...]] + ): + prstGeom = cast(CT_PresetGeometry2D, element(f"a:prstGeom{{prst={prst}}}/a:avLst")) + adjustments = AdjustmentCollection(prstGeom)._adjustments + actuals = tuple([(adj.name, adj.def_val) for adj in adjustments]) - assert len(adjustments) == len(expected) - assert actuals == expected + assert actuals == expected_values def it_should_load_adj_val_actuals_from_xml(self, load_adj_actuals_fixture_): prstGeom, expected_actuals, prstGeom_xml = load_adj_actuals_fixture_ @@ -187,41 +220,6 @@ def load_adj_actuals_fixture_(self, request): prstGeom_xml = prstGeom_bldr.xml return prstGeom, expected, prstGeom_xml - def _prstGeom_cases(): - return [ - # rect has no adjustments - ("rect", ()), - # chevron has one simple one - ("chevron", (("adj", 50000),)), - # one with several and some negative - ( - "accentBorderCallout1", - (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)), - ), - # another one with some negative - ( - "wedgeRoundRectCallout", - (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)), - ), - # one with values outside normal range - ( - "circularArrow", - ( - ("adj1", 12500), - ("adj2", 1142319), - ("adj3", 20457681), - ("adj4", 10800000), - ("adj5", 12500), - ), - ), - ] - - @pytest.fixture(params=_prstGeom_cases()) - def prstGeom_cases_(self, request): - prst, expected_values = request.param - prstGeom = a_prstGeom().with_nsdecls().with_prst(prst).with_child(an_avLst()).element - return prstGeom, prst, expected_values - def _effective_val_cases(): return [ ("rect", ()), @@ -261,8 +259,26 @@ def it_xml_escapes_the_basename_when_the_name_contains_special_characters(self): assert autoshape_type.prst == "noSmoking" assert autoshape_type.basename == ""No" Symbol" - def it_knows_the_default_adj_vals_for_its_autoshape_type(self, default_adj_vals_fixture_): - prst, default_adj_vals = default_adj_vals_fixture_ + @pytest.mark.parametrize( + ("prst", "default_adj_vals"), + [ + (MSO_SHAPE.RECTANGLE, ()), + (MSO_SHAPE.CHEVRON, (("adj", 50000),)), + ( + MSO_SHAPE.LEFT_CIRCULAR_ARROW, + ( + ("adj1", 12500), + ("adj2", -1142319), + ("adj3", 1142319), + ("adj4", 10800000), + ("adj5", 12500), + ), + ), + ], + ) + def it_knows_the_default_adj_vals_for_its_autoshape_type( + self, prst: MSO_SHAPE, default_adj_vals: tuple[AdjustmentValue, ...] + ): _default_adj_vals = AutoShapeType.default_adjustment_values(prst) assert _default_adj_vals == default_adj_vals @@ -270,7 +286,7 @@ def it_knows_the_autoshape_type_id_for_each_prst_key(self): assert AutoShapeType.id_from_prst("rect") == MSO_SHAPE.RECTANGLE def it_raises_when_asked_for_autoshape_type_id_with_a_bad_prst(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="MSO_AUTO_SHAPE_TYPE has no XML mapping for 'badPr"): AutoShapeType.id_from_prst("badPrst") def it_caches_autoshape_type_lookups(self): @@ -283,29 +299,6 @@ def it_raises_on_construction_with_bad_autoshape_type_id(self): with pytest.raises(KeyError): AutoShapeType(9999) - # fixtures ------------------------------------------------------- - - def _default_adj_vals_cases(): - return [ - (MSO_SHAPE.RECTANGLE, ()), - (MSO_SHAPE.CHEVRON, (("adj", 50000),)), - ( - MSO_SHAPE.LEFT_CIRCULAR_ARROW, - ( - ("adj1", 12500), - ("adj2", -1142319), - ("adj3", 1142319), - ("adj4", 10800000), - ("adj5", 12500), - ), - ), - ] - - @pytest.fixture(params=_default_adj_vals_cases()) - def default_adj_vals_fixture_(self, request): - prst, default_adj_vals = request.param - return prst, default_adj_vals - class DescribeShape(object): """Unit-test suite for `pptx.shapes.autoshape.Shape` object.""" diff --git a/tests/shapes/test_base.py b/tests/shapes/test_base.py index 8182323de..89632ca80 100644 --- a/tests/shapes/test_base.py +++ b/tests/shapes/test_base.py @@ -1,7 +1,11 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.shapes.base` module.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + import pytest from pptx.action import ActionSetting @@ -34,6 +38,11 @@ from ..unitutil.cxml import element, xml from ..unitutil.mock import class_mock, instance_mock, loose_mock +if TYPE_CHECKING: + from pptx.opc.package import XmlPart + from pptx.oxml.shapes import ShapeElement + from pptx.types import ProvidesPart + class DescribeBaseShape(object): """Unit-test suite for `pptx.shapes.base.BaseShape` objects.""" @@ -57,10 +66,47 @@ def it_can_change_its_name(self, name_set_fixture): shape.name = new_value assert shape._element.xml == expected_xml - def it_has_a_position(self, position_get_fixture): - shape, expected_left, expected_top = position_get_fixture - assert shape.left == expected_left - assert shape.top == expected_top + @pytest.mark.parametrize( + ("shape_cxml", "expected_x", "expected_y"), + [ + ("p:cxnSp/p:spPr", None, None), + ("p:cxnSp/p:spPr/a:xfrm", None, None), + ("p:cxnSp/p:spPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:graphicFrame/p:xfrm", None, None), + ("p:graphicFrame/p:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:grpSp/p:grpSpPr", None, None), + ("p:grpSp/p:grpSpPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:pic/p:spPr", None, None), + ("p:pic/p:spPr/a:xfrm", None, None), + ("p:pic/p:spPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ("p:sp/p:spPr", None, None), + ("p:sp/p:spPr/a:xfrm", None, None), + ("p:sp/p:spPr/a:xfrm/a:off{x=123,y=456}", 123, 456), + ], + ) + def it_has_a_position( + self, + shape_cxml: str, + expected_x: int | None, + expected_y: int | None, + provides_part: ProvidesPart, + ): + shape_elm = cast("ShapeElement", element(shape_cxml)) + + shape = BaseShape(shape_elm, provides_part) + + assert shape.left == expected_x + assert shape.top == expected_y + + @pytest.fixture + def provides_part(self) -> ProvidesPart: + + class FakeProvidesPart: + @property + def part(self) -> XmlPart: + raise NotImplementedError + + return FakeProvidesPart() def it_can_change_its_position(self, position_set_fixture): shape, left, top, expected_xml = position_set_fixture @@ -272,28 +318,6 @@ def phfmt_fixture(self, _PlaceholderFormat_, placeholder_format_): def phfmt_raise_fixture(self): return BaseShape(element("p:sp/p:nvSpPr/p:nvPr"), None) - @pytest.fixture( - params=[ - ("sp", False), - ("sp_with_off", True), - ("pic", False), - ("pic_with_off", True), - ("graphicFrame", False), - ("graphicFrame_with_off", True), - ("grpSp", False), - ("grpSp_with_off", True), - ("cxnSp", False), - ("cxnSp_with_off", True), - ] - ) - def position_get_fixture(self, request, left, top): - shape_elm_fixt_name, expect_values = request.param - shape_elm = request.getfixturevalue(shape_elm_fixt_name) - shape = BaseShape(shape_elm, None) - if not expect_values: - left = top = None - return shape, left, top - @pytest.fixture( params=[ ("sp", "sp_with_off"), @@ -363,9 +387,7 @@ def shadow_fixture(self, request, ShadowFormat_, shadow_): @pytest.fixture def ActionSetting_(self, request, action_setting_): - return class_mock( - request, "pptx.shapes.base.ActionSetting", return_value=action_setting_ - ) + return class_mock(request, "pptx.shapes.base.ActionSetting", return_value=action_setting_) @pytest.fixture def action_setting_(self, request): @@ -381,9 +403,7 @@ def cxnSp_with_ext(self, width, height): a_cxnSp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_ext().with_cx(width).with_cy(height)) - ) + an_spPr().with_child(an_xfrm().with_child(an_ext().with_cx(width).with_cy(height))) ) ).element @@ -393,9 +413,7 @@ def cxnSp_with_off(self, left, top): a_cxnSp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + an_spPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element @@ -442,9 +460,7 @@ def grpSp_with_off(self, left, top): a_grpSp() .with_nsdecls("p", "a") .with_child( - a_grpSpPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + a_grpSpPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element @@ -466,9 +482,7 @@ def pic_with_off(self, left, top): a_pic() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + an_spPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element @@ -478,9 +492,7 @@ def pic_with_ext(self, width, height): a_pic() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_ext().with_cx(width).with_cy(height)) - ) + an_spPr().with_child(an_xfrm().with_child(an_ext().with_cx(width).with_cy(height))) ) ).element @@ -536,9 +548,7 @@ def sp_with_ext(self, width, height): an_sp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_ext().with_cx(width).with_cy(height)) - ) + an_spPr().with_child(an_xfrm().with_child(an_ext().with_cx(width).with_cy(height))) ) ).element @@ -548,9 +558,7 @@ def sp_with_off(self, left, top): an_sp() .with_nsdecls() .with_child( - an_spPr().with_child( - an_xfrm().with_child(an_off().with_x(left).with_y(top)) - ) + an_spPr().with_child(an_xfrm().with_child(an_off().with_x(left).with_y(top))) ) ).element diff --git a/tests/shapes/test_connector.py b/tests/shapes/test_connector.py index 3bafa9f96..f61f0a029 100644 --- a/tests/shapes/test_connector.py +++ b/tests/shapes/test_connector.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Unit test suite for pptx.shapes.connector module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/shapes/test_freeform.py b/tests/shapes/test_freeform.py index 26ded32e2..dd5f53f0d 100644 --- a/tests/shapes/test_freeform.py +++ b/tests/shapes/test_freeform.py @@ -1,22 +1,27 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.shapes.freeform` module""" +from __future__ import annotations + import pytest from pptx.shapes.autoshape import Shape from pptx.shapes.freeform import ( + FreeformBuilder, _BaseDrawingOperation, _Close, - FreeformBuilder, _LineSegment, _MoveTo, ) from pptx.shapes.shapetree import SlideShapes +from pptx.util import Emu, Mm from ..unitutil.cxml import element, xml from ..unitutil.file import snippet_seq from ..unitutil.mock import ( + FixtureRequest, + Mock, call, initializer_mock, instance_mock, @@ -28,23 +33,20 @@ class DescribeFreeformBuilder(object): """Unit-test suite for `pptx.shapes.freeform.FreeformBuilder` objects.""" - def it_provides_a_constructor(self, new_fixture): - shapes_, start_x, start_y, x_scale, y_scale = new_fixture[:5] - _init_, start_x_int, start_y_int = new_fixture[5:] + def it_provides_a_constructor(self, shapes_: Mock, _init_: Mock): + start_x, start_y, x_scale, y_scale = 99.56, 200.49, 4.2, 2.4 + start_x_int, start_y_int = 100, 200 builder = FreeformBuilder.new(shapes_, start_x, start_y, x_scale, y_scale) - _init_.assert_called_once_with( - builder, shapes_, start_x_int, start_y_int, x_scale, y_scale - ) + _init_.assert_called_once_with(builder, shapes_, start_x_int, start_y_int, x_scale, y_scale) assert isinstance(builder, FreeformBuilder) - @pytest.mark.parametrize("close", (True, False)) - def it_can_add_straight_line_segments(self, request, close): + @pytest.mark.parametrize("close", [True, False]) + def it_can_add_straight_line_segments(self, request: FixtureRequest, close: bool): _add_line_segment_ = method_mock(request, FreeformBuilder, "_add_line_segment") _add_close_ = method_mock(request, FreeformBuilder, "_add_close") - - builder = FreeformBuilder(None, None, None, None, None) + builder = FreeformBuilder(None, None, None, None, None) # type: ignore return_value = builder.add_line_segments(((1, 2), (3, 4), (5, 6)), close) @@ -56,8 +58,10 @@ def it_can_add_straight_line_segments(self, request, close): assert _add_close_.call_args_list == ([call(builder)] if close else []) assert return_value is builder - def it_can_move_the_pen_location(self, move_to_fixture): - builder, x, y, _MoveTo_new_, move_to_ = move_to_fixture + def it_can_move_the_pen_location(self, _MoveTo_new_: Mock, move_to_: Mock): + x, y = 42, 24 + _MoveTo_new_.return_value = move_to_ + builder = FreeformBuilder(None, None, None, None, None) # type: ignore return_value = builder.move_to(x, y) @@ -65,46 +69,99 @@ def it_can_move_the_pen_location(self, move_to_fixture): assert builder._drawing_operations[-1] is move_to_ assert return_value is builder - def it_can_build_the_specified_freeform_shape(self, convert_fixture): - builder, origin_x, origin_y, sp = convert_fixture[:4] - apply_operation_to_, calls, shape_ = convert_fixture[4:] + def it_can_build_the_specified_freeform_shape( + self, + shapes_: Mock, + apply_operation_to_: Mock, + _add_freeform_sp_: Mock, + _start_path_: Mock, + shape_: Mock, + ): + origin_x, origin_y = Mm(42), Mm(24) + sp, path = element("p:sp"), element("a:path") + drawing_ops = ( + _LineSegment(None, None, None), # type: ignore + _LineSegment(None, None, None), # type: ignore + ) + shapes_._shape_factory.return_value = shape_ + _add_freeform_sp_.return_value = sp + _start_path_.return_value = path + builder = FreeformBuilder(shapes_, None, None, None, None) # type: ignore + builder._drawing_operations.extend(drawing_ops) + calls = [call(drawing_ops[0], path), call(drawing_ops[1], path)] shape = builder.convert_to_shape(origin_x, origin_y) - builder._add_freeform_sp.assert_called_once_with(builder, origin_x, origin_y) - builder._start_path.assert_called_once_with(builder, sp) + _add_freeform_sp_.assert_called_once_with(builder, origin_x, origin_y) + _start_path_.assert_called_once_with(builder, sp) assert apply_operation_to_.call_args_list == calls - builder._shapes._shape_factory.assert_called_once_with(sp) + shapes_._shape_factory.assert_called_once_with(sp) assert shape is shape_ - def it_knows_the_shape_x_offset(self, shape_offset_x_fixture): - builder, expected_value = shape_offset_x_fixture - x_offset = builder.shape_offset_x - assert x_offset == expected_value + @pytest.mark.parametrize( + ("start_x", "xs", "expected_value"), + [ + (Mm(0), (1, None, 2, 3), Mm(0)), + (Mm(6), (1, None, 2, 3), Mm(1)), + (Mm(50), (150, -5, None, 100), Mm(-5)), + ], + ) + def it_knows_the_shape_x_offset( + self, start_x: int, xs: tuple[int | None, ...], expected_value: int + ): + builder = FreeformBuilder(None, start_x, None, None, None) # type: ignore + drawing_ops = [_Close() if x is None else _LineSegment(builder, Mm(x), Mm(0)) for x in xs] + builder._drawing_operations.extend(drawing_ops) + + assert builder.shape_offset_x == expected_value - def it_knows_the_shape_y_offset(self, shape_offset_y_fixture): - builder, expected_value = shape_offset_y_fixture - y_offset = builder.shape_offset_y - assert y_offset == expected_value + @pytest.mark.parametrize( + ("start_y", "ys", "expected_value"), + [ + (Mm(0), (2, None, 6, 8), Mm(0)), + (Mm(4), (2, None, 6, 8), Mm(2)), + (Mm(19), (213, -22, None, 100), Mm(-22)), + ], + ) + def it_knows_the_shape_y_offset( + self, start_y: int, ys: tuple[int | None, ...], expected_value: int + ): + builder = FreeformBuilder(None, None, start_y, None, None) # type: ignore + drawing_ops = [_Close() if y is None else _LineSegment(builder, Mm(0), Mm(y)) for y in ys] + builder._drawing_operations.extend(drawing_ops) + + assert builder.shape_offset_y == expected_value - def it_adds_a_freeform_sp_to_help(self, sp_fixture): - builder, origin_x, origin_y, spTree, expected_xml = sp_fixture + def it_adds_a_freeform_sp_to_help( + self, _left_prop_: Mock, _top_prop_: Mock, _width_prop_: Mock, _height_prop_: Mock + ): + origin_x, origin_y = Emu(42), Emu(24) + spTree = element("p:spTree") + shapes = SlideShapes(spTree, None) # type: ignore + _left_prop_.return_value, _top_prop_.return_value = Emu(12), Emu(34) + _width_prop_.return_value, _height_prop_.return_value = 56, 78 + builder = FreeformBuilder(shapes, None, None, None, None) # type: ignore + expected_xml = snippet_seq("freeform")[0] sp = builder._add_freeform_sp(origin_x, origin_y) assert spTree.xml == expected_xml assert sp is spTree.xpath("p:sp")[0] - def it_adds_a_line_segment_to_help(self, add_seg_fixture): - builder, x, y, _LineSegment_new_, line_segment_ = add_seg_fixture + def it_adds_a_line_segment_to_help(self, _LineSegment_new_: Mock, line_segment_: Mock): + x, y = 4, 2 + _LineSegment_new_.return_value = line_segment_ + + builder = FreeformBuilder(None, None, None, None, None) # type: ignore builder._add_line_segment(x, y) _LineSegment_new_.assert_called_once_with(builder, x, y) assert builder._drawing_operations == [line_segment_] - def it_closes_a_contour_to_help(self, add_close_fixture): - builder, _Close_new_, close_ = add_close_fixture + def it_closes_a_contour_to_help(self, _Close_new_: Mock, close_: Mock): + _Close_new_.return_value = close_ + builder = FreeformBuilder(None, None, None, None, None) # type: ignore builder._add_close() @@ -126,8 +183,15 @@ def it_knows_the_freeform_width_to_help(self, width_fixture): width = builder._width assert width == expected_value - def it_knows_the_freeform_height_to_help(self, height_fixture): - builder, expected_value = height_fixture + @pytest.mark.parametrize( + ("dy", "y_scale", "expected_value"), + [(0, 2.0, 0), (24, 10.0, 240), (914400, 314.1, 287213040)], + ) + def it_knows_the_freeform_height_to_help( + self, dy: int, y_scale: float, expected_value: int, _dy_prop_: Mock + ): + _dy_prop_.return_value = dy + builder = FreeformBuilder(None, None, None, None, y_scale) # type: ignore height = builder._height assert height == expected_value @@ -141,7 +205,9 @@ def it_knows_the_local_coordinate_height_to_help(self, dy_fixture): dy = builder._dy assert dy == expected_value - def it_can_start_a_new_path_to_help(self, request, _dx_prop_, _dy_prop_): + def it_can_start_a_new_path_to_help( + self, request: FixtureRequest, _dx_prop_: Mock, _dy_prop_: Mock + ): _local_to_shape_ = method_mock( request, FreeformBuilder, "_local_to_shape", return_value=(101, 202) ) @@ -154,8 +220,7 @@ def it_can_start_a_new_path_to_help(self, request, _dx_prop_, _dy_prop_): _local_to_shape_.assert_called_once_with(builder, start_x, start_y) assert sp.xml == xml( - "p:sp/p:spPr/a:custGeom/a:pathLst/a:path{w=1001,h=2002}/a:moveTo" - "/a:pt{x=101,y=202}" + "p:sp/p:spPr/a:custGeom/a:pathLst/a:path{w=1001,h=2002}/a:moveTo" "/a:pt{x=101,y=202}" ) assert path is sp.xpath(".//a:path")[-1] @@ -166,39 +231,6 @@ def it_translates_local_to_shape_coordinates_to_help(self, local_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture - def add_close_fixture(self, _Close_new_, close_): - _Close_new_.return_value = close_ - builder = FreeformBuilder(None, None, None, None, None) - return builder, _Close_new_, close_ - - @pytest.fixture - def add_seg_fixture(self, _LineSegment_new_, line_segment_): - x, y = 4, 2 - _LineSegment_new_.return_value = line_segment_ - - builder = FreeformBuilder(None, None, None, None, None) - return builder, x, y, _LineSegment_new_, line_segment_ - - @pytest.fixture - def convert_fixture( - self, shapes_, apply_operation_to_, _add_freeform_sp_, _start_path_, shape_ - ): - origin_x, origin_y = 42, 24 - sp, path = element("p:sp"), element("a:path") - drawing_ops = ( - _BaseDrawingOperation(None, None, None), - _BaseDrawingOperation(None, None, None), - ) - shapes_._shape_factory.return_value = shape_ - _add_freeform_sp_.return_value = sp - _start_path_.return_value = path - - builder = FreeformBuilder(shapes_, None, None, None, None) - builder._drawing_operations.extend(drawing_ops) - calls = [call(drawing_ops[0], path), call(drawing_ops[1], path)] - return (builder, origin_x, origin_y, sp, apply_operation_to_, calls, shape_) - @pytest.fixture( params=[ (0, (1, None, 2, 3), 3), @@ -206,7 +238,7 @@ def convert_fixture( (50, (150, -5, None, 100), 155), ] ) - def dx_fixture(self, request): + def dx_fixture(self, request: FixtureRequest): start_x, xs, expected_value = request.param drawing_ops = [] for x in xs: @@ -226,7 +258,7 @@ def dx_fixture(self, request): (32, (160, -8, None, 101), 168), ] ) - def dy_fixture(self, request): + def dy_fixture(self, request: FixtureRequest): start_y, ys, expected_value = request.param drawing_ops = [] for y in ys: @@ -239,16 +271,8 @@ def dy_fixture(self, request): builder._drawing_operations.extend(drawing_ops) return builder, expected_value - @pytest.fixture(params=[(0, 2.0, 0), (24, 10.0, 240), (914400, 314.1, 287213040)]) - def height_fixture(self, request, _dy_prop_): - dy, y_scale, expected_value = request.param - _dy_prop_.return_value = dy - - builder = FreeformBuilder(None, None, None, None, y_scale) - return builder, expected_value - @pytest.fixture(params=[(0, 1.0, 0), (4, 10.0, 40), (914400, 914.3, 836035920)]) - def left_fixture(self, request, shape_offset_x_prop_): + def left_fixture(self, request: FixtureRequest, shape_offset_x_prop_: Mock): offset_x, x_scale, expected_value = request.param shape_offset_x_prop_.return_value = offset_x @@ -256,7 +280,7 @@ def left_fixture(self, request, shape_offset_x_prop_): return builder, expected_value @pytest.fixture - def local_fixture(self, shape_offset_x_prop_, shape_offset_y_prop_): + def local_fixture(self, shape_offset_x_prop_: Mock, shape_offset_y_prop_: Mock): local_x, local_y = 123, 456 shape_offset_x_prop_.return_value = 23 shape_offset_y_prop_.return_value = 156 @@ -266,70 +290,9 @@ def local_fixture(self, shape_offset_x_prop_, shape_offset_y_prop_): return builder, local_x, local_y, expected_value @pytest.fixture - def move_to_fixture(self, _MoveTo_new_, move_to_): - x, y = 42, 24 - _MoveTo_new_.return_value = move_to_ - - builder = FreeformBuilder(None, None, None, None, None) - return builder, x, y, _MoveTo_new_, move_to_ - - @pytest.fixture - def new_fixture(self, shapes_, _init_): - start_x, start_y, x_scale, y_scale = 99.56, 200.49, 4.2, 2.4 - start_x_int, start_y_int = 100, 200 - return ( - shapes_, - start_x, - start_y, - x_scale, - y_scale, - _init_, - start_x_int, - start_y_int, - ) - - @pytest.fixture( - params=[ - (0, (1, None, 2, 3), 0), - (6, (1, None, 2, 3), 1), - (50, (150, -5, None, 100), -5), - ] - ) - def shape_offset_x_fixture(self, request): - start_x, xs, expected_value = request.param - drawing_ops = [] - for x in xs: - if x is None: - drawing_ops.append(_Close()) - else: - drawing_ops.append(_BaseDrawingOperation(None, x, None)) - - builder = FreeformBuilder(None, start_x, None, None, None) - builder._drawing_operations.extend(drawing_ops) - return builder, expected_value - - @pytest.fixture( - params=[ - (0, (2, None, 6, 8), 0), - (4, (2, None, 6, 8), 2), - (19, (213, -22, None, 100), -22), - ] - ) - def shape_offset_y_fixture(self, request): - start_y, ys, expected_value = request.param - drawing_ops = [] - for y in ys: - if y is None: - drawing_ops.append(_Close()) - else: - drawing_ops.append(_BaseDrawingOperation(None, None, y)) - - builder = FreeformBuilder(None, None, start_y, None, None) - builder._drawing_operations.extend(drawing_ops) - return builder, expected_value - - @pytest.fixture - def sp_fixture(self, _left_prop_, _top_prop_, _width_prop_, _height_prop_): + def sp_fixture( + self, _left_prop_: Mock, _top_prop_: Mock, _width_prop_: Mock, _height_prop_: Mock + ): origin_x, origin_y = 42, 24 spTree = element("p:spTree") shapes = SlideShapes(spTree, None) @@ -340,10 +303,8 @@ def sp_fixture(self, _left_prop_, _top_prop_, _width_prop_, _height_prop_): expected_xml = snippet_seq("freeform")[0] return builder, origin_x, origin_y, spTree, expected_xml - @pytest.fixture( - params=[(0, 11.0, 0), (100, 10.36, 1036), (914242, 943.1, 862221630)] - ) - def top_fixture(self, request, shape_offset_y_prop_): + @pytest.fixture(params=[(0, 11.0, 0), (100, 10.36, 1036), (914242, 943.1, 862221630)]) + def top_fixture(self, request: FixtureRequest, shape_offset_y_prop_: Mock): offset_y, y_scale, expected_value = request.param shape_offset_y_prop_.return_value = offset_y @@ -351,7 +312,7 @@ def top_fixture(self, request, shape_offset_y_prop_): return builder, expected_value @pytest.fixture(params=[(0, 1.0, 0), (42, 10.0, 420), (914400, 914.4, 836127360)]) - def width_fixture(self, request, _dx_prop_): + def width_fixture(self, request: FixtureRequest, _dx_prop_: Mock): dx, x_scale, expected_value = request.param _dx_prop_.return_value = dx @@ -361,85 +322,83 @@ def width_fixture(self, request, _dx_prop_): # fixture components ----------------------------------- @pytest.fixture - def _add_freeform_sp_(self, request): + def _add_freeform_sp_(self, request: FixtureRequest): return method_mock(request, FreeformBuilder, "_add_freeform_sp", autospec=True) @pytest.fixture - def apply_operation_to_(self, request): - return method_mock( - request, _BaseDrawingOperation, "apply_operation_to", autospec=True - ) + def apply_operation_to_(self, request: FixtureRequest): + return method_mock(request, _LineSegment, "apply_operation_to", autospec=True) @pytest.fixture - def close_(self, request): + def close_(self, request: FixtureRequest): return instance_mock(request, _Close) @pytest.fixture - def _Close_new_(self, request): + def _Close_new_(self, request: FixtureRequest): return method_mock(request, _Close, "new", autospec=False) @pytest.fixture - def _dx_prop_(self, request): + def _dx_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_dx") @pytest.fixture - def _dy_prop_(self, request): + def _dy_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_dy") @pytest.fixture - def _height_prop_(self, request): + def _height_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_height") @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, FreeformBuilder, autospec=True) @pytest.fixture - def _left_prop_(self, request): + def _left_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_left") @pytest.fixture - def line_segment_(self, request): + def line_segment_(self, request: FixtureRequest): return instance_mock(request, _LineSegment) @pytest.fixture - def _LineSegment_new_(self, request): + def _LineSegment_new_(self, request: FixtureRequest): return method_mock(request, _LineSegment, "new", autospec=False) @pytest.fixture - def move_to_(self, request): + def move_to_(self, request: FixtureRequest): return instance_mock(request, _MoveTo) @pytest.fixture - def _MoveTo_new_(self, request): + def _MoveTo_new_(self, request: FixtureRequest): return method_mock(request, _MoveTo, "new", autospec=False) @pytest.fixture - def shape_(self, request): + def shape_(self, request: FixtureRequest): return instance_mock(request, Shape) @pytest.fixture - def shape_offset_x_prop_(self, request): + def shape_offset_x_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "shape_offset_x") @pytest.fixture - def shape_offset_y_prop_(self, request): + def shape_offset_y_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "shape_offset_y") @pytest.fixture - def shapes_(self, request): + def shapes_(self, request: FixtureRequest): return instance_mock(request, SlideShapes) @pytest.fixture - def _start_path_(self, request): + def _start_path_(self, request: FixtureRequest): return method_mock(request, FreeformBuilder, "_start_path", autospec=True) @pytest.fixture - def _top_prop_(self, request): + def _top_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_top") @pytest.fixture - def _width_prop_(self, request): + def _width_prop_(self, request: FixtureRequest): return property_mock(request, FreeformBuilder, "_width") @@ -508,7 +467,7 @@ def apply_fixture(self): return close, path, expected_xml @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, _Close, autospec=True) @@ -551,11 +510,11 @@ def new_fixture(self, builder_, _init_): # fixture components ----------------------------------- @pytest.fixture - def builder_(self, request): + def builder_(self, request: FixtureRequest): return instance_mock(request, FreeformBuilder) @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, _LineSegment, autospec=True) @@ -598,9 +557,9 @@ def new_fixture(self, builder_, _init_): # fixture components ----------------------------------- @pytest.fixture - def builder_(self, request): + def builder_(self, request: FixtureRequest): return instance_mock(request, FreeformBuilder) @pytest.fixture - def _init_(self, request): + def _init_(self, request: FixtureRequest): return initializer_mock(request, _MoveTo, autospec=True) diff --git a/tests/shapes/test_graphfrm.py b/tests/shapes/test_graphfrm.py index 5f2250111..3324fcfe0 100644 --- a/tests/shapes/test_graphfrm.py +++ b/tests/shapes/test_graphfrm.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for pptx.shapes.graphfrm module.""" +from __future__ import annotations + import pytest from pptx.chart.chart import Chart @@ -62,9 +62,7 @@ def it_provides_access_to_its_chart_part(self, request, chart_part_): ), ) def it_knows_whether_it_contains_a_chart(self, graphicData_uri, expected_value): - graphicFrame = element( - "p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri - ) + graphicFrame = element("p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri) assert GraphicFrame(graphicFrame, None).has_chart is expected_value @pytest.mark.parametrize( @@ -76,9 +74,7 @@ def it_knows_whether_it_contains_a_chart(self, graphicData_uri, expected_value): ), ) def it_knows_whether_it_contains_a_table(self, graphicData_uri, expected_value): - graphicFrame = element( - "p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri - ) + graphicFrame = element("p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % graphicData_uri) assert GraphicFrame(graphicFrame, None).has_table is expected_value def it_provides_access_to_the_OleFormat_object(self, request): @@ -127,10 +123,7 @@ def it_raises_on_shadow(self): ) def it_knows_its_shape_type(self, uri, oleObj_child, expected_value): graphicFrame = element( - ( - "p:graphicFrame/a:graphic/a:graphicData{uri=%s}/p:oleObj/p:%s" - % (uri, oleObj_child) - ) + ("p:graphicFrame/a:graphic/a:graphicData{uri=%s}/p:oleObj/p:%s" % (uri, oleObj_child)) if oleObj_child else "p:graphicFrame/a:graphic/a:graphicData{uri=%s}" % uri ) diff --git a/tests/shapes/test_group.py b/tests/shapes/test_group.py index f9e1248d4..93c06d029 100644 --- a/tests/shapes/test_group.py +++ b/tests/shapes/test_group.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Test suite for pptx.shapes.group module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest diff --git a/tests/shapes/test_picture.py b/tests/shapes/test_picture.py index 3be7c6b89..75728da21 100644 --- a/tests/shapes/test_picture.py +++ b/tests/shapes/test_picture.py @@ -1,8 +1,6 @@ -# encoding: utf-8 - """Test suite for pptx.shapes.picture module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -10,7 +8,7 @@ from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE from pptx.parts.image import Image from pptx.parts.slide import SlidePart -from pptx.shapes.picture import _BasePicture, _MediaFormat, Movie, Picture +from pptx.shapes.picture import Movie, Picture, _BasePicture, _MediaFormat from pptx.util import Pt from ..unitutil.cxml import element, xml @@ -206,9 +204,7 @@ def image_(self, request): @pytest.fixture def _MediaFormat_(self, request, media_format_): - return class_mock( - request, "pptx.shapes.picture._MediaFormat", return_value=media_format_ - ) + return class_mock(request, "pptx.shapes.picture._MediaFormat", return_value=media_format_) @pytest.fixture def media_format_(self, request): diff --git a/tests/shapes/test_placeholder.py b/tests/shapes/test_placeholder.py index 75b0814ca..4d9b26ea0 100644 --- a/tests/shapes/test_placeholder.py +++ b/tests/shapes/test_placeholder.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.shapes.placeholder` module.""" +from __future__ import annotations + import pytest from pptx.chart.data import ChartData @@ -12,9 +12,7 @@ from pptx.parts.slide import NotesSlidePart, SlidePart from pptx.shapes.placeholder import ( BasePlaceholder, - _BaseSlidePlaceholder, ChartPlaceholder, - _InheritsDimensions, LayoutPlaceholder, MasterPlaceholder, NotesSlidePlaceholder, @@ -22,6 +20,8 @@ PlaceholderGraphicFrame, PlaceholderPicture, TablePlaceholder, + _BaseSlidePlaceholder, + _InheritsDimensions, ) from pptx.shapes.shapetree import NotesSlidePlaceholders from pptx.slide import NotesMaster, SlideLayout, SlideMaster @@ -151,9 +151,7 @@ def layout_placeholder_(self, request): @pytest.fixture def part_prop_(self, request, slide_part_): - return property_mock( - request, _BaseSlidePlaceholder, "part", return_value=slide_part_ - ) + return property_mock(request, _BaseSlidePlaceholder, "part", return_value=slide_part_) @pytest.fixture def slide_layout_(self, request): @@ -205,9 +203,7 @@ def idx_fixture(self, request): placeholder = BasePlaceholder(shape_elm, None) return placeholder, expected_idx - @pytest.fixture( - params=[(None, ST_Direction.HORZ), (ST_Direction.VERT, ST_Direction.VERT)] - ) + @pytest.fixture(params=[(None, ST_Direction.HORZ), (ST_Direction.VERT, ST_Direction.VERT)]) def orient_fixture(self, request): orient, expected_orient = request.param ph_bldr = a_ph() @@ -279,9 +275,7 @@ def shape_elm_factory(tagname, ph_type, idx): "pic": a_ph().with_type("pic").with_idx(idx), "tbl": a_ph().with_type("tbl").with_idx(idx), }[ph_type] - return ( - root_bldr.with_child(nvXxPr_bldr.with_child(an_nvPr().with_child(ph_bldr))) - ).element + return (root_bldr.with_child(nvXxPr_bldr.with_child(an_nvPr().with_child(ph_bldr)))).element class DescribeChartPlaceholder(object): @@ -439,9 +433,7 @@ def notes_slide_part_(self, request): @pytest.fixture def part_prop_(self, request, notes_slide_part_): - return property_mock( - request, NotesSlidePlaceholder, "part", return_value=notes_slide_part_ - ) + return property_mock(request, NotesSlidePlaceholder, "part", return_value=notes_slide_part_) class DescribePicturePlaceholder(object): @@ -482,10 +474,7 @@ def it_creates_a_pic_element_to_help(self, request, image_size, crop_attr_names) return_value=(42, "bar", image_size), ) picture_ph = PicturePlaceholder( - element( - "p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" - ",cy=99})" - ), + element("p:sp/(p:nvSpPr/p:cNvPr{id=2,name=foo},p:spPr/a:xfrm/a:ext{cx=99" ",cy=99})"), None, ) diff --git a/tests/shapes/test_shapetree.py b/tests/shapes/test_shapetree.py index 63c1ee290..3cf1ab225 100644 --- a/tests/shapes/test_shapetree.py +++ b/tests/shapes/test_shapetree.py @@ -1,18 +1,21 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit test suite for pptx.shapes.shapetree module""" +from __future__ import annotations + +import io + import pytest -from pptx.compat import BytesIO from pptx.chart.data import ChartData from pptx.enum.chart import XL_CHART_TYPE from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_CONNECTOR, PP_PLACEHOLDER, PROG_ID +from pptx.media import SPEAKER_IMAGE_BYTES, Video from pptx.oxml import parse_xml from pptx.oxml.shapes.groupshape import CT_GroupShape from pptx.oxml.shapes.picture import CT_Picture from pptx.oxml.shapes.shared import BaseShapeElement, ST_Direction -from pptx.media import SPEAKER_IMAGE_BYTES, Video from pptx.parts.image import ImagePart from pptx.parts.slide import SlidePart from pptx.shapes.autoshape import AutoShapeType, Shape @@ -23,32 +26,32 @@ from pptx.shapes.group import GroupShape from pptx.shapes.picture import Movie, Picture from pptx.shapes.placeholder import ( - _BaseSlidePlaceholder, LayoutPlaceholder, MasterPlaceholder, NotesSlidePlaceholder, + _BaseSlidePlaceholder, ) from pptx.shapes.shapetree import ( - _BaseGroupShapes, BasePlaceholders, BaseShapeFactory, - _BaseShapes, GroupShapes, LayoutPlaceholders, - _LayoutShapeFactory, LayoutShapes, MasterPlaceholders, - _MasterShapeFactory, MasterShapes, - _MoviePicElementCreator, NotesSlidePlaceholders, - _NotesSlideShapeFactory, NotesSlideShapes, - _OleObjectElementCreator, - _SlidePlaceholderFactory, SlidePlaceholders, SlideShapeFactory, SlideShapes, + _BaseGroupShapes, + _BaseShapes, + _LayoutShapeFactory, + _MasterShapeFactory, + _MoviePicElementCreator, + _NotesSlideShapeFactory, + _OleObjectElementCreator, + _SlidePlaceholderFactory, ) from pptx.slide import SlideLayout, SlideMaster from pptx.table import Table @@ -218,8 +221,7 @@ def len_fixture(self): ("p:spTree/p:nvSpPr/(p:cNvPr{id=foo},p:cNvPr{id=2})", 3), ("p:spTree/p:nvSpPr/(p:cNvPr{id=1fo},p:cNvPr{id=2})", 3), ( - "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" - "cNvPr{id=1},p:cNvPr{id=4})", + "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" "cNvPr{id=1},p:cNvPr{id=4})", 5, ), ] @@ -244,9 +246,7 @@ def next_id_fixture(self, request): ) def ph_name_fixture(self, request): ph_type, sp_id, orient, expected_name = request.param - spTree = element( - "p:spTree/(p:cNvPr{name=Title 1},p:cNvPr{name=Table Placeholder " "3})" - ) + spTree = element("p:spTree/(p:cNvPr{name=Title 1},p:cNvPr{name=Table Placeholder " "3})") shapes = SlideShapes(spTree, None) return shapes, ph_type, sp_id, orient, expected_name @@ -264,8 +264,7 @@ def turbo_fixture(self, request): ("p:spTree/p:nvSpPr/p:cNvPr{id=2}", True), ("p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=3})", False), ( - "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" - "cNvPr{id=1},p:cNvPr{id=4})", + "p:spTree/p:nvSpPr/(p:cNvPr{id=1},p:cNvPr{id=1},p:" "cNvPr{id=1},p:cNvPr{id=4})", True, ), ] @@ -319,9 +318,7 @@ def it_can_add_a_chart( graphic_frame = shapes.add_chart(XL_CHART_TYPE.PIE, x, y, cx, cy, chart_data_) - shapes.part.add_chart_part.assert_called_once_with( - XL_CHART_TYPE.PIE, chart_data_ - ) + shapes.part.add_chart_part.assert_called_once_with(XL_CHART_TYPE.PIE, chart_data_) _add_chart_graphicFrame_.assert_called_once_with(shapes, "rId42", x, y, cx, cy) _recalculate_extents_.assert_called_once_with(shapes) _shape_factory_.assert_called_once_with(shapes, graphicFrame) @@ -347,9 +344,7 @@ def it_can_provide_a_freeform_builder(self, freeform_fixture): builder = shapes.build_freeform(start_x, start_y, scale) - FreeformBuilder_new_.assert_called_once_with( - shapes, start_x, start_y, x_scale, y_scale - ) + FreeformBuilder_new_.assert_called_once_with(shapes, start_x, start_y, x_scale, y_scale) assert builder is builder_ def it_can_add_a_group_shape(self, group_fixture): @@ -622,9 +617,7 @@ def add_textbox_sp_fixture(self, _next_shape_id_prop_): return shapes, x, y, cx, cy, expected_xml @pytest.fixture - def connector_fixture( - self, _add_cxnSp_, _shape_factory_, _recalculate_extents_, connector_ - ): + def connector_fixture(self, _add_cxnSp_, _shape_factory_, _recalculate_extents_, connector_): shapes = _BaseGroupShapes(element("p:spTree"), None) connector_type = MSO_CONNECTOR.STRAIGHT begin_x, begin_y, end_x, end_y = 1, 2, 3, 4 @@ -766,9 +759,7 @@ def shape_fixture( ) @pytest.fixture - def textbox_fixture( - self, _add_textbox_sp_, _recalculate_extents_, _shape_factory_, shape_ - ): + def textbox_fixture(self, _add_textbox_sp_, _recalculate_extents_, _shape_factory_, shape_): shapes = _BaseGroupShapes(None, None) x, y, cx, cy = 31, 32, 33, 34 sp = element("p:sp") @@ -782,9 +773,7 @@ def textbox_fixture( @pytest.fixture def _add_chart_graphicFrame_(self, request): - return method_mock( - request, _BaseGroupShapes, "_add_chart_graphicFrame", autospec=True - ) + return method_mock(request, _BaseGroupShapes, "_add_chart_graphicFrame", autospec=True) @pytest.fixture def _add_cxnSp_(self, request): @@ -792,9 +781,7 @@ def _add_cxnSp_(self, request): @pytest.fixture def _add_pic_from_image_part_(self, request): - return method_mock( - request, _BaseGroupShapes, "_add_pic_from_image_part", autospec=True - ) + return method_mock(request, _BaseGroupShapes, "_add_pic_from_image_part", autospec=True) @pytest.fixture def _add_sp_(self, request): @@ -854,9 +841,7 @@ def picture_(self, request): @pytest.fixture def _recalculate_extents_(self, request): - return method_mock( - request, _BaseGroupShapes, "_recalculate_extents", autospec=True - ) + return method_mock(request, _BaseGroupShapes, "_recalculate_extents", autospec=True) @pytest.fixture def shape_(self, request): @@ -1260,9 +1245,7 @@ def it_can_add_a_movie(self, movie_fixture): _MoviePicElementCreator_, movie_pic = movie_fixture[9:11] _add_video_timing_, _shape_factory_, movie_ = movie_fixture[11:] - movie = shapes.add_movie( - movie_file, x, y, cx, cy, poster_frame_image, mime_type - ) + movie = shapes.add_movie(movie_file, x, y, cx, cy, poster_frame_image, mime_type) _MoviePicElementCreator_.new_movie_pic.assert_called_once_with( shapes, shape_id_, movie_file, x, y, cx, cy, poster_frame_image, mime_type @@ -1419,15 +1402,11 @@ def movie_(self, request): @pytest.fixture def _MoviePicElementCreator_(self, request): - return class_mock( - request, "pptx.shapes.shapetree._MoviePicElementCreator", autospec=True - ) + return class_mock(request, "pptx.shapes.shapetree._MoviePicElementCreator", autospec=True) @pytest.fixture def _next_shape_id_prop_(self, request, shape_id_): - return property_mock( - request, SlideShapes, "_next_shape_id", return_value=shape_id_ - ) + return property_mock(request, SlideShapes, "_next_shape_id", return_value=shape_id_) @pytest.fixture def placeholder_(self, request): @@ -1554,9 +1533,7 @@ def parent_(self, request): @pytest.fixture def ph_bldr(self): - return an_sp().with_child( - an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1))) - ) + return an_sp().with_child(an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1)))) class DescribeLayoutPlaceholders(object): @@ -1679,9 +1656,7 @@ def master_placeholder_(self, request): @pytest.fixture def ph_bldr(self): - return an_sp().with_child( - an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1))) - ) + return an_sp().with_child(an_nvSpPr().with_child(an_nvPr().with_child(a_ph().with_idx(1)))) @pytest.fixture def slide_master_(self, request): @@ -1848,17 +1823,35 @@ def it_adds_the_poster_frame_image_to_help(self, pfrm_rId_fixture): poster_frame_rId = movie_pic_element_creator._poster_frame_rId - slide_part_.get_or_add_image_part.assert_called_once_with( - poster_frame_image_file - ) + slide_part_.get_or_add_image_part.assert_called_once_with(poster_frame_image_file) assert poster_frame_rId == expected_value - def it_gets_the_poster_frame_image_file_to_help(self, pfrm_img_fixture): - movie_pic_element_creator, BytesIO_ = pfrm_img_fixture[:2] - calls, expected_value = pfrm_img_fixture[2:] + def it_gets_the_poster_frame_image_from_the_specified_path_to_help( + self, request: pytest.FixtureRequest + ): + BytesIO_ = class_mock(request, "pptx.shapes.shapetree.io.BytesIO") + movie_pic_element_creator = _MoviePicElementCreator( + None, None, None, None, None, None, None, "image.png", None # type: ignore + ) + image_file = movie_pic_element_creator._poster_frame_image_file - assert BytesIO_.call_args_list == calls - assert image_file == expected_value + + BytesIO_.assert_not_called() + assert image_file == "image.png" + + def but_it_gets_the_poster_frame_image_from_the_default_bytes_when_None_specified( + self, request: pytest.FixtureRequest + ): + stream_ = instance_mock(request, io.BytesIO) + BytesIO_ = class_mock(request, "pptx.shapes.shapetree.io.BytesIO", return_value=stream_) + movie_pic_element_creator = _MoviePicElementCreator( + None, None, None, None, None, None, None, None, None # type: ignore + ) + + image_file = movie_pic_element_creator._poster_frame_image_file + + BytesIO_.assert_called_once_with(SPEAKER_IMAGE_BYTES) + assert image_file == stream_ def it_gets_the_video_part_rIds_to_help(self, part_rIds_fixture): movie_pic_element_creator, slide_part_ = part_rIds_fixture[:2] @@ -1886,9 +1879,7 @@ def media_rId_fixture(self, _video_part_rIds_prop_): return movie_pic_element_creator, expected_value @pytest.fixture - def movie_pic_fixture( - self, shapes_, _MoviePicElementCreator_init_, _pic_prop_, pic_ - ): + def movie_pic_fixture(self, shapes_, _MoviePicElementCreator_init_, _pic_prop_, pic_): shape_id, movie_file, x, y, cx, cy = 42, "movie.mp4", 1, 2, 3, 4 poster_frame_image, mime_type = "image.png", "video/mp4" return ( @@ -1917,25 +1908,8 @@ def part_rIds_fixture(self, slide_part_, video_, _slide_part_prop_, _video_prop_ _video_prop_.return_value = video_ return (movie_pic_element_creator, slide_part_, video_, media_rId, video_rId) - @pytest.fixture(params=["image.png", None]) - def pfrm_img_fixture(self, request, BytesIO_, stream_): - poster_frame_file = request.param - movie_pic_element_creator = _MoviePicElementCreator( - None, None, None, None, None, None, None, poster_frame_file, None - ) - if poster_frame_file is None: - calls = [call(SPEAKER_IMAGE_BYTES)] - BytesIO_.return_value = stream_ - expected_value = stream_ - else: - calls = [] - expected_value = poster_frame_file - return movie_pic_element_creator, BytesIO_, calls, expected_value - @pytest.fixture - def pfrm_rId_fixture( - self, _slide_part_prop_, slide_part_, _poster_frame_image_file_prop_ - ): + def pfrm_rId_fixture(self, _slide_part_prop_, slide_part_, _poster_frame_image_file_prop_): movie_pic_element_creator = _MoviePicElementCreator( None, None, None, None, None, None, None, None, None ) @@ -2021,10 +1995,6 @@ def video_fixture(self, video_, from_path_or_file_like_): # fixture components --------------------------------------------- - @pytest.fixture - def BytesIO_(self, request): - return class_mock(request, "pptx.shapes.shapetree.BytesIO") - @pytest.fixture def from_path_or_file_like_(self, request): return method_mock(request, Video, "from_path_or_file_like", autospec=False) @@ -2047,15 +2017,11 @@ def pic_(self): @pytest.fixture def _pic_prop_(self, request, pic_): - return property_mock( - request, _MoviePicElementCreator, "_pic", return_value=pic_ - ) + return property_mock(request, _MoviePicElementCreator, "_pic", return_value=pic_) @pytest.fixture def _poster_frame_image_file_prop_(self, request): - return property_mock( - request, _MoviePicElementCreator, "_poster_frame_image_file" - ) + return property_mock(request, _MoviePicElementCreator, "_poster_frame_image_file") @pytest.fixture def _poster_frame_rId_prop_(self, request): @@ -2077,10 +2043,6 @@ def slide_part_(self, request): def _slide_part_prop_(self, request): return property_mock(request, _MoviePicElementCreator, "_slide_part") - @pytest.fixture - def stream_(self, request): - return instance_mock(request, BytesIO) - @pytest.fixture def video_(self, request): return instance_mock(request, Video) @@ -2145,18 +2107,10 @@ def it_provides_a_graphicFrame_interface_method(self, request, shapes_): def it_creates_the_graphicFrame_element(self, request): shape_id, x, y, cx, cy = 7, 1, 2, 3, 4 - property_mock( - request, _OleObjectElementCreator, "_shape_name", return_value="Object 42" - ) - property_mock( - request, _OleObjectElementCreator, "_ole_object_rId", return_value="rId42" - ) - property_mock( - request, _OleObjectElementCreator, "_progId", return_value="Excel.Sheet.42" - ) - property_mock( - request, _OleObjectElementCreator, "_icon_rId", return_value="rId24" - ) + property_mock(request, _OleObjectElementCreator, "_shape_name", return_value="Object 42") + property_mock(request, _OleObjectElementCreator, "_ole_object_rId", return_value="rId42") + property_mock(request, _OleObjectElementCreator, "_progId", return_value="Excel.Sheet.42") + property_mock(request, _OleObjectElementCreator, "_icon_rId", return_value="rId24") property_mock(request, _OleObjectElementCreator, "_cx", return_value=cx) property_mock(request, _OleObjectElementCreator, "_cy", return_value=cy) element_creator = _OleObjectElementCreator( @@ -2248,7 +2202,10 @@ def it_determines_the_shape_height_to_help(self, cy_arg, prog_id, expected_value @pytest.mark.parametrize( "icon_height_arg, expected_value", - ((Emu(666666), Emu(666666)), (None, Emu(609600)),), + ( + (Emu(666666), Emu(666666)), + (None, Emu(609600)), + ), ) def it_determines_the_icon_height_to_help(self, icon_height_arg, expected_value): element_creator = _OleObjectElementCreator( @@ -2266,9 +2223,7 @@ def it_determines_the_icon_height_to_help(self, icon_height_arg, expected_value) (None, PROG_ID.XLSX, "xlsx-icon.emf"), ), ) - def it_resolves_the_icon_image_file_to_help( - self, icon_file_arg, prog_id, expected_value - ): + def it_resolves_the_icon_image_file_to_help(self, icon_file_arg, prog_id, expected_value): element_creator = _OleObjectElementCreator( None, None, None, prog_id, None, None, None, None, icon_file_arg, None, None ) diff --git a/tests/test_action.py b/tests/test_action.py index 33877eeae..dd0193ca6 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -1,15 +1,13 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.action` module.""" -from __future__ import unicode_literals +from __future__ import annotations import pytest from pptx.action import ActionSetting, Hyperlink from pptx.enum.action import PP_ACTION from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.package import Part +from pptx.opc.package import XmlPart from pptx.parts.slide import SlidePart from pptx.slide import Slide @@ -51,8 +49,7 @@ def it_can_change_its_slide_jump_target( _clear_click_action_.assert_called_once_with(action_setting) part_.relate_to.assert_called_once_with(slide_part_, RT.SLIDE) assert action_setting._element.xml == xml( - "p:cNvPr{a:b=c,r:s=t}/a:hlinkClick{action=ppaction://hlinksldjump,r:id=rI" - "d42}", + "p:cNvPr{a:b=c,r:s=t}/a:hlinkClick{action=ppaction://hlinksldjump,r:id=rI" "d42}", ) def but_it_clears_the_target_slide_if_None_is_assigned(self, _clear_click_action_): @@ -209,9 +206,7 @@ def target_get_fixture(self, request, action_prop_, _slide_index_prop_, part_pro return action_setting, expected_value @pytest.fixture(params=[(PP_ACTION.NEXT_SLIDE, 2), (PP_ACTION.PREVIOUS_SLIDE, 0)]) - def target_raise_fixture( - self, request, action_prop_, part_prop_, _slide_index_prop_ - ): + def target_raise_fixture(self, request, action_prop_, part_prop_, _slide_index_prop_): action_type, slide_idx = request.param action_setting = ActionSetting(None, None) action_prop_.return_value = action_type @@ -240,7 +235,7 @@ def hyperlink_(self, request): @pytest.fixture def part_(self, request): - return instance_mock(request, Part) + return instance_mock(request, XmlPart) @pytest.fixture def part_prop_(self, request): diff --git a/tests/test_api.py b/tests/test_api.py index b44573031..a48f48912 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.api` module.""" -""" -Test suite for pptx.api module -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import os diff --git a/tests/test_media.py b/tests/test_media.py index 9e42db9e5..be72f6e0e 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -1,16 +1,16 @@ -# encoding: utf-8 - """Unit test suite for `pptx.media` module.""" +from __future__ import annotations + +import io + import pytest -from pptx.compat import BytesIO from pptx.media import Video from .unitutil.file import absjoin, test_file_dir from .unitutil.mock import initializer_mock, instance_mock, method_mock, property_mock - TEST_VIDEO_PATH = absjoin(test_file_dir, "dummy.mp4") @@ -87,9 +87,7 @@ def ext_fixture(self, request): video = Video(None, mime_type, filename) return video, expected_value - @pytest.fixture( - params=[("foobar.mp4", None, "foobar.mp4"), (None, "vid", "movie.vid")] - ) + @pytest.fixture(params=[("foobar.mp4", None, "foobar.mp4"), (None, "vid", "movie.vid")]) def filename_fixture(self, request, ext_prop_): filename, ext, expected_value = request.param video = Video(None, None, filename) @@ -105,7 +103,7 @@ def from_blob_fixture(self, Video_init_): def from_stream_fixture(self, video_, from_blob_): with open(TEST_VIDEO_PATH, "rb") as f: blob = f.read() - movie_stream = BytesIO(blob) + movie_stream = io.BytesIO(blob) mime_type = "video/mp4" from_blob_.return_value = video_ return movie_stream, mime_type, blob, video_ diff --git a/tests/test_package.py b/tests/test_package.py index 5e32e74ce..ee02af2d6 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.package` module.""" +from __future__ import annotations + import os import pytest @@ -11,12 +13,11 @@ from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import Part, _Relationship from pptx.opc.packuri import PackURI -from pptx.package import _ImageParts, _MediaParts, Package +from pptx.package import Package, _ImageParts, _MediaParts from pptx.parts.coreprops import CorePropertiesPart from pptx.parts.image import Image, ImagePart from pptx.parts.media import MediaPart - from .unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock @@ -159,9 +160,7 @@ def it_can_iterate_over_the_package_image_parts(self, iter_fixture): image_parts, expected_parts = iter_fixture assert list(image_parts) == expected_parts - def it_can_get_a_matching_image_part( - self, Image_, image_, image_part_, _find_by_sha1_ - ): + def it_can_get_a_matching_image_part(self, Image_, image_, image_part_, _find_by_sha1_): Image_.from_file.return_value = image_ _find_by_sha1_.return_value = image_part_ image_parts = _ImageParts(None) diff --git a/tests/test_presentation.py b/tests/test_presentation.py index 03d2b027a..7c5315143 100644 --- a/tests/test_presentation.py +++ b/tests/test_presentation.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.presentation` module.""" -""" -Test suite for pptx.presentation module. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations import pytest @@ -71,9 +67,7 @@ def it_provides_access_to_its_slide_master(self, master_fixture): def it_provides_access_to_its_slide_masters(self, masters_fixture): prs, SlideMasters_, slide_masters_, expected_xml = masters_fixture slide_masters = prs.slide_masters - SlideMasters_.assert_called_once_with( - prs._element.xpath("p:sldMasterIdLst")[0], prs - ) + SlideMasters_.assert_called_once_with(prs._element.xpath("p:sldMasterIdLst")[0], prs) assert slide_masters is slide_masters_ assert prs._element.xml == expected_xml @@ -93,9 +87,7 @@ def core_props_fixture(self, prs_part_, core_properties_): @pytest.fixture def layouts_fixture(self, masters_prop_, slide_layouts_): prs = Presentation(None, None) - masters_prop_.return_value.__getitem__.return_value.slide_layouts = ( - slide_layouts_ - ) + masters_prop_.return_value.__getitem__.return_value.slide_layouts = slide_layouts_ return prs, slide_layouts_ @pytest.fixture @@ -134,9 +126,7 @@ def save_fixture(self, prs_part_): file_ = "foobar.docx" return prs, file_, prs_part_ - @pytest.fixture( - params=[("p:presentation", None), ("p:presentation/p:sldSz{cy=42}", 42)] - ) + @pytest.fixture(params=[("p:presentation", None), ("p:presentation/p:sldSz{cy=42}", 42)]) def sld_height_get_fixture(self, request): prs_cxml, expected_value = request.param prs = Presentation(element(prs_cxml), None) @@ -154,9 +144,7 @@ def sld_height_set_fixture(self, request): expected_xml = xml(expected_cxml) return prs, 914400, expected_xml - @pytest.fixture( - params=[("p:presentation", None), ("p:presentation/p:sldSz{cx=42}", 42)] - ) + @pytest.fixture(params=[("p:presentation", None), ("p:presentation/p:sldSz{cx=42}", 42)]) def sld_width_get_fixture(self, request): prs_cxml, expected_value = request.param prs = Presentation(element(prs_cxml), None) @@ -224,9 +212,7 @@ def slide_layouts_(self, request): @pytest.fixture def SlideMasters_(self, request, slide_masters_): - return class_mock( - request, "pptx.presentation.SlideMasters", return_value=slide_masters_ - ) + return class_mock(request, "pptx.presentation.SlideMasters", return_value=slide_masters_) @pytest.fixture def slide_master_(self, request): diff --git a/tests/test_shared.py b/tests/test_shared.py index e2d6bdc01..72a0ebc0e 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,7 +1,7 @@ -# encoding: utf-8 - """Unit-test suite for `pptx.shared` module.""" +from __future__ import annotations + import pytest from pptx.opc.package import XmlPart diff --git a/tests/test_slide.py b/tests/test_slide.py index d4a1bdeef..74b528c3b 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.slide` module.""" +from __future__ import annotations + import pytest from pptx.dml.fill import FillFormat @@ -23,9 +25,6 @@ SlideShapes, ) from pptx.slide import ( - _Background, - _BaseMaster, - _BaseSlide, NotesMaster, NotesSlide, Slide, @@ -34,6 +33,9 @@ SlideMaster, SlideMasters, Slides, + _Background, + _BaseMaster, + _BaseSlide, ) from pptx.text.text import TextFrame @@ -71,9 +73,7 @@ def background_fixture(self, _Background_, background_): _Background_.return_value = background_ return slide, _Background_, cSld, background_ - @pytest.fixture( - params=[("p:sld/p:cSld", ""), ("p:sld/p:cSld{name=Foobar}", "Foobar")] - ) + @pytest.fixture(params=[("p:sld/p:cSld", ""), ("p:sld/p:cSld{name=Foobar}", "Foobar")]) def name_get_fixture(self, request): sld_cxml, expected_name = request.param base_slide = _BaseSlide(element(sld_cxml), None) @@ -149,9 +149,7 @@ def subclass_fixture(self): @pytest.fixture def MasterPlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.MasterPlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.MasterPlaceholders", return_value=placeholders_) @pytest.fixture def MasterShapes_(self, request, shapes_): @@ -169,9 +167,7 @@ def shapes_(self, request): class DescribeNotesSlide(object): """Unit-test suite for `pptx.slide.NotesSlide` objects.""" - def it_can_clone_the_notes_master_placeholders( - self, request, notes_master_, shapes_ - ): + def it_can_clone_the_notes_master_placeholders(self, request, notes_master_, shapes_): placeholders = notes_master_.placeholders = ( BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=body}"), None), BaseShape(element("p:sp/p:nvSpPr/p:nvPr/p:ph{type=dt}"), None), @@ -233,9 +229,7 @@ def notes_ph_fixture(self, request, placeholders_prop_): return notes_slide, expected_value @pytest.fixture(params=[True, False]) - def notes_tf_fixture( - self, request, notes_placeholder_prop_, placeholder_, text_frame_ - ): + def notes_tf_fixture(self, request, notes_placeholder_prop_, placeholder_, text_frame_): has_text_frame = request.param notes_slide = NotesSlide(None, None) if has_text_frame: @@ -269,15 +263,11 @@ def notes_master_(self, request): @pytest.fixture def notes_placeholder_prop_(self, request, placeholder_): - return property_mock( - request, NotesSlide, "notes_placeholder", return_value=placeholder_ - ) + return property_mock(request, NotesSlide, "notes_placeholder", return_value=placeholder_) @pytest.fixture def NotesSlidePlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.NotesSlidePlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.NotesSlidePlaceholders", return_value=placeholders_) @pytest.fixture def NotesSlideShapes_(self, request, shapes_): @@ -293,9 +283,7 @@ def placeholders_(self, request): @pytest.fixture def placeholders_prop_(self, request, placeholders_): - return property_mock( - request, NotesSlide, "placeholders", return_value=placeholders_ - ) + return property_mock(request, NotesSlide, "placeholders", return_value=placeholders_) @pytest.fixture def shapes_(self, request): @@ -436,9 +424,7 @@ def placeholders_(self, request): @pytest.fixture def SlidePlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.SlidePlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.SlidePlaceholders", return_value=placeholders_) @pytest.fixture def SlideShapes_(self, request, shapes_): @@ -616,9 +602,7 @@ def it_can_iterate_its_clonable_placeholders(self, cloneable_fixture): cloneable = list(slide_layout.iter_cloneable_placeholders()) assert cloneable == expected_placeholders - def it_provides_access_to_its_placeholders( - self, LayoutPlaceholders_, placeholders_ - ): + def it_provides_access_to_its_placeholders(self, LayoutPlaceholders_, placeholders_): sldLayout = element("p:sldLayout/p:cSld/p:spTree") spTree = sldLayout.xpath("//p:spTree")[0] slide_layout = SlideLayout(sldLayout, None) @@ -675,9 +659,7 @@ def it_knows_which_slides_are_based_on_it( ((PP_PLACEHOLDER.SLIDE_NUMBER, PP_PLACEHOLDER.FOOTER), ()), ] ) - def cloneable_fixture( - self, request, placeholders_prop_, placeholder_, placeholder_2_ - ): + def cloneable_fixture(self, request, placeholders_prop_, placeholder_, placeholder_2_): ph_types, expected_indices = request.param slide_layout = SlideLayout(None, None) placeholder_.element.ph_type = ph_types[0] @@ -702,9 +684,7 @@ def used_by_fixture(self, request, presentation_, slide_, slide_2_): @pytest.fixture def LayoutPlaceholders_(self, request, placeholders_): - return class_mock( - request, "pptx.slide.LayoutPlaceholders", return_value=placeholders_ - ) + return class_mock(request, "pptx.slide.LayoutPlaceholders", return_value=placeholders_) @pytest.fixture def LayoutShapes_(self, request, shapes_): @@ -716,9 +696,7 @@ def package_(self, request): @pytest.fixture def part_prop_(self, request, slide_layout_part_): - return property_mock( - request, SlideLayout, "part", return_value=slide_layout_part_ - ) + return property_mock(request, SlideLayout, "part", return_value=slide_layout_part_) @pytest.fixture def placeholder_(self, request): @@ -734,9 +712,7 @@ def placeholders_(self, request): @pytest.fixture def placeholders_prop_(self, request, placeholders_): - return property_mock( - request, SlideLayout, "placeholders", return_value=placeholders_ - ) + return property_mock(request, SlideLayout, "placeholders", return_value=placeholders_) @pytest.fixture def presentation_(self, request): @@ -775,9 +751,7 @@ def it_supports_len(self, len_fixture): assert len(slide_layouts) == expected_value def it_can_iterate_its_slide_layouts(self, part_prop_, slide_master_part_): - sldLayoutIdLst = element( - "p:sldLayoutIdLst/(p:sldLayoutId{r:id=a},p:sldLayoutId{r:id=b})" - ) + sldLayoutIdLst = element("p:sldLayoutIdLst/(p:sldLayoutId{r:id=a},p:sldLayoutId{r:id=b})") _slide_layouts = [ SlideLayout(element("p:sldLayout"), None), SlideLayout(element("p:sldLayout"), None), @@ -795,9 +769,7 @@ def it_can_iterate_its_slide_layouts(self, part_prop_, slide_master_part_): def it_supports_indexed_access(self, slide_layout_, part_prop_, slide_master_part_): part_prop_.return_value = slide_master_part_ slide_master_part_.related_slide_layout.return_value = slide_layout_ - slide_layouts = SlideLayouts( - element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None - ) + slide_layouts = SlideLayouts(element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None) slide_layout = slide_layouts[0] @@ -805,15 +777,11 @@ def it_supports_indexed_access(self, slide_layout_, part_prop_, slide_master_par assert slide_layout is slide_layout_ def but_it_raises_on_index_out_of_range(self, part_prop_): - slide_layouts = SlideLayouts( - element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None - ) + slide_layouts = SlideLayouts(element("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId1}"), None) with pytest.raises(IndexError): slide_layouts[1] - def it_can_find_a_slide_layout_by_name( - self, _iter_, slide_layout_, slide_layout_2_ - ): + def it_can_find_a_slide_layout_by_name(self, _iter_, slide_layout_, slide_layout_2_): _iter_.return_value = iter((slide_layout_, slide_layout_2_)) slide_layout_2_.name = "pick me!" slide_layouts = SlideLayouts(None, None) @@ -871,14 +839,10 @@ def it_can_remove_an_unused_slide_layout( slide_layouts.remove(slide_layout_) - assert slide_layouts._sldLayoutIdLst.xml == xml( - "p:sldLayoutIdLst/p:sldLayoutId{r:id=rId2}" - ) + assert slide_layouts._sldLayoutIdLst.xml == xml("p:sldLayoutIdLst/p:sldLayoutId{r:id=rId2}") slide_master_part_.drop_rel.assert_called_once_with("rId1") - def but_it_raises_on_attempt_to_remove_slide_layout_in_use( - self, slide_layout_, slide_ - ): + def but_it_raises_on_attempt_to_remove_slide_layout_in_use(self, slide_layout_, slide_): slide_layout_.used_by_slides = (slide_,) slide_layouts = SlideLayouts(None, None) @@ -964,9 +928,7 @@ def subclass_fixture(self): @pytest.fixture def SlideLayouts_(self, request, slide_layouts_): - return class_mock( - request, "pptx.slide.SlideLayouts", return_value=slide_layouts_ - ) + return class_mock(request, "pptx.slide.SlideLayouts", return_value=slide_layouts_) @pytest.fixture def slide_layouts_(self, request): @@ -1001,9 +963,7 @@ def it_raises_on_index_out_of_range(self, getitem_raises_fixture): @pytest.fixture def getitem_fixture(self, part_, slide_master_, part_prop_): - slide_masters = SlideMasters( - element("p:sldMasterIdLst/p:sldMasterId{r:id=rId1}"), None - ) + slide_masters = SlideMasters(element("p:sldMasterIdLst/p:sldMasterId{r:id=rId1}"), None) part_.related_slide_master.return_value = slide_master_ return slide_masters, part_, slide_master_, "rId1" @@ -1013,9 +973,7 @@ def getitem_raises_fixture(self, part_prop_): @pytest.fixture def iter_fixture(self, part_prop_): - sldMasterIdLst = element( - "p:sldMasterIdLst/(p:sldMasterId{r:id=a},p:sldMasterId{r:id=b})" - ) + sldMasterIdLst = element("p:sldMasterIdLst/(p:sldMasterId{r:id=a},p:sldMasterId{r:id=b})") slide_masters = SlideMasters(sldMasterIdLst, None) related_slide_master_ = part_prop_.return_value.related_slide_master calls = [call("a"), call("b")] diff --git a/tests/test_table.py b/tests/test_table.py index 1207ff275..c53f1261f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,7 +1,9 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.table` module.""" +from __future__ import annotations + import pytest from pptx.dml.fill import FillFormat @@ -10,13 +12,13 @@ from pptx.oxml.table import CT_Table, CT_TableCell, TcRange from pptx.shapes.graphfrm import GraphicFrame from pptx.table import ( + Table, _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, - Table, ) from pptx.text.text import TextFrame from pptx.util import Inches, Length, Pt @@ -68,9 +70,7 @@ def it_can_iterate_its_grid_cells(self, request, _Cell_): def it_provides_access_to_its_rows(self, request): rows_ = instance_mock(request, _RowCollection) - _RowCollection_ = class_mock( - request, "pptx.table._RowCollection", return_value=rows_ - ) + _RowCollection_ = class_mock(request, "pptx.table._RowCollection", return_value=rows_) tbl = element("a:tbl") table = Table(tbl, None) @@ -237,9 +237,7 @@ def it_can_change_its_margin_settings(self, margin_set_fixture): setattr(cell, margin_prop_name, new_value) assert cell._tc.xml == expected_xml - def it_raises_on_margin_assigned_other_than_int_or_None( - self, margin_raises_fixture - ): + def it_raises_on_margin_assigned_other_than_int_or_None(self, margin_raises_fixture): cell, margin_attr_name, val_of_invalid_type = margin_raises_fixture with pytest.raises(TypeError): setattr(cell, margin_attr_name, val_of_invalid_type) @@ -381,9 +379,7 @@ def anchor_set_fixture(self, request): def fill_fixture(self, cell): return cell - @pytest.fixture( - params=[("a:tc", 1), ("a:tc{gridSpan=2}", 1), ("a:tc{rowSpan=42}", 42)] - ) + @pytest.fixture(params=[("a:tc", 1), ("a:tc{gridSpan=2}", 1), ("a:tc{rowSpan=42}", 42)]) def height_fixture(self, request): tc_cxml, expected_value = request.param tc = element(tc_cxml) @@ -422,9 +418,7 @@ def margin_set_fixture(self, request): expected_xml = xml(expected_tc_cxml) return cell, margin_prop_name, new_value, expected_xml - @pytest.fixture( - params=["margin_left", "margin_right", "margin_top", "margin_bottom"] - ) + @pytest.fixture(params=["margin_left", "margin_right", "margin_top", "margin_bottom"]) def margin_raises_fixture(self, request): margin_prop_name = request.param cell = _Cell(element("a:tc"), None) @@ -489,9 +483,7 @@ def split_fixture(self, request): range_tcs = tuple(tcs[idx] for idx in range_tc_idxs) return origin_tc, range_tcs - @pytest.fixture( - params=[("a:tc", 1), ("a:tc{rowSpan=2}", 1), ("a:tc{gridSpan=24}", 24)] - ) + @pytest.fixture(params=[("a:tc", 1), ("a:tc{rowSpan=2}", 1), ("a:tc{gridSpan=24}", 24)]) def width_fixture(self, request): tc_cxml, expected_value = request.param tc = element(tc_cxml) @@ -561,8 +553,7 @@ def iter_fixture(self, request, _Cell_): cell_collection = _CellCollection(tr, None) expected_cells = [ - instance_mock(request, _Cell, name="cell%d" % idx) - for idx in range(len(tcs)) + instance_mock(request, _Cell, name="cell%d" % idx) for idx in range(len(tcs)) ] _Cell_.side_effect = expected_cells calls = [call(tc, cell_collection) for tc in tcs] @@ -601,9 +592,7 @@ def it_can_change_its_width(self, width_set_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture( - params=[("a:gridCol{w=914400}", Inches(1)), ("a:gridCol{w=10pt}", Pt(10))] - ) + @pytest.fixture(params=[("a:gridCol{w=914400}", Inches(1)), ("a:gridCol{w=10pt}", Pt(10))]) def width_get_fixture(self, request): gridCol_cxml, expected_value = request.param column = _Column(element(gridCol_cxml), None) diff --git a/tests/test_util.py b/tests/test_util.py index 4944d33f4..97e46fa4c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,21 +1,10 @@ -# encoding: utf-8 +"""Unit-test suite for `pptx.util` module.""" -""" -Test suite for pptx.util module. -""" - -from __future__ import absolute_import +from __future__ import annotations import pytest -from pptx.compat import to_unicode -from pptx.util import Length, Centipoints, Cm, Emu, Inches, Mm, Pt - - -def test_to_unicode_raises_on_non_string(): - """to_unicode(text) raises on *text* not a string""" - with pytest.raises(TypeError): - to_unicode(999) +from pptx.util import Centipoints, Cm, Emu, Inches, Length, Mm, Pt class DescribeLength(object): diff --git a/tests/text/test_fonts.py b/tests/text/test_fonts.py index 275052235..995c78dd2 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -1,19 +1,18 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.text.fonts` module.""" -from __future__ import unicode_literals +from __future__ import annotations import io -import pytest - from struct import calcsize -from pptx.compat import BytesIO +import pytest + from pptx.text.fonts import ( + FontFiles, _BaseTable, _Font, - FontFiles, _HeadTable, _NameTable, _Stream, @@ -85,9 +84,7 @@ def find_fixture(self, request, _installed_fonts_): return family_name, is_bold, is_italic, expected_path @pytest.fixture(params=[("darwin", ["a", "b"]), ("win32", ["c", "d"])]) - def font_dirs_fixture( - self, request, _os_x_font_directories_, _windows_font_directories_ - ): + def font_dirs_fixture(self, request, _os_x_font_directories_, _windows_font_directories_): platform, expected_dirs = request.param dirs_meth_mock = { "darwin": _os_x_font_directories_, @@ -172,9 +169,7 @@ def _os_x_font_directories_(self, request): @pytest.fixture def _windows_font_directories_(self, request): - return method_mock( - request, FontFiles, "_windows_font_directories", autospec=False - ) + return method_mock(request, FontFiles, "_windows_font_directories", autospec=False) class Describe_Font(object): @@ -227,9 +222,7 @@ def it_reads_the_header_to_help_read_font(self, request): # fixtures --------------------------------------------- - @pytest.fixture( - params=[("head", True, True), ("head", False, False), ("foob", True, False)] - ) + @pytest.fixture(params=[("head", True, True), ("head", False, False), ("foob", True, False)]) def bold_fixture(self, request, _tables_, head_table_): key, is_bold, expected_value = request.param head_table_.is_bold = is_bold @@ -245,9 +238,7 @@ def family_fixture(self, _tables_, name_table_): name_table_.family_name = expected_name return font, expected_name - @pytest.fixture( - params=[("head", True, True), ("head", False, False), ("foob", True, False)] - ) + @pytest.fixture(params=[("head", True, True), ("head", False, False), ("foob", True, False)]) def italic_fixture(self, request, _tables_, head_table_): key, is_italic, expected_value = request.param head_table_.is_italic = is_italic @@ -468,7 +459,7 @@ def italic_fixture(self, request, _macStyle_): @pytest.fixture def macStyle_fixture(self): bytes_ = b"xxxxyyyy....................................\xF0\xBA........" - stream = _Stream(BytesIO(bytes_)) + stream = _Stream(io.BytesIO(bytes_)) offset, length = 0, len(bytes_) head_table = _HeadTable(None, stream, offset, length) expected_value = 61626 @@ -503,9 +494,7 @@ def it_provides_access_to_its_names_to_help_props(self, request): _iter_names_.assert_called_once_with(name_table) assert names == {(0, 1): "Foobar", (3, 1): "Barfoo"} - def it_iterates_over_its_names_to_help_read_names( - self, request, _table_bytes_prop_ - ): + def it_iterates_over_its_names_to_help_read_names(self, request, _table_bytes_prop_): property_mock(request, _NameTable, "_table_header", return_value=(0, 3, 42)) _table_bytes_prop_.return_value = "xXx" _read_name_ = method_mock( @@ -533,9 +522,7 @@ def it_reads_the_table_header_to_help_read_names(self, header_fixture): def it_buffers_the_table_bytes_to_help_read_names(self, bytes_fixture): name_table, expected_value = bytes_fixture table_bytes = name_table._table_bytes - name_table._stream.read.assert_called_once_with( - name_table._offset, name_table._length - ) + name_table._stream.read.assert_called_once_with(name_table._offset, name_table._length) assert table_bytes == expected_value def it_reads_a_name_to_help_read_names(self, request): @@ -555,9 +542,7 @@ def it_reads_a_name_to_help_read_names(self, request): name_str_offset, ), ) - _read_name_text_ = method_mock( - request, _NameTable, "_read_name_text", return_value=name - ) + _read_name_text_ = method_mock(request, _NameTable, "_read_name_text", return_value=name) name_table = _NameTable(None, None, None, None) actual = name_table._read_name(bufr, idx, strs_offset) @@ -591,9 +576,7 @@ def it_reads_name_text_to_help_read_names(self, name_text_fixture): name_table._raw_name_string.assert_called_once_with( bufr, strings_offset, name_str_offset, length ) - name_table._decode_name.assert_called_once_with( - raw_name, platform_id, encoding_id - ) + name_table._decode_name.assert_called_once_with(raw_name, platform_id, encoding_id) assert name is name_ def it_reads_name_bytes_to_help_read_names(self, raw_fixture): diff --git a/tests/text/test_layout.py b/tests/text/test_layout.py index 2627660f2..6e2c83d6a 100644 --- a/tests/text/test_layout.py +++ b/tests/text/test_layout.py @@ -1,10 +1,12 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.text.layout` module.""" +from __future__ import annotations + import pytest -from pptx.text.layout import _BinarySearchTree, _Line, _LineSource, TextFitter +from pptx.text.layout import TextFitter, _BinarySearchTree, _Line, _LineSource from ..unitutil.mock import ( ANY, @@ -31,9 +33,7 @@ def it_can_determine_the_best_fit_font_size(self, request, line_source_): ) extents, max_size = (19, 20), 42 - font_size = TextFitter.best_fit_font_size( - "Foobar", extents, max_size, "foobar.ttf" - ) + font_size = TextFitter.best_fit_font_size("Foobar", extents, max_size, "foobar.ttf") _LineSource_.assert_called_once_with("Foobar") _init_.assert_called_once_with(line_source_, extents, "foobar.ttf") @@ -46,9 +46,7 @@ def it_finds_best_fit_font_size_to_help_best_fit(self, _best_fit_fixture): font_size = text_fitter._best_fit_font_size(max_size) - _BinarySearchTree_.from_ordered_sequence.assert_called_once_with( - range(1, max_size + 1) - ) + _BinarySearchTree_.from_ordered_sequence.assert_called_once_with(range(1, max_size + 1)) sizes_.find_max.assert_called_once_with(predicate_) assert font_size is font_size_ @@ -70,9 +68,7 @@ def it_provides_a_fits_inside_predicate_fn( text_lines, expected_value, ): - _wrap_lines_ = method_mock( - request, TextFitter, "_wrap_lines", return_value=text_lines - ) + _wrap_lines_ = method_mock(request, TextFitter, "_wrap_lines", return_value=text_lines) _rendered_size_.return_value = (None, 50) text_fitter = TextFitter(line_source_, extents, "foobar.ttf") @@ -80,9 +76,7 @@ def it_provides_a_fits_inside_predicate_fn( result = predicate(point_size) _wrap_lines_.assert_called_once_with(text_fitter, line_source_, point_size) - _rendered_size_.assert_called_once_with( - "Ty", point_size, text_fitter._font_file - ) + _rendered_size_.assert_called_once_with("Ty", point_size, text_fitter._font_file) assert result is expected_value def it_provides_a_fits_in_width_predicate_fn(self, fits_cx_pred_fixture): @@ -92,9 +86,7 @@ def it_provides_a_fits_in_width_predicate_fn(self, fits_cx_pred_fixture): predicate = text_fitter._fits_in_width_predicate(point_size) result = predicate(line) - _rendered_size_.assert_called_once_with( - line.text, point_size, text_fitter._font_file - ) + _rendered_size_.assert_called_once_with(line.text, point_size, text_fitter._font_file) assert result is expected_value def it_wraps_lines_to_help_best_fit(self, request): @@ -114,13 +106,9 @@ def it_wraps_lines_to_help_best_fit(self, request): call(text_fitter, remainder, 21), ] - def it_breaks_off_a_line_to_help_wrap( - self, request, line_source_, _BinarySearchTree_ - ): + def it_breaks_off_a_line_to_help_wrap(self, request, line_source_, _BinarySearchTree_): bst_ = instance_mock(request, _BinarySearchTree) - _fits_in_width_predicate_ = method_mock( - request, TextFitter, "_fits_in_width_predicate" - ) + _fits_in_width_predicate_ = method_mock(request, TextFitter, "_fits_in_width_predicate") _BinarySearchTree_.from_ordered_sequence.return_value = bst_ predicate_ = _fits_in_width_predicate_.return_value max_value_ = bst_.find_max.return_value diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 28f0e65a6..3a1a7a0bb 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -1,20 +1,21 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false """Unit-test suite for `pptx.text.text` module.""" -from __future__ import unicode_literals +from __future__ import annotations + +from typing import TYPE_CHECKING, cast import pytest -from pptx.compat import is_unicode from pptx.dml.color import ColorFormat from pptx.dml.fill import FillFormat from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, MSO_UNDERLINE, PP_ALIGN from pptx.opc.constants import RELATIONSHIP_TYPE as RT -from pptx.opc.package import Part +from pptx.opc.package import XmlPart from pptx.shapes.autoshape import Shape -from pptx.text.text import Font, _Hyperlink, _Paragraph, _Run, TextFrame +from pptx.text.text import Font, TextFrame, _Hyperlink, _Paragraph, _Run from pptx.util import Inches, Pt from ..oxml.unitdata.text import a_p, a_t, an_hlinkClick, an_r, an_rPr @@ -27,6 +28,9 @@ property_mock, ) +if TYPE_CHECKING: + from pptx.oxml.text import CT_TextBody, CT_TextParagraph + class DescribeTextFrame(object): """Unit-test suite for `pptx.text.text.TextFrame` object.""" @@ -40,10 +44,29 @@ def it_knows_its_autosize_setting(self, autosize_get_fixture): text_frame, expected_value = autosize_get_fixture assert text_frame.auto_size == expected_value - def it_can_change_its_autosize_setting(self, autosize_set_fixture): - text_frame, value, expected_xml = autosize_set_fixture + @pytest.mark.parametrize( + ("txBody_cxml", "value", "expected_cxml"), + [ + ("p:txBody/a:bodyPr", MSO_AUTO_SIZE.NONE, "p:txBody/a:bodyPr/a:noAutofit"), + ( + "p:txBody/a:bodyPr/a:noAutofit", + MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT, + "p:txBody/a:bodyPr/a:spAutoFit", + ), + ( + "p:txBody/a:bodyPr/a:spAutoFit", + MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE, + "p:txBody/a:bodyPr/a:normAutofit", + ), + ("p:txBody/a:bodyPr/a:normAutofit", None, "p:txBody/a:bodyPr"), + ], + ) + def it_can_change_its_autosize_setting( + self, txBody_cxml: str, value: MSO_AUTO_SIZE | None, expected_cxml: str + ): + text_frame = TextFrame(element(txBody_cxml), None) text_frame.auto_size = value - assert text_frame._txBody.xml == expected_xml + assert text_frame._txBody.xml == xml(expected_cxml) @pytest.mark.parametrize( "txBody_cxml", @@ -69,14 +92,41 @@ def it_can_change_its_margin_settings(self, margin_set_fixture): setattr(text_frame, prop_name, new_value) assert text_frame._txBody.xml == expected_xml - def it_knows_its_vertical_alignment(self, anchor_get_fixture): - text_frame, expected_value = anchor_get_fixture + @pytest.mark.parametrize( + ("txBody_cxml", "expected_value"), + [ + ("p:txBody/a:bodyPr", None), + ("p:txBody/a:bodyPr{anchor=t}", MSO_ANCHOR.TOP), + ("p:txBody/a:bodyPr{anchor=b}", MSO_ANCHOR.BOTTOM), + ], + ) + def it_knows_its_vertical_alignment(self, txBody_cxml: str, expected_value: MSO_ANCHOR | None): + text_frame = TextFrame(cast("CT_TextBody", element(txBody_cxml)), None) assert text_frame.vertical_anchor == expected_value - def it_can_change_its_vertical_alignment(self, anchor_set_fixture): - text_frame, new_value, expected_xml = anchor_set_fixture + @pytest.mark.parametrize( + ("txBody_cxml", "new_value", "expected_cxml"), + [ + ("p:txBody/a:bodyPr", MSO_ANCHOR.TOP, "p:txBody/a:bodyPr{anchor=t}"), + ( + "p:txBody/a:bodyPr{anchor=t}", + MSO_ANCHOR.MIDDLE, + "p:txBody/a:bodyPr{anchor=ctr}", + ), + ( + "p:txBody/a:bodyPr{anchor=ctr}", + MSO_ANCHOR.BOTTOM, + "p:txBody/a:bodyPr{anchor=b}", + ), + ("p:txBody/a:bodyPr{anchor=b}", None, "p:txBody/a:bodyPr"), + ], + ) + def it_can_change_its_vertical_alignment( + self, txBody_cxml: str, new_value: MSO_ANCHOR | None, expected_cxml: str + ): + text_frame = TextFrame(cast("CT_TextBody", element(txBody_cxml)), None) text_frame.vertical_anchor = new_value - assert text_frame._element.xml == expected_xml + assert text_frame._element.xml == xml(expected_cxml) def it_knows_its_word_wrap_setting(self, wrap_get_fixture): text_frame, expected_value = wrap_get_fixture @@ -105,9 +155,7 @@ def it_knows_the_part_it_belongs_to(self, text_frame_with_parent_): part = text_frame.part assert part is parent_.part - def it_knows_what_text_it_contains( - self, request, text_get_fixture, paragraphs_prop_ - ): + def it_knows_what_text_it_contains(self, request, text_get_fixture, paragraphs_prop_): paragraph_texts, expected_value = text_get_fixture paragraphs_prop_.return_value = tuple( instance_mock(request, _Paragraph, text=text) for text in paragraph_texts @@ -157,9 +205,7 @@ def it_calculates_its_best_fit_font_size_to_help_fit_text(self, size_font_fixtur font_size = text_frame._best_fit_font_size(family, max_size, bold, italic, None) FontFiles_.find.assert_called_once_with(family, bold, italic) - TextFitter_.best_fit_font_size.assert_called_once_with( - text, extents, max_size, font_file_ - ) + TextFitter_.best_fit_font_size.assert_called_once_with(text, extents, max_size, font_file_) assert font_size is font_size_ def it_calculates_its_effective_size_to_help_fit_text(self): @@ -200,40 +246,6 @@ def add_paragraph_fixture(self, request): expected_xml = xml(expected_cxml) return text_frame, expected_xml - @pytest.fixture( - params=[ - ("p:txBody/a:bodyPr", None), - ("p:txBody/a:bodyPr{anchor=t}", MSO_ANCHOR.TOP), - ("p:txBody/a:bodyPr{anchor=b}", MSO_ANCHOR.BOTTOM), - ] - ) - def anchor_get_fixture(self, request): - txBody_cxml, expected_value = request.param - text_frame = TextFrame(element(txBody_cxml), None) - return text_frame, expected_value - - @pytest.fixture( - params=[ - ("p:txBody/a:bodyPr", MSO_ANCHOR.TOP, "p:txBody/a:bodyPr{anchor=t}"), - ( - "p:txBody/a:bodyPr{anchor=t}", - MSO_ANCHOR.MIDDLE, - "p:txBody/a:bodyPr{anchor=ctr}", - ), - ( - "p:txBody/a:bodyPr{anchor=ctr}", - MSO_ANCHOR.BOTTOM, - "p:txBody/a:bodyPr{anchor=b}", - ), - ("p:txBody/a:bodyPr{anchor=b}", None, "p:txBody/a:bodyPr"), - ] - ) - def anchor_set_fixture(self, request): - txBody_cxml, new_value, expected_cxml = request.param - text_frame = TextFrame(element(txBody_cxml), None) - expected_xml = xml(expected_cxml) - return text_frame, new_value, expected_xml - @pytest.fixture( params=[ ("p:txBody/a:bodyPr", None), @@ -247,28 +259,6 @@ def autosize_get_fixture(self, request): text_frame = TextFrame(element(txBody_cxml), None) return text_frame, expected_value - @pytest.fixture( - params=[ - ("p:txBody/a:bodyPr", MSO_AUTO_SIZE.NONE, "p:txBody/a:bodyPr/a:noAutofit"), - ( - "p:txBody/a:bodyPr/a:noAutofit", - MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT, - "p:txBody/a:bodyPr/a:spAutoFit", - ), - ( - "p:txBody/a:bodyPr/a:spAutoFit", - MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE, - "p:txBody/a:bodyPr/a:normAutofit", - ), - ("p:txBody/a:bodyPr/a:normAutofit", None, "p:txBody/a:bodyPr"), - ] - ) - def autosize_set_fixture(self, request): - txBody_cxml, value, expected_cxml = request.param - text_frame = TextFrame(element(txBody_cxml), None) - expected_xml = xml(expected_cxml) - return text_frame, value, expected_xml - @pytest.fixture( params=[ ("p:txBody/a:bodyPr", "left", "emu", Inches(0.1)), @@ -389,9 +379,7 @@ def size_font_fixture(self, FontFiles_, TextFitter_, text_prop_, _extents_prop_) font_size, ) - @pytest.fixture( - params=[(["foobar"], "foobar"), (["foo", "bar", "baz"], "foo\nbar\nbaz")] - ) + @pytest.fixture(params=[(["foobar"], "foobar"), (["foo", "bar", "baz"], "foo\nbar\nbaz")]) def text_get_fixture(self, request): paragraph_texts, expected_value = request.param return paragraph_texts, expected_value @@ -545,9 +533,7 @@ def it_provides_access_to_its_fill(self, font): # fixtures --------------------------------------------- - @pytest.fixture( - params=[("a:rPr", None), ("a:rPr{b=0}", False), ("a:rPr{b=1}", True)] - ) + @pytest.fixture(params=[("a:rPr", None), ("a:rPr{b=0}", False), ("a:rPr{b=1}", True)]) def bold_get_fixture(self, request): rPr_cxml, expected_value = request.param font = Font(element(rPr_cxml)) @@ -566,9 +552,7 @@ def bold_set_fixture(self, request): expected_xml = xml(expected_rPr_cxml) return font, new_value, expected_xml - @pytest.fixture( - params=[("a:rPr", None), ("a:rPr{i=0}", False), ("a:rPr{i=1}", True)] - ) + @pytest.fixture(params=[("a:rPr", None), ("a:rPr{i=0}", False), ("a:rPr{i=1}", True)]) def italic_get_fixture(self, request): rPr_cxml, expected_value = request.param font = Font(element(rPr_cxml)) @@ -616,9 +600,7 @@ def language_id_set_fixture(self, request): expected_xml = xml(expected_rPr_cxml) return font, new_value, expected_xml - @pytest.fixture( - params=[("a:rPr", None), ("a:rPr/a:latin{typeface=Foobar}", "Foobar")] - ) + @pytest.fixture(params=[("a:rPr", None), ("a:rPr/a:latin{typeface=Foobar}", "Foobar")]) def name_get_fixture(self, request): rPr_cxml, expected_value = request.param font = Font(element(rPr_cxml)) @@ -647,9 +629,7 @@ def size_get_fixture(self, request): font = Font(element(rPr_cxml)) return font, expected_value - @pytest.fixture( - params=[("a:rPr", Pt(24), "a:rPr{sz=2400}"), ("a:rPr{sz=2400}", None, "a:rPr")] - ) + @pytest.fixture(params=[("a:rPr", Pt(24), "a:rPr{sz=2400}"), ("a:rPr{sz=2400}", None, "a:rPr")]) def size_set_fixture(self, request): rPr_cxml, new_value, expected_rPr_cxml = request.param font = Font(element(rPr_cxml)) @@ -705,9 +685,7 @@ def it_has_None_for_address_when_no_hyperlink_is_present(self, hlink): def it_can_set_the_target_url(self, hlink, rPr_with_hlinkClick_xml, url): hlink.address = url # verify ----------------------- - hlink.part.relate_to.assert_called_once_with( - url, RT.HYPERLINK, is_external=True - ) + hlink.part.relate_to.assert_called_once_with(url, RT.HYPERLINK, is_external=True) assert hlink._rPr.xml == rPr_with_hlinkClick_xml assert hlink.address == url @@ -717,9 +695,7 @@ def it_can_remove_the_hyperlink(self, remove_hlink_fixture_): assert hlink._rPr.xml == rPr_xml hlink.part.drop_rel.assert_called_once_with(rId) - def it_should_remove_the_hyperlink_when_url_set_to_empty_string( - self, remove_hlink_fixture_ - ): + def it_should_remove_the_hyperlink_when_url_set_to_empty_string(self, remove_hlink_fixture_): hlink, rPr_xml, rId = remove_hlink_fixture_ hlink.address = "" assert hlink._rPr.xml == rPr_xml @@ -733,16 +709,12 @@ def it_can_change_the_target_url(self, change_hlink_fixture_): # verify ----------------------- assert hlink._rPr.xml == new_rPr_xml hlink.part.drop_rel.assert_called_once_with(rId_existing) - hlink.part.relate_to.assert_called_once_with( - new_url, RT.HYPERLINK, is_external=True - ) + hlink.part.relate_to.assert_called_once_with(new_url, RT.HYPERLINK, is_external=True) # fixtures --------------------------------------------- @pytest.fixture - def change_hlink_fixture_( - self, request, hlink_with_hlinkClick, rId, rId_2, part_, url_2 - ): + def change_hlink_fixture_(self, request, hlink_with_hlinkClick, rId, rId_2, part_, url_2): hlinkClick_bldr = an_hlinkClick().with_rId(rId_2) new_rPr_xml = an_rPr().with_nsdecls("a", "r").with_child(hlinkClick_bldr).xml() part_.relate_to.return_value = rId_2 @@ -772,7 +744,7 @@ def part_(self, request, url, rId): Mock Part instance suitable for patching into _Hyperlink.part property. It returns url for target_ref() and rId for relate_to(). """ - part_ = instance_mock(request, Part) + part_ = instance_mock(request, XmlPart) part_.target_ref.return_value = url part_.relate_to.return_value = rId return part_ @@ -896,15 +868,34 @@ def it_knows_what_text_it_contains(self, text_get_fixture): text = paragraph.text assert text == expected_value - assert is_unicode(text) + assert isinstance(text, str) - def it_can_change_its_text(self, text_set_fixture): - p, value, expected_xml = text_set_fixture + @pytest.mark.parametrize( + ("p_cxml", "value", "expected_cxml"), + [ + ('a:p/(a:r/a:t"foo",a:r/a:t"bar")', "foobar", 'a:p/a:r/a:t"foobar"'), + ("a:p", "", "a:p"), + ("a:p", "foobar", 'a:p/a:r/a:t"foobar"'), + ("a:p", "foo\nbar", 'a:p/(a:r/a:t"foo",a:br,a:r/a:t"bar")'), + ("a:p", "\vfoo\n", 'a:p/(a:br,a:r/a:t"foo",a:br)'), + ("a:p", "\n\nfoo", 'a:p/(a:br,a:br,a:r/a:t"foo")'), + ("a:p", "foo\n", 'a:p/(a:r/a:t"foo",a:br)'), + ("a:p", "foo\x07\n", 'a:p/(a:r/a:t"foo_x0007_",a:br)'), + ("a:p", "ŮŦƑ-8\x1bliteral", 'a:p/a:r/a:t"ŮŦƑ-8_x001B_literal"'), + ( + "a:p", + "utf-8 unicode: Hér er texti", + 'a:p/a:r/a:t"utf-8 unicode: Hér er texti"', + ), + ], + ) + def it_can_change_its_text(self, p_cxml: str, value: str, expected_cxml: str): + p = cast("CT_TextParagraph", element(p_cxml)) paragraph = _Paragraph(p, None) paragraph.text = value - assert paragraph._element.xml == expected_xml + assert paragraph._element.xml == xml(expected_cxml) # fixtures --------------------------------------------- @@ -1128,32 +1119,6 @@ def text_get_fixture(self, request): p = element(p_cxml) return p, expected_value - @pytest.fixture( - params=[ - ('a:p/(a:r/a:t"foo",a:r/a:t"bar")', "foobar", 'a:p/a:r/a:t"foobar"'), - ("a:p", "", "a:p"), - ("a:p", "foobar", 'a:p/a:r/a:t"foobar"'), - ("a:p", "foo\nbar", 'a:p/(a:r/a:t"foo",a:br,a:r/a:t"bar")'), - ("a:p", "\vfoo\n", 'a:p/(a:br,a:r/a:t"foo",a:br)'), - ("a:p", "\n\nfoo", 'a:p/(a:br,a:br,a:r/a:t"foo")'), - ("a:p", "foo\n", 'a:p/(a:r/a:t"foo",a:br)'), - ("a:p", b"foo\x07\n", 'a:p/(a:r/a:t"foo_x0007_",a:br)'), - ("a:p", b"7-bit str", 'a:p/a:r/a:t"7-bit str"'), - ("a:p", b"8-\xc9\x93\xc3\xaf\xc8\xb6 str", 'a:p/a:r/a:t"8-ɓïȶ str"'), - ("a:p", "ŮŦƑ-8\x1bliteral", 'a:p/a:r/a:t"ŮŦƑ-8_x001B_literal"'), - ( - "a:p", - "utf-8 unicode: Hér er texti", - 'a:p/a:r/a:t"utf-8 unicode: Hér er texti"', - ), - ] - ) - def text_set_fixture(self, request): - p_cxml, value, expected_cxml = request.param - p = element(p_cxml) - expected_xml = xml(expected_cxml) - return p, value, expected_xml - # fixture components ----------------------------------- @pytest.fixture @@ -1193,7 +1158,7 @@ def it_can_get_the_text_of_the_run(self, text_get_fixture): run, expected_value = text_get_fixture text = run.text assert text == expected_value - assert is_unicode(text) + assert isinstance(text, str) @pytest.mark.parametrize( "r_cxml, new_value, expected_r_cxml", diff --git a/tests/unitdata.py b/tests/unitdata.py index f40fcaf8c..978647865 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -1,10 +1,6 @@ -# encoding: utf-8 +"""Shared objects for unit data builder modules.""" -""" -Shared objects for unit data builder modules -""" - -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import annotations from pptx.oxml import parse_xml from pptx.oxml.ns import nsdecls diff --git a/tests/unitutil/__init__.py b/tests/unitutil/__init__.py index 38eca7c27..7f5a5b584 100644 --- a/tests/unitutil/__init__.py +++ b/tests/unitutil/__init__.py @@ -1,8 +1,6 @@ -# encoding: utf-8 +"""Helper objects for unit testing.""" -""" -Helper objects for unit testing. -""" +from __future__ import annotations def count(start=0, step=1): diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index d44cb51d1..79e217c20 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -1,48 +1,48 @@ -# encoding: utf-8 +"""Parser for Compact XML Expression Language (CXEL) ('see-ex-ell'). -""" -Parser for Compact XML Expression Language (CXEL) ('see-ex-ell'), a compact -XML specification language I made up that's useful for producing XML element +CXEL is a compact XML specification language I made up that's useful for producing XML element trees suitable for unit testing. """ -from __future__ import print_function +from __future__ import annotations + +from typing import TYPE_CHECKING from pyparsing import ( - alphas, - alphanums, Combine, - dblQuotedString, - delimitedList, Forward, Group, Literal, Optional, - removeQuotes, - stringEnd, Suppress, Word, + alphanums, + alphas, + dblQuotedString, + delimitedList, + removeQuotes, + stringEnd, ) from pptx.oxml import parse_xml from pptx.oxml.ns import _nsmap as nsmap +if TYPE_CHECKING: + from pptx.oxml.xmlchemy import BaseOxmlElement # ==================================================================== # api functions # ==================================================================== -def element(cxel_str): - """ - Return an oxml element parsed from the XML generated from *cxel_str*. - """ +def element(cxel_str: str) -> BaseOxmlElement: + """Return an oxml element parsed from the XML generated from `cxel_str`.""" _xml = xml(cxel_str) return parse_xml(_xml) -def xml(cxel_str): - """Return the XML generated from *cxel_str*.""" +def xml(cxel_str: str) -> str: + """Return the XML generated from `cxel_str`.""" root_node.parseWithTabs() root_token = root_node.parseString(cxel_str) xml = root_token.element.xml @@ -274,9 +274,7 @@ def grammar(): child_node_list << (open_paren + delimitedList(node) + close_paren | node) root_node = ( - element("element") - + Group(Optional(slash + child_node_list))("child_node_list") - + stringEnd + element("element") + Group(Optional(slash + child_node_list))("child_node_list") + stringEnd ).setParseAction(connect_root_node_children) return root_node diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 0d25aac5a..938d42ef0 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,29 +1,24 @@ -# encoding: utf-8 - """Utility functions for loading files for unit testing.""" import os import sys - _thisdir = os.path.split(__file__)[0] test_file_dir = os.path.abspath(os.path.join(_thisdir, "..", "test_files")) -def absjoin(*paths): +def absjoin(*paths: str): return os.path.abspath(os.path.join(*paths)) -def snippet_bytes(snippet_file_name): +def snippet_bytes(snippet_file_name: str): """Return bytes read from snippet file having `snippet_file_name`.""" - snippet_file_path = os.path.join( - test_file_dir, "snippets", "%s.txt" % snippet_file_name - ) + snippet_file_path = os.path.join(test_file_dir, "snippets", "%s.txt" % snippet_file_name) with open(snippet_file_path, "rb") as f: return f.read().strip() -def snippet_seq(name, offset=0, count=sys.maxsize): +def snippet_seq(name: str, offset: int = 0, count: int = sys.maxsize): """ Return a tuple containing the unicode text snippets read from the snippet file having *name*. Snippets are delimited by a blank line. If specified, @@ -37,27 +32,25 @@ def snippet_seq(name, offset=0, count=sys.maxsize): return tuple(snippets[start:end]) -def snippet_text(snippet_file_name): +def snippet_text(snippet_file_name: str): """ Return the unicode text read from the test snippet file having *snippet_file_name*. """ - snippet_file_path = os.path.join( - test_file_dir, "snippets", "%s.txt" % snippet_file_name - ) + snippet_file_path = os.path.join(test_file_dir, "snippets", "%s.txt" % snippet_file_name) with open(snippet_file_path, "rb") as f: snippet_bytes = f.read() return snippet_bytes.decode("utf-8") -def testfile(name): +def testfile(name: str): """ Return the absolute path to test file having *name*. """ return absjoin(test_file_dir, name) -def testfile_bytes(*segments): +def testfile_bytes(*segments: str): """Return bytes of file at path formed by adding `segments` to test file dir.""" path = os.path.join(test_file_dir, *segments) with open(path, "rb") as f: diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 849a927cb..3b681d983 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,22 +1,28 @@ -# encoding: utf-8 - """Utility functions wrapping the excellent `mock` library.""" -from __future__ import absolute_import +from __future__ import annotations + +from typing import Any +from unittest import mock +from unittest.mock import ( + ANY, + MagicMock, + Mock, + PropertyMock, + call, + create_autospec, + mock_open, + patch, +) -import sys +from pytest import FixtureRequest, LogCaptureFixture # noqa: PT013 -if sys.version_info >= (3, 3): - from unittest import mock # noqa - from unittest.mock import ANY, call, MagicMock # noqa - from unittest.mock import create_autospec, Mock, mock_open, patch, PropertyMock -else: # pragma: no cover - import mock # noqa - from mock import ANY, call, MagicMock # noqa - from mock import create_autospec, Mock, mock_open, patch, PropertyMock +__all__ = ["ANY", "FixtureRequest", "LogCaptureFixture", "MagicMock", "call", "mock"] -def class_mock(request, q_class_name, autospec=True, **kwargs): +def class_mock( + request: FixtureRequest, q_class_name: str, autospec: bool = True, **kwargs: Any +) -> Mock: """Return a mock patching the class with qualified name *q_class_name*. The mock is autospec'ed based on the patched class unless the optional argument @@ -28,8 +34,10 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): return _patch.start() -def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): # pragma: no cover - """Return a mock for attribute (class variable) `attr_name` on `cls`. +def cls_attr_mock( + request: FixtureRequest, cls: type, attr_name: str, name: str | None = None, **kwargs: Any +) -> Mock: + """Return a mock for an attribute (class variable) `attr_name` on `cls`. Patch is reversed after pytest uses it. """ @@ -39,7 +47,9 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): # pragma: no c return _patch.start() -def function_mock(request, q_function_name, autospec=True, **kwargs): +def function_mock( + request: FixtureRequest, q_function_name: str, autospec: bool = True, **kwargs: Any +): """Return mock patching function with qualified name `q_function_name`. Patch is reversed after calling test returns. @@ -49,19 +59,23 @@ def function_mock(request, q_function_name, autospec=True, **kwargs): return _patch.start() -def initializer_mock(request, cls, autospec=True, **kwargs): +def initializer_mock(request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any): """Return mock for __init__() method on `cls`. The patch is reversed after pytest uses it. """ - _patch = patch.object( - cls, "__init__", autospec=autospec, return_value=None, **kwargs - ) + _patch = patch.object(cls, "__init__", autospec=autospec, return_value=None, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def instance_mock(request, cls, name=None, spec_set=True, **kwargs): +def instance_mock( + request: FixtureRequest, + cls: type, + name: str | None = None, + spec_set: bool = True, + **kwargs: Any, +) -> Mock: """Return mock for instance of `cls` that draws its spec from that class. The mock does not allow new attributes to be set on the instance. If `name` is @@ -73,7 +87,7 @@ def instance_mock(request, cls, name=None, spec_set=True, **kwargs): return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs) -def loose_mock(request, name=None, **kwargs): +def loose_mock(request: FixtureRequest, name: str | None = None, **kwargs: Any): """Return a "loose" mock, meaning it has no spec to constrain calls on it. Additional keyword arguments are passed through to Mock(). If called without a name, @@ -82,7 +96,9 @@ def loose_mock(request, name=None, **kwargs): return Mock(name=request.fixturename if name is None else name, **kwargs) -def method_mock(request, cls, method_name, autospec=True, **kwargs): +def method_mock( + request: FixtureRequest, cls: type, method_name: str, autospec: bool = True, **kwargs: Any +): """Return mock for method `method_name` on `cls`. The patch is reversed after pytest uses it. @@ -92,7 +108,7 @@ def method_mock(request, cls, method_name, autospec=True, **kwargs): return _patch.start() -def open_mock(request, module_name, **kwargs): +def open_mock(request: FixtureRequest, module_name: str, **kwargs: Any): """Return a mock for the builtin `open()` method in `module_name`.""" target = "%s.open" % module_name _patch = patch(target, mock_open(), create=True, **kwargs) @@ -100,14 +116,14 @@ def open_mock(request, module_name, **kwargs): return _patch.start() -def property_mock(request, cls, prop_name, **kwargs): +def property_mock(request: FixtureRequest, cls: type, prop_name: str, **kwargs: Any): """Return a mock for property `prop_name` on class `cls`.""" _patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def var_mock(request, q_var_name, **kwargs): +def var_mock(request: FixtureRequest, q_var_name: str, **kwargs: Any): """Return mock patching the variable with qualified name *q_var_name*.""" _patch = patch(q_var_name, **kwargs) request.addfinalizer(_patch.stop) diff --git a/typings/behave/__init__.pyi b/typings/behave/__init__.pyi new file mode 100644 index 000000000..f8ffc2058 --- /dev/null +++ b/typings/behave/__init__.pyi @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Callable + +from typing_extensions import TypeAlias + +from .runner import Context + +_ThreeArgStep: TypeAlias = Callable[[Context, str, str, str], None] +_TwoArgStep: TypeAlias = Callable[[Context, str, str], None] +_OneArgStep: TypeAlias = Callable[[Context, str], None] +_NoArgStep: TypeAlias = Callable[[Context], None] +_Step: TypeAlias = _NoArgStep | _OneArgStep | _TwoArgStep | _ThreeArgStep + +def given(phrase: str) -> Callable[[_Step], _Step]: ... +def when(phrase: str) -> Callable[[_Step], _Step]: ... +def then(phrase: str) -> Callable[[_Step], _Step]: ... diff --git a/typings/behave/runner.pyi b/typings/behave/runner.pyi new file mode 100644 index 000000000..aaea74dad --- /dev/null +++ b/typings/behave/runner.pyi @@ -0,0 +1,3 @@ +from types import SimpleNamespace + +class Context(SimpleNamespace): ... diff --git a/typings/lxml/_types.pyi b/typings/lxml/_types.pyi index a16fec3dd..34d2095db 100644 --- a/typings/lxml/_types.pyi +++ b/typings/lxml/_types.pyi @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Callable, Collection, Mapping, Protocol, TypeVar +from typing import Any, Callable, Collection, Literal, Mapping, Protocol, TypeVar from typing_extensions import TypeAlias @@ -25,6 +25,8 @@ _NSMapArg = Mapping[None, str] | Mapping[str, str] | Mapping[str | None, str] _NonDefaultNSMapArg = Mapping[str, str] +_OutputMethodArg = Literal["html", "text", "xml"] + _TagName: TypeAlias = str _TagSelector: TypeAlias = _TagName | Callable[..., _Element] diff --git a/typings/lxml/etree/_module_func.pyi b/typings/lxml/etree/_module_func.pyi index 067b25ce9..e2910f503 100644 --- a/typings/lxml/etree/_module_func.pyi +++ b/typings/lxml/etree/_module_func.pyi @@ -2,18 +2,37 @@ from __future__ import annotations -from .._types import _ElementOrTree +from typing import Literal, overload + +from .._types import _ElementOrTree, _OutputMethodArg from ..etree import HTMLParser, XMLParser from ._element import _Element def fromstring(text: str | bytes, parser: XMLParser | HTMLParser) -> _Element: ... -# Under XML Canonicalization (C14N) mode, most arguments are ignored, -# some arguments would even raise exception outright if specified. -def tostring( +# -- Native str, no XML declaration -- +@overload +def tostring( # type: ignore[overload-overlap] element_or_tree: _ElementOrTree, *, - encoding: str | type[str] | None = None, + encoding: type[str] | Literal["unicode"], + method: _OutputMethodArg = "xml", pretty_print: bool = False, with_tail: bool = True, + standalone: bool | None = None, + doctype: str | None = None, ) -> str: ... + +# -- bytes, str encoded with `encoding`, no XML declaration -- +@overload +def tostring( + element_or_tree: _ElementOrTree, + *, + encoding: str | None = None, + method: _OutputMethodArg = "xml", + xml_declaration: bool | None = None, + pretty_print: bool = False, + with_tail: bool = True, + standalone: bool | None = None, + doctype: str | None = None, +) -> bytes: ... From d5c95be6d51dbf1db6d99951bdb28efb86054de7 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 2 Aug 2024 15:25:14 -0700 Subject: [PATCH 61/69] fix: #943 Docstring implies Px subtype of Length https://github.com/scanny/python-pptx/issues/943 Remove mention of a `Px` subtype of `Length` since no such subtype exists. Turns out a pixel has various sizes on different systems so no standard unit of measure is possible. --- HISTORY.rst | 5 +++++ src/pptx/util.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1f5d4e58a..6cbff8b3b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History --------------- +0.6.24-dev0 ++++++++++++++++++++ + +- fix: #943 remove mention of a Px Length subtype + 0.6.23 (2023-11-02) +++++++++++++++++++ diff --git a/src/pptx/util.py b/src/pptx/util.py index bbe8ac204..fdec79298 100644 --- a/src/pptx/util.py +++ b/src/pptx/util.py @@ -7,7 +7,7 @@ class Length(int): - """Base class for length classes Inches, Emu, Cm, Mm, Pt, and Px. + """Base class for length classes Inches, Emu, Cm, Mm, and Pt. Provides properties for converting length values to convenient units. """ From 799b214a79ab24326ee4075265b3ed1adcf3c6cd Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 2 Aug 2024 16:17:29 -0700 Subject: [PATCH 62/69] fix: #972 next-slide-id fails when max used https://github.com/scanny/python-pptx/issues/972 Naive "max + 1" slide-id allocation algorithm assumed that slide-ids were assigned from bottom up. Apparently some client assigns slide ids from the top (2,147,483,647) down and the naive algorithm would assign an invalid slide-id in that case. Detect when the assigned id is out-of-range and fall-back to a robust algorithm for assigning a valid id based on a "first unused starting at bottom" policy. --- src/pptx/oxml/presentation.py | 25 +++++++++++++++++++++---- tests/oxml/test_presentation.py | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py index 12c6751f1..254472493 100644 --- a/src/pptx/oxml/presentation.py +++ b/src/pptx/oxml/presentation.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, cast from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne @@ -67,14 +67,31 @@ def add_sldId(self, rId: str) -> CT_SlideId: return self._add_sldId(id=self._next_id, rId=rId) @property - def _next_id(self): + def _next_id(self) -> int: """The next available slide ID as an `int`. Valid slide IDs start at 256. The next integer value greater than the max value in use is chosen, which minimizes that chance of reusing the id of a deleted slide. """ - id_str_lst = self.xpath("./p:sldId/@id") - return max([255] + [int(id_str) for id_str in id_str_lst]) + 1 + MIN_SLIDE_ID = 256 + MAX_SLIDE_ID = 2147483647 + + used_ids = [int(s) for s in cast(list[str], self.xpath("./p:sldId/@id"))] + simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1 + if simple_next <= MAX_SLIDE_ID: + return simple_next + + # -- fall back to search for next unused from bottom -- + valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_ID <= id <= MAX_SLIDE_ID)) + return ( + next( + candidate_id + for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_ID) + if candidate_id != used_id + ) + if valid_used_ids + else 256 + ) class CT_SlideMasterIdList(BaseOxmlElement): diff --git a/tests/oxml/test_presentation.py b/tests/oxml/test_presentation.py index 1607ab5cc..d2a47b27d 100644 --- a/tests/oxml/test_presentation.py +++ b/tests/oxml/test_presentation.py @@ -38,3 +38,26 @@ def it_can_add_a_sldId_element_as_a_child(self): def it_knows_the_next_available_slide_id(self, sldIdLst_cxml: str, expected_value: int): sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml)) assert sldIdLst._next_id == expected_value + + @pytest.mark.parametrize( + ("sldIdLst_cxml", "expected_value"), + [ + ("p:sldIdLst/p:sldId{id=2147483646}", 2147483647), + ("p:sldIdLst/p:sldId{id=2147483647}", 256), + # -- 2147483648 is not a valid id but shouldn't stop us from finding a one that is -- + ("p:sldIdLst/p:sldId{id=2147483648}", 256), + ("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483647})", 257), + ("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483647},p:sldId{id=257})", 258), + # -- 245 is also not a valid id but that shouldn't change the result either -- + ("p:sldIdLst/(p:sldId{id=245},p:sldId{id=2147483647},p:sldId{id=256})", 257), + ], + ) + def and_it_chooses_a_valid_slide_id_when_max_slide_id_is_used_for_a_slide( + self, sldIdLst_cxml: str, expected_value: int + ): + sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml)) + + slide_id = sldIdLst._next_id + + assert 256 <= slide_id <= 2147483647 + assert slide_id == expected_value From 284fc01850391064298ad0fb6da27568199ee4a2 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Fri, 2 Aug 2024 20:29:40 -0700 Subject: [PATCH 63/69] fix: #990 Turn off ZipFile strict_timestamps Accommodate system dates before 1980-01-01. This should have no effect for users with "normal" system clocks but allows the package to save files in the unusual case the system clock is set to a date prior to 1980. --- HISTORY.rst | 4 +++- src/pptx/__init__.py | 2 +- src/pptx/opc/serialized.py | 4 +++- tests/opc/test_serialized.py | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6cbff8b3b..88b6580b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,10 +3,12 @@ Release History --------------- -0.6.24-dev0 +0.6.24-dev3 +++++++++++++++++++ - fix: #943 remove mention of a Px Length subtype +- fix: #972 next-slide-id fails in rare cases +- fix: #990 do not require strict timestamps for Zip 0.6.23 (2023-11-02) +++++++++++++++++++ diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 0c951298c..0ed160555 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "0.6.23" +__version__ = "0.6.24-dev3" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py index eba628247..0942e33cb 100644 --- a/src/pptx/opc/serialized.py +++ b/src/pptx/opc/serialized.py @@ -239,7 +239,9 @@ def write(self, pack_uri: PackURI, blob: bytes) -> None: @lazyproperty def _zipf(self) -> zipfile.ZipFile: """`ZipFile` instance open for writing.""" - return zipfile.ZipFile(self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED) + return zipfile.ZipFile( + self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) class _ContentTypesItem: diff --git a/tests/opc/test_serialized.py b/tests/opc/test_serialized.py index d5b867c4e..8dcc8cf56 100644 --- a/tests/opc/test_serialized.py +++ b/tests/opc/test_serialized.py @@ -341,7 +341,9 @@ def it_provides_access_to_the_open_zip_file_to_help(self, request: FixtureReques zipf = pkg_writer._zipf - ZipFile_.assert_called_once_with("prs.pptx", "w", compression=zipfile.ZIP_DEFLATED) + ZipFile_.assert_called_once_with( + "prs.pptx", "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) assert zipf is ZipFile_.return_value # fixtures --------------------------------------------- From af6a8f7b83c9c33b5fedfa0987d54f817f3b3cba Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Wed, 31 Jul 2024 18:06:19 -0700 Subject: [PATCH 64/69] fix: #929 raises on JPEG with image/jpg MIME-type At least one client, perhaps Adobe PDF Converter, produces a PPTX with JPEG images mapped to the non-existent `image/jpg` MIME-type rather than the correct `image/jpeg`. Accept `image/jpg` as an alias for `image/jpeg`, which is consistent with the behavior of PowerPoint which loads these files without complaint. --- HISTORY.rst | 4 +++- features/prs-open-save.feature | 4 ++++ features/steps/presentation.py | 21 ++++++++++++++++++ .../steps/test_files/test-image-jpg-mime.pptx | Bin 0 -> 33400 bytes src/pptx/__init__.py | 4 +++- 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 features/steps/test_files/test-image-jpg-mime.pptx diff --git a/HISTORY.rst b/HISTORY.rst index 88b6580b9..73c3bacef 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,13 +3,15 @@ Release History --------------- -0.6.24-dev3 +0.6.24-dev4 +++++++++++++++++++ +- fix: #929 raises on JPEG with image/jpg MIME-type - fix: #943 remove mention of a Px Length subtype - fix: #972 next-slide-id fails in rare cases - fix: #990 do not require strict timestamps for Zip + 0.6.23 (2023-11-02) +++++++++++++++++++ diff --git a/features/prs-open-save.feature b/features/prs-open-save.feature index 4176adfd3..d60b2c242 100644 --- a/features/prs-open-save.feature +++ b/features/prs-open-save.feature @@ -33,3 +33,7 @@ Feature: Round-trip a presentation When I save and reload the presentation Then the external relationships are still there And the package has the expected number of .rels parts + + Scenario: Load presentation with invalid image/jpg MIME-type + Given a presentation with an image/jpg MIME-type + Then I can access the JPEG image diff --git a/features/steps/presentation.py b/features/steps/presentation.py index 0c1c6ba26..11a259545 100644 --- a/features/steps/presentation.py +++ b/features/steps/presentation.py @@ -5,6 +5,7 @@ import io import os import zipfile +from typing import TYPE_CHECKING, cast from behave import given, then, when from behave.runner import Context @@ -14,6 +15,10 @@ from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.util import Inches +if TYPE_CHECKING: + from pptx import presentation + from pptx.shapes.picture import Picture + # given =================================================== @@ -38,6 +43,11 @@ def given_a_presentation_having_no_notes_master(context: Context): context.prs = Presentation(test_pptx("prs-properties")) +@given("a presentation with an image/jpg MIME-type") +def given_prs_with_image_jpg_MIME_type(context): + context.prs = Presentation(test_pptx("test-image-jpg-mime")) + + @given("a presentation with external relationships") def given_prs_with_ext_rels(context: Context): context.prs = Presentation(test_pptx("ext-rels")) @@ -189,6 +199,17 @@ def then_the_package_has_the_expected_number_of_rels_parts(context: Context): assert member_count == 18, "expected 18, got %d" % member_count +@then("I can access the JPEG image") +def then_I_can_access_the_JPEG_image(context): + prs = cast("presentation.Presentation", context.prs) + slide = prs.slides[0] + picture = cast("Picture", slide.shapes[0]) + try: + picture.image + except AttributeError: + raise AssertionError("JPEG image not recognized") + + @then("the slide height matches the new value") def then_slide_height_matches_new_value(context: Context): presentation = context.presentation diff --git a/features/steps/test_files/test-image-jpg-mime.pptx b/features/steps/test_files/test-image-jpg-mime.pptx new file mode 100644 index 0000000000000000000000000000000000000000..857bd7ca29a7f32f1f9cd751b00a7b824fb4bd36 GIT binary patch literal 33400 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 Date: Sat, 3 Aug 2024 12:18:00 -0700 Subject: [PATCH 65/69] build: modernize build process - Move to `pyproject.toml` - Update support to 3.8+ --- Makefile | 42 ++++++++------- pyproject.toml | 45 ++++++++++++++++ requirements-dev.txt | 5 ++ requirements-docs.txt | 5 ++ requirements-test.txt | 1 + setup.cfg | 0 setup.py | 89 -------------------------------- src/pptx/opc/package.py | 5 +- src/pptx/opc/serialized.py | 3 +- src/pptx/oxml/presentation.py | 2 +- src/pptx/oxml/shapes/graphfrm.py | 2 +- src/pptx/shapes/freeform.py | 3 +- src/pptx/spec.py | 2 +- tox.ini | 37 +------------ 14 files changed, 89 insertions(+), 152 deletions(-) create mode 100644 requirements-dev.txt create mode 100644 requirements-docs.txt delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/Makefile b/Makefile index 94891a89b..7a0fddb2e 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,55 @@ BEHAVE = behave MAKE = make -PYTHON = python -SETUP = $(PYTHON) ./setup.py -TWINE = $(PYTHON) -m twine - -.PHONY: accept build clean cleandocs coverage docs opendocs +.PHONY: help help: @echo "Please use \`make ' where is one or more of" - @echo " accept run acceptance tests using behave" - @echo " clean delete intermediate work product and start fresh" - @echo " cleandocs delete cached HTML documentation and start fresh" - @echo " coverage run nosetests with coverage" - @echo " docs build HTML documentation using Sphinx (incremental)" - @echo " opendocs open local HTML documentation in browser" - @echo " readme update README.html from README.rst" - @echo " sdist generate a source distribution into dist/" - @echo " upload upload distribution tarball to PyPI" - + @echo " accept run acceptance tests using behave" + @echo " build generate both sdist and wheel suitable for upload to PyPI" + @echo " clean delete intermediate work product and start fresh" + @echo " cleandocs delete cached HTML documentation and start fresh" + @echo " coverage run nosetests with coverage" + @echo " docs build HTML documentation using Sphinx (incremental)" + @echo " opendocs open local HTML documentation in browser" + @echo " test-upload upload distribution to TestPyPI" + @echo " upload upload distribution tarball to PyPI" + +.PHONY: accept accept: $(BEHAVE) --stop +.PHONY: build build: rm -rf dist - $(SETUP) bdist_wheel sdist + python -m build + twine check dist/* +.PHONY: clean clean: find . -type f -name \*.pyc -exec rm {} \; find . -type f -name .DS_Store -exec rm {} \; rm -rf dist .coverage +.PHONY: cleandocs cleandocs: $(MAKE) -C docs clean +.PHONY: coverage coverage: py.test --cov-report term-missing --cov=pptx --cov=tests +.PHONY: docs docs: $(MAKE) -C docs html +.PHONY: opendocs opendocs: open docs/.build/html/index.html +.PHONY: test-upload test-upload: build - $(TWINE) upload --repository testpypi dist/* + twine upload --repository testpypi dist/* +.PHONY: upload upload: clean build - $(TWINE) upload dist/* + twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index 9868f5fc3..400cb6bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,45 @@ +[build-system] +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-pptx" +authors = [{name = "Steve Canny", email = "stcanny@gmail.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Office/Business :: Office Suites", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "Pillow>=3.3.2", + "XlsxWriter>=0.5.7", + "lxml>=3.1.0", + "typing_extensions>=4.9.0", +] +description = "Create, read, and update PowerPoint 2007+ (.pptx) files." +dynamic = ["version"] +keywords = ["powerpoint", "ppt", "pptx", "openxml", "office"] +license = { text = "MIT" } +readme = "README.rst" +requires-python = ">=3.8" + +[project.urls] +Changelog = "https://github.com/scanny/python-pptx/blob/master/HISTORY.rst" +Documentation = "https://python-pptx.readthedocs.io/en/latest/" +Homepage = "https://github.com/scanny/python-pptx" +Repository = "https://github.com/scanny/python-pptx" + [tool.black] line-length = 100 @@ -98,3 +140,6 @@ ignore = [ [tool.ruff.lint.isort] known-first-party = ["pptx"] + +[tool.setuptools.dynamic] +version = {attr = "pptx.__version__"} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..70096eab2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements-test.txt +build +ruff +setuptools>=61.0.0 +twine diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 000000000..90edd8e31 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +Sphinx==1.8.6 +Jinja2==2.11.3 +MarkupSafe==0.23 +alabaster<0.7.14 +-e . diff --git a/requirements-test.txt b/requirements-test.txt index b542c1af7..9ddd60fd7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,3 +5,4 @@ pytest>=2.5 pytest-coverage pytest-xdist ruff +tox diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e69de29bb..000000000 diff --git a/setup.py b/setup.py deleted file mode 100755 index 6d9a0d1a5..000000000 --- a/setup.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python - -import os -import re - -from setuptools import find_packages, setup - - -def ascii_bytes_from(path, *paths): - """ - Return the ASCII characters in the file specified by *path* and *paths*. - The file path is determined by concatenating *path* and any members of - *paths* with a directory separator in between. - """ - file_path = os.path.join(path, *paths) - with open(file_path) as f: - ascii_bytes = f.read() - return ascii_bytes - - -# read required text from files -thisdir = os.path.dirname(__file__) -init_py = ascii_bytes_from(thisdir, "src", "pptx", "__init__.py") -readme = ascii_bytes_from(thisdir, "README.rst") -history = ascii_bytes_from(thisdir, "HISTORY.rst") - -# Read the version from pptx.__version__ without importing the package -# (and thus attempting to import packages it depends on that may not be -# installed yet) -version = re.search(r'__version__ = "([^"]+)"', init_py).group(1) - - -NAME = "python-pptx" -VERSION = version -DESCRIPTION = "Generate and manipulate Open XML PowerPoint (.pptx) files" -KEYWORDS = "powerpoint ppt pptx office open xml" -AUTHOR = "Steve Canny" -AUTHOR_EMAIL = "python-pptx@googlegroups.com" -URL = "https://github.com/scanny/python-pptx" -LICENSE = "MIT" -PACKAGES = find_packages(where="src") -PACKAGE_DATA = {"pptx": ["templates/*"]} - -INSTALL_REQUIRES = ["lxml>=3.1.0", "Pillow>=3.3.2", "XlsxWriter>=0.5.7"] - -TEST_SUITE = "tests" -TESTS_REQUIRE = ["behave", "mock", "pyparsing>=2.0.1", "pytest"] - -CLASSIFIERS = [ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Topic :: Office/Business :: Office Suites", - "Topic :: Software Development :: Libraries", -] - -LONG_DESCRIPTION = readme + "\n\n" + history - -ZIP_SAFE = False - -params = { - "name": NAME, - "version": VERSION, - "description": DESCRIPTION, - "keywords": KEYWORDS, - "long_description": LONG_DESCRIPTION, - "long_description_content_type": "text/x-rst", - "author": AUTHOR, - "author_email": AUTHOR_EMAIL, - "url": URL, - "license": LICENSE, - "packages": PACKAGES, - "package_data": PACKAGE_DATA, - "package_dir": {"": "src"}, - "install_requires": INSTALL_REQUIRES, - "tests_require": TESTS_REQUIRE, - "test_suite": TEST_SUITE, - "classifiers": CLASSIFIERS, - "zip_safe": ZIP_SAFE, -} - -setup(**params) diff --git a/src/pptx/opc/package.py b/src/pptx/opc/package.py index 03ee5f43b..713759c54 100644 --- a/src/pptx/opc/package.py +++ b/src/pptx/opc/package.py @@ -7,8 +7,7 @@ from __future__ import annotations import collections -from collections.abc import Mapping -from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Set, cast +from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM from pptx.opc.constants import RELATIONSHIP_TYPE as RT @@ -428,7 +427,7 @@ def part(self): def _rel_ref_count(self, rId: str) -> int: """Return int count of references in this part's XML to `rId`.""" - return len([r for r in cast(list[str], self._element.xpath("//@r:id")) if r == rId]) + return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId]) class PartFactory: diff --git a/src/pptx/opc/serialized.py b/src/pptx/opc/serialized.py index 0942e33cb..92366708b 100644 --- a/src/pptx/opc/serialized.py +++ b/src/pptx/opc/serialized.py @@ -5,8 +5,7 @@ import os import posixpath import zipfile -from collections.abc import Container -from typing import IO, TYPE_CHECKING, Any, Sequence +from typing import IO, TYPE_CHECKING, Any, Container, Sequence from pptx.exc import PackageNotFoundError from pptx.opc.constants import CONTENT_TYPE as CT diff --git a/src/pptx/oxml/presentation.py b/src/pptx/oxml/presentation.py index 254472493..17997c2b1 100644 --- a/src/pptx/oxml/presentation.py +++ b/src/pptx/oxml/presentation.py @@ -76,7 +76,7 @@ def _next_id(self) -> int: MIN_SLIDE_ID = 256 MAX_SLIDE_ID = 2147483647 - used_ids = [int(s) for s in cast(list[str], self.xpath("./p:sldId/@id"))] + used_ids = [int(s) for s in cast("list[str]", self.xpath("./p:sldId/@id"))] simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1 if simple_next <= MAX_SLIDE_ID: return simple_next diff --git a/src/pptx/oxml/shapes/graphfrm.py b/src/pptx/oxml/shapes/graphfrm.py index cf32377c2..efa0b3632 100644 --- a/src/pptx/oxml/shapes/graphfrm.py +++ b/src/pptx/oxml/shapes/graphfrm.py @@ -112,7 +112,7 @@ def _oleObj(self) -> CT_OleObject | None: choices. The last one should suit best for reading purposes because it contains the lowest common denominator. """ - oleObjs = cast(list[CT_OleObject], self.xpath(".//p:oleObj")) + oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj")) return oleObjs[-1] if oleObjs else None diff --git a/src/pptx/shapes/freeform.py b/src/pptx/shapes/freeform.py index e05b3484f..afe87385e 100644 --- a/src/pptx/shapes/freeform.py +++ b/src/pptx/shapes/freeform.py @@ -2,8 +2,7 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import TYPE_CHECKING, Iterable, Iterator +from typing import TYPE_CHECKING, Iterable, Iterator, Sequence from pptx.util import Emu, lazyproperty diff --git a/src/pptx/spec.py b/src/pptx/spec.py index 1e7bffb36..e9d3b7d58 100644 --- a/src/pptx/spec.py +++ b/src/pptx/spec.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias -AdjustmentValue: TypeAlias = tuple[str, int] +AdjustmentValue: TypeAlias = "tuple[str, int]" class ShapeSpec(TypedDict): diff --git a/tox.ini b/tox.ini index 223cc0ffe..37acaa5fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,42 +1,9 @@ -# -# Configuration for tox and pytest - -[flake8] -exclude = dist,docs,*.egg-info,.git,lab,ref,_scratch,spec,.tox -ignore = - # -- E203 - whitespace before ':'. Black disagrees for slice expressions. - E203, - - # -- W503 - line break before binary operator. Black has a different opinion about - # -- this, that binary operators should appear at the beginning of new-line - # -- expression segments. I agree because right is ragged and left lines up. - W503 -max-line-length = 88 - [tox] -envlist = py27, py38, py311 -requires = virtualenv<20.22.0 -skip_missing_interpreters = false +envlist = py38, py39, py310, py311, py312 [testenv] -deps = - behave==1.2.5 - lxml>=3.1.0 - Pillow>=3.3.2 - pyparsing>=2.0.1 - pytest - XlsxWriter>=0.5.7 +deps = -rrequirements-test.txt commands = py.test -qx behave --format progress --stop --tags=-wip - -[testenv:py27] -deps = - behave==1.2.5 - lxml>=3.1.0 - mock - Pillow>=3.3.2 - pyparsing>=2.0.1 - pytest - XlsxWriter>=0.5.7 From 04a3e9d713c5f1306c331c4b54e4d6b408fac483 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 3 Aug 2024 12:24:14 -0700 Subject: [PATCH 66/69] release: prepare v1.0.0 release --- HISTORY.rst | 5 +++-- src/pptx/__init__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 73c3bacef..6f0835045 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,13 +3,14 @@ Release History --------------- -0.6.24-dev4 -+++++++++++++++++++ +1.0.0 (2024-08-03) +++++++++++++++++++ - fix: #929 raises on JPEG with image/jpg MIME-type - fix: #943 remove mention of a Px Length subtype - fix: #972 next-slide-id fails in rare cases - fix: #990 do not require strict timestamps for Zip +- Add type annotations 0.6.23 (2023-11-02) diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 27d6306a1..3baec7376 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "0.6.24-dev4" +__version__ = "1.0.0" sys.modules["pptx.exceptions"] = exceptions del sys From 31955c0f4965b57ef0532f1ce97548cdfa5deea3 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sat, 3 Aug 2024 14:44:02 -0700 Subject: [PATCH 67/69] docs: update docs build --- .readthedocs.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..125538586 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +version: 2 + +# -- set the OS, Python version and other tools you might need -- +build: + os: ubuntu-22.04 + tools: + python: "3.9" + +# -- build documentation in the "docs/" directory with Sphinx -- +sphinx: + configuration: docs/conf.py + # -- fail on all warnings to avoid broken references -- + # fail_on_warning: true + +# -- package versions required to build your documentation -- +# -- see https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -- +python: + install: + - requirements: requirements-docs.txt From 0f980cd944a3b2cdf671f5907a931c755c397526 Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Mon, 5 Aug 2024 10:01:49 -0700 Subject: [PATCH 68/69] fix(type): add py.typed Indicate availability of type annotations to type-checkers. --- HISTORY.rst | 6 ++++++ src/pptx/__init__.py | 2 +- src/pptx/py.typed | 0 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/pptx/py.typed diff --git a/HISTORY.rst b/HISTORY.rst index 6f0835045..b330da107 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +1.0.1 (2024-08-05) +++++++++++++++++++ + +- fix: #1000 add py.typed + + 1.0.0 (2024-08-03) ++++++++++++++++++ diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 3baec7376..aaf9cce06 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "1.0.0" +__version__ = "1.0.1" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/py.typed b/src/pptx/py.typed new file mode 100644 index 000000000..e69de29bb From 278b47b1dedd5b46ee84c286e77cdfb0bf4594be Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Tue, 6 Aug 2024 13:16:25 -0700 Subject: [PATCH 69/69] fix(enum): replace read-only enum values Some enum values like `MSO_SHAPE_TYPE.MIXED` are never produced by `python-pptx` and so were removed during the enum modernization in commit `01b86e64`. However, in at least one downstream system these values are referenced even though they can never actually occur. Replace these "read-only" values to avoid downstream breakage. --- HISTORY.rst | 5 +++++ src/pptx/__init__.py | 2 +- src/pptx/enum/chart.py | 7 +++++-- src/pptx/enum/dml.py | 12 ++++++++---- src/pptx/enum/lang.py | 3 +++ src/pptx/enum/shapes.py | 28 ++++++++++++++++++++++------ src/pptx/enum/text.py | 12 ++++++++++++ 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b330da107..e1c4e8faf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History --------------- +1.0.2 (2024-08-07) +++++++++++++++++++ + +- fix: #1003 restore read-only enum members + 1.0.1 (2024-08-05) ++++++++++++++++++ diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index aaf9cce06..fb5c2d7e4 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "1.0.1" +__version__ = "1.0.2" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/enum/chart.py b/src/pptx/enum/chart.py index 5e609ebd3..2599cf4dd 100644 --- a/src/pptx/enum/chart.py +++ b/src/pptx/enum/chart.py @@ -335,6 +335,9 @@ class XL_DATA_LABEL_POSITION(BaseXmlEnum): LEFT = (-4131, "l", "The data label is positioned to the left of the data point.") """The data label is positioned to the left of the data point.""" + MIXED = (6, "", "Data labels are in multiple positions (read-only).") + """Data labels are in multiple positions (read-only).""" + OUTSIDE_END = ( 2, "outEnd", @@ -370,8 +373,8 @@ class XL_LEGEND_POSITION(BaseXmlEnum): CORNER = (2, "tr", "In the upper-right corner of the chart border.") """In the upper-right corner of the chart border.""" - CUSTOM = (-4161, "", "A custom position.") - """A custom position.""" + CUSTOM = (-4161, "", "A custom position (read-only).") + """A custom position (read-only).""" LEFT = (-4131, "l", "Left of the chart.") """Left of the chart.""" diff --git a/src/pptx/enum/dml.py b/src/pptx/enum/dml.py index e9a14b13f..40d5c5cdf 100644 --- a/src/pptx/enum/dml.py +++ b/src/pptx/enum/dml.py @@ -319,8 +319,8 @@ class MSO_PATTERN_TYPE(BaseXmlEnum): ZIG_ZAG = (38, "zigZag", "Zig Zag") """Zig Zag""" - MIXED = (-2, "", "Mixed pattern") - """Mixed pattern""" + MIXED = (-2, "", "Mixed pattern (read-only).") + """Mixed pattern (read-only).""" MSO_PATTERN = MSO_PATTERN_TYPE @@ -394,8 +394,12 @@ class MSO_THEME_COLOR_INDEX(BaseXmlEnum): TEXT_2 = (15, "tx2", "Specifies the Text 2 theme color.") """Specifies the Text 2 theme color.""" - MIXED = (-2, "", "Indicates multiple theme colors are used, such as in a group shape.") - """Indicates multiple theme colors are used, such as in a group shape.""" + MIXED = ( + -2, + "", + "Indicates multiple theme colors are used, such as in a group shape (read-only).", + ) + """Indicates multiple theme colors are used, such as in a group shape (read-only).""" MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX diff --git a/src/pptx/enum/lang.py b/src/pptx/enum/lang.py index d2da5b6a5..a6bc1c8b4 100644 --- a/src/pptx/enum/lang.py +++ b/src/pptx/enum/lang.py @@ -680,3 +680,6 @@ class MSO_LANGUAGE_ID(BaseXmlEnum): ZULU = (1077, "zu-ZA", "The Zulu language.") """The Zulu language.""" + + MIXED = (-2, "", "More than one language in specified range (read-only).") + """More than one language in specified range (read-only).""" diff --git a/src/pptx/enum/shapes.py b/src/pptx/enum/shapes.py index b1dec8adc..86f521f40 100644 --- a/src/pptx/enum/shapes.py +++ b/src/pptx/enum/shapes.py @@ -748,6 +748,9 @@ class MSO_CONNECTOR_TYPE(BaseXmlEnum): STRAIGHT = (1, "line", "Straight line connector.") """Straight line connector.""" + MIXED = (-2, "", "Return value only; indicates a combination of other states.") + """Return value only; indicates a combination of other states.""" + MSO_CONNECTOR = MSO_CONNECTOR_TYPE @@ -843,6 +846,9 @@ class MSO_SHAPE_TYPE(BaseEnum): WEB_VIDEO = (26, "Web video") """Web video""" + MIXED = (-2, "Multiple shape types (read-only).") + """Multiple shape types (read-only).""" + MSO = MSO_SHAPE_TYPE @@ -871,6 +877,16 @@ class PP_MEDIA_TYPE(BaseEnum): SOUND = (1, "Audio media such as MP3.") """Audio media such as MP3.""" + MIXED = ( + -2, + "Return value only; indicates multiple media types, typically for a collection of shapes." + " May not be applicable in python-pptx.", + ) + """Return value only; indicates multiple media types. + + Typically for a collection of shapes. May not be applicable in python-pptx. + """ + class PP_PLACEHOLDER_TYPE(BaseXmlEnum): """Specifies one of the 18 distinct types of placeholder. @@ -937,14 +953,14 @@ class PP_PLACEHOLDER_TYPE(BaseXmlEnum): TITLE = (1, "title", "Title") """Title""" - VERTICAL_BODY = (6, "", "Vertical Body") - """Vertical Body""" + VERTICAL_BODY = (6, "", "Vertical Body (read-only).") + """Vertical Body (read-only).""" - VERTICAL_OBJECT = (17, "", "Vertical Object") - """Vertical Object""" + VERTICAL_OBJECT = (17, "", "Vertical Object (read-only).") + """Vertical Object (read-only).""" - VERTICAL_TITLE = (5, "", "Vertical Title") - """Vertical Title""" + VERTICAL_TITLE = (5, "", "Vertical Title (read-only).") + """Vertical Title (read-only).""" MIXED = (-2, "", "Return value only; multiple placeholders of differing types.") """Return value only; multiple placeholders of differing types.""" diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index 892385153..db266a3c9 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -57,6 +57,9 @@ class MSO_AUTO_SIZE(BaseEnum): ) """The font size is reduced as necessary to fit the text within the shape.""" + MIXED = (-2, "Return value only; indicates a combination of automatic sizing schemes are used.") + """Return value only; indicates a combination of automatic sizing schemes are used.""" + class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum): """ @@ -134,6 +137,9 @@ class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum): WORDS = (1, "words", "Specifies underlining words.") """Specifies underlining words.""" + MIXED = (-2, "", "Specifies a mix of underline types (read-only).") + """Specifies a mix of underline types (read-only).""" + MSO_UNDERLINE = MSO_TEXT_UNDERLINE_TYPE @@ -161,6 +167,9 @@ class MSO_VERTICAL_ANCHOR(BaseXmlEnum): BOTTOM = (4, "b", "Aligns text to bottom of text frame") """Aligns text to bottom of text frame""" + MIXED = (-2, "", "Return value only; indicates a combination of the other states.") + """Return value only; indicates a combination of the other states.""" + MSO_ANCHOR = MSO_VERTICAL_ANCHOR @@ -214,5 +223,8 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): THAI_DISTRIBUTE = (6, "thaiDist", "Thai distributed") """Thai distributed""" + MIXED = (-2, "", "Multiple alignments are present in a set of paragraphs (read-only).") + """Multiple alignments are present in a set of paragraphs (read-only).""" + PP_ALIGN = PP_PARAGRAPH_ALIGNMENT