finalcif 170__tar.gz → 172__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 (201) hide show
  1. {finalcif-170 → finalcif-172}/.gitignore +2 -0
  2. {finalcif-170 → finalcif-172}/PKG-INFO +2 -2
  3. finalcif-172/finalcif/__init__.py +3 -0
  4. {finalcif-170 → finalcif-172}/finalcif/appwindow.py +107 -27
  5. {finalcif-170 → finalcif-172}/finalcif/cif/checkcif/checkcif.py +188 -4
  6. {finalcif-170 → finalcif-172}/finalcif/gui/dialogs.py +23 -1
  7. {finalcif-170 → finalcif-172}/finalcif/gui/import_selector.py +34 -21
  8. {finalcif-170 → finalcif-172}/finalcif/gui/import_selector_ui.py +4 -11
  9. {finalcif-170 → finalcif-172}/finalcif/gui/import_selector_ui.ui +2 -9
  10. {finalcif-170 → finalcif-172}/finalcif/report/templated_report.py +76 -1
  11. finalcif-172/finalcif/template/miew/LICENSES +23 -0
  12. finalcif-172/finalcif/template/miew/Miew.css +2 -0
  13. finalcif-172/finalcif/template/miew/Miew.min.js +12 -0
  14. finalcif-172/finalcif/template/miew/lodash.min.js +140 -0
  15. finalcif-172/finalcif/template/miew/three.js +7 -0
  16. finalcif-172/finalcif/template/report_fastmolwidget.tmpl +621 -0
  17. finalcif-172/finalcif/tools/mol_file_writer.py +164 -0
  18. {finalcif-170 → finalcif-172}/pyproject.toml +1 -1
  19. {finalcif-170 → finalcif-172}/uv.lock +86 -86
  20. finalcif-170/finalcif/__init__.py +0 -3
  21. finalcif-170/finalcif/displaymol/vtk_molecule.py +0 -116
  22. finalcif-170/finalcif/tools/__init__.py +0 -0
  23. {finalcif-170 → finalcif-172}/.gitattributes +0 -0
  24. {finalcif-170 → finalcif-172}/.githooks/pre-push +0 -0
  25. {finalcif-170 → finalcif-172}/.python-version +0 -0
  26. {finalcif-170 → finalcif-172}/.ruff.toml +0 -0
  27. {finalcif-170 → finalcif-172}/.run/Make Documentation MAC.run.xml +0 -0
  28. {finalcif-170 → finalcif-172}/.run/compile_ui_files.run.xml +0 -0
  29. {finalcif-170 → finalcif-172}/.run/make documentation Windows.run.xml +0 -0
  30. {finalcif-170 → finalcif-172}/AGENTS.md +0 -0
  31. {finalcif-170 → finalcif-172}/CODE_OF_CONDUCT.md +0 -0
  32. {finalcif-170 → finalcif-172}/CONTRIBUTING.md +0 -0
  33. {finalcif-170 → finalcif-172}/Finalcif_installer_win.spec +0 -0
  34. {finalcif-170 → finalcif-172}/Finalcif_linux_onefile.spec +0 -0
  35. {finalcif-170 → finalcif-172}/Finalcif_mac.spec +0 -0
  36. {finalcif-170 → finalcif-172}/LICENSE +0 -0
  37. {finalcif-170 → finalcif-172}/README.md +0 -0
  38. {finalcif-170 → finalcif-172}/finalcif/app_path.py +0 -0
  39. {finalcif-170 → finalcif-172}/finalcif/cif/__init__.py +0 -0
  40. {finalcif-170 → finalcif-172}/finalcif/cif/all_cif_dicts.py +0 -0
  41. {finalcif-170 → finalcif-172}/finalcif/cif/atoms.py +0 -0
  42. {finalcif-170 → finalcif-172}/finalcif/cif/checkcif/__init__.py +0 -0
  43. {finalcif-170 → finalcif-172}/finalcif/cif/cif_dict_foo.py +0 -0
  44. {finalcif-170 → finalcif-172}/finalcif/cif/cif_file_io.py +0 -0
  45. {finalcif-170 → finalcif-172}/finalcif/cif/cif_keyword_mapping.py +0 -0
  46. {finalcif-170 → finalcif-172}/finalcif/cif/cif_order.py +0 -0
  47. {finalcif-170 → finalcif-172}/finalcif/cif/cod/__init__.py +0 -0
  48. {finalcif-170 → finalcif-172}/finalcif/cif/cod/deposit.py +0 -0
  49. {finalcif-170 → finalcif-172}/finalcif/cif/cod/deposit_check.py +0 -0
  50. {finalcif-170 → finalcif-172}/finalcif/cif/cod/deposition_list.py +0 -0
  51. {finalcif-170 → finalcif-172}/finalcif/cif/cod/doi.py +0 -0
  52. {finalcif-170 → finalcif-172}/finalcif/cif/cod/upload.py +0 -0
  53. {finalcif-170 → finalcif-172}/finalcif/cif/cod/website_parser.py +0 -0
  54. {finalcif-170 → finalcif-172}/finalcif/cif/core_dict.py +0 -0
  55. {finalcif-170 → finalcif-172}/finalcif/cif/hkl.py +0 -0
  56. {finalcif-170 → finalcif-172}/finalcif/cif/modulation_dict.py +0 -0
  57. {finalcif-170 → finalcif-172}/finalcif/cif/powder_dict.py +0 -0
  58. {finalcif-170 → finalcif-172}/finalcif/cif/reference.py +0 -0
  59. {finalcif-170 → finalcif-172}/finalcif/cif/restraints_dict.py +0 -0
  60. {finalcif-170 → finalcif-172}/finalcif/cif/text.py +0 -0
  61. {finalcif-170 → finalcif-172}/finalcif/cif/twin_dict.py +0 -0
  62. {finalcif-170 → finalcif-172}/finalcif/cif/vrf_entry.py +0 -0
  63. {finalcif-170 → finalcif-172}/finalcif/ciforder/__init__.py +0 -0
  64. {finalcif-170 → finalcif-172}/finalcif/ciforder/order.py +0 -0
  65. {finalcif-170 → finalcif-172}/finalcif/ciforder/order_ui.py +0 -0
  66. {finalcif-170 → finalcif-172}/finalcif/ciforder/order_ui.ui +0 -0
  67. {finalcif-170 → finalcif-172}/finalcif/datafiles/__init__.py +0 -0
  68. {finalcif-170 → finalcif-172}/finalcif/datafiles/bruker_data.py +0 -0
  69. {finalcif-170 → finalcif-172}/finalcif/datafiles/bruker_frame.py +0 -0
  70. {finalcif-170 → finalcif-172}/finalcif/datafiles/ccdc_mail.py +0 -0
  71. {finalcif-170 → finalcif-172}/finalcif/datafiles/data.py +0 -0
  72. {finalcif-170 → finalcif-172}/finalcif/datafiles/p4p_reader.py +0 -0
  73. {finalcif-170 → finalcif-172}/finalcif/datafiles/sadabs.py +0 -0
  74. {finalcif-170 → finalcif-172}/finalcif/datafiles/saint.py +0 -0
  75. {finalcif-170 → finalcif-172}/finalcif/datafiles/shelx_lst.py +0 -0
  76. {finalcif-170 → finalcif-172}/finalcif/datafiles/utils.py +0 -0
  77. {finalcif-170/finalcif/displaymol → finalcif-172/finalcif/equip_property}/__init__.py +0 -0
  78. {finalcif-170 → finalcif-172}/finalcif/equip_property/author_loop_templates.py +0 -0
  79. {finalcif-170 → finalcif-172}/finalcif/equip_property/equipment.py +0 -0
  80. {finalcif-170 → finalcif-172}/finalcif/equip_property/properties.py +0 -0
  81. {finalcif-170 → finalcif-172}/finalcif/equip_property/tools.py +0 -0
  82. {finalcif-170 → finalcif-172}/finalcif/finalcif_start.py +0 -0
  83. {finalcif-170/finalcif/equip_property → finalcif-172/finalcif/gui}/__init__.py +0 -0
  84. {finalcif-170 → finalcif-172}/finalcif/gui/block_combobox.py +0 -0
  85. {finalcif-170 → finalcif-172}/finalcif/gui/checkcif_browser.py +0 -0
  86. {finalcif-170 → finalcif-172}/finalcif/gui/cif_table_model.py +0 -0
  87. {finalcif-170 → finalcif-172}/finalcif/gui/cif_table_view.py +0 -0
  88. {finalcif-170 → finalcif-172}/finalcif/gui/combobox.py +0 -0
  89. {finalcif-170 → finalcif-172}/finalcif/gui/custom_classes.py +0 -0
  90. {finalcif-170 → finalcif-172}/finalcif/gui/edit_button.py +0 -0
  91. {finalcif-170 → finalcif-172}/finalcif/gui/equipmenttable.py +0 -0
  92. {finalcif-170 → finalcif-172}/finalcif/gui/file_editor.py +0 -0
  93. {finalcif-170 → finalcif-172}/finalcif/gui/finalcif_gui_ui.py +0 -0
  94. {finalcif-170 → finalcif-172}/finalcif/gui/finalcif_gui_ui.ui +0 -0
  95. {finalcif-170 → finalcif-172}/finalcif/gui/loop_creator.py +0 -0
  96. {finalcif-170 → finalcif-172}/finalcif/gui/loop_creator_ui.py +0 -0
  97. {finalcif-170 → finalcif-172}/finalcif/gui/loop_creator_ui.ui +0 -0
  98. {finalcif-170 → finalcif-172}/finalcif/gui/loops.py +0 -0
  99. {finalcif-170 → finalcif-172}/finalcif/gui/mainstackwidget.py +0 -0
  100. {finalcif-170 → finalcif-172}/finalcif/gui/mixins.py +0 -0
  101. {finalcif-170 → finalcif-172}/finalcif/gui/new_key_dialog.py +0 -0
  102. {finalcif-170 → finalcif-172}/finalcif/gui/new_key_dialog_ui.py +0 -0
  103. {finalcif-170 → finalcif-172}/finalcif/gui/new_key_dialog_ui.ui +0 -0
  104. {finalcif-170 → finalcif-172}/finalcif/gui/plaintextedit.py +0 -0
  105. {finalcif-170 → finalcif-172}/finalcif/gui/playground.py +0 -0
  106. {finalcif-170 → finalcif-172}/finalcif/gui/propertytable.py +0 -0
  107. {finalcif-170 → finalcif-172}/finalcif/gui/shelx_navigation.py +0 -0
  108. {finalcif-170 → finalcif-172}/finalcif/gui/single_line_textedit.py +0 -0
  109. {finalcif-170 → finalcif-172}/finalcif/gui/spell_check_edit.py +0 -0
  110. {finalcif-170 → finalcif-172}/finalcif/gui/squeeze_dialog.py +0 -0
  111. {finalcif-170 → finalcif-172}/finalcif/gui/syntax_highlighter.py +0 -0
  112. {finalcif-170 → finalcif-172}/finalcif/gui/text_templates_ui.py +0 -0
  113. {finalcif-170 → finalcif-172}/finalcif/gui/text_templates_ui.ui +0 -0
  114. {finalcif-170 → finalcif-172}/finalcif/gui/text_value_editor.py +0 -0
  115. {finalcif-170 → finalcif-172}/finalcif/gui/timers.py +0 -0
  116. {finalcif-170 → finalcif-172}/finalcif/gui/validators.py +0 -0
  117. {finalcif-170 → finalcif-172}/finalcif/gui/vrf_classes.py +0 -0
  118. {finalcif-170 → finalcif-172}/finalcif/gui/vzs_viewer.py +0 -0
  119. {finalcif-170/finalcif/gui → finalcif-172/finalcif/icon}/__init__.py +0 -0
  120. {finalcif-170 → finalcif-172}/finalcif/icon/finalcif.png +0 -0
  121. {finalcif-170 → finalcif-172}/finalcif/icon/finalcif2.ico +0 -0
  122. {finalcif-170 → finalcif-172}/finalcif/icon/finalcif2.png +0 -0
  123. {finalcif-170 → finalcif-172}/finalcif/icon/multitable.ico +0 -0
  124. {finalcif-170 → finalcif-172}/finalcif/icon/multitable.png +0 -0
  125. {finalcif-170 → finalcif-172}/finalcif/icon/multitable.xcf +0 -0
  126. {finalcif-170 → finalcif-172}/finalcif/report/__init__.py +0 -0
  127. {finalcif-170 → finalcif-172}/finalcif/report/archive_report.py +0 -0
  128. {finalcif-170/finalcif/icon → finalcif-172/finalcif/report/gui}/__init__.py +0 -0
  129. {finalcif-170 → finalcif-172}/finalcif/report/gui/mainwindow.py +0 -0
  130. {finalcif-170 → finalcif-172}/finalcif/report/gui/mainwindow.ui +0 -0
  131. {finalcif-170 → finalcif-172}/finalcif/report/mtools.py +0 -0
  132. {finalcif-170 → finalcif-172}/finalcif/report/references.py +0 -0
  133. {finalcif-170 → finalcif-172}/finalcif/report/report_text.py +0 -0
  134. {finalcif-170 → finalcif-172}/finalcif/report/symm.py +0 -0
  135. {finalcif-170 → finalcif-172}/finalcif/report/tables.py +0 -0
  136. {finalcif-170/finalcif/report/gui → finalcif-172/finalcif/template}/__init__.py +0 -0
  137. {finalcif-170 → finalcif-172}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
  138. {finalcif-170 → finalcif-172}/finalcif/template/machines/D8_VENTURE.cif +0 -0
  139. {finalcif-170 → finalcif-172}/finalcif/template/mathml2omml.xsl +0 -0
  140. {finalcif-170 → finalcif-172}/finalcif/template/report.tex +0 -0
  141. {finalcif-170 → finalcif-172}/finalcif/template/report.tmpl +0 -0
  142. {finalcif-170 → finalcif-172}/finalcif/template/report2.tex +0 -0
  143. {finalcif-170 → finalcif-172}/finalcif/template/report_default.docx +0 -0
  144. {finalcif-170 → finalcif-172}/finalcif/template/template1.docx +0 -0
  145. {finalcif-170 → finalcif-172}/finalcif/template/template_for_multitable.docx +0 -0
  146. {finalcif-170 → finalcif-172}/finalcif/template/template_text.docx +0 -0
  147. {finalcif-170 → finalcif-172}/finalcif/template/template_without_text.docx +0 -0
  148. {finalcif-170 → finalcif-172}/finalcif/template/templates.py +0 -0
  149. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/FakePlasTeX/FakeContext.py +0 -0
  150. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/FakePlasTeX/FakeTokenizer.py +0 -0
  151. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/FakePlasTeX/README.md +0 -0
  152. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/FakePlasTeX/TokenizerPassThru.py +0 -0
  153. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/LICENSE +0 -0
  154. {finalcif-170/finalcif/template → finalcif-172/finalcif/template/unicode2latex}/__init__.py +0 -0
  155. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/u2l.py +0 -0
  156. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/unicode-math-table.tex +0 -0
  157. {finalcif-170 → finalcif-172}/finalcif/template/unicode2latex/unicode-math-xetex.sty +0 -0
  158. {finalcif-170 → finalcif-172}/finalcif/template/xsl/convert.py +0 -0
  159. {finalcif-170 → finalcif-172}/finalcif/template/xsl/xmltohtml.xsl +0 -0
  160. {finalcif-170 → finalcif-172}/finalcif/template/xsl/xmltolatex.xsl +0 -0
  161. {finalcif-170/finalcif/template/unicode2latex → finalcif-172/finalcif/tools}/__init__.py +0 -0
  162. {finalcif-170 → finalcif-172}/finalcif/tools/chemparse.py +0 -0
  163. {finalcif-170 → finalcif-172}/finalcif/tools/download.py +0 -0
  164. {finalcif-170 → finalcif-172}/finalcif/tools/dsrmath.py +0 -0
  165. {finalcif-170 → finalcif-172}/finalcif/tools/formal_charge.py +0 -0
  166. {finalcif-170 → finalcif-172}/finalcif/tools/misc.py +0 -0
  167. {finalcif-170 → finalcif-172}/finalcif/tools/options.py +0 -0
  168. {finalcif-170 → finalcif-172}/finalcif/tools/platon.py +0 -0
  169. {finalcif-170 → finalcif-172}/finalcif/tools/pupdate.py +0 -0
  170. {finalcif-170 → finalcif-172}/finalcif/tools/selfupdate.py +0 -0
  171. {finalcif-170 → finalcif-172}/finalcif/tools/settings.py +0 -0
  172. {finalcif-170 → finalcif-172}/finalcif/tools/shred.py +0 -0
  173. {finalcif-170 → finalcif-172}/finalcif/tools/space_groups.py +0 -0
  174. {finalcif-170 → finalcif-172}/finalcif/tools/spgr_format.py +0 -0
  175. {finalcif-170 → finalcif-172}/finalcif/tools/squeeze.py +0 -0
  176. {finalcif-170 → finalcif-172}/finalcif/tools/statusbar.py +0 -0
  177. {finalcif-170 → finalcif-172}/finalcif/tools/sumformula.py +0 -0
  178. {finalcif-170 → finalcif-172}/finalcif/tools/z_from_packing.py +0 -0
  179. {finalcif-170 → finalcif-172}/finalcif.exe +0 -0
  180. {finalcif-170 → finalcif-172}/platon/README.txt +0 -0
  181. {finalcif-170 → finalcif-172}/platon/compile_platon.bat +0 -0
  182. {finalcif-170 → finalcif-172}/platon/platon_special.exe +0 -0
  183. {finalcif-170 → finalcif-172}/platon/platon_special.f +0 -0
  184. {finalcif-170 → finalcif-172}/pylock.toml +0 -0
  185. {finalcif-170 → finalcif-172}/run_and_capture.py +0 -0
  186. {finalcif-170 → finalcif-172}/run_finalcif +0 -0
  187. {finalcif-170 → finalcif-172}/run_finalcif.bat +0 -0
  188. {finalcif-170 → finalcif-172}/screenshots/finalcif_checkcif.png +0 -0
  189. {finalcif-170 → finalcif-172}/screenshots/finalcif_details.png +0 -0
  190. {finalcif-170 → finalcif-172}/screenshots/finalcif_loops.png +0 -0
  191. {finalcif-170 → finalcif-172}/screenshots/finalcif_loops2.png +0 -0
  192. {finalcif-170 → finalcif-172}/screenshots/finalcif_main.png +0 -0
  193. {finalcif-170 → finalcif-172}/screenshots/finalcif_report.png +0 -0
  194. {finalcif-170 → finalcif-172}/screenshots/finalcif_responses.png +0 -0
  195. {finalcif-170 → finalcif-172}/screenshots/loops_column_buttons.png +0 -0
  196. {finalcif-170 → finalcif-172}/work/compare_z_platon.py +0 -0
  197. {finalcif-170 → finalcif-172}/work/diagnose_1519506.py +0 -0
  198. {finalcif-170 → finalcif-172}/work/diagnose_z.py +0 -0
  199. {finalcif-170 → finalcif-172}/work/investigate_failures.py +0 -0
  200. {finalcif-170 → finalcif-172}/work/validate_cod.py +0 -0
  201. {finalcif-170 → finalcif-172}/work/validate_zprime.py +0 -0
