Skip to content

Commit 4382db7

Browse files
author
Steve Canny
committed
opc: add PartFactory.part_class_selector callable
1 parent 2862897 commit 4382db7

File tree

2 files changed

+104
-24
lines changed

2 files changed

+104
-24
lines changed

docx/opc/package.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,26 @@ def package(self):
317317
class PartFactory(object):
318318
"""
319319
Provides a way for client code to specify a subclass of |Part| to be
320-
constructed by |Unmarshaller| based on its content type.
320+
constructed by |Unmarshaller| based on its content type and/or a custom
321+
callable. Setting ``PartFactory.part_class_selector`` to a callable
322+
object will cause that object to be called with the parameters
323+
``content_type, reltype``, once for each part in the package. If the
324+
callable returns an object, it is used as the class for that part. If it
325+
returns |None|, part class selection falls back to the content type map
326+
defined in ``PartFactory.part_type_for``. If no class is returned from
327+
either of these, the class contained in ``PartFactory.default_part_type``
328+
is used to construct the part, which is by default ``opc.package.Part``.
321329
"""
330+
part_class_selector = None
322331
part_type_for = {}
323332
default_part_type = Part
324333

325334
def __new__(cls, partname, content_type, reltype, blob, package):
326-
PartClass = cls._part_cls_for(content_type)
335+
PartClass = None
336+
if cls.part_class_selector is not None:
337+
PartClass = cls.part_class_selector(content_type, reltype)
338+
if PartClass is None:
339+
PartClass = cls._part_cls_for(content_type)
327340
return PartClass.load(partname, content_type, blob, package)
328341

329342
@classmethod

tests/opc/test_package.py

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -381,40 +381,89 @@ def target_ref_fixture_(self, request, part):
381381

382382
class DescribePartFactory(object):
383383

384+
def it_constructs_part_from_selector_if_defined(
385+
self, cls_selector_fixture):
386+
# fixture ----------------------
387+
(cls_selector_fn_, partname, content_type, reltype, blob, package,
388+
CustomPartClass_, part_of_custom_type_) = cls_selector_fixture
389+
# exercise ---------------------
390+
PartFactory.part_class_selector = cls_selector_fn_
391+
part = PartFactory(partname, content_type, reltype, blob, package)
392+
# verify -----------------------
393+
cls_selector_fn_.assert_called_once_with(content_type, reltype)
394+
CustomPartClass_.load.assert_called_once_with(
395+
partname, content_type, blob, package
396+
)
397+
assert part is part_of_custom_type_
398+
384399
def it_constructs_custom_part_type_for_registered_content_types(
385400
self, part_args_, CustomPartClass_, part_of_custom_type_):
386401
# fixture ----------------------
387-
partname, content_type, reltype, pkg, blob = part_args_
402+
partname, content_type, reltype, package, blob = part_args_
388403
# exercise ---------------------
389404
PartFactory.part_type_for[content_type] = CustomPartClass_
390-
part = PartFactory(partname, content_type, reltype, pkg, blob)
405+
part = PartFactory(partname, content_type, reltype, blob, package)
391406
# verify -----------------------
392407
CustomPartClass_.load.assert_called_once_with(
393-
partname, content_type, pkg, blob
408+
partname, content_type, blob, package
394409
)
395410
assert part is part_of_custom_type_
396411

397412
def it_constructs_part_using_default_class_when_no_custom_registered(
398413
self, part_args_2_, DefaultPartClass_, part_of_default_type_):
399-
partname, content_type, reltype, pkg, blob = part_args_2_
400-
part = PartFactory(partname, content_type, reltype, pkg, blob)
414+
partname, content_type, reltype, blob, package = part_args_2_
415+
part = PartFactory(partname, content_type, reltype, blob, package)
401416
DefaultPartClass_.load.assert_called_once_with(
402-
partname, content_type, pkg, blob
417+
partname, content_type, blob, package
403418
)
404419
assert part is part_of_default_type_
405420

406421
# fixtures ---------------------------------------------
407422

