finalcif 153__tar.gz → 154__tar.gz

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.
Files changed (165) hide show
  1. {finalcif-153 → finalcif-154}/.gitignore +1 -0
  2. {finalcif-153 → finalcif-154}/PKG-INFO +1 -1
  3. finalcif-154/finalcif/__init__.py +2 -0
  4. {finalcif-153 → finalcif-154}/finalcif/appwindow.py +20 -0
  5. {finalcif-153 → finalcif-154}/finalcif/cif/atoms.py +3 -3
  6. {finalcif-153 → finalcif-154}/finalcif/cif/cif_file_io.py +36 -27
  7. {finalcif-153 → finalcif-154}/finalcif/equip_property/author_loop_templates.py +14 -23
  8. {finalcif-153 → finalcif-154}/finalcif/finalcif_start.py +1 -1
  9. {finalcif-153 → finalcif-154}/finalcif/report/templated_report.py +62 -39
  10. {finalcif-153 → finalcif-154}/finalcif/template/report.tmpl +7 -1
  11. {finalcif-153 → finalcif-154}/finalcif/tools/platon.py +10 -7
  12. {finalcif-153 → finalcif-154}/finalcif/tools/sumformula.py +0 -2
  13. finalcif-154/platon/README.txt +29 -0
  14. finalcif-154/platon/compile_platon.bat +4 -0
  15. finalcif-154/platon/platon_special.exe +0 -0
  16. finalcif-154/platon/platon_special.f +167105 -0
  17. finalcif-153/finalcif/__init__.py +0 -2
  18. {finalcif-153 → finalcif-154}/.gitattributes +0 -0
  19. {finalcif-153 → finalcif-154}/.python-version +0 -0
  20. {finalcif-153 → finalcif-154}/.ruff.toml +0 -0
  21. {finalcif-153 → finalcif-154}/.run/Make Documentation MAC.run.xml +0 -0
  22. {finalcif-153 → finalcif-154}/.run/compile_ui_files.run.xml +0 -0
  23. {finalcif-153 → finalcif-154}/.run/make documentation Windows.run.xml +0 -0
  24. {finalcif-153 → finalcif-154}/CODE_OF_CONDUCT.md +0 -0
  25. {finalcif-153 → finalcif-154}/CONTRIBUTING.md +0 -0
  26. {finalcif-153 → finalcif-154}/Finalcif_installer_win.spec +0 -0
  27. {finalcif-153 → finalcif-154}/Finalcif_linux_onefile.spec +0 -0
  28. {finalcif-153 → finalcif-154}/Finalcif_mac.spec +0 -0
  29. {finalcif-153 → finalcif-154}/Hydrogen_solution_type.cif +0 -0
  30. {finalcif-153 → finalcif-154}/LICENSE +0 -0
  31. {finalcif-153 → finalcif-154}/README.md +0 -0
  32. {finalcif-153 → finalcif-154}/finalcif/app_path.py +0 -0
  33. {finalcif-153 → finalcif-154}/finalcif/cif/__init__.py +0 -0
  34. {finalcif-153 → finalcif-154}/finalcif/cif/all_cif_dicts.py +0 -0
  35. {finalcif-153 → finalcif-154}/finalcif/cif/checkcif/__init__.py +0 -0
  36. {finalcif-153 → finalcif-154}/finalcif/cif/checkcif/checkcif.py +0 -0
  37. {finalcif-153 → finalcif-154}/finalcif/cif/cif_dict_foo.py +0 -0
  38. {finalcif-153 → finalcif-154}/finalcif/cif/cif_order.py +0 -0
  39. {finalcif-153 → finalcif-154}/finalcif/cif/cod/__init__.py +0 -0
  40. {finalcif-153 → finalcif-154}/finalcif/cif/cod/deposit.py +0 -0
  41. {finalcif-153 → finalcif-154}/finalcif/cif/cod/deposit_check.py +0 -0
  42. {finalcif-153 → finalcif-154}/finalcif/cif/cod/deposition_list.py +0 -0
  43. {finalcif-153 → finalcif-154}/finalcif/cif/cod/doi.py +0 -0
  44. {finalcif-153 → finalcif-154}/finalcif/cif/cod/upload.py +0 -0
  45. {finalcif-153 → finalcif-154}/finalcif/cif/cod/website_parser.py +0 -0
  46. {finalcif-153 → finalcif-154}/finalcif/cif/core_dict.py +0 -0
  47. {finalcif-153 → finalcif-154}/finalcif/cif/hkl.py +0 -0
  48. {finalcif-153 → finalcif-154}/finalcif/cif/modulation_dict.py +0 -0
  49. {finalcif-153 → finalcif-154}/finalcif/cif/powder_dict.py +0 -0
  50. {finalcif-153 → finalcif-154}/finalcif/cif/reference.py +0 -0
  51. {finalcif-153 → finalcif-154}/finalcif/cif/restraints_dict.py +0 -0
  52. {finalcif-153 → finalcif-154}/finalcif/cif/text.py +0 -0
  53. {finalcif-153 → finalcif-154}/finalcif/cif/twin_dict.py +0 -0
  54. {finalcif-153 → finalcif-154}/finalcif/ciforder/__init__.py +0 -0
  55. {finalcif-153 → finalcif-154}/finalcif/ciforder/order.py +0 -0
  56. {finalcif-153 → finalcif-154}/finalcif/ciforder/order_ui.py +0 -0
  57. {finalcif-153 → finalcif-154}/finalcif/ciforder/order_ui.ui +0 -0
  58. {finalcif-153 → finalcif-154}/finalcif/datafiles/__init__.py +0 -0
  59. {finalcif-153 → finalcif-154}/finalcif/datafiles/bruker_data.py +0 -0
  60. {finalcif-153 → finalcif-154}/finalcif/datafiles/bruker_frame.py +0 -0
  61. {finalcif-153 → finalcif-154}/finalcif/datafiles/ccdc_mail.py +0 -0
  62. {finalcif-153 → finalcif-154}/finalcif/datafiles/data.py +0 -0
  63. {finalcif-153 → finalcif-154}/finalcif/datafiles/p4p_reader.py +0 -0
  64. {finalcif-153 → finalcif-154}/finalcif/datafiles/sadabs.py +0 -0
  65. {finalcif-153 → finalcif-154}/finalcif/datafiles/saint.py +0 -0
  66. {finalcif-153 → finalcif-154}/finalcif/datafiles/shelx_lst.py +0 -0
  67. {finalcif-153 → finalcif-154}/finalcif/datafiles/utils.py +0 -0
  68. {finalcif-153 → finalcif-154}/finalcif/displaymol/__init__.py +0 -0
  69. {finalcif-153 → finalcif-154}/finalcif/displaymol/molecule2D.py +0 -0
  70. {finalcif-153 → finalcif-154}/finalcif/displaymol/sdm.py +0 -0
  71. {finalcif-153 → finalcif-154}/finalcif/displaymol/vtk_molecule.py +0 -0
  72. {finalcif-153 → finalcif-154}/finalcif/equip_property/__init__.py +0 -0
  73. {finalcif-153 → finalcif-154}/finalcif/equip_property/equipment.py +0 -0
  74. {finalcif-153 → finalcif-154}/finalcif/equip_property/properties.py +0 -0
  75. {finalcif-153 → finalcif-154}/finalcif/equip_property/tools.py +0 -0
  76. {finalcif-153 → finalcif-154}/finalcif/gui/__init__.py +0 -0
  77. {finalcif-153 → finalcif-154}/finalcif/gui/block_combobox.py +0 -0
  78. {finalcif-153 → finalcif-154}/finalcif/gui/combobox.py +0 -0
  79. {finalcif-153 → finalcif-154}/finalcif/gui/custom_classes.py +0 -0
  80. {finalcif-153 → finalcif-154}/finalcif/gui/dialogs.py +0 -0
  81. {finalcif-153 → finalcif-154}/finalcif/gui/edit_button.py +0 -0
  82. {finalcif-153 → finalcif-154}/finalcif/gui/equipmenttable.py +0 -0
  83. {finalcif-153 → finalcif-154}/finalcif/gui/file_editor.py +0 -0
  84. {finalcif-153 → finalcif-154}/finalcif/gui/finalcif_gui_ui.py +0 -0
  85. {finalcif-153 → finalcif-154}/finalcif/gui/finalcif_gui_ui.ui +0 -0
  86. {finalcif-153 → finalcif-154}/finalcif/gui/import_selector.py +0 -0
  87. {finalcif-153 → finalcif-154}/finalcif/gui/import_selector_ui.py +0 -0
  88. {finalcif-153 → finalcif-154}/finalcif/gui/import_selector_ui.ui +0 -0
  89. {finalcif-153 → finalcif-154}/finalcif/gui/loop_creator.py +0 -0
  90. {finalcif-153 → finalcif-154}/finalcif/gui/loop_creator_ui.py +0 -0
  91. {finalcif-153 → finalcif-154}/finalcif/gui/loop_creator_ui.ui +0 -0
  92. {finalcif-153 → finalcif-154}/finalcif/gui/loops.py +0 -0
  93. {finalcif-153 → finalcif-154}/finalcif/gui/mainstackwidget.py +0 -0
  94. {finalcif-153 → finalcif-154}/finalcif/gui/mixins.py +0 -0
  95. {finalcif-153 → finalcif-154}/finalcif/gui/new_key_dialog.py +0 -0
  96. {finalcif-153 → finalcif-154}/finalcif/gui/new_key_dialog_ui.py +0 -0
  97. {finalcif-153 → finalcif-154}/finalcif/gui/new_key_dialog_ui.ui +0 -0
  98. {finalcif-153 → finalcif-154}/finalcif/gui/plaintextedit.py +0 -0
  99. {finalcif-153 → finalcif-154}/finalcif/gui/playground.py +0 -0
  100. {finalcif-153 → finalcif-154}/finalcif/gui/propertytable.py +0 -0
  101. {finalcif-153 → finalcif-154}/finalcif/gui/spell_check_edit.py +0 -0
  102. {finalcif-153 → finalcif-154}/finalcif/gui/text_templates_ui.py +0 -0
  103. {finalcif-153 → finalcif-154}/finalcif/gui/text_templates_ui.ui +0 -0
  104. {finalcif-153 → finalcif-154}/finalcif/gui/text_value_editor.py +0 -0
  105. {finalcif-153 → finalcif-154}/finalcif/gui/validators.py +0 -0
  106. {finalcif-153 → finalcif-154}/finalcif/gui/vrf_classes.py +0 -0
  107. {finalcif-153 → finalcif-154}/finalcif/icon/__init__.py +0 -0
  108. {finalcif-153 → finalcif-154}/finalcif/icon/finalcif.png +0 -0
  109. {finalcif-153 → finalcif-154}/finalcif/icon/finalcif2.ico +0 -0
  110. {finalcif-153 → finalcif-154}/finalcif/icon/finalcif2.png +0 -0
  111. {finalcif-153 → finalcif-154}/finalcif/icon/multitable.ico +0 -0
  112. {finalcif-153 → finalcif-154}/finalcif/icon/multitable.png +0 -0
  113. {finalcif-153 → finalcif-154}/finalcif/icon/multitable.xcf +0 -0
  114. {finalcif-153 → finalcif-154}/finalcif/report/__init__.py +0 -0
  115. {finalcif-153 → finalcif-154}/finalcif/report/archive_report.py +0 -0
  116. {finalcif-153 → finalcif-154}/finalcif/report/gui/__init__.py +0 -0
  117. {finalcif-153 → finalcif-154}/finalcif/report/gui/mainwindow.py +0 -0
  118. {finalcif-153 → finalcif-154}/finalcif/report/gui/mainwindow.ui +0 -0
  119. {finalcif-153 → finalcif-154}/finalcif/report/mtools.py +0 -0
  120. {finalcif-153 → finalcif-154}/finalcif/report/references.py +0 -0
  121. {finalcif-153 → finalcif-154}/finalcif/report/report_text.py +0 -0
  122. {finalcif-153 → finalcif-154}/finalcif/report/symm.py +0 -0
  123. {finalcif-153 → finalcif-154}/finalcif/report/tables.py +0 -0
  124. {finalcif-153 → finalcif-154}/finalcif/template/__init__.py +0 -0
  125. {finalcif-153 → finalcif-154}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
  126. {finalcif-153 → finalcif-154}/finalcif/template/machines/D8_VENTURE.cif +0 -0
  127. {finalcif-153 → finalcif-154}/finalcif/template/mathml2omml.xsl +0 -0
  128. {finalcif-153 → finalcif-154}/finalcif/template/report_default.docx +0 -0
  129. {finalcif-153 → finalcif-154}/finalcif/template/template1.docx +0 -0
  130. {finalcif-153 → finalcif-154}/finalcif/template/template_for_multitable.docx +0 -0
  131. {finalcif-153 → finalcif-154}/finalcif/template/template_text.docx +0 -0
  132. {finalcif-153 → finalcif-154}/finalcif/template/template_without_text.docx +0 -0
  133. {finalcif-153 → finalcif-154}/finalcif/template/templates.py +0 -0
  134. {finalcif-153 → finalcif-154}/finalcif/template/xsl/convert.py +0 -0
  135. {finalcif-153 → finalcif-154}/finalcif/template/xsl/xmltohtml.xsl +0 -0
  136. {finalcif-153 → finalcif-154}/finalcif/tools/__init__.py +0 -0
  137. {finalcif-153 → finalcif-154}/finalcif/tools/chemparse.py +0 -0
  138. {finalcif-153 → finalcif-154}/finalcif/tools/download.py +0 -0
  139. {finalcif-153 → finalcif-154}/finalcif/tools/dsrmath.py +0 -0
  140. {finalcif-153 → finalcif-154}/finalcif/tools/misc.py +0 -0
  141. {finalcif-153 → finalcif-154}/finalcif/tools/options.py +0 -0
  142. {finalcif-153 → finalcif-154}/finalcif/tools/pupdate.py +0 -0
  143. {finalcif-153 → finalcif-154}/finalcif/tools/settings.py +0 -0
  144. {finalcif-153 → finalcif-154}/finalcif/tools/shred.py +0 -0
  145. {finalcif-153 → finalcif-154}/finalcif/tools/space_groups.py +0 -0
  146. {finalcif-153 → finalcif-154}/finalcif/tools/spgr_format.py +0 -0
  147. {finalcif-153 → finalcif-154}/finalcif/tools/statusbar.py +0 -0
  148. {finalcif-153 → finalcif-154}/finalcif.exe +0 -0
  149. {finalcif-153 → finalcif-154}/install_requirements +0 -0
  150. {finalcif-153 → finalcif-154}/install_requirements.bat +0 -0
  151. {finalcif-153 → finalcif-154}/pyproject.toml +0 -0
  152. {finalcif-153 → finalcif-154}/requirements.txt +0 -0
  153. {finalcif-153 → finalcif-154}/requirements_devel.txt +0 -0
  154. {finalcif-153 → finalcif-154}/requirements_doku.txt +0 -0
  155. {finalcif-153 → finalcif-154}/run_finalcif +0 -0
  156. {finalcif-153 → finalcif-154}/run_finalcif.bat +0 -0
  157. {finalcif-153 → finalcif-154}/screenshots/finalcif_checkcif.png +0 -0
  158. {finalcif-153 → finalcif-154}/screenshots/finalcif_details.png +0 -0
  159. {finalcif-153 → finalcif-154}/screenshots/finalcif_loops.png +0 -0
  160. {finalcif-153 → finalcif-154}/screenshots/finalcif_loops2.png +0 -0
  161. {finalcif-153 → finalcif-154}/screenshots/finalcif_main.png +0 -0
  162. {finalcif-153 → finalcif-154}/screenshots/finalcif_report.png +0 -0
  163. {finalcif-153 → finalcif-154}/screenshots/finalcif_responses.png +0 -0
  164. {finalcif-153 → finalcif-154}/update.exe +0 -0
  165. {finalcif-153 → finalcif-154}/uv.lock +0 -0
