biotite 1.0.0__cp311-cp311-macosx_11_0_arm64.whl → 1.1.0__cp311-cp311-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of biotite might be problematic. Click here for more details.

Files changed (92) hide show
  1. biotite/application/dssp/app.py +13 -3
  2. biotite/application/localapp.py +34 -0
  3. biotite/application/muscle/app3.py +2 -15
  4. biotite/application/muscle/app5.py +2 -2
  5. biotite/application/util.py +1 -1
  6. biotite/application/viennarna/rnaplot.py +6 -2
  7. biotite/database/rcsb/query.py +6 -6
  8. biotite/database/uniprot/check.py +20 -15
  9. biotite/database/uniprot/download.py +1 -1
  10. biotite/database/uniprot/query.py +1 -1
  11. biotite/sequence/align/alignment.py +16 -3
  12. biotite/sequence/align/banded.cpython-311-darwin.so +0 -0
  13. biotite/sequence/align/banded.pyx +5 -5
  14. biotite/sequence/align/kmeralphabet.cpython-311-darwin.so +0 -0
  15. biotite/sequence/align/kmeralphabet.pyx +17 -0
  16. biotite/sequence/align/kmersimilarity.cpython-311-darwin.so +0 -0
  17. biotite/sequence/align/kmertable.cpython-311-darwin.so +0 -0
  18. biotite/sequence/align/kmertable.pyx +52 -42
  19. biotite/sequence/align/localgapped.cpython-311-darwin.so +0 -0
  20. biotite/sequence/align/localungapped.cpython-311-darwin.so +0 -0
  21. biotite/sequence/align/matrix.py +273 -55
  22. biotite/sequence/align/matrix_data/3Di.mat +24 -0
  23. biotite/sequence/align/matrix_data/PB.license +21 -0
  24. biotite/sequence/align/matrix_data/PB.mat +18 -0
  25. biotite/sequence/align/multiple.cpython-311-darwin.so +0 -0
  26. biotite/sequence/align/pairwise.cpython-311-darwin.so +0 -0
  27. biotite/sequence/align/permutation.cpython-311-darwin.so +0 -0
  28. biotite/sequence/align/selector.cpython-311-darwin.so +0 -0
  29. biotite/sequence/align/tracetable.cpython-311-darwin.so +0 -0
  30. biotite/sequence/alphabet.py +3 -0
  31. biotite/sequence/codec.cpython-311-darwin.so +0 -0
  32. biotite/sequence/graphics/color_schemes/3di_flower.json +48 -0
  33. biotite/sequence/graphics/color_schemes/pb_flower.json +2 -1
  34. biotite/sequence/graphics/colorschemes.py +44 -11
  35. biotite/sequence/phylo/nj.cpython-311-darwin.so +0 -0
  36. biotite/sequence/phylo/tree.cpython-311-darwin.so +0 -0
  37. biotite/sequence/phylo/upgma.cpython-311-darwin.so +0 -0
  38. biotite/sequence/profile.py +86 -4
  39. biotite/sequence/seqtypes.py +124 -3
  40. biotite/setup_ccd.py +197 -0
  41. biotite/structure/__init__.py +4 -3
  42. biotite/structure/alphabet/__init__.py +25 -0
  43. biotite/structure/alphabet/encoder.py +332 -0
  44. biotite/structure/alphabet/encoder_weights_3di.kerasify +0 -0
  45. biotite/structure/alphabet/i3d.py +110 -0
  46. biotite/structure/alphabet/layers.py +86 -0
  47. biotite/structure/alphabet/pb.license +21 -0
  48. biotite/structure/alphabet/pb.py +171 -0
  49. biotite/structure/alphabet/unkerasify.py +122 -0
  50. biotite/structure/atoms.py +156 -43
  51. biotite/structure/bonds.cpython-311-darwin.so +0 -0
  52. biotite/structure/bonds.pyx +72 -21
  53. biotite/structure/celllist.cpython-311-darwin.so +0 -0
  54. biotite/structure/charges.cpython-311-darwin.so +0 -0
  55. biotite/structure/filter.py +1 -1
  56. biotite/structure/geometry.py +60 -113
  57. biotite/structure/info/__init__.py +1 -0
  58. biotite/structure/info/atoms.py +13 -13
  59. biotite/structure/info/bonds.py +12 -6
  60. biotite/structure/info/ccd.py +125 -32
  61. biotite/structure/info/{ccd/components.bcif → components.bcif} +0 -0
  62. biotite/structure/info/groups.py +63 -17
  63. biotite/structure/info/masses.py +9 -6
  64. biotite/structure/info/misc.py +15 -21
  65. biotite/structure/info/standardize.py +3 -2
  66. biotite/structure/io/mol/sdf.py +41 -40
  67. biotite/structure/io/pdb/convert.py +2 -0
  68. biotite/structure/io/pdb/file.py +74 -3
  69. biotite/structure/io/pdb/hybrid36.cpython-311-darwin.so +0 -0
  70. biotite/structure/io/pdbqt/file.py +32 -32
  71. biotite/structure/io/pdbx/__init__.py +1 -0
  72. biotite/structure/io/pdbx/bcif.py +32 -8
  73. biotite/structure/io/pdbx/cif.py +148 -107
  74. biotite/structure/io/pdbx/component.py +9 -4
  75. biotite/structure/io/pdbx/compress.py +321 -0
  76. biotite/structure/io/pdbx/convert.py +227 -68
  77. biotite/structure/io/pdbx/encoding.cpython-311-darwin.so +0 -0
  78. biotite/structure/io/pdbx/encoding.pyx +98 -17
  79. biotite/structure/io/trajfile.py +16 -16
  80. biotite/structure/molecules.py +141 -141
  81. biotite/structure/sasa.cpython-311-darwin.so +0 -0
  82. biotite/structure/segments.py +1 -2
  83. biotite/structure/util.py +73 -1
  84. biotite/version.py +2 -2
  85. {biotite-1.0.0.dist-info → biotite-1.1.0.dist-info}/METADATA +4 -1
  86. {biotite-1.0.0.dist-info → biotite-1.1.0.dist-info}/RECORD +88 -78
  87. biotite/structure/info/ccd/README.rst +0 -8
  88. biotite/structure/info/ccd/amino_acids.txt +0 -1663
  89. biotite/structure/info/ccd/carbohydrates.txt +0 -1135
  90. biotite/structure/info/ccd/nucleotides.txt +0 -798
  91. {biotite-1.0.0.dist-info → biotite-1.1.0.dist-info}/WHEEL +0 -0
  92. {biotite-1.0.0.dist-info → biotite-1.1.0.dist-info}/licenses/LICENSE.rst +0 -0
