Skip to content

Commit be75dde

Browse files
stsoukoclaude
andcommitted
chython 3.0: an arena-based compiled core
THE CORE. `chython.core._core` is one Cython translation unit and the only extension. Atoms and bonds live in a single contiguous arena buffer, addressed by a stable id that survives edits; `map_number` is a separate field, so a V2 `remap()` is `set_map_number()` when it meant atom-atom mapping and nothing when it meant renumbering. Mutation goes through an explicit edit session and derived data is recomputed on seal. SMILES and SMARTS read and write, isomorphism, rings, kekule/thiele, the canonical form (computed per component), stereo, fingerprints, InChI and pach are in it. Element, isotope and valence tables are generated C tables compiled from `elements.tsv`, `isotopes.tsv` and `valence_rules.tsv`, and each generator is re-run against its TSV by the suite so the two cannot drift. WHAT IS GONE. `algorithms/`, `containers/`, `files/`, `reactor/`, `periodictable/`, `utils/` and `transformation/`, including the modules never ported; three ratchets fail if one of those names becomes importable again. There is no metaclass-generated element class and no `CGRContainer` -- `ReactionModelingView` is the CGR-free view chytorch consumes. chython 2.24 remains reachable as an out-of-process differential oracle, which is the only sanctioned way to consult V2. THE LAYERS. `chemistry` holds chemical knowledge as TSV beside the passes applying it; `reactions` is its sibling, holding SMIRKS templates, the functional and protective corpora and mapping reconstruction; then `formats`, `depict`, `interop`. Nothing imports the façade and the dependency direction never reverses, both ratcheted. Registration onto the sealed container is by injection, not inheritance. INPUT IS GARBAGE BY DEFAULT. A reader never rejects a record for being chemically wrong: an illegal valence, a nonsense charge and an underivable hydrogen count are stored and logged, the last as a reserved `H_UNKNOWN` rather than a silent zero. Repair is an explicit pipeline afterwards -- `kekule()`, `standardize()`, `fix_resonance()`, `thiele()` -- and refusals live at the answer boundary alone. Documentation is `docs/`, where every Python sample is executed and every figure re-rendered and compared against the committed file. About 8700 tests; CI builds five platform rows and a sixth that adds the differential oracles and reports coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 20afdba commit be75dde

778 files changed

Lines changed: 209118 additions & 36925 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/python-package.yml‎

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
name: Build Python packages
22

3+
# Three jobs. libinchi is per-OS and the wheel is per-OS AND per-interpreter: building the C library
4+
# once per OS and downloading it into the twenty wheel jobs costs four cmake runs instead of twenty.
5+
# `setup.py` calls the same builder and returns early when the binary is already present, so the
6+
# download is all the wheel jobs need -- they never run cmake. The sdist is per-release, so it is
7+
# built once and by itself.
8+
39
on:
410
release:
511
types: [published]
@@ -17,13 +23,28 @@ jobs:
1723
submodules: true
1824
- name: Set up CMake
1925
uses: lukka/get-cmake@latest
26+
- name: Set up Python 3.10
27+
# THE OLDEST INTERPRETER IN THE WHEEL MATRIX, and pinned rather than left to the runner's default
28+
# python because on macOS this choice sets the wheel's minimum OS version for every interpreter.
29+
# `build_inchi.py` takes `CMAKE_OSX_DEPLOYMENT_TARGET` and `CMAKE_OSX_ARCHITECTURES` from
30+
# `sysconfig.get_platform()`, and `wheel`'s `calculate_macosx_platform_tag` then RAISES the tag of
31+
# any wheel carrying this dylib to cover it. A dylib stamped with the runner image's macOS
32+
# version tags all five mac wheels with that version; one built for the oldest floor in the matrix
33+
# cannot raise any of them.
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: "3.10"
2037
- name: Build libinchi
2138
run: python build_inchi.py
2239
- name: Upload libinchi artifact
2340
uses: actions/upload-artifact@v4
2441
with:
2542
name: libinchi-${{ matrix.os }}
26-
path: chython/files/libinchi/libinchi.*
43+
# build/inchi/, not chython/core/: the binary is a build output and no longer enters the source
44+
# tree. `if-no-files-found: error` is what makes a silent no-InChI release impossible -- the
45+
# builder warns and skips when cmake or the submodule are missing, which is right for a
46+
# developer's machine and wrong here.
47+
path: build/inchi/libinchi.*
2748
if-no-files-found: error
2849