@@ -226,3 +226,4 @@ tests/examples/report_multi-finalcif.docx
226
226
  tests/examples/work/cu_BruecknerJK_153F40_0m.lxt
227
227
  finalcif_keys_order.cif
228
228
  !/.python-version
229
+ /platon/*.mod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finalcif
3
- Version: 153
3
+ Version: 154
4
4
  Summary: CIF file editor
5
5
  Project-URL: Homepage, https://dkratzert.de/finalcif.html
6
6
  Project-URL: Bug Tracker, https://github.com/dkratzert/FinalCif/issues
@@ -0,0 +1,2 @@
1
+
2
+ VERSION = 154
@@ -127,6 +127,25 @@ class AppWindow(QMainWindow):
127
127
  if file:
128
128
  self.load_cif_file(file)
129
129
  self.load_recent_cifs_list()
130
+ self.ui.CheckcifPlaintextEdit.insertPlainText(
131
+ "\n"
132
+ " Offline CheckCIF uses PLATON from Anthony L. Spek, Utrecht University, The Netherlands."
133
+ "\n"
134
+ " https://platonsoft.nl\n"
135
+ " \n"
136
+ " Please cite PLATON in your publication if you use it.\n"
137
+ " \n"
138
+ " - PLATON References : \n"
139
+ " Spek, A.L. (2003). J. Appl. Cryst. 36, 7-13.\n"
140
+ " Spek, A.L. (2009). Acta Cryst. D65, 148-155.\n"
141
+ " Spek, A.L. (2015). Acta Cryst. C71, 9-18.\n"
142
+ " Spek, A.L. (2018). Inorg. Chim. Acta, 470, 232-237.\n"
143
+ " Spek, A.L. (2020). Acta Cryst. E76, 1-11.\n"
144
+ " \n"
145
+ " \n"
146
+ " - Recent versions of PLATON may be obtained from: https://platonsoft.nl/xraysoft\n"
147
+ " "
148
+ )
130
149
  self.set_checkcif_output_font(self.ui.CheckcifPlaintextEdit)
131
150
  # To make file drag&drop working:
132
151
  self.setAcceptDrops(True)
@@ -1073,6 +1092,7 @@ class AppWindow(QMainWindow):
1073
1092
  runner.finished.connect(lambda: self.ui.CheckcifButton.setEnabled(True))
1074
1093
  runner.run_process()
1075
1094
  runner.formula.connect(self.add_moiety_furmula)
1095
+ return None
1076
1096
 
1077
1097
  def add_moiety_furmula(self, formula_moiety):
1078
1098
  moiety = self.ui.cif_main_table.getTextFromKey(key='_chemical_formula_moiety', col=Column.CIF)
@@ -816,7 +816,7 @@ def get_atomlabel(input_atom: str) -> str:
816
816
  else:
817
817
  # print('*** {} is not a valid atom!! ***'.format(atom))
818
818
  raise KeyError
819
- except(IndexError):
819
+ except IndexError:
820
820
  # print('*** {} is not a valid atom! ***'.format(atom))
821
821
  raise KeyError
822
822
 
@@ -826,6 +826,6 @@ if __name__ == '__main__':
826
826
 
827
827
  failed, attempted = doctest.testmod() # verbose=True)
828
828
  if failed == 0:
829
- print('passed all {} tests!'.format(attempted))
829
+ print(f'passed all {attempted} tests!')
830
830
  else:
831
- print('{} of {} tests failed'.format(failed, attempted))
831
+ print(f'{failed} of {attempted} tests failed')
@@ -5,6 +5,7 @@
5
5
  # and you think this stuff is worth it, you can buy me a beer in return.
6
6
  # Dr. Daniel Kratzert
7
7
  # ----------------------------------------------------------------------------
8
+ import os
8
9
  import re
9
10
  from collections import namedtuple
10
11
  from collections.abc import Generator
@@ -13,6 +14,8 @@ from pathlib import Path
13
14
 
14
15
  import gemmi
15
16
 
17
+ from finalcif.cif import atoms
18
+
16
19
  if hasattr(gemmi, 'set_leak_warnings'):
17
20
  gemmi.set_leak_warnings(False)
18
21
 
@@ -24,8 +27,7 @@ from finalcif.cif.cif_order import order, special_keys, essential_keys, non_cent
24
27
  from finalcif.cif.hkl import HKL, Limit
25
28
  from finalcif.cif.text import utf8_to_str, quote, retranslate_delimiter
26
29
  from finalcif.datafiles.utils import DSRFind
27
- from finalcif.tools.misc import isnumeric, grouper, strip_finalcif_of_name, \
28
- _angstrom_to_x
30
+ from finalcif.tools.misc import isnumeric, grouper, strip_finalcif_of_name, _angstrom_to_x
29
31
 
30
32
 
31
33
  class GemmiError(Exception):
@@ -45,6 +47,7 @@ class CifContainer:
45
47
  new_block: Create a new block (new file) if a name is given. Otherwise, just
46
48
  the existing document is opened.
47
49
  """
50
+ self.chars_ok = True
48
51
  self.picometer: bool = False
49
52
  self._abs_hkl_details: dict[str, str] = {}
50
53
  if isinstance(file, str):
@@ -72,6 +75,7 @@ class CifContainer:
72
75
  self._on_load()
73
76
  self.order = []
74
77
  self.essential_keys = []
78
+ # May be overwritten later:
75
79
  self.set_order_keys(order)
76
80
  self.set_essential_keys(essential_keys)
77
81
 
@@ -123,7 +127,7 @@ class CifContainer:
123
127
  for num, line in enumerate(filepath.read_bytes().splitlines(keepends=True)):
124
128
  try:
125
129
  line.decode('ascii')
126
- except(UnicodeDecodeError):
130
+ except UnicodeDecodeError:
127
131
  line_numbers.append(num)
128
132
  return line_numbers
129
133
 
@@ -155,9 +159,8 @@ class CifContainer:
155
159
  self.dsr_used = DSRFind(self.res_file_data).dsr_used
156
160
  self.atomic_struct: gemmi.SmallStructure = gemmi.make_small_structure_from_block(self.block)
157
161
  # A dictionary to convert Atom names like 'C1_2' or 'Ga3' into Element names like 'C' or 'Ga'
158
- self._name2elements = dict(
159
- zip([x.upper() for x in self.block.find_loop('_atom_site_label')],
160
- [x.upper() for x in self.block.find_loop('_atom_site_type_symbol')], strict=True))
162
+ self._name2elements = dict(zip([x.upper() for x in self.block.find_loop('_atom_site_label')],
163
+ [y.upper() for y in self.block.find_loop('_atom_site_type_symbol')], strict=False))
161
164
  self.check_hkl_min_max()
162
165
 
163
166
  @property
@@ -211,8 +214,8 @@ class CifContainer:
211
214
  return doc
212
215
 
213
216
  def cif_as_string(self, without_hkl=False) -> str:
214
- opt = gemmi.cif.WriteOptions(gemmi.cif.Style.Indent35)
215
-
217
+ opt = gemmi.cif.WriteOptions()
218
+ opt.align_pairs = 33
216
219
  if without_hkl:
217
220
  # return a copy, do not delete hkl from original:
218
221
  doc = gemmi.cif.Document()
@@ -257,7 +260,6 @@ class CifContainer:
257
260
  f", {self.nangles()} angles")
258
261
 
259
262
  def file_is_there_and_writable(self) -> bool:
260
- import os
261
263
  return self.fileobj.exists() and self.fileobj.is_file() and os.access(self.fileobj, os.W_OK)
262
264
 
263
265
  def set_pair_delimited(self, key: str, txt: str):
@@ -278,13 +280,13 @@ class CifContainer:
278
280
 
279
281
  def save(self, filename: Path | None = None) -> None:
280
282
  """
281
- Saves the current cif file.
282
- :param filename: Name to save cif file to.
283
+ Saves the current cif file. It uses the '[basename]-finalcif.cif' suffix by default.
284
+ :param filename: Name to save cif file to if the default is not sufficient.
283
285
  """
284
286
  if not filename:
285
287
  filename = self.finalcif_file
286
288
  if self.is_empty():
287
- print(f'File {filename} is empty.')
289
+ print(f'DBG> CIF file {filename} is empty.')
288
290
  return
289
291
  if not self.is_writable(filename):
290
292
  raise PermissionError(f'Failed to open {filename.resolve()} for writing: Operation not permitted')
@@ -714,18 +716,18 @@ class CifContainer:
714
716
  Atoms from the CIF where values are returned as string like in the CIF with esds.
715
717
  """
716
718
  labels = self.block.find_loop('_atom_site_label')
717
- types = self.block.find_loop('_atom_site_type_symbol')
719
+ types = self.block.find_loop('_atom_site_type_symbol') or [atoms.get_atomlabel(x) for x in labels]
718
720
  x = self.block.find_loop('_atom_site_fract_x')
719
721
  y = self.block.find_loop('_atom_site_fract_y')
720
722
  z = self.block.find_loop('_atom_site_fract_z')
721
723
  part = self.block.find_loop('_atom_site_disorder_group') or ('0',) * len(labels)
722
724
  occ = self.block.find_loop('_atom_site_occupancy') or ('1.000000',) * len(labels)
723
- u_eq = self.block.find_loop('_atom_site_U_iso_or_equiv')
725
+ u_eq = self.block.find_loop('_atom_site_U_iso_or_equiv') or ('.',) * len(labels)
724
726
  atom = namedtuple('atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
725
727
  for label, type, x, y, z, part, occ, u_eq in zip(labels, types, x, y, z,
726
728
  part,
727
729
  occ,
728
- u_eq, strict=True):
730
+ u_eq, strict=False):
729
731
  if without_h and self.ishydrogen(label):
730
732
  continue
731
733
  if self.picometer:
@@ -761,7 +763,7 @@ class CifContainer:
761
763
  u13 = self.block.find_loop('_atom_site_aniso_U_13')
762
764
  u12 = self.block.find_loop('_atom_site_aniso_U_12')
763
765
  adp = namedtuple('adp', ('label', 'U11', 'U22', 'U33', 'U23', 'U13', 'U12'))
764
- for label, u11, u22, u33, u23, u13, u12 in zip(labels, u11, u22, u33, u23, u13, u12):
766
+ for label, u11, u22, u33, u23, u13, u12 in zip(labels, u11, u22, u33, u23, u13, u12, strict=True):
765
767
  if self.picometer:
766
768
  u11 = _angstrom_to_x(u11, factor=100 ** 2)
767
769
  u22 = _angstrom_to_x(u22, factor=100 ** 2)
@@ -817,7 +819,7 @@ class CifContainer:
817
819
  symm = self.block.find_loop('_geom_bond_site_symmetry_2')
818
820
  publ_loop = self.block.find_loop('_geom_bond_publ_flag') or len(label1) * ['?']
819
821
  bond = namedtuple('bond', ('label1', 'label2', 'dist', 'symm'))
820
- for label1, label2, dist, symm, publ in zip(label1, label2, dist, symm, publ_loop):
822
+ for label1, label2, dist, symm, publ in zip(label1, label2, dist, symm, publ_loop, strict=False):
821
823
  if (without_h and (self.ishydrogen(label1) or self.ishydrogen(label2))) or self.yes_not_set(publ):
822
824
  continue
823
825
  else:
@@ -855,17 +857,20 @@ class CifContainer:
855
857
  symm2 = self.block.find_loop('_geom_angle_site_symmetry_3')
856
858
  publ_loop = self.block.find_loop('_geom_angle_publ_flag') or len(label1) * ['?']
857
859
  angle = namedtuple('angle', ('label1', 'label2', 'label3', 'angle_val', 'symm1', 'symm2'))
858
- for label1, label2, label3, angle_val, symm1, symm2, publ in \
859
- zip(label1, label2, label3, angle_val, symm1, symm2, publ_loop, strict=True):
860
- if ((without_H and (self.ishydrogen(label1) or self.ishydrogen(label2) or
861
- self.ishydrogen(label3))) or (self.yes_not_set(publ))):
860
+ for label1, label2, label3, angle_val, symm1, symm2, publ in (
861
+ zip(label1, label2, label3, angle_val, symm1, symm2, publ_loop, strict=False)):
862
+ if ((without_H and (self.ishydrogen(label1) or self.ishydrogen(label2) or self.ishydrogen(label3))) or
863
+ self.yes_not_set(publ)):
862
864
  continue
863
865
  else:
864
866
  yield angle(label1=label1, label2=label2, label3=label3, angle_val=angle_val,
865
867
  symm1=self.checksymm(symm1), symm2=self.checksymm(symm2))
866
868
 
867
869
  def _iselement(self, name: str) -> str:
868
- return self._name2elements[name.upper()]
870
+ try:
871
+ return self._name2elements[name.upper()]
872
+ except KeyError:
873
+ return atoms.get_atomlabel(name)
869
874
 
870
875
  def natoms(self, without_h: bool = False) -> int:
871
876
  return len(tuple(self.atoms(without_h)))
@@ -902,12 +907,11 @@ class CifContainer:
902
907
  tors = namedtuple('Torsion',
903
908
  ('label1', 'label2', 'label3', 'label4', 'torsang', 'symm1', 'symm2', 'symm3', 'symm4'))
904
909
  for label1, label2, label3, label4, torsang, symm1, symm2, symm3, symm4, publ in zip(label1, label2, label3,
905
- # noqa: B020
906
910
  label4,
907
911
  torsang, symm1, symm2,
908
912
  symm3,
909
913
  symm4, publ_loop,
910
- strict=True):
914
+ strict=False):
911
915
  if ((without_h and (self.ishydrogen(label1) or self.ishydrogen(label2)
912
916
  or self.ishydrogen(label3) or self.ishydrogen(label3))) or self.yes_not_set(publ)):
913
917
  continue
@@ -930,7 +934,7 @@ class CifContainer:
930
934
  hydr = namedtuple('HydrogenBond', ('label_d', 'label_h', 'label_a', 'dist_dh', 'dist_ha', 'dist_da',
931
935
  'angle_dha', 'symm'))
932
936
  for label_d, label_h, label_a, dist_dh, dist_ha, dist_da, angle_dha, symm, publ in (
933
- zip(label_d, label_h, label_a, dist_dh, dist_ha, dist_da, angle_dha, symm, publ_loop)):
937
+ zip(label_d, label_h, label_a, dist_dh, dist_ha, dist_da, angle_dha, symm, publ_loop, strict=False)):
934
938
  if self.yes_not_set(publ):
935
939
  continue
936
940
  if self.picometer:
@@ -947,7 +951,12 @@ class CifContainer:
947
951
  Returns the key/value pairs of a cif file sorted by priority.
948
952
  """
949
953
  keys_without_values, keys_with_values = self.keys_with_essentials()
950
- return keys_without_values + [('These below are already in:', '---------------------')] + keys_with_values
954
+ return [*keys_without_values, ('These below are already in:', '---------------------'), *keys_with_values]
955
+
956
+ def pairs(self):
957
+ for item in self.block:
958
+ if item.pair is not None:
959
+ yield item.pair
951
960
 
952
961
  def _is_centrokey(self, key) -> bool:
953
962
  """
@@ -973,7 +982,7 @@ class CifContainer:
973
982
  # continue
974
983
  if self._is_centrokey(key):
975
984
  continue
976
- if not value or value == '?' or value == "'?'":
985
+ if not value or value in {'?', "'?'"}:
977
986
  questions.append((key, value))
978
987
  else:
979
988
  with_values.append((key, value))
@@ -171,34 +171,24 @@ class AuthorLoops:
171
171
  def save_author_to_loop(self) -> None:
172
172
  author = self.get_author_info()
173
173
  row = [author.name, author.address, author.email, author.phone, author.orcid, author.iucr_id, author.footnote]
174
- if author.contact_author:
175
- author_type = f'_{author.author_type}_contact_author_name'
176
- del row[-1] # contact author has no footnote
177
- else:
178
- author_type = f'_{author.author_type}_author_name'
179
- if self.cif.block.find_loop(author_type):
180
- gemmi_loop: Loop = self.cif.block.find_loop(author_type).get_loop()
181
- if tuple(row) in list(grouper(gemmi_loop.values, len(row))):
182
- self.app.status_bar.show_message('This author already exists.', 10)
174
+ author_loop_tags = self.get_author_loop(author)
175
+ table: gemmi.cif.Table = self.cif.block.find(author_loop_tags)
176
+ if table.width():
177
+ gemmi_loop: Loop = table.loop
178
+ if tuple(row) in list(grouper(gemmi_loop.values, gemmi_loop.width())):
179
+ self.app.status_bar.show_message('This author already exists.', 15)
183
180
  print('dbg> Author already exists.')
184
181
  return
185
182
  else:
186
- author_loop = self.get_author_loop(author)
187
- gemmi_loop = self.cif.init_loop(author_loop)
188
- self.check_if_loop_and_row_size_fit_together(gemmi_loop, row)
183
+ gemmi_loop = self.cif.init_loop(author_loop_tags)
184
+ table = self.cif.block.find(author_loop_tags)
185
+ if gemmi_loop.width() < len(row):
186
+ row = row[:gemmi_loop.width()]
187
+ if row not in table:
188
+ table.append_row(row)
189
189
  self.app.make_loops_tables()
190
190
  self.show_authors_list()
191
191
 
192
- def check_if_loop_and_row_size_fit_together(self, gemmi_loop: gemmi.cif.Loop, row: list[str]) -> None:
193
- if gemmi_loop.width() < len(row):
194
- cut_row = row[:gemmi_loop.width()]
195
- if cut_row not in gemmi_loop.values:
196
- gemmi_loop.add_row(cut_row)
197
- elif gemmi_loop.width() > len(row):
198
- show_general_warning(self, 'An author loop with larger size is already in the CIF. Can not proceed.')
199
- elif row not in gemmi_loop.values:
200
- gemmi_loop.add_row(row)
201
-
202
192
  def get_author_info(self) -> Author:
203
193
  if self.ui.authorEditTabWidget.currentWidget().objectName() == 'page_publication':
204
194
  name = quote(utf8_to_str(self.ui.FullNameLineEdit.text()))
@@ -445,4 +435,5 @@ if __name__ == '__main__':
445
435
  import pprint
446
436
 
447
437
  l = AuthorLoops(Ui_FinalCifWindow(), CifContainer('test-data/1000007.cif'), None)
448
- pprint.pprint(l.export_raw_data())
438
+ data = l.export_raw_data()
439
+ pprint.pprint(data)
@@ -53,7 +53,7 @@ def my_exception_hook(exctype: type[BaseException], value: BaseException, error_
53
53
 
54
54
  logfile = Path.home().joinpath(Path(r'finalcif-crash.txt'))
55
55
  try:
56
- logfile.write_text(errortext)
56
+ logfile.write_text(errortext, encoding='utf-8', errors='ignore')
57
57
  except PermissionError:
58
58
  pass
59
59
  sys.__excepthook__(exctype, value, error_traceback)
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import abc
3
4
  import dataclasses
4
5
  import enum
5
6
  import itertools
6
7
  import pathlib
7
8
  import re
8
9
  import sys
9
- from abc import ABC
10
10
  from collections import namedtuple
11
11
  from collections.abc import Iterator
12
12
  from contextlib import suppress
@@ -499,7 +499,7 @@ class Disorder:
499
499
  return xml_to_html(self.rt)
500
500
 
501
501
 
502
- class Formatter(ABC):
502
+ class Formatter(abc.ABC):
503
503
  def __init__(self, options: Options, cif: CifContainer) -> None:
504
504
  self.literature: dict[str, Reference] = {'finalcif' : ref.FinalCifReference(),
505
505
  'ccdc' : ref.CCDCReference(),
@@ -513,17 +513,21 @@ class Formatter(ABC):
513
513
  self._torsions = TorsionAngles(cif, without_h=options.without_h)
514
514
  self._hydrogens = HydrogenBonds(cif)
515
515
 
516
+ @abc.abstractmethod
516
517
  def get_bonds(self):
517
- raise NotImplementedError
518
+ ...
518
519
 
520
+ @abc.abstractmethod
519
521
  def get_angles(self):
520
- raise NotImplementedError
522
+ ...
521
523
 
524
+ @abc.abstractmethod
522
525
  def get_torsion_angles(self) -> list[dict[str, RichText]] | list[Torsion]:
523
- raise NotImplementedError
526
+ ...
524
527
 
528
+ @abc.abstractmethod
525
529
  def get_hydrogen_bonds(self) -> list[dict[str, RichText]] | list[HydrogenBond]:
526
- raise NotImplementedError
530
+ ...
527
531
 
528
532
  def get_bonds_angles_symminfo(self) -> str:
529
533
  return self._bonds_angles.symminfo
@@ -537,8 +541,9 @@ class Formatter(ABC):
537
541
  def get_crystallization_method(self, cif: CifContainer) -> str:
538
542
  return string_to_utf8(gstr(cif['_exptl_crystal_recrystallization_method']))
539
543
 
544
+ @abc.abstractmethod
540
545
  def get_radiation(self, cif: CifContainer) -> str | RichText:
541
- raise NotImplementedError
546
+ ...
542
547
 
543
548
  def get_wavelength(self, cif: CifContainer) -> str:
544
549
  try:
@@ -547,8 +552,9 @@ class Formatter(ABC):
547
552
  except ValueError:
548
553
  return ''
549
554
 
555
+ @abc.abstractmethod
550
556
  def hkl_index_limits(self, cif: CifContainer) -> str:
551
- raise NotImplementedError
557
+ ...
552
558
 
553
559
  def make_3d(self, cif: CifContainer, options: Options) -> str:
554
560
  raise NotImplementedError
@@ -559,8 +565,9 @@ class Formatter(ABC):
559
565
  def make_picture(self, options: Options, picfile: Path, tpl_doc: DocxTemplate):
560
566
  raise NotImplementedError
561
567
 
568
+ @abc.abstractmethod
562
569
  def format_sum_formula(self, sum_formula: str) -> str:
563
- raise NotImplementedError
570
+ ...
564
571
 
565
572
  def hydrogen_atoms_refinement(self, cif: CifContainer) -> RichText | str:
566
573
  """
@@ -729,34 +736,45 @@ class Formatter(ABC):
729
736
  self.literature['solution'] = ref.SHELXDReference()
730
737
  return string_to_utf8(solution_prog.split('(')[0]).strip()
731
738
 
732
- def _tvalue(self, tval: str) -> str:
739
+ def t_minvalue(self, cif: CifContainer) -> str:
740
+ t_min = cif['_exptl_absorpt_correction_T_min']
741
+ with suppress(ValueError):
742
+ return f'{float(t_min):.4f}'
743
+ return '?'
744
+
745
+ def t_maxvalue(self, cif: CifContainer) -> str:
746
+ t_max = cif['_exptl_absorpt_correction_T_max']
733
747
  with suppress(ValueError):
734
- return f'{float(tval):.4f}'
735
- return tval
748
+ return f'{float(t_max) :.4f}'
749
+ return '?'
736
750
 
737
- def get_atomic_coordinates(self, cif: CifContainer) -> Iterator[dict[str, str]]:
751
+ def get_atomic_coordinates(self, cif: CifContainer) -> tuple[dict[str, str], ...]:
752
+ coords = []
738
753
  for at in cif.atoms(without_h=False):
739
- yield {'label': at.label,
740
- 'type' : at.type,
741
- 'x' : at.x.replace('-', minus_sign),
742
- 'y' : at.y.replace('-', minus_sign),
743
- 'z' : at.z.replace('-', minus_sign),
744
- 'part' : at.part.replace('-', minus_sign),
745
- 'occ' : at.occ.replace('-', minus_sign),
746
- 'u_eq' : at.u_eq.replace('-', minus_sign)}
747
-
748
- def get_displacement_parameters(self, cif: CifContainer) -> Iterator[AdpWithMinus]:
754
+ coords.append({
755
+ 'label': at.label,
756
+ 'type' : at.type,
757
+ 'x' : at.x.replace('-', minus_sign),
758
+ 'y' : at.y.replace('-', minus_sign),
759
+ 'z' : at.z.replace('-', minus_sign),
760
+ 'part' : at.part.replace('-', minus_sign),
761
+ 'occ' : at.occ.replace('-', minus_sign),
762
+ 'u_eq' : at.u_eq.replace('-', minus_sign)
763
+ })
764
+ return tuple(coords)
765
+
766
+ def get_displacement_parameters(self, cif: CifContainer) -> tuple[AdpWithMinus, ...]:
749
767
  """
750
- Yields the anisotropic displacement parameters. With hypehens replaced to minus signs.
768
+ Returns anisotropic displacement parameters with hyphens replaced by minus signs.
751
769
  """
752
- for label, u11, u22, u33, u23, u13, u12 in cif.displacement_parameters():
753
- yield AdpWithMinus(label=label,
754
- U11=u11.replace('-', minus_sign),
755
- U22=u22.replace('-', minus_sign),
756
- U33=u33.replace('-', minus_sign),
757
- U12=u12.replace('-', minus_sign),
758
- U13=u13.replace('-', minus_sign),
759
- U23=u23.replace('-', minus_sign))
770
+ return tuple(AdpWithMinus(label=label,
771
+ U11=u11.replace('-', minus_sign),
772
+ U22=u22.replace('-', minus_sign),
773
+ U33=u33.replace('-', minus_sign),
774
+ U12=u12.replace('-', minus_sign),
775
+ U13=u13.replace('-', minus_sign),
776
+ U23=u23.replace('-', minus_sign))
777
+ for label, u11, u22, u33, u23, u13, u12 in cif.displacement_parameters())
760
778
 
761
779
  def get_completeness(self, cif: CifContainer) -> str:
762
780
  try:
@@ -897,9 +915,13 @@ class HtmlFormatter(Formatter):
897
915
  # spgrxml = s.to_mathml(cif.space_group)
898
916
  except KeyError:
899
917
  spgrxml = '<math xmlns="http://www.w3.org/1998/Math/MathML">?</math>'
900
- return f'{spgrxml} ({cif.spgr_number})'
918
+ try:
919
+ number = cif.spgr_number
920
+ except AttributeError:
921
+ return '?'
922
+ return f'{spgrxml} ({number})'
901
923
 
902
- def make_picture(self, options: Options, picfile: Path, _: None):
924
+ def make_picture(self, options: Options, picfile: Path, _: None) -> str:
903
925
  picture_path = ''
904
926
  if options.report_text and picfile and picfile.exists():
905
927
  picture_path = str(picfile.resolve())
@@ -1043,7 +1065,7 @@ class RichTextFormatter(Formatter):
1043
1065
  radiation.add(radline, italic=True, subscript=True)
1044
1066
  return radiation
1045
1067
 
1046
- def make_picture(self, options: Options, picfile: Path, tpl_doc: DocxTemplate):
1068
+ def make_picture(self, options: Options, picfile: Path, tpl_doc: DocxTemplate) -> InlineImage | None:
1047
1069
  if options.report_text and picfile and picfile.exists():
1048
1070
  return InlineImage(tpl_doc, str(picfile.resolve()), width=Cm(options.picture_width))
1049
1071
  return None
@@ -1138,6 +1160,7 @@ class TemplatedReport:
1138
1160
  show_general_warning(parent=None, window_title='Warning', warn_text='Document generation failed',
1139
1161
  info_text=str(e))
1140
1162
  print(e)
1163
+ raise
1141
1164
  return False
1142
1165
 
1143
1166
  def make_templated_html_report(self,
@@ -1173,6 +1196,7 @@ class TemplatedReport:
1173
1196
  except Exception as e:
1174
1197
  show_general_warning(parent=None, window_title='Warning', warn_text='Document generation failed',
1175
1198
  info_text=str(e))
1199
+ raise
1176
1200
  return False
1177
1201
  return True
1178
1202
 
@@ -1245,10 +1269,8 @@ class TemplatedReport:
1245
1269
  'restraints' : this_or_quest(cif['_refine_ls_number_restraints']),
1246
1270
  'parameters' : this_or_quest(cif['_refine_ls_number_parameters']),
1247
1271
  'goof' : this_or_quest(cif['_refine_ls_goodness_of_fit_ref']),
1248
- 't_min' : self.text_formatter._tvalue(
1249
- this_or_quest(cif['_exptl_absorpt_correction_T_min'])),
1250
- 't_max' : self.text_formatter._tvalue(
1251
- this_or_quest(cif['_exptl_absorpt_correction_T_max'])),
1272
+ 't_min' : self.text_formatter.t_minvalue(cif),
1273
+ 't_max' : self.text_formatter.t_maxvalue(cif),
1252
1274
  'ls_R_factor_gt' : this_or_quest(cif['_refine_ls_R_factor_gt']),
1253
1275
  'ls_wR_factor_gt' : this_or_quest(cif['_refine_ls_wR_factor_gt']),
1254
1276
  'ls_R_factor_all' : this_or_quest(cif['_refine_ls_R_factor_all']),
@@ -1316,6 +1338,7 @@ if __name__ == '__main__':
1316
1338
  data = Path('tests')
1317
1339
  testcif = Path(data / 'examples/1979688.cif').absolute()
1318
1340
  testcif = Path(r'test-data/p31c.cif').absolute()
1341
+ testcif = Path(r"D:\Downloads\9008564.cif").absolute()
1319
1342
  cif = CifContainer(testcif)
1320
1343
 
1321
1344
  pic = pathlib.Path("screenshots/finalcif_checkcif.png")
@@ -76,7 +76,13 @@
76
76
  used {{ radiation }} radiation {% if wavelength %}(λ = {{ wavelength }} Å){% endif %}.
77
77
  All data were integrated with {{ integration_progr }} yielding {{ cif._diffrn_reflns_number }}
78
78
  reflections of which {{ cif._reflns_number_total }} where independent and
79
- {{ '%0.1f'| format((cif._reflns_number_gt|float() / cif._reflns_number_total|float()) * 100) }} %
79
+ {% set gt = cif._reflns_number_gt | float %}
80
+ {% set total = cif._reflns_number_total | float %}
81
+ {% if total > 0 %}
82
+ {{ (gt / total * 100) | round(1) }} %
83
+ {% else %}
84
+ n/a
85
+ {% endif %}
80
86
  were greater than 2σ(<i>F</i><sup>2</sup>).<sup>[{{ literature.integration|ref_num }}]</sup> A
81
87
  {{ abstype }} absorption correction using {{ abs_details }} was applied.<sup>[{{ literature.absorption|ref_num }}]</sup>
82
88
  The structure was solved by
@@ -43,7 +43,7 @@ class PlatonRunner(QtCore.QObject):
43
43
  self.cif_file.with_suffix('.chk').unlink(missing_ok=True)
44
44
  self.process.start(self.platon_exe, ["-U", str(self.cif_file.name)])
45
45
 
46
- def _onfinished(self):
46
+ def _onfinished(self) -> None:
47
47
  self._on_ready_read()
48
48
  # os.chdir(self._origdir)
49
49
  self._parse_chk_file()
@@ -51,11 +51,11 @@ class PlatonRunner(QtCore.QObject):
51
51
  self.finished.emit(True)
52
52
  self.delete_orphaned_files()
53
53
 
54
- def _on_ready_read(self):
54
+ def _on_ready_read(self) -> None:
55
55
  output = self.process.readAllStandardOutput().data().decode()
56
56
  self.log_widget.appendPlainText(output)
57
57
 
58
- def _monitor_output_log(self):
58
+ def _monitor_output_log(self) -> None:
59
59
  while not self.is_stopped:
60
60
  self.tick.emit('#')
61
61
  time.sleep(0.3)
@@ -68,13 +68,13 @@ class PlatonRunner(QtCore.QObject):
68
68
  except FileNotFoundError:
69
69
  break
70
70
 
71
- def _stop_program(self):
71
+ def _stop_program(self) -> None:
72
72
  self.is_stopped = True
73
73
  # if self.process and self.process.state() == QProcess.Running:
74
74
  self.process.terminate()
75
75
  self.finished.emit(True)
76
76
 
77
- def _parse_chk_file(self):
77
+ def _parse_chk_file(self) -> None:
78
78
  try:
79
79
  self.chk_file_text = self.cif_file.with_suffix('.chk').read_text(encoding='latin1', errors='ignore')
80
80
  except FileNotFoundError as e:
@@ -89,6 +89,9 @@ class PlatonRunner(QtCore.QObject):
89
89
 
90
90
  @property
91
91
  def platon_exe(self) -> str:
92
+ special_platon = Path(__file__).resolve().parent.parent.parent.joinpath('platon/platon_special.exe')
93
+ if special_platon.exists():
94
+ return str(special_platon)
92
95
  if sys.platform.startswith('win'):
93
96
  in_pwt = r'C:\pwt\platon.exe'
94
97
  else:
@@ -103,10 +106,10 @@ class PlatonRunner(QtCore.QObject):
103
106
  def kill(self):
104
107
  if sys.platform.startswith('win'):
105
108
  with suppress(FileNotFoundError):
106
- subprocess.run(["taskkill", "/f", "/im", "platon.exe"], shell=False)
109
+ subprocess.run(["taskkill", "/f", "/im", "platon.exe"], check=False, shell=False)
107
110
  if sys.platform[:5] in ('linux', 'darwi'):
108
111
  with suppress(FileNotFoundError):
109
- subprocess.run(["killall", "platon"], shell=False)
112
+ subprocess.run(["killall", "platon"], check=False, shell=False)
110
113
 
111
114
  def delete_orphaned_files(self):
112
115
  # delete orphaned files:
@@ -1,5 +1,3 @@
1
- from typing import Union, Dict
2
-
3
1
  from finalcif.tools import chemparse
4
2
 
5
3