@@ -241,3 +241,5 @@ finalcif_keys_order.cif
241
241
  /work/dicts/cif_ms.dic
242
242
  /work/dicts/cif_pow.dic
243
243
  /work/dicts/ddl.dic
244
+ /test-data/4060314-finalcif.zip
245
+ /test-data/report_4060314-finalcif.html
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finalcif
3
- Version: 170
3
+ Version: 172
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
@@ -17,7 +17,7 @@ Requires-Dist: chardet
17
17
  Requires-Dist: charset-normalizer
18
18
  Requires-Dist: docxcompose==2.2.0
19
19
  Requires-Dist: docxtpl[subdoc]>=0.20.2
20
- Requires-Dist: fastmolwidget>=0.9.7
20
+ Requires-Dist: fastmolwidget>=1.0.0
21
21
  Requires-Dist: gemmi>=0.7.5
22
22
  Requires-Dist: html2text
23
23
  Requires-Dist: lxml>=6.1.1
@@ -0,0 +1,3 @@
1
+
2
+ VERSION = 172
3
+ __version__ = VERSION
@@ -14,18 +14,19 @@ import sys
14
14
  import threading
15
15
  import time
16
16
  import warnings
17
+ from collections.abc import Callable
17
18
  from contextlib import suppress
18
19
  from datetime import datetime