2950
binary:
@@ -41,17 +62,71 @@ jobs:
4162
uses: actions/download-artifact@v4
4263
with:
4364
name: libinchi-${{ matrix.os }}
44-
path: chython/files/libinchi/
65+
path: build/inchi/
4566
- name: Set up Python ${{ matrix.python-version }}
4667
uses: actions/setup-python@v5
4768
with:
4869
python-version: ${{ matrix.python-version }}
4970
- name: Install dependencies
5071
run: |
51-
python -m pip install --upgrade pip poetry twine
72+
python -m pip install --upgrade pip build twine
5273
- name: Build wheel
74+
# PEP 517, because the backend is setuptools: `poetry build` served the poetry backend this
75+
# project used through 2.24 and would now build nothing at all. `build` reads
76+
# `[build-system] requires` and installs Cython itself, so there is no separate install step
77+
# for it.
78+
run: |
79+
python -m build --wheel
80+
- name: Retag the Linux wheel for manylinux
81+
if: runner.os == 'Linux'
82+
# THE MIGRATION'S ONE SILENT CASUALTY. poetry-core tagged wheels from `packaging.tags`, so 2.24
83+
# published `manylinux_2_39_x86_64` and `manylinux_2_39_aarch64` with no repair step and nobody
84+
# had to think about it. setuptools' `bdist_wheel` does not: it emits `linux_x86_64`, which PyPI
85+
# rejects outright, so without this step the first V3 release fails at upload on Linux only.
86+
#
87+
# The tag is computed rather than written down -- it is precisely what poetry-core computed -- so
88+
# it tracks the runner's glibc instead of freezing 2.39 into a file that would then lie the day
89+
# the runner image is bumped. `auditwheel repair` is the stronger tool, since it verifies the
90+
# claim against the binaries rather than asserting it; it is not used here because a wheel built
91+
# on glibc 2.39 and tagged 2.39 cannot be overstating its requirement, and because auditwheel
92+
# would also rewrite libinchi.so, which nothing links against and which is loaded by path.
5393
run: |
54-
poetry build -f wheel
94+
python -m pip install --upgrade wheel
95+
tag=$(python -c 'from packaging.tags import sys_tags; print(next(iter(sys_tags())).platform)')
96+
echo "retagging as $tag"
97+
python -m wheel tags --remove --platform-tag "$tag" dist/*.whl
5598
- name: Publish package
5699
run: |
57100
twine upload -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --non-interactive --skip-existing dist/*
101+
102+
sdist:
103+
# WITHOUT THIS THE RELEASE IS WHEELS ONLY, and `pip install chython` outside the matrix above
104+
# answers "no matching distribution": musl, FreeBSD, macOS x86_64, a CPython newer than the one
105+
# this matrix was written for. A source distribution turns each of those into a build from source.
106+
#
107+
# Once, not per matrix cell: an sdist is a property of the source and every cell would produce the
108+
# same file. `MANIFEST.in` is what makes it buildable -- the .pyx and .pxi layers and
109+
# `build_inchi.py`, and not the generated .c, so an install from it cythonizes the sources it ships.
110+
# It does NOT ship the INCHI submodule, so a build from the sdist warns and produces a wheel without
111+
# InChI; `core/__init__.py` falls back silently and that surface raises at the call.
112+
runs-on: ubuntu-24.04
113+
steps:
114+
- uses: actions/checkout@v4
115+
- name: Set up Python
116+
uses: actions/setup-python@v5
117+
with:
118+
python-version: "3.12"
119+
- name: Install dependencies
120+
run: |
121+
python -m pip install --upgrade pip build twine
122+
- name: Build sdist
123+
run: |
124+
python -m build --sdist
125+
- name: Check metadata
126+
# The sdist is what PyPI renders the project page from, and a README it cannot render is rejected
127+
# at upload -- for the whole release, since `twine` uploads nothing when one file fails the check.
128+
run: |
129+
twine check dist/*
130+
- name: Publish sdist
131+
run: |
132+
twine upload -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --non-interactive --skip-existing dist/*

‎.github/workflows/test.yml‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: Test
2+
3+
# The suite on every push and pull request. Free: this repository is public, so standard runners --
4+
# including Linux arm64, macOS and Windows -- carry no minute charge.
5+
#
6+
# `python-package.yml` publishes and runs only on `release: published`, which meant nothing checked a
7+
# commit. The two files divide by question: that one asks whether the artifacts are right, this one
8+
# whether the code is.
9+
10+
on:
11+
push:
12+
branches: [master]
13+
pull_request:
14+
workflow_dispatch:
15+
16+
# A second push to the same ref makes the first run's answer stale, so it is cancelled rather than
17+
# finished.
18+
concurrency:
19+
group: test-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
defaults:
23+
run:
24+
shell: bash # one quoting dialect; the runners all provide it
25+
26+
jobs:
27+
test:
28+
runs-on: ${{ matrix.os }}
29+
strategy:
30+
# Every row runs: the interesting failure is which rows differ, and the first one to fail would
31+
# otherwise cancel exactly the comparison that identifies the cause.
32+
fail-fast: false
33+
matrix:
34+
include:
35+
# THE OLDEST AND NEWEST INTERPRETERS on the platform everything else is compared against.
36+
- {os: ubuntu-24.04, python: "3.10"}
37+
- {os: ubuntu-24.04, python: "3.14"}
38+
# THE ARCH THAT IS NOT LIKE THE OTHERS. Plain `char` is unsigned here and signed everywhere
39+
# else, and the flags that reconcile that (`chython/test/test_release_build.py`) are asserted
40+
# from a declaration; this row is what runs the assertions on the arch itself.
41+
- {os: ubuntu-24.04-arm, python: "3.12"}
42+
# ONE ROW PER TOOLCHAIN: clang with an Apple SDK, and MSVC.
43+
- {os: macos-latest, python: "3.12"}
44+
- {os: windows-latest, python: "3.12"}
45+
steps:
46+
- uses: actions/checkout@v4
47+
with:
48+
submodules: true # INCHI, or every InChI test skips
49+
- name: Set up CMake
50+
uses: lukka/get-cmake@latest
51+
- name: Set up Python ${{ matrix.python }}
52+
uses: actions/setup-python@v5
53+
with:
54+
python-version: ${{ matrix.python }}
55+
cache: pip
56+
cache-dependency-path: pyproject.toml
57+
- name: Install the build tools
58+
# Named here rather than left to build isolation because the next step calls `setup.py` directly.
59+
run: |
60+
python -m pip install --upgrade pip
61+
python -m pip install "cython>=3.1" "setuptools>=77" wheel
62+
- name: Build in place
63+
# `pytest chython/` imports the package out of the source tree, so the extension has to be there;
64+
# an editable install alone leaves that to `editable_wheel`'s internals. This is also what stages
65+
# libinchi beside the extension, which `core/__init__.py` loads relative to its own `__file__`.
66+
run: python setup.py build_ext --inplace
67+
- name: Install
68+
# `--no-build-isolation` so this reuses the objects the step above compiled instead of compiling
69+
# the translation unit a second time in an isolated environment. The extras are the ones the
70+
# suite reads: `ml` is numpy, without which the fingerprint and matrix tests skip, and `rdkit` is
71+
# the differential oracle in `formats/test/oracles.py`.
72+
run: python -m pip install -e ".[ml,rdkit]" --group dev --no-build-isolation
73+
- name: Confirm libinchi loaded
74+
# A LOUD FAILURE FOR A SILENT ONE. Without the library about a hundred tests skip and the suite
75+
# still passes, so a broken staging step or a missing submodule reads as green.
76+
run: |
77+
python -c "from chython import inchi_library_loaded; assert inchi_library_loaded(), 'libinchi did not load'"
78+
- name: Test
79+
# `test_performance.py` asserts RATIOS against RDKit, which is the right discipline for a
80+
# developer's machine and still not one for a shared runner whose neighbour decides the numbers.
81+
# It is a benchmark, run by hand; nothing else in the suite is timing-sensitive.
82+
run: python -m pytest chython/ -q --deselect chython/test/test_performance.py
83+
84+
oracles:
85+
# THE SAME SUITE WITH THE DIFFERENTIAL ORACLES, on one row and by itself: measured, they turn 584
86+
# skips into tests and the run from 3m15s into 17m41s. Paying that on all five rows would make the
87+
# answer to "did I break the build" arrive five times later, so the matrix above stays the fast
88+
# signal and this job runs beside it.
89+
#
90+
# `extra-clean2d` and `extra-clean3d` by name rather than their members, so the CI cannot drift from
91+
# what a user installing them gets. jpype comes with the first and its 63 tests still skip, needing
92+
# a JChem jar that is licensed and not here; `CHYTHON_CDK_JAR` is unset, so 219 CDK comparisons skip
93+
# too.
94+
runs-on: ubuntu-24.04
95+
steps:
96+
- uses: actions/checkout@v4
97+
with:
98+
submodules: true
99+
- name: Set up CMake
100+
uses: lukka/get-cmake@latest
101+
- name: Set up Python
102+
uses: actions/setup-python@v5
103+
with:
104+
python-version: "3.12"
105+
cache: pip
106+
cache-dependency-path: pyproject.toml
107+
- name: Install the build tools
108+
run: |
109+
python -m pip install --upgrade pip
110+
python -m pip install "cython>=3.1" "setuptools>=77" wheel
111+
- name: Build in place
112+
run: python setup.py build_ext --inplace
113+
- name: Install
114+
run: |
115+
python -m pip install -e ".[ml,rdkit,extra-clean2d,extra-clean3d]" --group dev --no-build-isolation
116+
- name: Confirm libinchi loaded
117+
run: |
118+
python -c "from chython import inchi_library_loaded; assert inchi_library_loaded(), 'libinchi did not load'"
119+
- name: Test, under coverage
120+
# THE README BADGE'S NUMBER COMES FROM THIS JOB, because this is the run where nothing skips for a
121+
# missing toolkit: on a fast-matrix row `interop/_indigo.py` would read as uncovered rather than as
122+
# never imported. What it measures and what it omits are in `[tool.coverage]` in pyproject.toml,
123+
# so the number is reproducible from a checkout. Measured cost of the wrapper on this suite: 245s
124+
# against 186s, about a third, paid on the job that is already the slow one.
125+
run: |
126+
python -m coverage run -m pytest chython/ -q --deselect chython/test/test_performance.py
127+
python -m coverage report
128+
python -m coverage xml
129+
- name: Upload coverage
130+
# Only from this repository -- a fork has no project to upload to -- and never fatal: a badge that
131+
# went stale is not the suite going red, and the step above already asserted the suite.
132+
if: github.repository == 'chython/chython'
133+
uses: codecov/codecov-action@v5
134+
with:
135+
files: coverage.xml
136+
fail_ci_if_error: false
137+
# REQUIRED for the badge. Codecov allows a tokenless upload from a pull request, including a
138+
# fork's, but refuses one to the default branch -- `Token required because branch is protected`,
139+
# which is its own rule and not GitHub's branch protection. Unset, the badge reads `unknown`
140+
# while this step still reports success, so the number's absence is read here and not in CI.
141+
token: ${{ secrets.CODECOV_TOKEN }}

‎.gitignore‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ MANIFEST
2525
node_modules/
2626
_build/
2727

28+
# specs, plans and research notes: working material for the tree, not part of the documentation Sphinx
29+
# builds out of `docs/`.
30+
docs/superpowers/
31+
2832
# local scratch and benchmark artifacts (keep out of the public repo).
2933
# broad globs; already-tracked files stay tracked.
3034
*.pkl
@@ -37,3 +41,12 @@ _build/
3741
*.csv
3842
*.txt
3943
*.zip
44+
45+
# ...except a corpus that is package data. `golden_subset.smi` is named in
46+
# `[tool.setuptools.package-data]` and read by `reactions/test/test_attention.py`, whose agreement floor
47+
# is a number about it -- a floor whose corpus the repo hides is a claim nobody can recheck.
48+
!chython/reactions/test/golden_subset.smi
49+
50+
# coverage's data file and the report the CI uploads; the settings are `[tool.coverage]` in pyproject.toml
51+
.coverage
52+
coverage.xml

‎.readthedocs.yaml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# `.readthedocs.yaml` and not `readthedocs.yml`: the dotted name is the one Read the Docs documents,
2+
# and it is the only spelling a project created today can use. Both are read for now.
3+
4+
version: 2
5+
6+
build:
7+
os: "ubuntu-24.04"
8+
tools:
9+
python: "3.12"
10+
11+
sphinx:
12+
configuration: docs/conf.py
13+
# A warning here is a broken cross-reference or a dropped file, which renders as plain text on a page
14+
# nobody rebuilds. Safe to require because the build is warning-free under `-W` today; `docs/` holds
15+
# no autodoc directive, so nothing in this build imports chython and the extension is never compiled.
16+
fail_on_warning: true
17+
18+
python:
19+
install:
20+
- requirements: docs/requirements.txt

0 commit comments

Comments
 (0)