File size: 13,509 Bytes
d015b2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
"""Objects shared by docx modules."""

from __future__ import annotations

import functools
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Generic,
    Iterator,
    List,
    Tuple,
    TypeVar,
    cast,
)

if TYPE_CHECKING:
    import docx.types as t
    from docx.opc.part import XmlPart
    from docx.oxml.xmlchemy import BaseOxmlElement
    from docx.parts.story import StoryPart


class Length(int):
    """Base class for length constructor classes Inches, Cm, Mm, Px, and Emu.

    Behaves as an int count of English Metric Units, 914,400 to the inch, 36,000 to the
    mm. Provides convenience unit conversion methods in the form of read-only
    properties. Immutable.
    """

    _EMUS_PER_INCH = 914400
    _EMUS_PER_CM = 360000
    _EMUS_PER_MM = 36000
    _EMUS_PER_PT = 12700
    _EMUS_PER_TWIP = 635

    def __new__(cls, emu: int):
        return int.__new__(cls, emu)

    @property
    def cm(self):
        """The equivalent length expressed in centimeters (float)."""
        return self / float(self._EMUS_PER_CM)

    @property
    def emu(self):
        """The equivalent length expressed in English Metric Units (int)."""
        return self

    @property
    def inches(self):
        """The equivalent length expressed in inches (float)."""
        return self / float(self._EMUS_PER_INCH)

    @property
    def mm(self):
        """The equivalent length expressed in millimeters (float)."""
        return self / float(self._EMUS_PER_MM)

    @property
    def pt(self):
        """Floating point length in points."""
        return self / float(self._EMUS_PER_PT)

    @property
    def twips(self):
        """The equivalent length expressed in twips (int)."""
        return int(round(self / float(self._EMUS_PER_TWIP)))


class Inches(Length):
    """Convenience constructor for length in inches, e.g. ``width = Inches(0.5)``."""

    def __new__(cls, inches: float):
        emu = int(inches * Length._EMUS_PER_INCH)
        return Length.__new__(cls, emu)


