mdinterface 1.5.3__tar.gz → 1.5.4__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.
- {mdinterface-1.5.3/mdinterface.egg-info → mdinterface-1.5.4}/PKG-INFO +1 -1
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/__init__.py +2 -2
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/core/topology.py +3 -2
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/io/lammpswriter.py +45 -13
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/simulationbox.py +9 -3
- {mdinterface-1.5.3 → mdinterface-1.5.4/mdinterface.egg-info}/PKG-INFO +1 -1
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface.egg-info/SOURCES.txt +2 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface.egg-info/top_level.txt +1 -0
- mdinterface-1.5.4/scripts/extract_changelog_summary.py +78 -0
- mdinterface-1.5.4/tests/test_changelog_summary.py +91 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/LICENSE +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/MANIFEST.in +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/README.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/assets/mdinterface.png +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/api/database.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/api/externals.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/api/io.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/api/polymer.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/api/simcell.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/api/specie.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/assets/mdinterface.png +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/guide/database.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/guide/logging.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/guide/polymer.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/guide/simcell.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/guide/specie.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/index.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/installation.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/quickstart.md +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/docs/requirements.txt +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/electrode_interface.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/legacy/make_POSCAR.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/legacy/make_box.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/legacy/make_polymer.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/legacy/make_solvent_box.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/legacy/make_specie_ligpargen_resp.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/multilayer.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/multisolvent_box.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/polymer/polymer_piperion.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/sandwich_from_traj.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/examples/solvent_box.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/box.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/builder.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/continuum2sim.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/polymerize.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/snippets.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/build/solvent.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/config.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/core/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/core/polymer.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/core/specie.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/database/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/database/graphene.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/database/ions.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/database/metals.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/database/molecules.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/database/nobles.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/aimd.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/ase.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/ligpargen.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/obabel.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/optimization.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/externals/pyscf.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/io/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/io/gromacswriter.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/io/packmol.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/io/read.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/read/cp2ktraj.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/read/lammpstraj.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/read/read.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/read/trajectory.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/read/xyztraj.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/auxiliary.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/draw.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/graphs.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/logger.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/map.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/poisson.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/rings.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface/utils/units.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface.egg-info/dependency_links.txt +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/mdinterface.egg-info/requires.txt +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/pyproject.toml +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/requirements.txt +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/setup.cfg +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/setup.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/__init__.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/test_auxiliary.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/test_builder.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/test_database.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/test_solvent.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/test_specie.py +0 -0
- {mdinterface-1.5.3 → mdinterface-1.5.4}/tests/test_topology.py +0 -0
|
@@ -6,8 +6,8 @@ Initially developed to construct electrolyte/electrode interfaces, it is also we
|
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = '1.5.
|
|
10
|
-
__date__ = '
|
|
9
|
+
__version__ = '1.5.4'
|
|
10
|
+
__date__ = '12 Aug. 2026'
|
|
11
11
|
__author__ = 'Fabrice Roncoroni'
|
|
12
12
|
__all__ = ['SimulationBox', 'SimCell', 'BoxBuilder', "Specie", "Polymer"]
|
|
13
13
|
|
|
@@ -249,13 +249,14 @@ class Dihedral(Topology):
|
|
|
249
249
|
return (self.values == other.values and
|
|
250
250
|
(self.symbols == other.symbols or self.symbols == other.symbols[::-1]))
|
|
251
251
|
|
|
252
|
-
def write(self, fout):
|
|
252
|
+
def write(self, fout, idx=None):
|
|
253
253
|
atype = "{}-{}-{}-{}".format(*self.symbols)
|
|
254
254
|
if self._values[-1] is not None:
|
|
255
255
|
value = "{:>7.4f} {:>7.4f} {:>7.4f} {:>7.4f} {:>7.4f}".format(*self.values)
|
|
256
256
|
else:
|
|
257
257
|
value = "{:>7.4f} {:>7.4f} {:>7.4f} {:>7.4f}".format(*self.values[:-1])
|
|
258
|
-
fout.write("{:>5} {} # {:<8} | {}\n".format(
|
|
258
|
+
fout.write("{:>5} {} # {:<8} | {}\n".format(
|
|
259
|
+
idx if idx is not None else self.id, value, atype, self.resname))
|
|
259
260
|
|
|
260
261
|
def __bool__(self):
|
|
261
262
|
return any([bool(val) for val in self.values])
|
|
@@ -25,6 +25,16 @@ btype_sections = {'bond':'Bonds', 'angle':'Angles',
|
|
|
25
25
|
'dihedral':'Dihedrals', 'improper':'Impropers'}
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _renumber_bonded_types(raw_types):
|
|
29
|
+
"""
|
|
30
|
+
Map raw per-item bond/angle/dihedral/improper type ids -- which may be
|
|
31
|
+
sparse or non-contiguous (e.g. when a Specie template is reused across
|
|
32
|
+
several layers) -- to a contiguous 1..N range ordered by numeric value.
|
|
33
|
+
"""
|
|
34
|
+
unique_sorted = sorted({int(t) for t in raw_types})
|
|
35
|
+
return {t: i + 1 for i, t in enumerate(unique_sorted)}
|
|
36
|
+
|
|
37
|
+
|
|
28
38
|
class DATAWriter(base.WriterBase):
|
|
29
39
|
"""Write out the current time step as a LAMMPS DATA file.
|
|
30
40
|
|
|
@@ -161,9 +171,14 @@ class DATAWriter(base.WriterBase):
|
|
|
161
171
|
self.f.write('\n')
|
|
162
172
|
self.f.write('{}\n'.format(btype_sections[bonds.btype]))
|
|
163
173
|
self.f.write('\n')
|
|
174
|
+
try:
|
|
175
|
+
remap = _renumber_bonded_types(bond.type for bond in bonds)
|
|
176
|
+
except (TypeError, ValueError):
|
|
177
|
+
remap = None
|
|
164
178
|
for bond, i in zip(bonds, range(1, len(bonds)+1)):
|
|
165
179
|
try:
|
|
166
|
-
|
|
180
|
+
btype_id = remap[int(bond.type)] if remap is not None else int(bond.type)
|
|
181
|
+
self.f.write('{:d} {:d} '.format(i, btype_id)+\
|
|
167
182
|
' '.join((bond.atoms.indices + 1).astype(str))+'\n')
|
|
168
183
|
except TypeError:
|
|
169
184
|
errmsg = (f"LAMMPS DATAWriter: Trying to write bond, but bond "
|
|
@@ -337,18 +352,20 @@ def write_lammps_coefficients(
|
|
|
337
352
|
# Write Pair Coefficients
|
|
338
353
|
fout.write("Pair Coeffs\n\n")
|
|
339
354
|
|
|
340
|
-
|
|
355
|
+
present_types = list(np.unique(system.atoms.types))
|
|
341
356
|
atom_types = sorted_attributes.get("atoms", [])
|
|
357
|
+
written = set()
|
|
342
358
|
for atom in atom_types:
|
|
343
|
-
if atom.extended_label not in
|
|
359
|
+
if atom.extended_label not in present_types or atom.extended_label in written:
|
|
344
360
|
continue
|
|
361
|
+
written.add(atom.extended_label)
|
|
362
|
+
idx = present_types.index(atom.extended_label) + 1
|
|
345
363
|
|
|
346
364
|
eps = atom.eps if atom.eps is not None else 0
|
|
347
365
|
sig = atom.sig if atom.sig is not None else 0
|
|
348
366
|
|
|
349
367
|
fout.write("{:>5} {:>12.8f} {:>12.8f} # {}\n".format(
|
|
350
368
|
idx, eps, sig, atom.extended_label))
|
|
351
|
-
idx += 1
|
|
352
369
|
|
|
353
370
|
# Write Bond Coefficients
|
|
354
371
|
bond_types = sorted_attributes.get("bonds", [])
|
|
@@ -356,9 +373,13 @@ def write_lammps_coefficients(
|
|
|
356
373
|
fout.write("\n")
|
|
357
374
|
fout.write("Bond Coeffs\n\n")
|
|
358
375
|
|
|
376
|
+
bond_remap = _renumber_bonded_types(system.bonds.types()) if len(system.bonds) else {}
|
|
377
|
+
written_bonds = set()
|
|
359
378
|
for bond in bond_types:
|
|
360
|
-
if bond.id not in
|
|
379
|
+
if int(bond.id) not in bond_remap or bond.id in written_bonds:
|
|
361
380
|
continue
|
|
381
|
+
written_bonds.add(bond.id)
|
|
382
|
+
idx = bond_remap[int(bond.id)]
|
|
362
383
|
|
|
363
384
|
kr = bond.kr if bond.kr is not None else 0
|
|
364
385
|
r0 = bond.r0 if bond.r0 is not None else 0
|
|
@@ -366,7 +387,7 @@ def write_lammps_coefficients(
|
|
|
366
387
|
btype = "{}-{}".format(*bond.symbols)
|
|
367
388
|
|
|
368
389
|
fout.write("{:>5} {:>10.6f} {:>10.6f} # {:<5} | {}\n".format(
|
|
369
|
-
|
|
390
|
+
idx, kr, r0, btype, bond.resname))
|
|
370
391
|
|
|
371
392
|
# Write Angle Coefficients
|
|
372
393
|
angle_types = sorted_attributes.get("angles", [])
|
|
@@ -374,9 +395,13 @@ def write_lammps_coefficients(
|
|
|
374
395
|
fout.write("\n")
|
|
375
396
|
fout.write("Angle Coeffs\n\n")
|
|
376
397
|
|
|
398
|
+
angle_remap = _renumber_bonded_types(system.angles.types()) if len(system.angles) else {}
|
|
399
|
+
written_angles = set()
|
|
377
400
|
for angle in angle_types:
|
|
378
|
-
if angle.id not in
|
|
401
|
+
if int(angle.id) not in angle_remap or angle.id in written_angles:
|
|
379
402
|
continue
|
|
403
|
+
written_angles.add(angle.id)
|
|
404
|
+
idx = angle_remap[int(angle.id)]
|
|
380
405
|
|
|
381
406
|
kr = angle.kr if angle.kr is not None else 0
|
|
382
407
|
theta0 = angle.theta0 if angle.theta0 is not None else 0
|
|
@@ -384,7 +409,7 @@ def write_lammps_coefficients(
|
|
|
384
409
|
atype = "{}-{}-{}".format(*angle.symbols)
|
|
385
410
|
|
|
386
411
|
fout.write("{:>5} {:>10.6f} {:>10.6f} # {:<8} | {}\n".format(
|
|
387
|
-
|
|
412
|
+
idx, kr, theta0, atype, angle.resname))
|
|
388
413
|
|
|
389
414
|
# Write Dihedral Coefficients
|
|
390
415
|
dihedral_types = sorted_attributes.get("dihedrals", [])
|
|
@@ -392,26 +417,33 @@ def write_lammps_coefficients(
|
|
|
392
417
|
fout.write("\n")
|
|
393
418
|
fout.write("Dihedral Coeffs\n\n")
|
|
394
419
|
|
|
420
|
+
dihedral_remap = _renumber_bonded_types(system.dihedrals.types()) if len(system.dihedrals) else {}
|
|
421
|
+
written_dihedrals = set()
|
|
395
422
|
for dihedral in dihedral_types:
|
|
396
|
-
if dihedral.id not in
|
|
423
|
+
if int(dihedral.id) not in dihedral_remap or dihedral.id in written_dihedrals:
|
|
397
424
|
continue
|
|
425
|
+
written_dihedrals.add(dihedral.id)
|
|
398
426
|
|
|
399
|
-
dihedral.write(fout)
|
|
427
|
+
dihedral.write(fout, idx=dihedral_remap[int(dihedral.id)])
|
|
400
428
|
|
|
401
429
|
# Write Improper Coefficients
|
|
402
430
|
improper_types = sorted_attributes.get("impropers", [])
|
|
403
431
|
if improper_types and system.impropers.types():
|
|
404
432
|
fout.write("\n")
|
|
405
433
|
fout.write("Improper Coeffs\n\n")
|
|
406
|
-
|
|
434
|
+
|
|
435
|
+
improper_remap = _renumber_bonded_types(system.impropers.types()) if len(system.impropers) else {}
|
|
436
|
+
written_impropers = set()
|
|
407
437
|
for improper in improper_types:
|
|
408
|
-
if improper.id not in
|
|
438
|
+
if int(improper.id) not in improper_remap or improper.id in written_impropers:
|
|
409
439
|
continue
|
|
440
|
+
written_impropers.add(improper.id)
|
|
441
|
+
idx = improper_remap[int(improper.id)]
|
|
410
442
|
|
|
411
443
|
atype = "{}-{}-{}-{}".format(*improper.symbols)
|
|
412
444
|
value = "{:>7.4f} {:>2d} {:>2d}".format(*improper.values)
|
|
413
445
|
|
|
414
|
-
fout.write("{:>5} {} # {:<2} | {}\n".format(
|
|
446
|
+
fout.write("{:>5} {} # {:<2} | {}\n".format(idx, value, atype, improper.resname))
|
|
415
447
|
|
|
416
448
|
fout.write("\n")
|
|
417
449
|
|
|
@@ -22,7 +22,6 @@ import numpy as np
|
|
|
22
22
|
|
|
23
23
|
import shutil
|
|
24
24
|
import warnings
|
|
25
|
-
warnings.filterwarnings('ignore')
|
|
26
25
|
|
|
27
26
|
#%%
|
|
28
27
|
|
|
@@ -56,7 +55,14 @@ class SimulationBox():
|
|
|
56
55
|
def __init__(self, solvent: Optional[Any] = None, solute: Optional[Union[Any, List[Any]]] = None,
|
|
57
56
|
interface: Optional[Any] = None, enderface: Optional[Any] = None,
|
|
58
57
|
miderface: Optional[Any] = None) -> None:
|
|
59
|
-
|
|
58
|
+
|
|
59
|
+
warnings.warn(
|
|
60
|
+
"SimulationBox is deprecated and will be removed in a future version. "
|
|
61
|
+
"Use SimCell instead.",
|
|
62
|
+
DeprecationWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
|
|
60
66
|
# start species
|
|
61
67
|
self._setup_species(solvent, solute, interface, enderface, miderface)
|
|
62
68
|
|
|
@@ -367,7 +373,7 @@ class SimulationBox():
|
|
|
367
373
|
ion_pos = layer["ion_pos"]
|
|
368
374
|
|
|
369
375
|
# make solvent box
|
|
370
|
-
solvent = make_solvent_box(self.species, self.
|
|
376
|
+
solvent = make_solvent_box(self.species, self._solvent, self._solute,
|
|
371
377
|
[xsize, ysize, zsize], solv_rho,
|
|
372
378
|
nions, concentration, conmodel, ion_pos, nsolvent)
|
|
373
379
|
|
|
@@ -83,9 +83,11 @@ mdinterface/utils/map.py
|
|
|
83
83
|
mdinterface/utils/poisson.py
|
|
84
84
|
mdinterface/utils/rings.py
|
|
85
85
|
mdinterface/utils/units.py
|
|
86
|
+
scripts/extract_changelog_summary.py
|
|
86
87
|
tests/__init__.py
|
|
87
88
|
tests/test_auxiliary.py
|
|
88
89
|
tests/test_builder.py
|
|
90
|
+
tests/test_changelog_summary.py
|
|
89
91
|
tests/test_database.py
|
|
90
92
|
tests/test_solvent.py
|
|
91
93
|
tests/test_specie.py
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract a version's summary paragraph from CHANGELOG.md.
|
|
3
|
+
|
|
4
|
+
Usage: python scripts/extract_changelog_summary.py VERSION
|
|
5
|
+
|
|
6
|
+
Prints the summary paragraph to stdout and exits 0 on success. Exits 1 with
|
|
7
|
+
an error on stderr if the version's changelog header, or its summary
|
|
8
|
+
paragraph, is missing.
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extract_summary(changelog_text: str, version: str) -> str:
|
|
15
|
+
"""Return the summary paragraph for `## [version]` in changelog_text.
|
|
16
|
+
|
|
17
|
+
The summary is every non-blank line immediately following the header
|
|
18
|
+
(after skipping any blank lines right after it), stopping at the next
|
|
19
|
+
blank line or the next '## '/'### ' header — i.e. exactly one paragraph.
|
|
20
|
+
"""
|
|
21
|
+
header = f"## [{version}]"
|
|
22
|
+
lines = changelog_text.splitlines()
|
|
23
|
+
|
|
24
|
+
header_idx = None
|
|
25
|
+
for i, line in enumerate(lines):
|
|
26
|
+
if line.startswith(header):
|
|
27
|
+
header_idx = i
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
if header_idx is None:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"No changelog header found for version {version!r} "
|
|
33
|
+
f"(expected a line starting with {header!r})"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
i = header_idx + 1
|
|
37
|
+
while i < len(lines) and lines[i].strip() == "":
|
|
38
|
+
i += 1
|
|
39
|
+
|
|
40
|
+
summary_lines = []
|
|
41
|
+
while i < len(lines):
|
|
42
|
+
line = lines[i]
|
|
43
|
+
if line.strip() == "" or line.startswith("## ") or line.startswith("### "):
|
|
44
|
+
break
|
|
45
|
+
summary_lines.append(line)
|
|
46
|
+
i += 1
|
|
47
|
+
|
|
48
|
+
summary = "\n".join(summary_lines).strip()
|
|
49
|
+
if not summary:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"No summary paragraph found under {header!r} in CHANGELOG.md - "
|
|
52
|
+
"add one before the first '### ' group"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return summary
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main(argv):
|
|
59
|
+
if len(argv) != 2:
|
|
60
|
+
print("Usage: extract_changelog_summary.py VERSION", file=sys.stderr)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
version = argv[1]
|
|
64
|
+
changelog_path = Path(__file__).resolve().parent.parent / "CHANGELOG.md"
|
|
65
|
+
changelog_text = changelog_path.read_text()
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
summary = extract_summary(changelog_text, version)
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
print(summary)
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
_SCRIPT_PATH = Path(__file__).resolve().parent.parent / "scripts" / "extract_changelog_summary.py"
|
|
7
|
+
_spec = importlib.util.spec_from_file_location("extract_changelog_summary", _SCRIPT_PATH)
|
|
8
|
+
_module = importlib.util.module_from_spec(_spec)
|
|
9
|
+
_spec.loader.exec_module(_module)
|
|
10
|
+
extract_summary = _module.extract_summary
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_extract_summary_found():
|
|
14
|
+
changelog = """# Changelog
|
|
15
|
+
|
|
16
|
+
## [Unreleased]
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [1.5.4] - 2026-08-11
|
|
21
|
+
|
|
22
|
+
Fixes a silent LAMMPS output corruption bug.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Something
|
|
26
|
+
"""
|
|
27
|
+
assert extract_summary(changelog, "1.5.4") == "Fixes a silent LAMMPS output corruption bug."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_extract_summary_multiline():
|
|
31
|
+
changelog = """## [1.5.4] - 2026-08-11
|
|
32
|
+
|
|
33
|
+
Fixes a silent LAMMPS output corruption bug affecting systems that reuse
|
|
34
|
+
the same Specie template across multiple layers.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Something
|
|
38
|
+
"""
|
|
39
|
+
expected = (
|
|
40
|
+
"Fixes a silent LAMMPS output corruption bug affecting systems that reuse\n"
|
|
41
|
+
"the same Specie template across multiple layers."
|
|
42
|
+
)
|
|
43
|
+
assert extract_summary(changelog, "1.5.4") == expected
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_missing_version_header_raises():
|
|
47
|
+
changelog = """## [1.5.3] - 2026-07-09
|
|
48
|
+
|
|
49
|
+
Some summary.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- Something
|
|
53
|
+
"""
|
|
54
|
+
with pytest.raises(ValueError, match="No changelog header found"):
|
|
55
|
+
extract_summary(changelog, "1.5.4")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_missing_summary_paragraph_raises():
|
|
59
|
+
changelog = """## [1.5.4] - 2026-08-11
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- Something
|
|
63
|
+
"""
|
|
64
|
+
with pytest.raises(ValueError, match="No summary paragraph found"):
|
|
65
|
+
extract_summary(changelog, "1.5.4")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_second_paragraph_not_included():
|
|
69
|
+
changelog = """## [1.5.4] - 2026-08-11
|
|
70
|
+
|
|
71
|
+
First paragraph only.
|
|
72
|
+
|
|
73
|
+
Second paragraph should not be included.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- Something
|
|
77
|
+
"""
|
|
78
|
+
assert extract_summary(changelog, "1.5.4") == "First paragraph only."
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_header_prefix_does_not_match_longer_version():
|
|
82
|
+
# "## [1.5.4]" must not match when looking for "1.5" or "1.5.40"
|
|
83
|
+
changelog = """## [1.5.40] - 2026-08-11
|
|
84
|
+
|
|
85
|
+
Wrong version's summary.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
- Something
|
|
89
|
+
"""
|
|
90
|
+
with pytest.raises(ValueError, match="No changelog header found"):
|
|
91
|
+
extract_summary(changelog, "1.5.4")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|