19
20
  from math import sin, radians
20
21
  from pathlib import Path, WindowsPath
21
22
  from typing import cast, TYPE_CHECKING
22
23
 
24
+
23
25
  if TYPE_CHECKING:
24
26
  from finalcif.cif.checkcif.checkcif import MyHTMLParser
25
27
 
26
28
  from fastmolwidget import Atomtuple
27
29
  from fastmolwidget.part_combo import PartFilterWidget
28
- from fastmolwidget.sdm import SDM
29
30
 
30
31
  from finalcif.gui.shelx_navigation import find_shelx_line_for_atom, scroll_and_highlight_shelx_atom
31
32
  from finalcif.gui.syntax_highlighter import ShelxSyntaxHighlighter
@@ -60,7 +61,7 @@ from finalcif.gui.custom_classes import Column, light_green, yellow, light_blue,
60
61
  white, light_red, MyTableWidgetItem, blue
61
62
  from finalcif.gui.cif_table_model import CifRowData
62
63
  from finalcif.gui.dialogs import show_update_warning, unable_to_open_message, show_general_warning, \
63
- cif_file_open_dialog, \
64
+ cif_file_open_dialog, fcf_file_open_dialog, \
64
65
  bad_z_message, show_res_checksum_warning, show_hkl_checksum_warning, cif_file_save_dialog, show_yes_now_question, \
