From baa1e21cdb56f3a32305c9671e248b2afa0fee6c Mon Sep 17 00:00:00 2001 From: "Troy N. Smith" Date: Wed, 2 Sep 2026 21:02:02 -0600 Subject: [PATCH] Fixed bug with InChI not being populated by listener --- CHANGELOG.md | 4 +++ pixi.lock | 53 ++++++++++++++++++++++++--------------- src/autostorage/events.py | 30 ++++++++++++++-------- tests/test_events.py | 37 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f6b8a..87268e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed + +- **Identity auto-population event listeners** (`add_inchi_identities_before_flush`, `add_smiles_extras_before_flush`, `add_hill_extras_before_flush`): Now correctly handle `StationaryPointRow` objects created with relationship objects (`geometry=geo_row`) in addition to foreign key IDs (`geometry_id=geom.id`). Previously, the listeners only checked `geometry_id` and used `session.get()` to load the geometry, which failed when objects were created with relationships and added together without intermediate flushes (as in batch workflow patterns). The fix tries the relationship first, then falls back to FK-based loading, supporting both creation patterns. + ## [0.0.14] - 2026-09-01 ### Changed -- **Bump** `automol` to v0.0.23 to resolve dependency installations diff --git a/pixi.lock b/pixi.lock index be33dc3..f4332f3 100644 --- a/pixi.lock +++ b/pixi.lock @@ -169,7 +169,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.2-pyhd8ed1ab_0.conda - - conda_source: autostorage[3ad14945] @ . + - conda_source: autostorage[fae6965b] @ . - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/1b/291dd75b5ed494eb484801f39e6572192d322e6cd3d68fab8a6dde743c48/graphrc-1.3.7-py3-none-any.whl @@ -480,7 +480,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.6.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - - conda_source: autostorage[3ad14945] @ . + - conda_source: autostorage[fae6965b] @ . - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/06/b4f06ca7afb5d9e942c642980c308ffcfa1fa0e8b0a3ddbec78483ef1614/keepachangelog-2.0.0-py3-none-any.whl @@ -1847,18 +1847,18 @@ packages: - libpsl >=0.22.0,<0.23.0a0 size: 70092 timestamp: 1783936855082 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.14.7-hdc7f604_104_cp314.conda - build_number: 104 - sha256: ea102c446220e9b8ad2125e38e0f4a0d9b04a7fc10193eca8d130bee507ce9e2 - md5: eb63e79ef1ac24c034d5b6ab3bcbd00c +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.14.7-hdc7f604_106_cp314.conda + build_number: 106 + sha256: 5f879892bd439c3d94f4a97b06c214b9a19c4b92f74d84f0305c6ca1b2b239b2 + md5: 28af2158bb8dbb62e55b0af3b44f9d4a depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15 - libstdcxx >=15 license: Python-2.0 run_exports: {} - size: 10507939 - timestamp: 1788161778972 + size: 10520530 + timestamp: 1788383918299 - conda: https://conda.anaconda.org/conda-forge/linux-64/libraqm-0.10.5-h6406941_1.conda sha256: 7c9e562842f193f772ef0ba681f238a2d9e5ef637588b6e46eb30cc6aa1940c8 md5: fa63517815747363c41b439ff9301db1 @@ -2030,6 +2030,19 @@ packages: - libuuid >=2.42.2,<3.0a0 size: 40017 timestamp: 1781625522462 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.3-hcfc3c73_0.conda + sha256: aa58bbba56644ffd062a4a9b358782c5eb7560ccead2ab8f7c5c6ede0a7d33a6 + md5: 74a0a409d9f4561265d36b789d3f398a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libuuid >=2.42.3,<3.0a0 + size: 39998 + timestamp: 1788347719520 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_0.conda sha256: e28e4519223f78b3163599ca89c3f2d80bfb53e907e7fc74e806e60d1efa578b md5: 4e33d49bf4fc853855a3b00643aa5484 @@ -2525,10 +2538,10 @@ packages: size: 37398694 timestamp: 1781258934574 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.7-hcd007b5_104_cp314.conda - build_number: 104 - sha256: 5aaf3af8d4f99541fef4e746ae58677acda6ce02cfa5751abc3d1cc29ea8f732 - md5: 663015cba592a7375ca3c465af84186d +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.7-hcd007b5_106_cp314.conda + build_number: 106 + sha256: 4cd05407d6b07d00fd5b6cc78bd2f60ae5e21f777eb26ead82be2e3751c610a1 + md5: 56ee91e118243e46bfc0c41e4030d354 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 @@ -2538,9 +2551,9 @@ packages: - libgcc >=15 - liblzma >=5.8.3,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libpython 3.14.7 hdc7f604_104_cp314 + - libpython 3.14.7 hdc7f604_106_cp314 - libsqlite >=3.53.4,<4.0a0 - - libuuid >=2.42.2,<3.0a0 + - libuuid >=2.42.3,<3.0a0 - libzlib >=1.3.2,<2.0a0 - ncurses >=6.6,<7.0a0 - openssl >=3.5.8,<4.0a0 @@ -2555,8 +2568,8 @@ packages: - python_abi 3.14.* *_cp314 noarch: - python - size: 26435588 - timestamp: 1788161824322 + size: 26484459 + timestamp: 1788383955577 python_site_packages_path: lib/python3.14/site-packages - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda sha256: ef7df29b38ef04ec67a8888a4aa039973eaa377e8c4b59a7be0a1c50cd7e4ac6 @@ -5253,7 +5266,7 @@ packages: run_exports: {} size: 24190 timestamp: 1779159948016 -- conda_source: autostorage[3ad14945] @ . +- conda_source: autostorage[fae6965b] @ . variants: target_platform: noarch depends: @@ -5271,14 +5284,14 @@ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.2.0-he0feb66_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.14.7-hdc7f604_104_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.14.7-hdc7f604_106_cp314.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.4-h13e7031_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.3-hcfc3c73_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.7-hcd007b5_104_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.7-hcd007b5_106_cp314.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.12.9-h86a270d_0.conda diff --git a/src/autostorage/events.py b/src/autostorage/events.py index 9cce88c..00ad3bb 100644 --- a/src/autostorage/events.py +++ b/src/autostorage/events.py @@ -228,11 +228,11 @@ def add_inchi_identities_before_flush( if not isinstance(obj, StationaryPointRow): continue - if obj.geometry_id is None or obj.identities: - continue - - # Load the geometry - geometry_row = session.get(GeometryRow, obj.geometry_id) + # Get geometry - try relationship first (for `geometry=geo_row`), + # then load via FK (for `geometry_id=id`) + geometry_row = obj.geometry + if geometry_row is None and obj.geometry_id is not None: + geometry_row = session.get(GeometryRow, obj.geometry_id) if geometry_row is None: continue @@ -298,11 +298,15 @@ def add_smiles_extras_before_flush( if not isinstance(obj, StationaryPointRow): continue - if obj.geometry_id is None or not obj.identities: + # Skip if no identities attached yet + if not obj.identities: continue - # Load the geometry - geometry_row = session.get(GeometryRow, obj.geometry_id) + # Get geometry - try relationship first (for `geometry=geo_row`), + # then load via FK (for `geometry_id=id`) + geometry_row = obj.geometry + if geometry_row is None and obj.geometry_id is not None: + geometry_row = session.get(GeometryRow, obj.geometry_id) if geometry_row is None: continue @@ -349,11 +353,15 @@ def add_hill_extras_before_flush( if not isinstance(obj, StationaryPointRow): continue - if obj.geometry_id is None or not obj.identities: + # Skip if no identities attached yet + if not obj.identities: continue - # Load the geometry - geometry_row = session.get(GeometryRow, obj.geometry_id) + # Get geometry - try relationship first (for `geometry=geo_row`), + # then load via FK (for `geometry_id=id`) + geometry_row = obj.geometry + if geometry_row is None and obj.geometry_id is not None: + geometry_row = session.get(GeometryRow, obj.geometry_id) if geometry_row is None: continue diff --git a/tests/test_events.py b/tests/test_events.py index 5f5ec1a..1cec925 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1216,6 +1216,43 @@ def test_different_geometries_create_different_identities( identity_count = session.query(IdentityRow).count() assert identity_count == EXPECTED_IDENTITY_COUNT_TWO + def test_inchi_identity_added_with_relationship_object( + self, database: Database, make_model_opt: Callable[[], ModelRow] + ) -> None: + """InChI identity is added when StationaryPointRow uses relationship object.""" + with database.session() as session: + model = make_model_opt() + session.add(model) + session.flush() + + calc = CalculationRow( + calc_type="opt", + model_id=model.id, + input_provenance={}, + output_provenance={}, + ) + geom = GeometryRow( + symbols=["C", "O"], + coordinates=[[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]], + charge=0, + spin=1, + ) + + # Create StationaryPointRow with relationship objects (no IDs) + # This mimics the pattern used in the demo where objects are created + # and added together without intermediate flushes + stat_point = StationaryPointRow(calculation=calc, geometry=geom, order=0) + + session.add_all([calc, geom, stat_point]) + session.flush() + + # Identity should be auto-populated despite using relationship objects + assert len(stat_point.identities) == 1 + identity = stat_point.identities[0] + assert identity.kind == "stereoisomer" + assert identity.algorithm == "rdkit inchi" + assert identity.value.startswith("InChI=") + class TestAddSmilesExtras: """Tests for add_smiles_extras_before_flush event listener."""