class Cm(Length):
    """Convenience constructor for length in centimeters, e.g. ``height = Cm(12)``."""

    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, e.g. ``width =
    Emu(457200)``."""

    def __new__(cls, emu: int):
        return Length.__new__(cls, int(emu))


class Mm(Length):
    """Convenience constructor for length in millimeters, e.g. ``width = Mm(240.5)``."""

    def __new__(cls, mm: float):
        emu = int(mm * Length._EMUS_PER_MM)
        return Length.__new__(cls, emu)


class Pt(Length):
    """Convenience value class for specifying a length in points."""

    def __new__(cls, points: float):
        emu = int(points * Length._EMUS_PER_PT)
        return Length.__new__(cls, emu)


class Twips(Length):
    """Convenience constructor for length in twips, e.g. ``width = Twips(42)``.

    A twip is a twentieth of a point, 635 EMU.
    """

    def __new__(cls, twips: float):
        emu = int(twips * Length._EMUS_PER_TWIP)
        return Length.__new__(cls, emu)


class RGBColor(Tuple[int, int, int]):
    """Immutable value object defining a particular RGB color."""

    def __new__(cls, r: int, g: int, b: int):
        msg = "RGBColor() takes three integer values 0-255"
        for val in (r, g, b):
            if (
                not isinstance(val, int)  # pyright: ignore[reportUnnecessaryIsInstance]
                or val < 0
                or val > 255
            ):
                raise ValueError(msg)
        return super(RGBColor, cls).__new__(cls, (r, g, b))

    def __repr__(self):
        return "RGBColor(0x%02x, 0x%02x, 0x%02x)" % self

    def __str__(self):
        """Return a hex string rgb value, like '3C2F80'."""
        return "%02X%02X%02X" % self

    @classmethod
    def from_string(cls, rgb_hex_str: str) -> RGBColor:
        """Return a new instance from an RGB color hex string like ``'3C2F80'``."""
        r = int(rgb_hex_str[:2], 16)
        g = int(rgb_hex_str[2:4], 16)
        b = int(rgb_hex_str[4:], 16)
        return cls(r, g, b)


T = TypeVar("T")


class lazyproperty(Generic[T]):
    """Decorator like @property, but evaluated only on first access.

    Like @property, this can only be used to decorate methods having only a `self`
    parameter, and is accessed like an attribute on an instance, i.e. trailing
    parentheses are not used. Unlike @property, the decorated method is only evaluated
    on first access; the resulting value is cached and that same value returned on
    second and later access without re-evaluation of the method.

    Like @property, this class produces a *data descriptor* object, which is stored in
    the __dict__ of the *class* under the name of the decorated method ('fget'
    nominally). The cached value is stored in the __dict__ of the *instance* under that
    same name.

    Because it is a data descriptor (as opposed to a *non-data descriptor*), its
    `__get__()` method is executed on each access of the decorated attribute; the
    __dict__ item of the same name is "shadowed" by the descriptor.

    While this may represent a performance improvement over a property, its greater
    benefit may be its other characteristics. One common use is to construct
    collaborator objects, removing that "real work" from the constructor, while still
    only executing once. It also de-couples client code from any sequencing
    considerations; if it's accessed from more than one location, it's assured it will
    be ready whenever needed.

    Loosely based on: https://stackoverflow.com/a/6849299/1902513.

    A lazyproperty is read-only. There is no counterpart to the optional "setter" (or
    deleter) behavior of an @property. This is critically important to maintaining its
    immutability and idempotence guarantees. Attempting to assign to a lazyproperty
    raises AttributeError unconditionally.

    The parameter names in the methods below correspond to this usage example::

        class Obj(object)

            @lazyproperty
            def fget(self):
                return 'some result'

        obj = Obj()

    Not suitable for wrapping a function (as opposed to a method) because it is not
    callable."""

    def __init__(self, fget: Callable[..., T]) -> None:
        """*fget* is the decorated method (a "getter" function).

        A lazyproperty is read-only, so there is only an *fget* function (a regular
        @property can also have an fset and fdel function). This name was chosen for
        consistency with Python's `property` class which uses this name for the
        corresponding parameter.
        """
        # --- maintain a reference to the wrapped getter method
        self._fget = fget
        # --- and store the name of that decorated method
        self._name = fget.__name__
        # --- adopt fget's __name__, __doc__, and other attributes
        functools.update_wrapper(self, fget)  # pyright: ignore

    def __get__(self, obj: Any, type: Any = None) -> T:
        """Called on each access of 'fget' attribute on class or instance.

        *self* is this instance of a lazyproperty descriptor "wrapping" the property
        method it decorates (`fget`, nominally).

        *obj* is the "host" object instance when the attribute is accessed from an
        object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on
        the class, e.g. `Obj.fget`.

        *type* is the class hosting the decorated getter method (`fget`) on both class
        and instance attribute access.
        """
        # --- when accessed on class, e.g. Obj.fget, just return this descriptor
        # --- instance (patched above to look like fget).
        if obj is None:
            return self  # type: ignore

        # --- when accessed on instance, start by checking instance __dict__ for
        # --- item with key matching the wrapped function's name
        value = obj.__dict__.get(self._name)
        if value is None:
            # --- on first access, the __dict__ item will be absent. Evaluate fget()
            # --- and store that value in the (otherwise unused) host-object
            # --- __dict__ value of same name ('fget' nominally)
            value = self._fget(obj)
            obj.__dict__[self._name] = value
        return cast(T, value)

    def __set__(self, obj: Any, value: Any) -> None:
        """Raises unconditionally, to preserve read-only behavior.

        This decorator is intended to implement immutable (and idempotent) object
        attributes. For that reason, assignment to this property must be explicitly
        prevented.

        If this __set__ method was not present, this descriptor would become a
        *non-data descriptor*. That would be nice because the cached value would be
        accessed directly once set (__dict__ attrs have precedence over non-data
        descriptors on instance attribute lookup). The problem is, there would be
        nothing to stop assignment to the cached value, which would overwrite the result
        of `fget()` and break both the immutability and idempotence guarantees of this
        decorator.

        The performance with this __set__() method in place was roughly 0.4 usec per
        access when measured on a 2.8GHz development machine; so quite snappy and
        probably not a rich target for optimization efforts.
        """
        raise AttributeError("can't set attribute")


def write_only_property(f: Callable[[Any, Any], None]):
    """@write_only_property decorator.

    Creates a property (descriptor attribute) that accepts assignment, but not getattr
    (use in an expression).
    """
    docstring = f.__doc__

    return property(fset=f, doc=docstring)


class ElementProxy:
    """Base class for lxml element proxy classes.

    An element proxy class is one whose primary responsibilities are fulfilled by
    manipulating the attributes and child elements of an XML element. They are the most
    common type of class in python-docx other than custom element (oxml) classes.
    """

    def __init__(self, element: BaseOxmlElement, parent: t.ProvidesXmlPart | None = None):
        self._element = element
        self._parent = parent

    def __eq__(self, other: object):
        """Return |True| if this proxy object refers to the same oxml element as does
        `other`.

        ElementProxy objects are value objects and should maintain no mutable local
        state. Equality for proxy objects is defined as referring to the same XML
        element, whether or not they are the same proxy object instance.
        """
        if not isinstance(other, ElementProxy):
            return False
        return self._element is other._element

    def __ne__(self, other: object):
        if not isinstance(other, ElementProxy):
            return True
        return self._element is not other._element

    @property
    def element(self):
        """The lxml element proxied by this object."""
        return self._element

    @property
    def part(self) -> XmlPart:
        """The package part containing this object."""
        if self._parent is None:
            raise ValueError("part is not accessible from this element")
        return self._parent.part


class Parented:
    """Provides common services for document elements that occur below a part but may
    occasionally require an ancestor object to provide a service, such as add or drop a
    relationship.

    Provides ``self._parent`` attribute to subclasses.
    """

    def __init__(self, parent: t.ProvidesXmlPart):
        self._parent = parent

    @property
    def part(self):
        """The package part containing this object."""
        return self._parent.part


class StoryChild:
    """A document element within a story part.

    Story parts include DocumentPart and Header/FooterPart and can contain block items
    (paragraphs and tables). Items from the block-item subtree occasionally require an
    ancestor object to provide access to part-level or package-level items like styles
    or images or to add or drop a relationship.

    Provides `self._parent` attribute to subclasses.
    """

    def __init__(self, parent: t.ProvidesStoryPart):
        self._parent = parent

    @property
    def part(self) -> StoryPart:
        """The package part containing this object."""
        return self._parent.part


class TextAccumulator:
    """Accepts `str` fragments and joins them together, in order, on `.pop().

    Handy when text in a stream is broken up arbitrarily and you want to join it back
    together within certain bounds. The optional `separator` argument determines how
    the text fragments are punctuated, defaulting to the empty string.
    """

    def __init__(self, separator: str = ""):
        self._separator = separator
        self._texts: List[str] = []

    def push(self, text: str) -> None:
        """Add a text fragment to the accumulator."""
        self._texts.append(text)

    def pop(self) -> Iterator[str]:
        """Generate sero-or-one str from those accumulated.

        Using `yield from accum.pop()` in a generator setting avoids producing an empty
        string when no text is in the accumulator.
        """
        if not self._texts:
            return
        text = self._separator.join(self._texts)
        self._texts.clear()
        yield text