65
66
  video_file_open_dialog
66
67
  from finalcif.gui.finalcif_gui_ui import Ui_FinalCifWindow
@@ -76,6 +77,7 @@ from finalcif.gui.vrf_classes import MyVRFContainer
76
77
  from finalcif.template.templates import ReportTemplates
77
78
  from finalcif.tools.download import MyDownloader
78
79
  from finalcif.tools.dsrmath import my_isnumeric
80
+ from finalcif.tools.mol_file_writer import grown_atoms
79
81
  from finalcif.tools.misc import (next_path, celltxt, to_float, is_database_number,
80
82
  open_file, strip_finalcif_of_name, file_age_in_days, open_in_text_editor,
81
83
  running_inside_unit_test as is_unit_test)
@@ -163,8 +165,10 @@ class AppWindow(QMainWindow):
163
165
  super().__init__()
164
166
  self.thread_download = None
165
167
  self.ckf = None
168
+ self.htmlfile: Path | None = None
169
+ self.pdf_htmlfile: Path | None = None
166
170
  self.thread_version = None
167
- self.worker = None
171
+ self._downloaders: set[MyDownloader] = set()
168
172
  self._z_thread: _ZEstimatorThread | None = None
169
173
  self.structure_factor_thread: _StructureFactorReportThread | None = None
170
174
  self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
@@ -989,11 +993,36 @@ class AppWindow(QMainWindow):
989
993
  print('Skipping version.txt download because NO_NETWORK variable is set.')
990
994
  return
991
995
  mainurl = "https://dkratzert.de/files/finalcif/version.txt"