408423
@pytest.fixture
409-
def part_of_custom_type_(self, request):
410-
return instance_mock(request, Part)
424+
def blob_(self, request):
425+
return instance_mock(request, str)
426+
427+
@pytest.fixture
428+
def blob_2_(self, request):
429+
return instance_mock(request, str)
430+
431+
@pytest.fixture
432+
def cls_selector_fixture(
433+
self, request, cls_selector_fn_, partname_, content_type_,
434+
reltype_, blob_, package_, CustomPartClass_,
435+
part_of_custom_type_):
436+
def reset_part_class_selector():
437+
PartFactory.part_class_selector = original_part_class_selector
438+
original_part_class_selector = PartFactory.part_class_selector
439+
request.addfinalizer(reset_part_class_selector)
440+
return (
441+
cls_selector_fn_, partname_, content_type_, reltype_,
442+
blob_, package_, CustomPartClass_, part_of_custom_type_
443+
)
444+
445+
@pytest.fixture
446+
def cls_selector_fn_(self, request, CustomPartClass_):
447+
return loose_mock(request, return_value=CustomPartClass_)
448+
449+
@pytest.fixture
450+
def content_type_(self, request):
451+
return instance_mock(request, str)
452+
453+
@pytest.fixture
454+
def content_type_2_(self, request):
455+
return instance_mock(request, str)
411456

412457
@pytest.fixture
413458
def CustomPartClass_(self, request, part_of_custom_type_):
414459
CustomPartClass_ = Mock(name='CustomPartClass', spec=Part)
415460
CustomPartClass_.load.return_value = part_of_custom_type_
416461
return CustomPartClass_
417462

463+
@pytest.fixture
464+
def part_of_custom_type_(self, request):
465+
return instance_mock(request, Part)
466+
418467
@pytest.fixture
419468
def part_of_default_type_(self, request):
420469
return instance_mock(request, Part)
@@ -428,22 +477,40 @@ def DefaultPartClass_(self, request, part_of_default_type_):
428477
return DefaultPartClass_
429478

430479
@pytest.fixture
431-
def part_args_(self, request):
432-
partname_ = PackURI('/foo/bar.xml')
433-
content_type_ = 'content/type'
434-
reltype_ = 'reltype1'
435-
pkg_ = instance_mock(request, OpcPackage, name="pkg_")
436-
blob_ = b'blob_'
437-
return partname_, content_type_, reltype_, pkg_, blob_
480+
def package_(self, request):
481+
return instance_mock(request, OpcPackage)
482+
483+
@pytest.fixture
484+
def package_2_(self, request):
485+
return instance_mock(request, OpcPackage)
486+
487+
@pytest.fixture
488+
def partname_(self, request):
489+
return instance_mock(request, PackURI)
490+
491+
@pytest.fixture
492+
def partname_2_(self, request):
493+
return instance_mock(request, PackURI)
494+
495+
@pytest.fixture
496+
def part_args_(
497+
self, request, partname_, content_type_, reltype_, package_,
498+
blob_):
499+
return partname_, content_type_, reltype_, blob_, package_
500+
501+
@pytest.fixture
502+
def part_args_2_(
503+
self, request, partname_2_, content_type_2_, reltype_2_,
504+
package_2_, blob_2_):
505+
return partname_2_, content_type_2_, reltype_2_, blob_2_, package_2_
506+
507+
@pytest.fixture
508+
def reltype_(self, request):
509+
return instance_mock(request, str)
438510

439511
@pytest.fixture
440-
def part_args_2_(self, request):
441-
partname_2_ = PackURI('/bar/foo.xml')
442-
content_type_2_ = 'foobar/type'
443-
reltype_2_ = 'reltype2'
444-
pkg_2_ = instance_mock(request, OpcPackage, name="pkg_2_")
445-
blob_2_ = b'blob_2_'
446-
return partname_2_, content_type_2_, reltype_2_, pkg_2_, blob_2_
512+
def reltype_2_(self, request):
513+
return instance_mock(request, str)
447514

448515

449516
class Describe_Relationship(object):

0 commit comments

Comments
 (0)