1#! /usr/bin/env python 2# -*- coding: utf-8 -*- 3 4############################################################################## 5## DendroPy Phylogenetic Computing Library. 6## 7## Copyright 2010-2015 Jeet Sukumaran and Mark T. Holder. 8## All rights reserved. 9## 10## See "LICENSE.rst" for terms and conditions of usage. 11## 12## If you use this work or any portion thereof in published work, 13## please cite it as: 14## 15## Sukumaran, J. and M. T. Holder. 2010. DendroPy: a Python library 16## for phylogenetic computing. Bioinformatics 26: 1569-1571. 17## 18############################################################################## 19 20""" 21Infrastructure for phylogenetic data objects. 22""" 23 24import os 25import copy 26import sys 27import collections 28from dendropy.utility.textprocessing import StringIO 29if not (sys.version_info.major >= 3 and sys.version_info.minor >= 4): 30 from dendropy.utility.filesys import pre_py34_open as open 31from dendropy.utility import container 32from dendropy.utility import bibtex 33from dendropy.utility import textprocessing 34from dendropy.utility import urlio 35from dendropy.utility import error 36from dendropy.utility import deprecate 37 38############################################################################## 39## Keyword Processor 40 41def _extract_serialization_target_keyword(kwargs, target_type): 42 target_type_keywords = ["file", "path", "url", "data", "stream", "string"] 43 found_kw = [] 44 for kw in target_type_keywords: 45 if kw in kwargs: 46 found_kw.append(kw) 47 if not found_kw: 48 raise TypeError("{} not specified; exactly one of the following keyword arguments required to be specified: {}".format(target_type, target_type_keywords)) 49 if len(found_kw) > 1: 50 raise TypeError("{} specified multiple times: {}".format(target_type, found_kw)) 51 target = kwargs.pop(found_kw[0]) 52 if "schema" not in kwargs: 53 raise TypeError("Mandatory keyword argument 'schema' not specified") 54 schema = kwargs.pop("schema") 55 return found_kw[0], target, schema 56 57############################################################################## 58## DataObject 59 60class DataObject(object): 61 62 """ 63 Base class for all phylogenetic data objects. 64 """ 65 66 def __init__(self, label=None): 67 self._label = None 68 if label is not None: 69 self._set_label(label) 70 71 def _get_label(self): 72 return self._label 73 def _set_label(self, v): 74 # self._label = str(v) if v is not None else v 75 self._label = v 76 label = property(_get_label, _set_label) 77 78 def clone(self, depth=1): 79 """ 80 Creates and returns a copy of ``self``. 81 82 Parameters 83 ---------- 84 depth : integer 85 The depth of the copy: 86 87 - 0: shallow-copy: All member objects are references, 88 except for :attr:``annotation_set`` of top-level object and 89 member |Annotation| objects: these are full, 90 independent instances (though any complex objects in the 91 ``value`` field of |Annotation| objects are also 92 just references). 93 - 1: taxon-namespace-scoped copy: All member objects are full 94 independent instances, *except* for |TaxonNamespace| 95 and |Taxon| instances: these are references. 96 - 2: Exhaustive deep-copy: all objects are cloned. 97 """ 98 if depth == 0: 99 return copy.copy(self) 100 elif depth == 1: 101 return self.taxon_namespace_scoped_copy(memo=None) 102 elif depth == 2: 103 return copy.deepcopy(self) 104 else: 105 raise TypeError("Unsupported cloning depth: {}".format(depth)) 106 107 def taxon_namespace_scoped_copy(self, memo=None): 108 """ 109 Cloning level: 1. 110 Taxon-namespace-scoped copy: All member objects are full independent 111 instances, *except* for |TaxonNamespace| and |Taxon| 112 objects: these are preserved as references. 113 """ 114 raise NotImplementedError 115 116############################################################################## 117## Deserializable 118 119class Deserializable(object): 120 """ 121 Mixin class which all classes that require deserialization should subclass. 122 """ 123 124 def _parse_and_create_from_stream(cls, stream, schema, **kwargs): 125 """ 126 Subclasses need to implement this method to create 127 and return and instance of themselves read from the 128 stream. 129 """ 130 raise NotImplementedError 131 _parse_and_create_from_stream = classmethod(_parse_and_create_from_stream) 132 133 def _get_from(cls, **kwargs): 134 """ 135 Factory method to return new object of this class from an external 136 source by dispatching calls to more specialized ``get_from_*`` methods. 137 Implementing classes will have a publically-exposed method, ``get()``, 138 that wraps a call to this method. This allows for class-specific 139 documentation of keyword arguments. E.g.:: 140 141 @classmethod 142 def get(cls, **kwargs): 143 ''' 144 ... (documentation) ... 145 ''' 146 return cls._get_from(**kwargs) 147 148 """ 149 try: 150 src_type, src, schema = _extract_serialization_target_keyword(kwargs, "Source") 151 except Exception as e: 152 raise e 153 if src_type == "file" or src_type == "stream": 154 return cls.get_from_stream(src=src, schema=schema, **kwargs) 155 elif src_type == "path": 156 return cls.get_from_path(src=src, schema=schema, **kwargs) 157 elif src_type == "data" or src_type == "string": 158 return cls.get_from_string(src=src, schema=schema, **kwargs) 159 elif src_type == "url": 160 return cls.get_from_url(src=src, schema=schema, **kwargs) 161 else: 162 raise ValueError("Unsupported source type: {}".format(src_type)) 163 _get_from = classmethod(_get_from) 164 165 def get_from_stream(cls, src, schema, **kwargs): 166 """ 167 Factory method to return new object of this class from file-like object 168 ``src``. 169 170 Parameters 171 ---------- 172 src : file or file-like 173 Source of data. 174 schema : string 175 Specification of data format (e.g., "nexus"). 176 \*\*kwargs : keyword arguments, optional 177 Arguments to customize parsing, instantiation, processing, and 178 accession of objects read from the data source, including schema- 179 or format-specific handling. These will be passed to the underlying 180 schema-specific reader for handling. 181 182 Returns 183 ------- 184 pdo : phylogenetic data object 185 New instance of object, constructed and populated from data given 186 in source. 187 """ 188 return cls._parse_and_create_from_stream(stream=src, 189 schema=schema, 190 **kwargs) 191 get_from_stream = classmethod(get_from_stream) 192 193 def get_from_path(cls, src, schema, **kwargs): 194 """ 195 Factory method to return new object of this class from file 196 specified by string ``src``. 197 198 Parameters 199 ---------- 200 src : string 201 Full file path to source of data. 202 schema : string 203 Specification of data format (e.g., "nexus"). 204 \*\*kwargs : keyword arguments, optional 205 Arguments to customize parsing, instantiation, processing, and 206 accession of objects read from the data source, including schema- 207 or format-specific handling. These will be passed to the underlying 208 schema-specific reader for handling. 209 210 Returns 211 ------- 212 pdo : phylogenetic data object 213 New instance of object, constructed and populated from data given 214 in source. 215 """ 216 with open(src, "r", newline=None) as fsrc: 217 return cls._parse_and_create_from_stream(stream=fsrc, 218 schema=schema, 219 **kwargs) 220 get_from_path = classmethod(get_from_path) 221 222 def get_from_string(cls, src, schema, **kwargs): 223 """ 224 Factory method to return new object of this class from string ``src``. 225 226 Parameters 227 ---------- 228 src : string 229 Data as a string. 230 schema : string 231 Specification of data format (e.g., "nexus"). 232 \*\*kwargs : keyword arguments, optional 233 Arguments to customize parsing, instantiation, processing, and 234 accession of objects read from the data source, including schema- 235 or format-specific handling. These will be passed to the underlying 236 schema-specific reader for handling. 237 238 Returns 239 ------- 240 pdo : phylogenetic data object 241 New instance of object, constructed and populated from data given 242 in source. 243 """ 244 ssrc = StringIO(src) 245 return cls._parse_and_create_from_stream(stream=ssrc, 246 schema=schema, 247 **kwargs) 248 get_from_string = classmethod(get_from_string) 249 250 def get_from_url(cls, src, schema, strip_markup=False, **kwargs): 251 """ 252 Factory method to return a new object of this class from 253 URL given by ``src``. 254 255 Parameters 256 ---------- 257 src : string 258 URL of location providing source of data. 259 schema : string 260 Specification of data format (e.g., "nexus"). 261 \*\*kwargs : keyword arguments, optional 262 Arguments to customize parsing, instantiation, processing, and 263 accession of objects read from the data source, including schema- 264 or format-specific handling. These will be passed to the underlying 265 schema-specific reader for handling. 266 267 Returns 268 ------- 269 pdo : phylogenetic data object 270 New instance of object, constructed and populated from data given 271 in source. 272 """ 273 text = urlio.read_url(src, strip_markup=strip_markup) 274 ssrc = StringIO(text) 275 try: 276 return cls._parse_and_create_from_stream( 277 stream=ssrc, 278 schema=schema, 279 **kwargs) 280 except error.DataParseError: 281 sys.stderr.write(text) 282 raise 283 get_from_url = classmethod(get_from_url) 284 285 286############################################################################## 287## MultiReadabe 288 289class MultiReadable(object): 290 """ 291 Mixin class which all classes that support multiple (e.g., aggregative) deserialization should subclass. 292 """ 293 294 def _parse_and_add_from_stream(self, stream, schema, **kwargs): 295 """ 296 Populates/constructs objects of this type from ``schema``-formatted 297 data in the file-like object source ``stream``. 298 299 Parameters 300 ---------- 301 stream : file or file-like 302 Source of data. 303 schema : string 304 Specification of data format (e.g., "nexus"). 305 \*\*kwargs : keyword arguments, optional 306 Arguments to customize parsing, instantiation, processing, and 307 accession of objects read from the data source, including schema- 308 or format-specific handling. These will be passed to the underlying 309 schema-specific reader for handling. 310 311 Returns 312 ------- 313 n : ``int`` or ``tuple`` [``int``] 314 A value indicating size of data read, where "size" depends on 315 the object: 316 317 - |Tree|: **undefined** 318 - |TreeList|: number of trees 319 - |CharacterMatrix|: number of sequences 320 - |DataSet|: ``tuple`` (number of taxon namespaces, number of tree lists, number of matrices) 321 322 """ 323 raise NotImplementedError 324 325 def _read_from(self, **kwargs): 326 """ 327 Add data to objects of this class from an external 328 source by dispatching calls to more specialized ``read_from_*`` methods. 329 Implementing classes will have a publically-exposed method, ``read()``, 330 that wraps a call to this method. This allows for class-specific 331 documentation of keyword arguments. E.g.:: 332 333 def read(self, **kwargs): 334 ''' 335 ... (documentation) ... 336 ''' 337 return MultiReadable._read_from(self, **kwargs) 338 339 """ 340 try: 341 src_type, src, schema = _extract_serialization_target_keyword(kwargs, "Source") 342 except Exception as e: 343 raise e 344 if src_type == "file" or src_type == "stream": 345 return self.read_from_stream(src=src, schema=schema, **kwargs) 346 elif src_type == "path": 347 return self.read_from_path(src=src, schema=schema, **kwargs) 348 elif src_type == "data" or src_type == "string": 349 return self.read_from_string(src=src, schema=schema, **kwargs) 350 elif src_type == "url": 351 return self.read_from_url(src=src, schema=schema, **kwargs) 352 else: 353 raise ValueError("Unsupported source type: {}".format(src_type)) 354 355 def read_from_stream(self, src, schema, **kwargs): 356 """ 357 Reads from file (exactly equivalent to just ``read()``, provided 358 here as a separate method for completeness. 359 360 Parameters 361 ---------- 362 fileobj : file or file-like 363 Source of data. 364 schema : string 365 Specification of data format (e.g., "nexus"). 366 \*\*kwargs : keyword arguments, optional 367 Arguments to customize parsing, instantiation, processing, and 368 accession of objects read from the data source, including schema- 369 or format-specific handling. These will be passed to the underlying 370 schema-specific reader for handling. 371 372 Returns 373 ------- 374 n : ``tuple`` [integer] 375 A value indicating size of data read, where "size" depends on 376 the object: 377 378 - |Tree|: **undefined** 379 - |TreeList|: number of trees 380 - |CharacterMatrix|: number of sequences 381 - |DataSet|: ``tuple`` (number of taxon namespaces, number of tree lists, number of matrices) 382 383 """ 384 return self._parse_and_add_from_stream(stream=src, schema=schema, **kwargs) 385 386 def read_from_path(self, src, schema, **kwargs): 387 """ 388 Reads data from file specified by ``filepath``. 389 390 Parameters 391 ---------- 392 filepath : file or file-like 393 Full file path to source of data. 394 schema : string 395 Specification of data format (e.g., "nexus"). 396 \*\*kwargs : keyword arguments, optional 397 Arguments to customize parsing, instantiation, processing, and 398 accession of objects read from the data source, including schema- 399 or format-specific handling. These will be passed to the underlying 400 schema-specific reader for handling. 401 402 Returns 403 ------- 404 n : ``tuple`` [integer] 405 A value indicating size of data read, where "size" depends on 406 the object: 407 408 - |Tree|: **undefined** 409 - |TreeList|: number of trees 410 - |CharacterMatrix|: number of sequences 411 - |DataSet|: ``tuple`` (number of taxon namespaces, number of tree lists, number of matrices) 412 """ 413 with open(src, "r", newline=None) as fsrc: 414 return self._parse_and_add_from_stream(stream=fsrc, schema=schema, **kwargs) 415 416 def read_from_string(self, src, schema, **kwargs): 417 """ 418 Reads a string. 419 420 Parameters 421 ---------- 422 src_str : string 423 Data as a string. 424 schema : string 425 Specification of data format (e.g., "nexus"). 426 \*\*kwargs : keyword arguments, optional 427 Arguments to customize parsing, instantiation, processing, and 428 accession of objects read from the data source, including schema- 429 or format-specific handling. These will be passed to the underlying 430 schema-specific reader for handling. 431 432 Returns 433 ------- 434 n : ``tuple`` [integer] 435 A value indicating size of data read, where "size" depends on 436 the object: 437 438 - |Tree|: **undefined** 439 - |TreeList|: number of trees 440 - |CharacterMatrix|: number of sequences 441 - |DataSet|: ``tuple`` (number of taxon namespaces, number of tree lists, number of matrices) 442 """ 443 s = StringIO(src) 444 return self._parse_and_add_from_stream(stream=s, schema=schema, **kwargs) 445 446 def read_from_url(self, src, schema, **kwargs): 447 """ 448 Reads a URL source. 449 450 Parameters 451 ---------- 452 src : string 453 URL of location providing source of data. 454 schema : string 455 Specification of data format (e.g., "nexus"). 456 \*\*kwargs : keyword arguments, optional 457 Arguments to customize parsing, instantiation, processing, and 458 accession of objects read from the data source, including schema- 459 or format-specific handling. These will be passed to the underlying 460 schema-specific reader for handling. 461 462 Returns 463 ------- 464 n : ``tuple`` [integer] 465 A value indicating size of data read, where "size" depends on 466 the object: 467 468 - |Tree|: **undefined** 469 - |TreeList|: number of trees 470 - |CharacterMatrix|: number of sequences 471 - |DataSet|: ``tuple`` (number of taxon namespaces, number of tree lists, number of matrices) 472 """ 473 src_str = urlio.read_url(src) 474 s = StringIO(src_str) 475 return self._parse_and_add_from_stream(stream=s, schema=schema, **kwargs) 476 477############################################################################## 478## NonMultiReadable 479 480class NonMultiReadable(object): 481 """ 482 Mixin to enforce transition from DendroPy 3 to DendroPy 4 API 483 """ 484 485 def error(self, funcname): 486 read_from_func = funcname 487 get_from_func = funcname.replace("read", "get") 488 raise TypeError(("\n".join(( 489 "The '{classname}' class no longer supports ", 490 "(re-)population by re-reading data from an external ", 491 "source. Instantiate a new object using, for example, ", 492 "'{classname}.{get_from_func}()' and bind it to", 493 "the variable name instead. That is, instead of:", 494 "", 495 " x.{read_from_func}(...)", 496 "", 497 "use:", 498 "", 499 " x = {classname}.{get_from_func}(...)", 500 "", 501 "", 502 ))).format(classname=self.__class__.__name__, get_from_func=get_from_func, read_from_func=read_from_func)) 503 def read(self, stream, schema, **kwargs): 504 raise NotImplementedError() 505 def read_from_stream(self, fileobj, schema, **kwargs): 506 self.error("read_from_stream") 507 def read_from_path(self, filepath, schema, **kwargs): 508 self.error("read_from_path") 509 def read_from_string(self, src_str, schema, **kwargs): 510 self.error("read_from_string") 511 def read_from_url(self, url, schema, **kwargs): 512 self.error("read_from_url") 513 514############################################################################## 515## Serializable 516 517class Serializable(object): 518 """ 519 Mixin class which all classes that require serialization should subclass. 520 """ 521 522 def _format_and_write_to_stream(self, stream, schema, **kwargs): 523 """ 524 Writes the object to the file-like object ``stream`` in ``schema`` 525 schema. 526 """ 527 raise NotImplementedError 528 529 def _write_to(self, **kwargs): 530 """ 531 Write this object to an external resource by dispatching calls to more 532 specialized ``write_to_*`` methods. Implementing classes will have a 533 publically-exposed method, ``write()``, that wraps a call to this 534 method. This allows for class-specific documentation of keyword 535 arguments. E.g.:: 536 537 def write(self, **kwargs): 538 ''' 539 ... (documentation) ... 540 ''' 541 return Serializable._write_to(self, **kwargs) 542 543 """ 544 try: 545 dest_type, dest, schema = _extract_serialization_target_keyword(kwargs, "Destination") 546 except Exception as e: 547 raise e 548 if dest_type == "file": 549 return self.write_to_stream(dest=dest, schema=schema, **kwargs) 550 elif dest_type == "path": 551 return self.write_to_path(dest=dest, schema=schema, **kwargs) 552 else: 553 raise ValueError("Unsupported source type: {}".format(dest_type)) 554 555 def write(self, **kwargs): 556 """ 557 Writes out ``self`` in ``schema`` format. 558 559 **Mandatory Destination-Specification Keyword Argument (Exactly One of the Following Required):** 560 561 - **file** (*file*) -- File or file-like object opened for writing. 562 - **path** (*str*) -- Path to file to which to write. 563 564 **Mandatory Schema-Specification Keyword Argument:** 565 566 - **schema** (*str*) -- Identifier of format of data. See 567 "|Schemas|" for more details. 568 569 **Optional Schema-Specific Keyword Arguments:** 570 571 These provide control over how the data is formatted, and supported 572 argument names and values depend on the schema as specified by the 573 value passed as the "``schema``" argument. See "|Schemas|" for more 574 details. 575 576 Examples 577 -------- 578 579 :: 580 581 d.write(path="path/to/file.dat", 582 schema="nexus", 583 preserve_underscores=True) 584 f = open("path/to/file.dat") 585 d.write(file=f, 586 schema="nexus", 587 preserve_underscores=True) 588 589 """ 590 return Serializable._write_to(self, **kwargs) 591 592 def write_to_stream(self, dest, schema, **kwargs): 593 """ 594 Writes to file-like object ``dest``. 595 """ 596 return self._format_and_write_to_stream(stream=dest, schema=schema, **kwargs) 597 598 def write_to_path(self, dest, schema, **kwargs): 599 """ 600 Writes to file specified by ``dest``. 601 """ 602 with open(os.path.expandvars(os.path.expanduser(dest)), "w") as f: 603 return self._format_and_write_to_stream(stream=f, schema=schema, **kwargs) 604 605 def as_string(self, schema, **kwargs): 606 """ 607 Composes and returns string representation of the data. 608 609 **Mandatory Schema-Specification Keyword Argument:** 610 611 - **schema** (*str*) -- Identifier of format of data. See 612 "|Schemas|" for more details. 613 614 **Optional Schema-Specific Keyword Arguments:** 615 616 These provide control over how the data is formatted, and supported 617 argument names and values depend on the schema as specified by the 618 value passed as the "``schema``" argument. See "|Schemas|" for more 619 details. 620 621 """ 622 s = StringIO() 623 self._format_and_write_to_stream(stream=s, schema=schema, **kwargs) 624 return s.getvalue() 625 626############################################################################## 627## Annotable 628 629class Annotable(object): 630 """ 631 Mixin class which all classes that need to persist object attributes 632 or other information as metadata should subclass. 633 """ 634 635 def _get_annotations(self): 636 if not hasattr(self, "_annotations"): 637 self._annotations = AnnotationSet(self) 638 return self._annotations 639 def _set_annotations(self, annotations): 640 if hasattr(self, "_annotations") \ 641 and annotations is self._annotations \ 642 and self._annotations.target is self: 643 return 644 if not isinstance(annotations, AnnotationSet): 645 raise ValueError("Cannot set 'annotations' to object of type '{}'".format(type(annotations))) 646 old_target = annotations.target 647 self._annotations = annotations 648 self._annotations.target = self 649 for a in self._annotations: 650 if a.is_attribute and a._value[0] is old_target: 651 a.target = self 652 annotations = property(_get_annotations, _set_annotations) 653 654 def _has_annotations(self): 655 return hasattr(self, "_annotations") and len(self._annotations) > 0 656 has_annotations = property(_has_annotations) 657 658 def copy_annotations_from(self, 659 other, 660 attribute_object_mapper=None): 661 """ 662 Copies annotations from ``other``, which must be of |Annotable| 663 type. 664 665 Copies are deep-copies, in that the |Annotation| objects added 666 to the ``annotation_set`` |AnnotationSet| collection of ``self`` are 667 independent copies of those in the ``annotate_set`` collection of 668 ``other``. However, dynamic bound-attribute annotations retain references 669 to the original objects as given in ``other``, which may or may not be 670 desirable. This is handled by updated the objects to which attributes 671 are bound via mappings found in ``attribute_object_mapper``. 672 In dynamic bound-attribute annotations, the ``_value`` attribute of the 673 annotations object (:attr:`Annotation._value`) is a tuple consisting of 674 "``(obj, attr_name)``", which instructs the |Annotation| object to 675 return "``getattr(obj, attr_name)``" (via: "``getattr(*self._value)``") 676 when returning the value of the Annotation. "``obj``" is typically the object 677 to which the |AnnotationSet| belongs (i.e., ``self``). When a copy 678 of |Annotation| is created, the object reference given in the 679 first element of the ``_value`` tuple of dynamic bound-attribute 680 annotations are unchanged, unless the id of the object reference is fo 681 682 Parameters 683 ---------- 684 685 ``other`` : |Annotable| 686 Source of annotations to copy. 687 688 ``attribute_object_mapper`` : dict 689 Like the ``memo`` of ``__deepcopy__``, maps object id's to objects. The 690 purpose of this is to update the parent or owner objects of dynamic 691 attribute annotations. 692 If a dynamic attribute |Annotation| 693 gives object ``x`` as the parent or owner of the attribute (that is, 694 the first element of the :attr:`Annotation._value` tuple is 695 ``other``) and ``id(x)`` is found in ``attribute_object_mapper``, 696 then in the copy the owner of the attribute is changed to 697 ``attribute_object_mapper[id(x)]``. 698 If ``attribute_object_mapper`` is |None| (default), then the 699 following mapping is automatically inserted: ``id(other): self``. 700 That is, any references to ``other`` in any |Annotation| 701 object will be remapped to ``self``. If really no reattribution 702 mappings are desired, then an empty dictionary should be passed 703 instead. 704 705 """ 706 if hasattr(other, "_annotations"): 707 if attribute_object_mapper is None: 708 attribute_object_mapper = {id(object):self} 709 for a1 in other._annotations: 710 a2 = a1.clone(attribute_object_mapper=attribute_object_mapper) 711 if a2.is_attribute and a2._value[0] is other: 712 a2._value = (attribute_object_mapper.get(id(other), other), a2._value[1]) 713 self.annotations.add(a2) 714 715 def deep_copy_annotations_from(self, other, memo=None): 716 """ 717 Note that all references to ``other`` in any annotation value (and 718 sub-annotation, and sub-sub-sub-annotation, etc.) will be 719 replaced with references to ``self``. This may not always make sense 720 (i.e., a reference to a particular entity may be absolute regardless of 721 context). 722 """ 723 if hasattr(other, "_annotations"): 724 # if not isinstance(self, other.__class__) or not isinstance(other, self.__class__): 725 if type(self) is not type(other): 726 raise TypeError("Cannot deep-copy annotations from different type (unable to assume object equivalence in dynamic or nested annotations)") 727 if memo is None: 728 memo = {} 729 for a1 in other._annotations: 730 a2 = copy.deepcopy(a1, memo=memo) 731 memo[id(a1)] = a2 732 if a2.is_attribute and a1._value[0] is other: 733 a2._value = (self, a1._value[1]) 734 self.annotations.add(a2) 735 if hasattr(self, "_annotations"): 736 memo[id(other._annotations)] = self._annotations 737 738 # def __copy__(self): 739 # o = self.__class__.__new__(self.__class__) 740 # for k in self.__dict__: 741 # if k == "_annotations": 742 # continue 743 # o.__dict__[k] = self.__dict__[k] 744 # o.copy_annotations_from(self) 745 746 def __copy__(self, memo=None): 747 """ 748 Cloning level: 0. 749 :attr:``annotation_set`` of top-level object and member |Annotation| 750 objects are full, independent instances. All other member objects (include 751 objects referenced by dynamically-bound attribute values of 752 |Annotation| objects) are references. 753 All member objects are references, except for 754 """ 755 if memo is None: 756 memo = {} 757 other = self.__class__() 758 memo[id(self)] = other 759 for k in self.__dict__: 760 if k == "_annotations": 761 continue 762 other.__dict__[k] = copy.copy(self.__dict__[k]) 763 memo[id(self.__dict__[k])] = other.__dict__[k] 764 self.deep_copy_annotations_from(other, memo=memo) 765 766 def __deepcopy__(self, memo=None): 767 # ensure clone map 768 if memo is None: 769 memo = {} 770 # get or create clone of self 771 try: 772 other = memo[id(self)] 773 except KeyError: 774 # create object without initialization 775 # other = type(self).__new__(self.__class__) 776 other = self.__class__.__new__(self.__class__) 777 # store 778 memo[id(self)] = other 779 # copy other attributes first, skipping annotations 780 for k in self.__dict__: 781 if k == "_annotations": 782 continue 783 if k in other.__dict__: 784 continue 785 other.__dict__[k] = copy.deepcopy(self.__dict__[k], memo) 786 memo[id(self.__dict__[k])] = other.__dict__[k] 787 # assert id(self.__dict__[k]) in memo 788 # create annotations 789 other.deep_copy_annotations_from(self, memo) 790 # return 791 return other 792 793############################################################################## 794## Annotation 795 796class Annotation(Annotable): 797 """ 798 Metadata storage, composition and persistance, with the following attributes: 799 800 * ``name`` 801 * ``value`` 802 * ``datatype_hint`` 803 * ``name_prefix`` 804 * ``namespace`` 805 * ``annotate_as_reference`` 806 * ``is_hidden`` 807 * ``real_value_format_specifier`` - format specifier for printing or rendering 808 values as string, given in Python's format specification 809 mini-language. E.g., '.8f', '4E', '>04d'. 810 811 """ 812 813 def __init__(self, 814 name, 815 value, 816 datatype_hint=None, 817 name_prefix=None, 818 namespace=None, 819 name_is_prefixed=False, 820 is_attribute=False, 821 annotate_as_reference=False, 822 is_hidden=False, 823 label=None, 824 real_value_format_specifier=None, 825 ): 826 self._value = value 827 self.is_attribute = is_attribute 828 if name_is_prefixed: 829 self.prefixed_name = name 830 if name_prefix is not None: 831 self._name_prefix = name_prefix 832 else: 833 self.name = name 834 self._name_prefix = name_prefix 835 self.datatype_hint = datatype_hint 836 self._namespace = None 837 self.namespace = namespace 838 self.annotate_as_reference = annotate_as_reference 839 self.is_hidden = is_hidden 840 self.real_value_format_specifier = real_value_format_specifier 841 842 def __eq__(self, o): 843 return self is o 844 # if not isinstance(o, self.__class__): 845 # return False 846 # if self._value != o._value: 847 # return False 848 # if self.is_attribute != o.is_attribute: 849 # return False 850 # if self.is_attribute and o.is_attribute: 851 # if getattr(*self._value) != getattr(*o._value): 852 # return False 853 # # at this point, we have established that the values 854 # # are equal 855 # return (self.name == o.name 856 # and self._name_prefix == o._name_prefix 857 # and self.datatype_hint == o.datatype_hint 858 # and self._namespace == o._namespace 859 # and self.annotate_as_reference == o.annotate_as_reference 860 # and self.is_hidden == o.is_hidden 861 # and ( ((not hasattr(self, "_annotations")) and (not hasattr(o, "_annotations"))) 862 # or (hasattr(self, "_annotations") and hasattr(o, "_annotations") and self._annotations == o._annotations))) 863 864 def __hash__(self): 865 return id(self) 866 867 def __str__(self): 868 return "{}='{}'".format(self.name, self.value) 869 870 def __copy__(self): 871 return self.clone() 872 873 # def __deepcopy__(self, memo=None): 874 # if memo is None: 875 # memo = {} 876 # o = self.__class__.__new__(self.__class__) 877 # memo[id(self)] = o 878 # for k in self.__dict__: 879 # # if k not in o.__dict__: # do not add attributes already added by base class 880 # print("--->{}: {}".format(id(o), k)) 881 # o.__dict__[k] = copy.deepcopy(self.__dict__[k], memo) 882 # memo[id(self.__dict__[k])] = o.__dict__[k] 883 # return o 884 885 def clone(self, attribute_object_mapper=None): 886 """ 887 Essentially a shallow-copy, except that any objects in the ``_value`` 888 field with an ``id`` found in ``attribute_object_mapper`` will be replaced 889 with ``attribute_object_mapper[id]``. 890 """ 891 o = self.__class__.__new__(self.__class__) 892 if attribute_object_mapper is None: 893 attribute_object_mapper = {id(self):o} 894 if hasattr(self, "_annotations"): 895 o.copy_annotations_from(self) 896 for k in self.__dict__: 897 if k == "_annotations": 898 continue 899 o.__dict__[k] = self.__dict__[k] 900 return o 901 902 def is_match(self, **kwargs): 903 match = True 904 for k, v in kwargs.items(): 905 if k == "name_prefix": 906 if self.name_prefix != v: 907 return False 908 elif k == "prefixed_name": 909 if self.prefixed_name != v: 910 return False 911 elif k == "namespace": 912 if self.namespace != v: 913 return False 914 elif k == "value": 915 if self.value != v: 916 return False 917 elif hasattr(self, k): 918 if getattr(self, k) != v: 919 return False 920 return True 921 922 def _get_value(self): 923 if self.is_attribute: 924 return getattr(*self._value) 925 else: 926 return self._value 927 def _set_value(self, value): 928 self._value = value 929 value = property(_get_value, _set_value) 930 931 def _get_name_prefix(self): 932 if self._name_prefix is None: 933 self._name_prefix = "dendropy" 934 return self._name_prefix 935 def _set_name_prefix(self, prefix): 936 self._name_prefix = prefix 937 name_prefix = property(_get_name_prefix, _set_name_prefix) 938 939 def _get_namespace(self): 940 if self._namespace is None: 941 self._namespace = "http://packages.python.org/DendroPy/" 942 return self._namespace 943 def _set_namespace(self, prefix): 944 self._namespace = prefix 945 namespace = property(_get_namespace, _set_namespace) 946 947 def _get_prefixed_name(self): 948 return "{}:{}".format(self.name_prefix, self.name) 949 def _set_prefixed_name(self, prefixed_name): 950 self._name_prefix, self.name = textprocessing.parse_curie_standard_qualified_name(prefixed_name) 951 prefixed_name = property(_get_prefixed_name, _set_prefixed_name) 952 953############################################################################## 954## AnnotationSet 955 956class AnnotationSet(container.OrderedSet): 957 958 def __init__(self, target, *args): 959 container.OrderedSet.__init__(self, *args) 960 self.target = target 961 962 def __eq__(self, other): 963 if not isinstance(other, self.__class__): 964 return False 965 return (container.OrderedSet.__eq__(self, other)) 966 #and self.target is other.target) # we consider two 967 # AnnotationSet objects equal even if their targets are 968 # different; this is because (a) the target is convenience 969 # artifact, so client code calls to ``add_bound_attribute`` do 970 # not need to specify an owner, and (b) the target is not part 971 # of the contents of the AnnotationSet 972 973 def __str__(self): 974 return "AnnotationSet([{}])".format(( ", ".join(str(a) for a in self))) 975 976 def __deepcopy__(self, memo): 977 try: 978 o = self.__class__(target=memo[id(self.target)]) 979 except KeyError: 980 raise KeyError("deepcopy error: object id {} not found: {}".format(id(self.target), repr(self.target))) 981 memo[id(self)] = o 982 for a in self: 983 x = copy.deepcopy(a, memo) 984 memo[id(a)] = x 985 o.add(x) 986 return o 987 988 def __getitem__(self, name): 989 """ 990 Experimental! Inefficient! Volatile! Subject to change! 991 """ 992 if isinstance(name, int): 993 return container.OrderedSet.__getitem__(self, name) 994 for a in self: 995 if a.name == name: 996 return a 997 a = self.add_new(name, "") 998 return a 999 1000 def __setitem__(self, name, value): 1001 """ 1002 Experimental! Inefficient! Volatile! Subject to change! 1003 """ 1004 if isinstance(name, int): 1005 container.OrderedSet.__setitem__(self, name, value) 1006 for a in self: 1007 if a.name == name: 1008 a.value = value 1009 return 1010 self.add_new(name=name, value=value) 1011 1012 def add_new(self, 1013 name, 1014 value, 1015 datatype_hint=None, 1016 name_prefix=None, 1017 namespace=None, 1018 name_is_prefixed=False, 1019 is_attribute=False, 1020 annotate_as_reference=False, 1021 is_hidden=False, 1022 real_value_format_specifier=None, 1023 ): 1024 """ 1025 Add an annotation. 1026 1027 Parameters 1028 ---------- 1029 name : string 1030 The property/subject/field of the annotation (e.g. "color", 1031 "locality", "dc:citation") 1032 value: string 1033 The content of the annotation. 1034 datatype_hint : string, optional 1035 Mainly for NeXML output (e.g. "xsd:string"). 1036 namespace_prefix : string, optional 1037 Mainly for NeXML output (e.g. "dc:"). 1038 namespace : string, optional 1039 Mainly for NeXML output (e.g. "http://www.w3.org/XML/1998/namespace"). 1040 name_is_prefixed : string, optional 1041 Mainly for NeXML *input*: name will be split into prefix and local part 1042 before storage (e.g., "dc:citations" will result in prefix = "dc" and 1043 name="citations") 1044 is_attribute : boolean, optional 1045 If value is passed as a tuple of (object, "attribute_name") and this 1046 is True, then actual content will be the result of calling 1047 ``getattr(object, "attribute_name")``. 1048 annotate_as_reference : boolean, optional 1049 The value should be interpreted as a URI that points to content. 1050 is_hidden : boolean, optional 1051 Do not write or print this annotation when writing data. 1052 real_value_format_specifier : str 1053 Format specifier for printing or rendering values as string, given 1054 in Python's format specification mini-language. E.g., '.8f', '4E', 1055 '>04d'. 1056 1057 Returns 1058 ------- 1059 annotation : |Annotation| 1060 The new |Annotation| created. 1061 """ 1062 if not name_is_prefixed: 1063 if name_prefix is None and namespace is None: 1064 name_prefix = "dendropy" 1065 namespace = "http://packages.python.org/DendroPy/" 1066 elif name_prefix is None: 1067 raise TypeError("Cannot specify 'name_prefix' for unqualified name without specifying 'namespace'") 1068 elif namespace is None: 1069 raise TypeError("Cannot specify 'namespace' for unqualified name without specifying 'name_prefix'") 1070 else: 1071 if namespace is None: 1072 raise TypeError("Cannot specify qualified name without specifying 'namespace'") 1073 annote = Annotation( 1074 name=name, 1075 value=value, 1076 datatype_hint=datatype_hint, 1077 name_prefix=name_prefix, 1078 namespace=namespace, 1079 name_is_prefixed=name_is_prefixed, 1080 is_attribute=is_attribute, 1081 annotate_as_reference=annotate_as_reference, 1082 is_hidden=is_hidden, 1083 real_value_format_specifier=real_value_format_specifier, 1084 ) 1085 return self.add(annote) 1086 1087 def add_bound_attribute(self, 1088 attr_name, 1089 annotation_name=None, 1090 datatype_hint=None, 1091 name_prefix=None, 1092 namespace=None, 1093 name_is_prefixed=False, 1094 annotate_as_reference=False, 1095 is_hidden=False, 1096 real_value_format_specifier=None, 1097 owner_instance=None, 1098 ): 1099 """ 1100 Add an attribute of an object as a dynamic annotation. The value of the 1101 annotation will be dynamically bound to the value of the attribute. 1102 1103 Parameters 1104 ---------- 1105 attr_name : string 1106 The (string) name of the attribute to be used as the source of the 1107 content or value of the annotation. 1108 annotation_name : string, optional 1109 Use this string as the annotation field/name rather than the attribute 1110 name. 1111 datatype_hint : string, optional 1112 Mainly for NeXML output (e.g. "xsd:string"). 1113 namespace_prefix : string, optional 1114 Mainly for NeXML output (e.g. "dc:"). 1115 namespace : string, optional 1116 Mainly for NeXML output (e.g. "http://www.w3.org/XML/1998/namespace"). 1117 name_is_prefixed : string, optional 1118 Mainly for NeXML *input*: name will be split into prefix and local part 1119 before storage (e.g., "dc:citations" will result in prefix = "dc" and 1120 name="citations") 1121 annotate_as_reference : bool, optional 1122 The value should be interpreted as a URI that points to content. 1123 is_hidden : bool, optional 1124 Do not write or print this annotation when writing data. 1125 owner_instance : object, optional 1126 The object whose attribute is to be used as the value of the 1127 annotation. Defaults to ``self.target``. 1128 1129 Returns 1130 ------- 1131 annotation : |Annotation| 1132 The new |Annotation| created. 1133 """ 1134 if annotation_name is None: 1135 annotation_name = attr_name 1136 if owner_instance is None: 1137 owner_instance = self.target 1138 if not hasattr(owner_instance, attr_name): 1139 raise AttributeError(attr_name) 1140 if not name_is_prefixed: 1141 if name_prefix is None and namespace is None: 1142 name_prefix = "dendropy" 1143 namespace = "http://packages.python.org/DendroPy/" 1144 elif name_prefix is None: 1145 raise TypeError("Cannot specify 'name_prefix' for unqualified name without specifying 'namespace'") 1146 elif namespace is None: 1147 raise TypeError("Cannot specify 'namespace' for unqualified name without specifying 'name_prefix'") 1148 else: 1149 if namespace is None: 1150 raise TypeError("Cannot specify qualified name without specifying 'namespace'") 1151 annote = Annotation( 1152 name=annotation_name, 1153 value=(owner_instance, attr_name), 1154 datatype_hint=datatype_hint, 1155 name_prefix=name_prefix, 1156 namespace=namespace, 1157 name_is_prefixed=name_is_prefixed, 1158 is_attribute=True, 1159 annotate_as_reference=annotate_as_reference, 1160 is_hidden=is_hidden, 1161 real_value_format_specifier=real_value_format_specifier, 1162 ) 1163 return self.add(annote) 1164 1165 def add_citation(self, 1166 citation, 1167 read_as="bibtex", 1168 store_as="bibtex", 1169 name_prefix=None, 1170 namespace=None, 1171 is_hidden=False): 1172 """ 1173 Add a citation as an annotation. 1174 1175 Parameters 1176 ---------- 1177 citation : string or dict or `BibTexEntry` 1178 The citation to be added. If a string, then it must be a 1179 BibTex-formatted entry. If a dictionary, then it must have 1180 BibTex fields as keys and contents as values. 1181 read_as : string, optional 1182 Specifies the format/schema/structure of the citation. Currently 1183 only supports 'bibtex'. 1184 store_as : string, optional 1185 Specifies how to record the citation, with one of the 1186 following strings as values: "bibtex" (a set of annotations, where 1187 each BibTex field becomes a separate annotation); "prism" 1188 (a set of PRISM [Publishing Requirements for Industry Standard 1189 Metadata] annotations); "dublin" (A set of of Dublic Core 1190 annotations). Defaults to "bibtex". 1191 name_prefix : string, optional 1192 Mainly for NeXML output (e.g. "dc:"). 1193 namespace : string, optional 1194 Mainly for NeXML output (e.g. "http://www.w3.org/XML/1998/namespace"). 1195 is_hidden : boolean, optional 1196 Do not write or print this annotation when writing data. 1197 1198 Returns 1199 ------- 1200 annotation : |Annotation| 1201 The new |Annotation| created. 1202 """ 1203 if read_as == "bibtex": 1204 return self.add_bibtex(citation=citation, 1205 store_as=store_as, 1206 name_prefix=name_prefix, 1207 namespace=namespace, 1208 is_hidden=is_hidden) 1209 else: 1210 raise ValueError("Source format '{}' is not supported".format(read_as)) 1211 1212 def add_bibtex(self, 1213 citation, 1214 store_as="bibtex", 1215 name_prefix=None, 1216 namespace=None, 1217 is_hidden=False): 1218 """ 1219 Add a citation as an annotation. 1220 1221 Parameters 1222 ---------- 1223 citation : string or dict or `BibTexEntry` 1224 The citation to be added. If a string, then it must be a 1225 BibTex-formatted entry. If a dictionary, then it must have 1226 BibTex fields as keys and contents as values. 1227 store_as : string, optional 1228 Specifies how to record the citation, with one of the 1229 following strings as values: "bibtex" (a set of annotations, where 1230 each BibTex field becomes a separate annotation); "prism" 1231 (a set of PRISM [Publishing Requirements for Industry Standard 1232 Metadata] annotations); "dublin" (A set of of Dublic Core 1233 annotations). Defaults to "bibtex". 1234 name_prefix : string, optional 1235 Mainly for NeXML output (e.g. "dc:"). 1236 namespace : string, optional 1237 Mainly for NeXML output (e.g. "http://www.w3.org/XML/1998/namespace"). 1238 is_hidden : boolean, optional 1239 Do not write or print this annotation when writing data. 1240 1241 Returns 1242 ------- 1243 annotation : |Annotation| 1244 The new |Annotation| created. 1245 """ 1246 bt = bibtex.BibTexEntry(citation) 1247 bt_dict = bt.fields_as_dict() 1248 1249 if name_prefix is None and namespace is not None: 1250 raise TypeError("Cannot specify 'name_prefix' for unqualified name without specifying 'namespace'") 1251 elif namespace is None and name_prefix is not None: 1252 raise TypeError("Cannot specify 'namespace' for unqualified name without specifying 'name_prefix'") 1253 1254 if store_as.lower().startswith("bibtex"): 1255 if name_prefix is None and namespace is None: 1256 name_prefix = "bibtex" 1257 namespace = "http://www.edutella.org/bibtex#" 1258 self.add_new( 1259 name="bibtype", 1260 value=bt.bibtype, 1261 datatype_hint="xsd:string", 1262 name_prefix=name_prefix, 1263 namespace=namespace, 1264 name_is_prefixed=False, 1265 is_attribute=False, 1266 annotate_as_reference=False, 1267 is_hidden=is_hidden) 1268 self.add_new( 1269 name="citekey", 1270 value=bt.citekey, 1271 datatype_hint="xsd:string", 1272 name_prefix=name_prefix, 1273 namespace=namespace, 1274 name_is_prefixed=False, 1275 is_attribute=False, 1276 annotate_as_reference=False, 1277 is_hidden=is_hidden) 1278 for entry_key, entry_value in bt_dict.items(): 1279 self.add_new( 1280 name=entry_key, 1281 value=entry_value, 1282 datatype_hint="xsd:string", 1283 name_prefix=name_prefix, 1284 namespace=namespace, 1285 name_is_prefixed=False, 1286 is_attribute=False, 1287 annotate_as_reference=False, 1288 is_hidden=is_hidden) 1289 # elif store_as.lower().startswith("bibtex-record"): 1290 # if name_prefix is None and namespace is None: 1291 # name_prefix = "dendropy" 1292 # namespace = "http://packages.python.org/DendroPy/" 1293 # self.add_new( 1294 # name="bibtex", 1295 # value=bt.as_compact_bibtex(), 1296 # datatype_hint="xsd:string", 1297 # name_is_prefixed=False, 1298 # name_prefix=name_prefix, 1299 # namespace=namespace, 1300 # is_attribute=False, 1301 # annotate_as_reference=False, 1302 # is_hidden=is_hidden) 1303 elif store_as.lower().startswith("prism"): 1304 prism_map = { 1305 'volume': bt_dict.get('volume', None), 1306 'publicationName': bt_dict.get('journal', None), 1307 'pageRange': bt_dict.get('pages', None), 1308 'publicationDate': bt_dict.get('year', None), 1309 } 1310 if name_prefix is None and namespace is None: 1311 name_prefix = "prism" 1312 namespace = "http://prismstandard.org/namespaces/1.2/basic/" 1313 for field, value in prism_map.items(): 1314 if value is None: 1315 continue 1316 self.add_new( 1317 name=field, 1318 value=value, 1319 datatype_hint="xsd:string", 1320 name_prefix=name_prefix, 1321 namespace=namespace, 1322 name_is_prefixed=False, 1323 is_attribute=False, 1324 annotate_as_reference=False, 1325 is_hidden=is_hidden) 1326 elif store_as.lower().startswith("dublin"): 1327 dc_map = { 1328 'title': bt_dict.get('title', None), 1329 'creator': bt_dict.get('author', None), 1330 'publisher': bt_dict.get('journal', None), 1331 'date': bt_dict.get('year', None), 1332 } 1333 if name_prefix is None and namespace is None: 1334 name_prefix = "dc" 1335 namespace = "http://purl.org/dc/elements/1.1/" 1336 for field, value in dc_map.items(): 1337 if value is None: 1338 continue 1339 self.add_new( 1340 name=field, 1341 value=value, 1342 datatype_hint="xsd:string", 1343 name_is_prefixed=False, 1344 name_prefix=name_prefix, 1345 namespace=namespace, 1346 is_attribute=False, 1347 annotate_as_reference=False, 1348 is_hidden=is_hidden) 1349 else: 1350 raise ValueError("Unrecognized composition specification: '{}'".format(store_as)) 1351 1352 def findall(self, **kwargs): 1353 """ 1354 Returns AnnotationSet of Annotation objects associated with self.target 1355 that match based on *all* criteria specified in keyword arguments:: 1356 1357 >>> notes = tree.annotations.findall(name="color") 1358 >>> notes = tree.annotations.findall(namespace="http://packages.python.org/DendroPy/") 1359 >>> notes = tree.annotations.findall(namespace="http://packages.python.org/DendroPy/", 1360 name="color") 1361 >>> notes = tree.annotations.findall(name_prefix="dc") 1362 >>> notes = tree.annotations.findall(prefixed_name="dc:color") 1363 1364 If no matches are found, the return AnnotationSet is empty. 1365 1366 If no keyword arguments are given, *all* annotations are returned:: 1367 1368 >>> notes = tree.annotations.findall() 1369 1370 Returns 1371 ------- 1372 results : |AnnotationSet| or |None| 1373 |AnnotationSet| containing |Annotation| objects that 1374 match criteria, or |None| if no matching annotations found. 1375 """ 1376 results = [] 1377 for a in self: 1378 if a.is_match(**kwargs): 1379 results.append(a) 1380 results = AnnotationSet(self.target, results) 1381 return results 1382 1383 def find(self, **kwargs): 1384 """ 1385 Returns the *first* Annotation associated with self.target 1386 which matches based on *all* criteria specified in keyword arguments:: 1387 1388 >>> note = tree.annotations.find(name="color") 1389 >>> note = tree.annotations.find(name_prefix="dc", name="color") 1390 >>> note = tree.annotations.find(prefixed_name="dc:color") 1391 1392 If no match is found, None is returned. 1393 1394 If no keyword arguments are given, a TypeError is raised. 1395 1396 Returns 1397 ------- 1398 results : |Annotation| or |None| 1399 First |Annotation| object found that matches criteria, or 1400 |None| if no matching annotations found. 1401 """ 1402 if "default" in kwargs: 1403 default = kwargs["default"] 1404 del kwargs["default"] 1405 else: 1406 default = None 1407 if not kwargs: 1408 raise TypeError("Search criteria not specified") 1409 for a in self: 1410 if a.is_match(**kwargs): 1411 return a 1412 return default 1413 1414 def get_value(self, name, default=None): 1415 """ 1416 Returns the *value* of the *first* Annotation associated with 1417 self.target which has ``name`` in the name field. 1418 1419 If no match is found, then ``default`` is returned. 1420 1421 Parameters 1422 ---------- 1423 name : string 1424 Name of |Annotation| object whose value is to be returned. 1425 1426 default : any, optional 1427 Value to return if no matching |Annotation| object found. 1428 1429 Returns 1430 ------- 1431 results : |Annotation| or |None| 1432 ``value`` of first |Annotation| object found that matches 1433 criteria, or |None| if no matching annotations found. 1434 """ 1435 for a in self: 1436 if a.is_match(name=name): 1437 return a.value 1438 return default 1439 1440 def require_value(self, name): 1441 """ 1442 Returns the *value* of the *first* Annotation associated with 1443 self.target which has ``name`` in the name field. 1444 1445 If no match is found, then KeyError is raised. 1446 1447 Parameters 1448 ---------- 1449 name : string 1450 Name of |Annotation| object whose value is to be returned. 1451 1452 Returns 1453 ------- 1454 results : |Annotation| or |None| 1455 ``value`` of first |Annotation| object found that matches 1456 criteria. 1457 """ 1458 v = self.get_value(name, default=None) 1459 if v is None: 1460 raise KeyError(name) 1461 return v 1462 1463 def drop(self, **kwargs): 1464 """ 1465 Removes Annotation objects that match based on *all* criteria specified 1466 in keyword arguments. 1467 1468 Remove all annotation objects with ``name`` == 1469 "color":: 1470 1471 >>> tree.annotations.drop(name="color") 1472 1473 Remove all annotation objects with ``namespace`` == 1474 "http://packages.python.org/DendroPy/":: 1475 1476 >>> tree.annotations.drop(namespace="http://packages.python.org/DendroPy/") 1477 1478 Remove all annotation objects with ``namespace`` == 1479 "http://packages.python.org/DendroPy/" *and* ``name`` == "color":: 1480 1481 >>> tree.annotations.drop(namespace="http://packages.python.org/DendroPy/", 1482 name="color") 1483 1484 Remove all annotation objects with ``name_prefix`` == "dc":: 1485 1486 >>> tree.annotations.drop(name_prefix="dc") 1487 1488 Remove all annotation objects with ``prefixed_name`` == "dc:color":: 1489 1490 >>> tree.annotations.drop(prefixed_name="dc:color") 1491 1492 If no keyword argument filter criteria are given, *all* annotations are 1493 removed:: 1494 1495 >>> tree.annotations.drop() 1496 1497 Returns 1498 ------- 1499 results : |AnnotationSet| 1500 |AnnotationSet| containing |Annotation| objects that 1501 were removed. 1502 """ 1503 to_remove = [] 1504 for a in self: 1505 if a.is_match(**kwargs): 1506 to_remove.append(a) 1507 for a in to_remove: 1508 self.remove(a) 1509 return AnnotationSet(self.target, to_remove) 1510 1511 def values_as_dict(self, **kwargs): 1512 """ 1513 Returns annotation set as a dictionary. The keys and values for the dictionary will 1514 be generated based on the following keyword arguments: 1515 1516 Keyword Arguments 1517 ----------------- 1518 key_attr : string 1519 String specifying an Annotation object attribute name to be used 1520 as keys for the dictionary. 1521 key_fn : string 1522 Function that takes an Annotation object as an argument and returns 1523 the value to be used as a key for the dictionary. 1524 value_attr : string 1525 String specifying an Annotation object attribute name to be used 1526 as values for the dictionary. 1527 value_fn : string 1528 Function that takes an Annotation object as an argument and returns 1529 the value to be used as a value for the dictionary. 1530 1531 At most one of ``key_attr`` or ``key_fn`` can be specified. If neither 1532 is specified, then by default the keys are generated from Annotation.name. 1533 At most one of ``value_attr`` or ``value_fn`` can be specified. If neither 1534 is specified, then by default the values are generated from Annotation.value. 1535 Key collisions will result in the dictionary entry for that key being 1536 overwritten. 1537 1538 Returns 1539 ------- 1540 values : dict 1541 """ 1542 if "key_attr" in kwargs and "key_fn" in kwargs: 1543 raise TypeError("Cannot specify both 'key_attr' and 'key_fn'") 1544 elif "key_attr" in kwargs: 1545 key_attr = kwargs["key_attr"] 1546 key_fn = lambda a: getattr(a, key_attr) 1547 elif "key_fn" in kwargs: 1548 key_fn = kwargs["key_fn"] 1549 else: 1550 key_fn = lambda a: a.name 1551 if "value_attr" in kwargs and "value_fn" in kwargs: 1552 raise TypeError("Cannot specify both 'value_attr' and 'value_fn'") 1553 elif "value_attr" in kwargs: 1554 value_attr = kwargs["value_attr"] 1555 value_fn = lambda a: getattr(a, value_attr) 1556 elif "value_fn" in kwargs: 1557 value_fn = kwargs["value_fn"] 1558 else: 1559 value_fn = lambda a: a.value 1560 d = {} 1561 for a in self: 1562 d[key_fn(a)] = value_fn(a) 1563 return d 1564 1565