992
- # parent must be None, otherwise it can't be moved to a thread:
993
- self.worker = MyDownloader(mainurl, parent=None)
994
- self.worker.loaded.connect(self.is_update_necessary)
995
- self.thread_version = threading.Thread(target=self.worker.download, daemon=True)
996
- self.thread_version.start()
996
+ self.thread_version = self._start_download(mainurl, self.is_update_necessary)
997
+
998
+ def _start_download(self, url: str, on_loaded: Callable[[bytes], None]) -> threading.Thread:
999
+ """Download `url` in a daemon thread and hand the content to `on_loaded`.
1000
+
1001
+ The worker must not have a parent, otherwise it could not be moved to a thread, so it
1002
+ is kept in `_downloaders` until it is done. Without that reference it would be
1003
+ garbage collected as soon as the next download starts, and its queued signals would
1004
+ never reach the main thread.
1005
+ """
1006
+ worker = MyDownloader(url, parent=None)
1007
+ self._downloaders.add(worker)
1008
+ worker.loaded.connect(on_loaded)
1009
+ worker.failed.connect(self._download_failed)
1010
+ worker.finished.connect(self._download_finished)
1011
+ thread = threading.Thread(target=worker.download, daemon=True)
1012
+ thread.start()
1013
+ return thread
1014
+
1015
+ def _download_finished(self) -> None:
1016
+ # These slots run in the main thread, so 'loaded' was already delivered here:
1017
+ self._forget_downloader(cast(MyDownloader, self.sender()))
1018
+
1019
+ def _download_failed(self, status_code: int) -> None:
1020
+ worker = cast(MyDownloader, self.sender())
1021
+ print(f'Download of {worker.url} failed with status code {status_code}.')
1022
+ self._forget_downloader(worker)
1023
+
1024
+ def _forget_downloader(self, worker: MyDownloader) -> None:
1025
+ self._downloaders = {each for each in self._downloaders if each is not worker}
997
1026
 
998
1027
  def is_update_necessary(self, content: bytes) -> None:
999
1028
  """
@@ -1060,10 +1089,7 @@ class AppWindow(QMainWindow):
1060
1089
  print('Skipping check.def download because NO_NETWORK variable is set.')
1061
1090
  return
1062
1091
  url = 'http://www.platonsoft.nl/xraysoft/unix/platon/check.def'
1063
- self.worker = MyDownloader(url, parent=None)
1064
- self.worker.loaded.connect(self._save_checkdef)
1065
- self.thread_download = threading.Thread(target=self.worker.download, daemon=True)
1066
- self.thread_download.start()
1092
+ self.thread_download = self._start_download(url, self._save_checkdef)
1067
1093
 
1068
1094
  def _save_checkdef(self, reply: bytes) -> None:
1069
1095
  """
@@ -1150,6 +1176,53 @@ class AppWindow(QMainWindow):
1150
1176
  def _checkcif_failed(self, txt: str) -> None:
1151
1177
  self.ui.CheckCifLogPlainTextEdit.appendHtml(f'<b>{txt}</b>')
1152
1178
 
1179
+ def _log_checkcif(self, txt: str) -> None:
1180
+ self.ui.CheckCifLogPlainTextEdit.appendPlainText(txt)
1181
+ app.processEvents()
1182
+
1183
+ def _resolve_structure_factor_upload(self, html_file: Path) -> None:
1184
+ """
1185
+ Answers the CheckCIF page that asks for an explicit upload of the structure factor file.
1186
+
1187
+ The structure factor file belonging to the current CIF is searched next to it. If no
1188
+ matching file is found, the user is asked for one. When the user cancels the dialog, the
1189
+ check is continued without the remaining structure factors.
1190
+ """
1191
+ from finalcif.cif.checkcif.checkcif import (fix_iucr_urls, needs_structure_factor_upload,
1192
+ submit_without_structure_factors, upload_structure_factors)
1193
+ try:
1194
+ html = html_file.read_text('utf-8', 'ignore')
1195
+ except OSError:
1196
+ return
1197
+ if not needs_structure_factor_upload(html):
1198
+ return
1199
+ self.ui.CheckCifLogPlainTextEdit.appendHtml(
1200
+ '<b>CheckCIF requests the structure factor file for this CIF.</b>')
1201
+ app.processEvents()
1202
+ fcf_file = self._get_structure_factor_file()
1203
+ if fcf_file:
1204
+ new_html = upload_structure_factors(response_html=html, fcf_file=fcf_file,
1205
+ url=self.options.checkcif_url, progress=self._log_checkcif)
1206
+ else:
1207
+ self._log_checkcif('No structure factor file selected. '
1208
+ 'Continuing without the remaining structure factors.')
1209
+ new_html = submit_without_structure_factors(response_html=html, url=self.options.checkcif_url,
1210
+ progress=self._log_checkcif)
1211
+ if new_html == html:
1212
+ return
1213
+ with suppress(OSError):
1214
+ html_file.write_text(fix_iucr_urls(new_html), encoding='utf-8')
1215
+
1216
+ def _get_structure_factor_file(self) -> Path | None:
1217
+ from finalcif.cif.checkcif.checkcif import find_matching_fcf_file
1218
+ fcf_file = find_matching_fcf_file(self.cif.finalcif_file)
1219
+ if fcf_file:
1220
+ self._log_checkcif(f'Found structure factor file {fcf_file.name}.')
1221
+ return fcf_file
1222
+ self._log_checkcif('No structure factor file found for the current CIF. Please select one.')
1223
+ filename = fcf_file_open_dialog(parent=self, last_dir=str(self.cif.finalcif_file.parent))
1224
+ return Path(filename) if filename else None
1225
+
1153
1226
  def _ckf_progress(self, txt: str) -> None:
1154
1227
  self.ui.CheckCifLogPlainTextEdit.appendPlainText(txt)
1155
1228
 
@@ -1160,6 +1233,9 @@ class AppWindow(QMainWindow):
1160
1233
  from finalcif.cif.checkcif.checkcif import MyHTMLParser, AlertHelp
1161
1234
  self.ui.CheckcifHTMLOnlineButton.setEnabled(True)
1162
1235
  self.ui.CheckcifPDFOnlineButton.setEnabled(True)
1236
+
1237
+ self._resolve_structure_factor_upload(self.htmlfile)
1238
+
1163
1239
  try:
1164
1240
  parser = MyHTMLParser(self.htmlfile.read_text())
1165
1241
  except FileNotFoundError:
@@ -1167,22 +1243,27 @@ class AppWindow(QMainWindow):
1167
1243
  self.ui.CheckCifLogPlainTextEdit.appendHtml('<b>CheckCIF failed to finish. '
1168
1244
  'Please try it at https://checkcif.iucr.org/ instead.</b>')
1169
1245
  return
1246
+
1170
1247
  self.checkcif_browser = CheckCifBrowser(parent=self, checkdef=self.checkdef)
