Skip to content

Commit 9331620

Browse files
committed
UPD: auto-update RDKit version for attribution; fail tests if SmilesDrawer attribution is out of sync
1 parent 485b3a7 commit 9331620

9 files changed

Lines changed: 84 additions & 29 deletions

File tree

.github/workflows/gui-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
- name: Lint
3232
run: npx eslint src --ext .ts,.tsx,.js
3333

34+
- name: Check SmilesDrawer version attribution
35+
run: npm run check:smiles-drawer-version
36+
3437
- name: Test
3538
run: npm test -- --watchAll=false
3639
env:

gui/src/client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"scripts": {
3939
"start": "craco start",
4040
"build": "craco build",
41-
"test": "craco test"
41+
"test": "craco test",
42+
"check:smiles-drawer-version": "node scripts/checkSmilesDrawerVersion.js"
4243
},
4344
"eslintConfig": {
4445
"extends": [
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env node
2+
// Fails if DrawingAttribution.tsx's hardcoded SMILES_DRAWER_VERSION drifts from the
3+
// version npm actually resolved in package-lock.json. Needed because smiles-drawer's
4+
// package.json is blocked from import (its own "exports" field), so the "Drawn with
5+
// SmilesDrawer vX" caption can't read the version at runtime the way ReactionScheme
6+
// does for RDKit -- see DrawingAttribution.tsx for that comparison.
7+
const fs = require("fs");
8+
const path = require("path");
9+
10+
const root = path.join(__dirname, "..");
11+
12+
const lockfile = JSON.parse(fs.readFileSync(path.join(root, "package-lock.json"), "utf8"));
13+
const resolvedVersion = lockfile.packages?.["node_modules/smiles-drawer"]?.version;
14+
15+
if (!resolvedVersion) {
16+
console.error("checkSmilesDrawerVersion: could not find node_modules/smiles-drawer in package-lock.json");
17+
process.exit(1);
18+
}
19+
20+
const attributionPath = path.join(root, "src/components/DrawingAttribution.tsx");
21+
const attributionSource = fs.readFileSync(attributionPath, "utf8");
22+
const match = attributionSource.match(/SMILES_DRAWER_VERSION\s*=\s*"([^"]+)"/);
23+
24+
if (!match) {
25+
console.error(`checkSmilesDrawerVersion: could not find SMILES_DRAWER_VERSION in ${attributionPath}`);
26+
process.exit(1);
27+
}
28+
29+
const hardcodedVersion = match[1];
30+
31+
if (hardcodedVersion !== resolvedVersion) {
32+
console.error(
33+
`checkSmilesDrawerVersion: DrawingAttribution.tsx says smiles-drawer v${hardcodedVersion}, ` +
34+
`but package-lock.json resolves it to v${resolvedVersion}. ` +
35+
`Update SMILES_DRAWER_VERSION in src/components/DrawingAttribution.tsx to match.`
36+
);
37+
process.exit(1);
38+
}
39+
40+
console.log(`checkSmilesDrawerVersion: OK (v${resolvedVersion})`);

gui/src/client/src/components/DrawingAttribution.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,31 @@ import React from "react";
22
import Typography from "@mui/material/Typography";
33
import type { SxProps, Theme } from "@mui/material/styles";
44

5-
// Neither version is importable at runtime -- smiles-drawer's package.json is blocked
6-
// by its own "exports" field, and RDKit drawings are produced server-side (see
7-
// gui/src/server/routes/rules.py). Both are kept in sync by hand with the resolved
8-
// version in package-lock.json and pyproject.toml's `rdkit==` pin, respectively.
9-
const RDKIT_VERSION = "2025.9.1";
5+
// smiles-drawer's package.json is blocked from import by its own "exports" field, so
6+
// this is kept in sync by hand with the resolved version in package-lock.json --
7+
// scripts/checkSmilesDrawerVersion.js fails CI/lint if the two drift apart.
108
const SMILES_DRAWER_VERSION = "2.4.1";
119

12-
type DrawingLibrary = "smiles-drawer" | "rdkit";
13-
14-
const LIBRARY_LABEL: Record<DrawingLibrary, string> = {
15-
"smiles-drawer": `SmilesDrawer v${SMILES_DRAWER_VERSION}`,
16-
rdkit: `RDKit v${RDKIT_VERSION}`,
17-
};
10+
type DrawingAttributionProps =
11+
| { library: "smiles-drawer"; sx?: SxProps<Theme> }
12+
// RDKit drawings are produced server-side (see gui/src/server/routes/rules.py), so
13+
// there's no local constant to hardcode -- the version is only known once the
14+
// caller has actually fetched a drawing and the server reported what rendered it.
15+
| { library: "rdkit"; version: string; sx?: SxProps<Theme> };
1816

1917
// A single caption attributing one or more structure/reaction drawings above it to the
2018
// library that rendered them. Place once per diagram -- if a diagram contains multiple
2119
// drawings from the same library (e.g. a compound plus its reconstructions), attribute
2220
// the whole group once rather than repeating this per drawing.
23-
export const DrawingAttribution: React.FC<{ library: DrawingLibrary; sx?: SxProps<Theme> }> = ({ library, sx }) => {
21+
export const DrawingAttribution: React.FC<DrawingAttributionProps> = (props) => {
22+
const label = props.library === "smiles-drawer" ? `SmilesDrawer v${SMILES_DRAWER_VERSION}` : `RDKit v${props.version}`;
2423
return (
2524
<Typography
2625
variant="caption"
2726
color="text.secondary"
28-
sx={{ display: "block", fontStyle: "italic", ...sx }}
27+
sx={{ display: "block", fontStyle: "italic", ...props.sx }}
2928
>
30-
Drawn with {LIBRARY_LABEL[library]}
29+
Drawn with {label}
3130
</Typography>
3231
);
3332
};

gui/src/client/src/components/workspace/ReactionScheme.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import DOMPurify from "dompurify";
1616
import { useColorScheme } from "@mui/material/styles";
1717
import { useQuery } from "@tanstack/react-query";
1818
import { fetchReactionSchemeSvg } from "../../features/rules/api";
19+
import { DrawingAttribution } from "../DrawingAttribution";
1920

2021
export function ReactionScheme({ id, smarts }: { id: string; smarts: string }) {
2122
const { mode, systemMode } = useColorScheme();
@@ -31,7 +32,7 @@ export function ReactionScheme({ id, smarts }: { id: string; smarts: string }) {
3132
// Server-rendered SVG markup gets injected raw via dangerouslySetInnerHTML, so it
3233
// must be sanitized first -- same rationale/profile as SvgViewer.
3334
const sanitizedSvg = React.useMemo(
34-
() => (svgQuery.data ? DOMPurify.sanitize(svgQuery.data, { USE_PROFILES: { svg: true, svgFilters: true } }) : null),
35+
() => (svgQuery.data ? DOMPurify.sanitize(svgQuery.data.svg, { USE_PROFILES: { svg: true, svgFilters: true } }) : null),
3536
[svgQuery.data]
3637
);
3738

@@ -43,7 +44,7 @@ export function ReactionScheme({ id, smarts }: { id: string; smarts: string }) {
4344
);
4445
}
4546

46-
if (svgQuery.error || !sanitizedSvg) {
47+
if (svgQuery.error || !sanitizedSvg || !svgQuery.data) {
4748
return (
4849
<Alert severity="warning" variant="outlined" sx={{ py: 0 }}>
4950
Could not render this reaction ({smarts}).
@@ -52,9 +53,12 @@ export function ReactionScheme({ id, smarts }: { id: string; smarts: string }) {
5253
}
5354

5455
return (
55-
<Box
56-
sx={{ "& svg": { display: "block", maxWidth: "100%", height: "auto" } }}
57-
dangerouslySetInnerHTML={{ __html: sanitizedSvg }}
58-
/>
56+
<>
57+
<Box
58+
sx={{ "& svg": { display: "block", maxWidth: "100%", height: "auto" } }}
59+
dangerouslySetInnerHTML={{ __html: sanitizedSvg }}
60+
/>
61+
<DrawingAttribution library="rdkit" version={svgQuery.data.rdkitVersion} />
62+
</>
5963
);
6064
}

gui/src/client/src/components/workspace/WorkspaceRules.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ function ReactionRuleRow({ rule }: { rule: ReactionRule }) {
199199
<Box sx={{ ...horizontalScrollSx, pb: 1 }}>
200200
<ReactionScheme id={rule.id} smarts={rule.smarts} />
201201
</Box>
202-
<DrawingAttribution library="rdkit" />
203202
</Box>
204203

205204
<PropsList props={rule.props} />

gui/src/client/src/features/rules/api.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getJson } from "../http";
2-
import { RuleSetRespSchema, type RuleSetResp } from "./types";
3-
import { ItemDrawingResultSchema } from "../drawing/types";
2+
import { RuleSetRespSchema, ReactionSchemeSvgRespSchema, type RuleSetResp, type ReactionSchemeSvgResp } from "./types";
43

54
// The whole default rule set, fetched once and cached by the caller (see
65
// WorkspaceRules) -- it's small (a few hundred rules total) and effectively static
@@ -15,11 +14,10 @@ export async function fetchReactionSchemeSvg(
1514
ruleId: string,
1615
theme: "light" | "dark",
1716
signal?: AbortSignal
18-
): Promise<string> {
19-
const data = await getJson(
17+
): Promise<ReactionSchemeSvgResp> {
18+
return getJson(
2019
`/api/reactionSchemeSvg/${encodeURIComponent(ruleId)}?theme=${theme}`,
21-
ItemDrawingResultSchema,
20+
ReactionSchemeSvgRespSchema,
2221
signal
2322
);
24-
return data.svg;
2523
}

gui/src/client/src/features/rules/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ export const RuleSetRespSchema = z.object({
2929
reactionRules: z.array(ReactionRuleSchema),
3030
});
3131
export type RuleSetResp = z.output<typeof RuleSetRespSchema>;
32+
33+
export const ReactionSchemeSvgRespSchema = z.object({
34+
svg: z.string(),
35+
// The RDKit version that rendered `svg` -- reported by the server (see
36+
// routes/rules.py's reaction_scheme_svg) rather than pinned client-side, so the
37+
// "Drawn with RDKit vX" attribution in ReactionScheme can never drift from what
38+
// actually rendered it.
39+
rdkitVersion: z.string(),
40+
});
41+
export type ReactionSchemeSvgResp = z.output<typeof ReactionSchemeSvgRespSchema>;

gui/src/server/routes/rules.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import threading
44

5+
import rdkit
56
from flask import Blueprint, Response, jsonify, request
67
from rdkit.Chem.Draw import rdMolDraw2D
78

@@ -154,4 +155,4 @@ def reaction_scheme_svg(rule_id: str) -> tuple[Response, int]:
154155
return jsonify({"error": "Unknown reaction rule id"}), 404
155156

156157
svg = _get_reaction_svg(rule, theme)
157-
return jsonify({"svg": svg}), 200
158+
return jsonify({"svg": svg, "rdkitVersion": rdkit.__version__}), 200

0 commit comments

Comments
 (0)