@@ -7,7 +7,6 @@ __author__ = "Patrick Kunzmann"
7
7
  __all__ = ["CIFFile", "CIFBlock", "CIFCategory", "CIFColumn", "CIFData"]
8
8
 
9
9
  import itertools
10
- import re
11
10
  from collections.abc import MutableMapping, Sequence
12
11
  import numpy as np
13
12
  from biotite.file import (
@@ -357,7 +356,7 @@ class CIFCategory(_Component, MutableMapping):
357
356
  return CIFBlock
358
357
 
359
358
  @staticmethod
360
- def deserialize(text, expect_whitespace=True):
359
+ def deserialize(text):
361
360
  lines = [line.strip() for line in text.splitlines() if not _is_empty(line)]
362
361
 
363
362
  if _is_loop_start(lines[0]):
@@ -370,9 +369,9 @@ class CIFCategory(_Component, MutableMapping):
370
369
  if category_name is None:
371
370
  raise DeserializationError("Failed to parse category name")
372
371
 
373
- lines = _to_single(lines, is_looped)
372
+ lines = _to_single(lines)
374
373
  if is_looped:
375
- category_dict = CIFCategory._deserialize_looped(lines, expect_whitespace)
374
+ category_dict = CIFCategory._deserialize_looped(lines)
376
375
  else:
377
376
  category_dict = CIFCategory._deserialize_single(lines)
378
377
  return CIFCategory(category_dict, category_name)
@@ -416,6 +415,9 @@ class CIFCategory(_Component, MutableMapping):
416
415
  raise ValueError("At least one column must remain")
417
416
  del self._columns[key]
418
417
 
418
+ def __contains__(self, key):
419
+ return key in self._columns
420
+
419
421
  def __iter__(self):
420
422
  return iter(self._columns)
421
423
 
@@ -439,15 +441,32 @@ class CIFCategory(_Component, MutableMapping):
439
441
  Process a category where each field has a single value.
440
442
  """
441
443
  category_dict = {}
442
- for line in lines:
443
- parts = _split_one_line(line)
444
- column_name = parts[0].split(".")[1]
445
- column = parts[1]
446
- category_dict[column_name] = CIFColumn(column)
444
+ line_i = 0
445
+ while line_i < len(lines):
446
+ line = lines[line_i]
447
+ parts = list(_split_one_line(line))
448
+ if len(parts) == 2:
449
+ # Standard case -> name and value in one line
450
+ name_part, value_part = parts
451
+ line_i += 1
452
+ elif len(parts) == 1:
453
+ # Value is a multiline value on the next line
454
+ name_part = parts[0]
455
+ parts = list(_split_one_line(lines[line_i + 1]))
456
+ if len(parts) == 1:
457
+ value_part = parts[0]
458
+ else:
459
+ raise DeserializationError(f"Failed to parse line '{line}'")
460
+ line_i += 2
461
+ elif len(parts) == 0:
462
+ raise DeserializationError("Empty line within category")
463
+ else:
464
+ raise DeserializationError(f"Failed to parse line '{line}'")
465
+ category_dict[name_part.split(".")[1]] = CIFColumn(value_part)
447
466
  return category_dict
448
467
 
449
468
  @staticmethod
450
- def _deserialize_looped(lines, expect_whitespace):
469
+ def _deserialize_looped(lines):
451
470
  """
452
471
  Process a category where each field has multiple values
453
472
  (category is a table).
@@ -468,26 +487,22 @@ class CIFCategory(_Component, MutableMapping):
468
487
  data_lines = lines[i:]
469
488
  # Rows may be split over multiple lines -> do not rely on
470
489
  # row-line-alignment at all and simply cycle through columns
471
- column_names = itertools.cycle(column_names)
490
+ column_indices = itertools.cycle(range(len(column_names)))
472
491
  for data_line in data_lines:
473
- # If whitespace is expected in quote protected values,
474
- # use regex-based _split_one_line() to split
475
- # Otherwise use much more faster whitespace split
476
- # and quote removal if applicable.
477
- if expect_whitespace:
478
- values = _split_one_line(data_line)
479
- else:
480
- values = data_line.split()
481
- for k in range(len(values)):
482
- # Remove quotes
483
- if (values[k][0] == '"' and values[k][-1] == '"') or (
484
- values[k][0] == "'" and values[k][-1] == "'"
485
- ):
486
- values[k] = values[k][1:-1]
492
+ values = _split_one_line(data_line)
487
493
  for val in values:
488
- column_name = next(column_names)
494
+ column_index = next(column_indices)
495
+ column_name = column_names[column_index]
489
496
  category_dict[column_name].append(val)
490
497
 
498
+ # Check if all columns have the same length
499
+ # Otherwise, this would indicate a parsing error or an invalid CIF file
500
+ column_index = next(column_indices)
501
+ if column_index != 0:
502
+ raise DeserializationError(
503
+ "Category contains columns with different lengths"
504
+ )
505
+
491
506
  return category_dict
492
507
 
493
508
  def _serialize_single(self):
@@ -496,7 +511,8 @@ class CIFCategory(_Component, MutableMapping):
496
511
  # "+3" Because of three whitespace chars after longest key
497
512
  req_len = max_len + 3
498
513
  return [
499
- key.ljust(req_len) + _multiline(_quote(column.as_item()))
514
+ # Remove potential terminal newlines from multiline values
515
+ (key.ljust(req_len) + _escape(column.as_item())).strip()
500
516
  for key, column in zip(keys, self.values())
501
517
  ]
502
518
 
@@ -508,7 +524,7 @@ class CIFCategory(_Component, MutableMapping):
508
524
  array = column.as_array(str)
509
525
  # Quote before measuring the number of chars,
510
526
  # as the quote characters modify the length
511
- array = np.array([_multiline(_quote(element)) for element in array])
527
+ array = np.array([_escape(element) for element in array])
512
528
  column_arrays.append(array)
513
529
 
514
530
  # Number of characters the longest string in the column needs
@@ -522,7 +538,8 @@ class CIFCategory(_Component, MutableMapping):
522
538
  for j, array in enumerate(column_arrays):
523
539
  value_lines[i] += array[i].ljust(column_n_chars[j])
524
540
  # Remove trailing justification of last column
525
- value_lines[i].rstrip()
541
+ # and potential terminal newlines from multiline values
542
+ value_lines[i] = value_lines[i].strip()
526
543
 
527
544
  return ["loop_"] + key_lines + value_lines
528
545
 
@@ -541,6 +558,17 @@ class CIFBlock(_Component, MutableMapping):
541
558
  The keys are the category names and the values are the
542
559
  :class:`CIFCategory` objects.
543
560
  By default, an empty block is created.
561
+ name : str, optional
562
+ The name of the block.
563
+ This is only used for serialization and is automatically set,
564
+ when the :class:`CIFBlock` is added to a :class:`CIFFile`.
565
+ It only needs to be set manually, when the block is directly
566
+ serialized.
567
+
568
+ Attributes
569
+ ----------
570
+ name : str
571
+ The name of the block.
544
572
 
545
573
  Notes
546
574
  -----
@@ -552,13 +580,15 @@ class CIFBlock(_Component, MutableMapping):
552
580
  --------
553
581
 
554
582
  >>> # Add category on creation
555
- >>> block = CIFBlock({"foo": CIFCategory({"some_column": 1})})
583
+ >>> block = CIFBlock({"foo": CIFCategory({"some_column": 1})}, name="baz")
556
584
  >>> # Add category later on
557
585
  >>> block["bar"] = CIFCategory({"another_column": [2, 3]})
558
586
  >>> # Access a column
559
587
  >>> print(block["bar"]["another_column"].as_array())
560
588
  ['2' '3']
561
589
  >>> print(block.serialize())
590
+ data_baz
591
+ #
562
592
  _foo.some_column 1
563
593
  #
564
594
  loop_
@@ -568,11 +598,20 @@ class CIFBlock(_Component, MutableMapping):
568
598
  #
569
599
  """
570
600
 
571
- def __init__(self, categories=None):
601
+ def __init__(self, categories=None, name=None):
602
+ self._name = name
572
603
  if categories is None:
573
604
  categories = {}
574
605
  self._categories = categories
575
606
 
607
+ @property
608
+ def name(self):
609
+ return self._name
610
+
611
+ @name.setter
612
+ def name(self, name):
613
+ self._name = name
614
+
576
615
  @staticmethod
577
616
  def subcomponent_class():
578
617
  return CIFCategory
@@ -606,7 +645,10 @@ class CIFBlock(_Component, MutableMapping):
606
645
  return CIFBlock(_create_element_dict(lines, category_names, category_starts))
607
646
 
608
647
  def serialize(self):
609
- text_blocks = []
648
+ if self._name is None:
649
+ raise SerializationError("Block name is required")
650
+ # The block starts with the black name line followed by a comment line
651
+ text_blocks = ["data_" + self._name + "\n#\n"]
610
652
  for category_name, category in self._categories.items():
611
653
  if isinstance(category, str):
612
654
  # Category is already stored as lines
@@ -629,15 +671,7 @@ class CIFBlock(_Component, MutableMapping):
629
671
  # Element is stored in serialized form
630
672
  # -> must be deserialized first
631
673
  try:
632
- # Special optimization for "atom_site":
633
- # Even if the values are quote protected,
634
- # no whitespace is expected in escaped values
635
- # Therefore slow regex-based _split_one_line() call is not necessary
636
- if key == "atom_site":
637
- expect_whitespace = False
638
- else:
639
- expect_whitespace = True
640
- category = CIFCategory.deserialize(category, expect_whitespace)
674
+ category = CIFCategory.deserialize(category)
641
675
  except Exception:
642
676
  raise DeserializationError(f"Failed to deserialize category '{key}'")
643
677
  # Update with deserialized object
@@ -655,6 +689,9 @@ class CIFBlock(_Component, MutableMapping):
655
689
  def __delitem__(self, key):
656
690
  del self._categories[key]
657
691
 
692
+ def __contains__(self, key):
693
+ return key in self._categories
694
+
658
695
  def __iter__(self):
659
696
  return iter(self._categories)
660
697
 
@@ -778,14 +815,12 @@ class CIFFile(_Component, File, MutableMapping):
778
815
  def serialize(self):
779
816
  text_blocks = []
780
817
  for block_name, block in self._blocks.items():
781
- text_blocks.append("data_" + block_name + "\n")
782
- # A comment line is set after the block indicator
783
- text_blocks.append("#\n")
784
818
  if isinstance(block, str):
785
819
  # Block is already stored as text
786
820
  text_blocks.append(block)
787
821
  else:
788
822
  try:
823
+ block.name = block_name
789
824
  text_blocks.append(block.serialize())
790
825
  except Exception:
791
826
  raise SerializationError(
@@ -856,11 +891,15 @@ class CIFFile(_Component, File, MutableMapping):
856
891
  def __setitem__(self, key, block):
857
892
  if not isinstance(block, CIFBlock):
858
893
  raise TypeError(f"Expected 'CIFBlock', but got '{type(block).__name__}'")
894
+ block.name = key
859
895
  self._blocks[key] = block
860
896
 
861
897
  def __delitem__(self, key):
862
898
  del self._blocks[key]
863
899
 
900
+ def __contains__(self, key):
901
+ return key in self._blocks
902
+
864
903
  def __iter__(self):
865
904
  return iter(self._blocks)
866
905
 
@@ -893,7 +932,7 @@ def _create_element_dict(lines, element_names, element_starts):
893
932
  # Lazy deserialization
894
933
  # -> keep as text for now and deserialize later if needed
895
934
  return {
896
- element_name: "\n".join(lines[element_starts[i] : element_starts[i + 1]])
935
+ element_name: "\n".join(lines[element_starts[i] : element_starts[i + 1]]) + "\n"
897
936
  for i, element_name in enumerate(element_names)
898
937
  }
899
938
 
@@ -927,52 +966,50 @@ def _is_loop_start(line):
927
966
  return line.startswith("loop_")
928
967
 
929
968
 
930
- def _to_single(lines, is_looped):
931
- """
969
+ def _to_single(lines):
970
+ r"""
932
971
  Convert multiline values into singleline values
933
972
  (in terms of 'lines' list elements).
934
- Linebreaks are preserved.
973
+ Linebreaks are preserved as ``'\n'`` characters within a list element.
974
+ The initial ``';'`` character is also preserved, while the final ``';'`` character
975
+ is removed.
935
976
  """
936
- processed_lines = [None] * len(lines)
937
- in_i = 0
938
- out_i = 0
939
- while in_i < len(lines):
940
- if lines[in_i][0] == ";":
941
- # Multiline value
942
- multi_line_str = lines[in_i][1:]
943
- j = in_i + 1
944
- while lines[j] != ";":
945
- # Preserve linebreaks
946
- multi_line_str += "\n" + lines[j]
947
- j += 1
948
- if is_looped:
949
- # Create a line for the multiline string only
950
- processed_lines[out_i] = f"'{multi_line_str}'"
951
- out_i += 1
977
+ processed_lines = []
978
+ in_multi_line = False
979
+ mutli_line_value = []
980
+ for line in lines:
981
+ # Multiline value are enclosed by ';' at the start of the beginning and end line
982
+ if line[0] == ";":
983
+ if not in_multi_line:
984
+ # Start of multiline value
985
+ in_multi_line = True
986
+ mutli_line_value.append(line)
952
987
  else:
953
- # Append multiline string to previous line
954
- processed_lines[out_i - 1] += " " + f"'{multi_line_str}'"
955
- in_i = j + 1
956
-
957
- elif not is_looped and lines[in_i][0] != "_":
958
- # Singleline value in the line after the corresponding key
959
- processed_lines[out_i - 1] += " " + lines[in_i]
960
- in_i += 1
961
-
988
+ # End of multiline value
989
+ in_multi_line = False
990
+ # The current line contains only the end character ';'
991
+ # Hence this line is not added to the processed lines
992
+ processed_lines.append("\n".join(mutli_line_value))
993
+ mutli_line_value = []
962
994
  else:
963
- # Normal singleline value in the same row as the key
964
- processed_lines[out_i] = lines[in_i]
965
- in_i += 1
966
- out_i += 1
967
-
968
- return [line for line in processed_lines if line is not None]
995
+ if in_multi_line:
996
+ mutli_line_value.append(line)
997
+ else:
998
+ processed_lines.append(line)
999
+ return processed_lines
969
1000
 
970
1001
 
971
- def _quote(value):
1002
+ def _escape(value):
972
1003
  """
973
- A less secure but much quicker version of ``shlex.quote()``.
1004
+ Escape special characters in a value to make it compatible with CIF.
974
1005
  """
975
- if len(value) == 0:
1006
+ if "\n" in value:
1007
+ # A value with linebreaks must be represented as multiline value
1008
+ return _multiline(value)
1009
+ elif "'" in value and '"' in value:
1010
+ # If both quote types are present, you cannot use them for escaping
1011
+ return _multiline(value)
1012
+ elif len(value) == 0:
976
1013
  return "''"
977
1014
  elif value[0] == "_":
978
1015
  return "'" + value + "'"
@@ -990,12 +1027,10 @@ def _quote(value):
990
1027
 
991
1028
  def _multiline(value):
992
1029
  """
993
- Convert a string containing linebreaks into CIF-compatible
1030
+ Convert a string that may contain linebreaks into CIF-compatible
994
1031
  multiline string.
995
1032
  """
996
- if "\n" in value:
997
- return "\n;" + value + "\n;\n"
998
- return value
1033
+ return "\n;" + value + "\n;\n"
999
1034
 
1000
1035
 
1001
1036
  def _split_one_line(line):
@@ -1003,27 +1038,33 @@ def _split_one_line(line):
1003
1038
  Split a line into its fields.
1004
1039
  Supporting embedded quotes (' or "), like `'a dog's life'` to `a dog's life`
1005
1040
  """
1006
- # Define the patterns for different types of fields
1007
- single_quote_pattern = r"('(?:'(?! )|[^'])*')(?:\s|$)"
1008
- double_quote_pattern = r'("(?:"(?! )|[^"])*")(?:\s|$)'
1009
- unquoted_pattern = r"([^\s]+)"
1010
-
1011
- # Combine the patterns using alternation
1012
- combined_pattern = (
1013
- f"{single_quote_pattern}|{double_quote_pattern}|{unquoted_pattern}"
1014
- )
1015
-
1016
- # Find all matches
1017
- matches = re.findall(combined_pattern, line)
1018
-
1019
- # Extract non-empty groups from the matches
1020
- fields = []
1021
- for match in matches:
1022
- field = next(group for group in match if group)
1023
- if field[0] == field[-1] == "'" or field[0] == field[-1] == '"':
1024
- field = field[1:-1]
1025
- fields.append(field)
1026
- return fields
1041
+ # Special case of multiline value, where the line starts with ';'
1042
+ if line[0] == ";":
1043
+ yield line[1:]
1044
+ elif "'" in line or '"' in line:
1045
+ # Quoted values in the line
1046
+ while line:
1047
+ # Strip leading whitespace(s)
1048
+ stripped_line = line.lstrip()
1049
+ # Split the line on whitespace
1050
+ word, _, line = stripped_line.partition(" ")
1051
+ # Handle the case where the word start with a quote
1052
+ if word.startswith(("'", '"')):
1053
+ # Set the separator to the quote found
1054
+ separator = word[0]
1055
+ # Handle the case of a quoted word without space
1056
+ if word.endswith(separator) and len(word) > 1:
1057
+ # Yield the word without the opening and closing quotes
1058
+ yield word[1:-1]
1059
+ continue
1060
+ # split the word on the separator
1061
+ word, _, line = stripped_line[1:].partition(separator)
1062
+
1063
+ yield word
1064
+ else:
1065
+ # No quoted values in the line -> simple whitespace split
1066
+ for line in line.split():
1067
+ yield line
1027
1068
 
1028
1069
 
1029
1070
  def _arrayfy(data):
@@ -171,10 +171,10 @@ class _HierarchicalContainer(_Component, MutableMapping, metaclass=ABCMeta):
171
171
  Parameters
172
172
  ----------
173
173
  store_key_in: str, optional
174
- If given, the key of each element is stored as value in the
175
- serialized element.
176
- This is basically the reverse operation of `take_key_from` in
177
- :meth:`_deserialize_elements()`.
174
+ If given, the key of each element is stored as value in the
175
+ serialized element.
176
+ This is basically the reverse operation of `take_key_from` in
177
+ :meth:`_deserialize_elements()`.
178
178
  """
179
179
  serialized_elements = []
180
180
  for key, element in self._elements.items():
@@ -223,6 +223,11 @@ class _HierarchicalContainer(_Component, MutableMapping, metaclass=ABCMeta):
223
223
  def __delitem__(self, key):
224
224
  del self._elements[key]
225
225
 
226
+ # Implement `__contains__()` explicitly,
227
+ # because the mixin method unnecessarily deserializes the value, if available
228
+ def __contains__(self, key):
229
+ return key in self._elements
230
+
226
231
  def __iter__(self):
227
232
  return iter(self._elements)
228
233