1171
1248
  self.ui.htmlCHeckCifGridLayout.addWidget(self.checkcif_browser)
1172
1249
  self.ui.MainStackedWidget.go_to_checkcif_page()
1173
1250
  self.ui.CheckCIFResultsTabWidget.setCurrentIndex(1) # Index 1 is html page
1251
+
1174
1252
  # Save the structure image locally and point the HTML to it. The remote image URL
1175
1253
  # is session-specific and expires, so the locally saved copy is used for display.
1176
1254
  gif_file = self.cif.finalcif_file.with_suffix('.gif')
1177
1255
  parser.save_image(gif_file)
1256
+
1178
1257
  try:
1179
1258
  html = self.htmlfile.resolve().read_text('utf-8', 'ignore')
1180
1259
  local_images = {parser.imageurl: gif_file} if parser.imageurl else {}
1181
1260
  self.checkcif_browser.set_checkcif_html(html, local_images)
1182
1261
  except FileNotFoundError:
1183
1262
  print(f'{self.htmlfile} not found')
1263
+
1184
1264
  self._display_structure_factor_report(parser)
1185
1265
  self.ui.CheckCifLogPlainTextEdit.appendPlainText('CheckCIF Report finished.')
1266
+
1186
1267
  html_entries = parser.response_forms
1187
1268
  # Merge HTML-parsed entries into the main table:
1188
1269
  # - Entries already shown in the main table (source='cif') get their source badge
@@ -1192,6 +1273,7 @@ class AppWindow(QMainWindow):
1192
1273
  # the main table, marked 'checkcif'.
1193
1274
  a = AlertHelp(self.checkdef)
1194
1275
  table = self.ui.cif_main_table
1276
+
1195
1277
  # Build a lookup of already-saved CIF VRF entries once, outside the loop.
1196
1278
  existing_by_key = {e.key: e for e in self.cif.get_vrf_entries()}
1197
1279
  for entry in html_entries:
@@ -1216,6 +1298,7 @@ class AppWindow(QMainWindow):
1216
1298
  vrf.template_requested.connect(self._on_vrf_template_requested)
1217
1299
  self.validation_response_forms_list.append(vrf)
1218
1300
  table.place_vrf_widget(insert_pos, vrf)
1301
+
1219
1302
  if html_entries:
1220
1303
  n = len(html_entries)
1221
1304
  form_noun = 'form' if n == 1 else 'forms'
@@ -1309,6 +1392,14 @@ class AppWindow(QMainWindow):
1309
1392
  def _pdf_checkcif_finished(self) -> None:
1310
1393
  self.ui.CheckcifPDFOnlineButton.setEnabled(True)
1311
1394
  self.ui.CheckcifHTMLOnlineButton.setEnabled(True)
1395
+ if self.pdf_htmlfile is None:
1396
+ return
1397
+ self._resolve_structure_factor_upload(self.pdf_htmlfile)
1398
+ self.ckf.show_pdf_report()
1399
+ self.ui.CheckCifLogPlainTextEdit.appendPlainText('PDF Checkcif report finished.')
1400
+ with suppress(OSError):
1401
+ self.pdf_htmlfile.unlink(missing_ok=True)
1402
+ self.pdf_htmlfile = None
1312
1403
 
1313
1404
  def _on_vrf_deleted(self, vrf_widget) -> None:
1314
1405
  """
@@ -1360,14 +1451,14 @@ class AppWindow(QMainWindow):
1360
1451
  self.ui.CheckCifLogPlainTextEdit.appendHtml('<b>Unable to save CIF file. Aborting action...</b>')
1361
1452
  return None
1362
1453
  self.load_cif_file(self.cif.finalcif_file, current_block, load_changes=False)
1363
- htmlfile = self.cif.finalcif_file_prefixed(prefix='checkpdf-', suffix='.html')
1454
+ self.pdf_htmlfile = self.cif.finalcif_file_prefixed(prefix='checkpdf-', suffix='.html')
1364
1455
  try:
1365
- htmlfile.unlink()
1456
+ self.pdf_htmlfile.unlink()
1366
1457
  except (FileNotFoundError, PermissionError):
1367
1458
  pass
1368
1459
  self.ui.CheckCifLogPlainTextEdit.appendPlainText(
1369
1460
  f'Sending pdf report request to {self.options.checkcif_url} ...')
1370
- self.ckf = CheckCif(parent=self, cif=self.cif, outfile=htmlfile,
1461
+ self.ckf = CheckCif(parent=self, cif=self.cif, outfile=self.pdf_htmlfile,
1371
1462
  hkl_upload=(not self.ui.structfactCheckBox.isChecked()),
1372
1463
  pdf=True, url=self.options.checkcif_url,
1373
1464
  full_iucr=self.ui.fullIucrCheckBox.isChecked(),
@@ -1379,11 +1470,6 @@ class AppWindow(QMainWindow):
1379
1470
  self.ui.CheckcifPDFOnlineButton.setDisabled(True)
1380
1471
  self.ui.CheckcifHTMLOnlineButton.setDisabled(True)
1381
1472
  self.ckf.start()
1382
- self.ui.CheckCifLogPlainTextEdit.appendPlainText('PDF Checkcif report finished.')
1383
- try:
1384
- htmlfile.unlink()
1385
- except (FileNotFoundError, PermissionError):
1386
- pass
1387
1473
 
1388
1474
  def do_offline_checkcif(self) -> None:
1389
1475
  self.ui.CheckcifButton.setDisabled(True)
@@ -2342,13 +2428,7 @@ class AppWindow(QMainWindow):
2342
2428
 
2343
2429
  def _calc_grown_atoms(self):
2344
2430
  """Helper to generate the symmetry expanded atoms without drawing them."""
2345
- atoms_fract = tuple(self.cif.atoms_fract)
2346
- if not atoms_fract:
2347
- return []
2348
-
2349
- sdm = SDM(atoms_fract, self.cif.symmops, self.cif.cell[:6], centric=self.cif.is_centrosymm)
2350
- needsymm = sdm.calc_sdm()
2351
- return sdm.packer(sdm, needsymm)
2431
+ return grown_atoms(self.cif)
2352
2432
 
2353
2433
  def get_adps(self) -> dict:
2354
2434
  adp_dict = {}
@@ -5,13 +5,17 @@
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
+ from __future__ import annotations
9
+
8
10
  import re
9
11
  import subprocess
10
12
  import sys
11
13
  import time
14
+ from collections.abc import Callable
12
15
  from contextlib import suppress
13
16
  from html.parser import HTMLParser
14
17
  from pathlib import Path
18
+ from urllib.parse import urljoin
15
19
 
16
20
  import requests
17
21
  from qtpy.QtCore import QThread, Signal
@@ -21,6 +25,189 @@ from requests.exceptions import MissingSchema
21
25
  from finalcif.cif.cif_file_io import CifContainer
22
26
  from finalcif.cif.vrf_entry import VRFEntry
23
27
  from finalcif.gui.dialogs import show_general_warning
28
+ from finalcif.tools.misc import strip_finalcif_of_name
29
+
30
+ #: Text of the intermediate CheckCIF page that asks for an explicit upload of the
31
+ #: structure factor file.
32
+ STRUCTURE_FACTOR_REQUEST_TEXT = 'File name of structure factor file'
33
+ #: Name of the submit button of the form that continues without structure factors.
34
+ SUBMIT_WITHOUT_HKL_NAME = 'Qsubmitnow'
35
+ #: Maximum difference in seconds between the modification time of a CIF file and a
36
+ #: structure factor file in order to regard them as belonging together.
37
+ MAX_FCF_TIME_DIFFERENCE = 3600.0
38
+
39
+
40
+ class HtmlForm:
41
+ """A single HTML form of the CheckCIF intermediate page."""
42
+
43
+ def __init__(self, action: str = '') -> None:
44
+ self.action = action
45
+ self.hidden_data: dict[str, str] = {}
46
+ self.submit_data: dict[str, str] = {}
47
+ self.file_input_name: str = ''
48
+
49
+ @property
50
+ def payload(self) -> dict[str, str]:
51
+ return {**self.hidden_data, **self.submit_data}
52
+
53
+ def url(self, base_url: str) -> str:
54
+ return urljoin(base_url, self.action) if self.action else base_url
55
+
56
+
57
+ class CheckCifFormParser(HTMLParser):
58
+ """
59
+ Extracts the forms of the CheckCIF page that asks for a structure factor upload.
60
+ """
61
+
62
+ def __init__(self, html: str = '') -> None:
63
+ super().__init__()
64
+ self.forms: list[HtmlForm] = []
65
+ self._current_form: HtmlForm | None = None
66
+ if html:
67
+ self.feed(html)
68
+
69
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
70
+ attrs_dict = {key: value or '' for key, value in attrs}
71
+ if tag == 'form':
72
+ self._current_form = HtmlForm(action=attrs_dict.get('action', ''))
73
+ self.forms.append(self._current_form)
74
+ elif tag == 'input' and self._current_form is not None:
75
+ self._add_input(attrs_dict)
76
+
77
+ def _add_input(self, attrs_dict: dict[str, str]) -> None:
78
+ name = attrs_dict.get('name')
79
+ if not name or self._current_form is None:
80
+ return
81
+ input_type = attrs_dict.get('type', '').lower()
82
+ value = attrs_dict.get('value', '')
83
+ if input_type == 'hidden':
84
+ self._current_form.hidden_data[name] = value
85
+ elif input_type == 'submit':
86
+ self._current_form.submit_data[name] = value
87
+ elif input_type == 'file':
88
+ self._current_form.file_input_name = name
89
+
90
+ def handle_endtag(self, tag: str) -> None:
91
+ if tag == 'form':
92
+ self._current_form = None
93
+
94
+ @property
95
+ def fcf_upload_form(self) -> HtmlForm | None:
96
+ """The form with a file input field for the structure factor file."""
97
+ for form in self.forms:
98
+ if form.file_input_name:
99
+ return form
100
+ return None
101
+
102
+ @property
103
+ def no_structure_factors_form(self) -> HtmlForm | None:
104
+ """The form that continues the check without the remaining structure factors."""
105
+ for form in self.forms:
106
+ if SUBMIT_WITHOUT_HKL_NAME in form.submit_data:
107
+ return form
108
+ for form in self.forms:
109
+ if not form.file_input_name:
110
+ return form
111
+ return None
112
+
113
+
114
+ def needs_structure_factor_upload(html: str) -> bool:
115
+ """Whether the CheckCIF server answered with the structure factor upload form."""
116
+ return STRUCTURE_FACTOR_REQUEST_TEXT in html
117
+
118
+
119
+ def _fcf_candidates(cif_file: Path) -> list[Path]:
120
+ """Possible structure factor files belonging to *cif_file*, without duplicates."""
121
+ stem_without_finalcif = Path(strip_finalcif_of_name(cif_file.stem, till_name_ends=True)).name
122
+ names = [f'{cif_file.stem}.fcf', f'{stem_without_finalcif}.fcf']
123
+ candidates = []
124
+ for name in names:
125
+ candidate = cif_file.parent / name
126
+ if candidate.exists() and candidate not in candidates:
127
+ candidates.append(candidate)
128
+ return candidates
129
+
130
+
131
+ def _reference_cif_file(cif_file: Path) -> Path:
132
+ """
133
+ The CIF file the structure factors should be compared against. This is the file
134
+ without the '-finalcif' suffix, because that is the file the current CIF was made from.
135
+ """
136
+ stem_without_finalcif = Path(strip_finalcif_of_name(cif_file.stem, till_name_ends=True)).name
137
+ source_cif = cif_file.parent / f'{stem_without_finalcif}.cif'
138
+ return source_cif if source_cif.exists() else cif_file
139
+
140
+
141
+ def find_matching_fcf_file(cif_file: Path,
142
+ max_time_difference: float = MAX_FCF_TIME_DIFFERENCE) -> Path | None:
143
+ """
144
+ Returns the structure factor file that was created together with *cif_file*.
145
+
146
+ Files named like the CIF with and without the '-finalcif' suffix are considered.
147
+ A file is only accepted if its modification time differs by less than
148
+ *max_time_difference* seconds from the modification time of the CIF file the
149
+ current CIF was made from.
150
+ """
151
+ reference_file = _reference_cif_file(cif_file)
152
+ with suppress(OSError):
153
+ reference_time = reference_file.stat().st_mtime
154
+ matches = []
155
+ for candidate in _fcf_candidates(cif_file):
156
+ difference = abs(candidate.stat().st_mtime - reference_time)
157
+ if difference < max_time_difference:
158
+ matches.append((difference, candidate))
159
+ if matches:
160
+ return min(matches)[1]
161
+ return None
162
+
163
+
164
+ def _post_form(form: HtmlForm, base_url: str, files: dict | None = None) -> str:
165
+ response = requests.post(form.url(base_url), data=form.payload, files=files, timeout=900)
166
+ response.raise_for_status()
167
+ return response.text
168
+
169
+
170
+ def upload_structure_factors(response_html: str, fcf_file: Path, url: str,
171
+ progress: Callable[[str], None] = print) -> str:
172
+ """
173
+ Answers the structure factor upload form of CheckCIF with *fcf_file*.
174
+
175
+ Returns the final report or the unchanged *response_html* if the upload failed.
176
+ """
177
+ form = CheckCifFormParser(response_html).fcf_upload_form
178
+ if form is None:
179
+ progress('Could not find the structure factor upload form of CheckCIF.')
180
+ return response_html
181
+ if not fcf_file.exists():
182
+ progress(f'Structure factor file {fcf_file.name} not found.')
183
+ return response_html
184
+ try:
185
+ with fcf_file.open('rb') as file_handle:
186
+ files = {form.file_input_name: (fcf_file.name, file_handle, 'application/octet-stream')}
187
+ progress(f'Uploading structure factors from {fcf_file.name} ...')
188
+ html = _post_form(form, url, files=files)
189
+ progress('Structure factor upload finished.')
190
+ return html
191
+ except (OSError, requests.exceptions.RequestException) as e:
192
+ progress(f'Structure factor upload failed: {e}')
193
+ return response_html
194
+
195
+
196
+ def submit_without_structure_factors(response_html: str, url: str,
197
+ progress: Callable[[str], None] = print) -> str:
198
+ """
199
+ Continues a CheckCIF run without the remaining structure factors.
200
+ """
201
+ form = CheckCifFormParser(response_html).no_structure_factors_form
202
+ if form is None:
203
+ progress('Could not find the CheckCIF form to continue without structure factors.')
204
+ return response_html
205
+ try:
206
+ progress('Requesting the report without the remaining structure factors ...')
207
+ return _post_form(form, url)
208
+ except requests.exceptions.RequestException as e:
209
+ progress(f'Request without structure factors failed: {e}')
210
+ return response_html
24
211
 
25
212
 
26
213
  class CheckCif(QThread):
@@ -92,10 +279,7 @@ class CheckCif(QThread):
92
279
  print('html checkcif result could not be written.')
93
280
  return
94
281
  with suppress(Exception):
95
- # parameter missing_ok=True is only available after 3.8
96
- Path('finalcif_checkcif_tmp.cif').unlink()
97
- if self.pdf:
98
- self.finished.connect(self._open_pdf_result)
282
+ Path('finalcif_checkcif_tmp.cif').unlink(missing_ok=True)
99
283
 
100
284
  def _do_the_server_request(self, headers: dict, temp_cif: bytes) -> Response | None:
101
285
  req = None
@@ -19,6 +19,8 @@ from finalcif.tools.selfupdate import ElevationRefused, UpdateCancelled, UpdateE
19
19
 
20
20
  # Keeps the running downloads alive, a local variable would be garbage collected:
21
21
  _running_updates: set = set()
22
+ # The same for a message box without a parent:
23
+ _open_message_boxes: set = set()
22
24
 
23
25
 
24
26
  class InstallerDownload:
@@ -334,7 +336,7 @@ def show_update_warning(parent: QWidget | None, remote_version: int = 0) -> None
334
336
  "https://dkratzert.de/finalcif.html</a>"
335
337
  box = QMessageBox(parent)
336
338
  box.setTextFormat(QtCore.Qt.TextFormat.AutoText)
337
- box.setWindowTitle(" ")
339
+ box.setWindowTitle("FinalCif update")
338
340
  box.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextBrowserInteraction)
339
341
  if sys.platform.startswith("win"):
340
342
  warn_text += r"<br><br>Updating now will end all running FinalCIF programs!"
@@ -342,7 +344,12 @@ def show_update_warning(parent: QWidget | None, remote_version: int = 0) -> None
342
344
  update_button.clicked.connect(lambda: do_update_program(str(remote_version), parent))
343
345
  box.setText(warn_text.format(remote_version))
344
346
  box.setModal(True)
347
+ _open_message_boxes.add(box)
348
+ box.finished.connect(lambda _: _open_message_boxes.discard(box))
345
349
  box.show()
350
+ # The main window is shown right after this box during startup and covers it otherwise:
351
+ box.raise_()
352
+ box.activateWindow()
346
353
 
347
354
 
348
355
  def bad_z_message(parent: QWidget | None, z: float) -> None:
@@ -409,6 +416,21 @@ def cif_file_save_dialog(filename: str, parent: QWidget | None = None) -> str:
409
416
  return filename
410
417
 
411
418
 
419
+ def fcf_file_open_dialog(parent: QWidget | None = None, last_dir: str = '', options=None) -> str:
420
+ """
421
+ Returns the name of a structure factor file from a file dialog.
422
+ """
423
+ file_filter = "Structure factor file (*.fcf)"
424
+ filename, _ = compat.getopenfilename(parent=parent,
425
+ caption='Select the structure factor (.fcf) file for this CIF',
426
+ basedir=last_dir,
427
+ filters=file_filter,
428
+ selectedfilter=file_filter,
429
+ options=options
430
+ )
431
+ return filename
432
+
433
+
412
434
  def video_file_open_dialog(parent: QWidget | None = None, filter: str = "Video file (*.vzs; *.jpg)", last_dir='',
413
435
  options=None) -> str:
414
436
  filename, _ = compat.getopenfilename(parent=parent,