From 051fe0772ad44c4bdac8c32354264b2e2a4d49ac Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 17 Feb 2026 16:39:02 -0500 Subject: [PATCH 01/19] taking a stab at fixing all the mypy errors --- periodictable/__init__.py | 3 +++ periodictable/core.py | 2 +- periodictable/crystal_structure.py | 3 ++- periodictable/density.py | 8 ++++---- periodictable/formulas.py | 25 +++++++++++-------------- periodictable/mass.py | 6 +++--- pyproject.toml | 7 ++++++- 7 files changed, 30 insertions(+), 24 deletions(-) diff --git a/periodictable/__init__.py b/periodictable/__init__.py index 1219ef8..4fa4a9f 100644 --- a/periodictable/__init__.py +++ b/periodictable/__init__.py @@ -196,6 +196,9 @@ def __dir__(): livermorium = Lv = elements.symbol("Lv") tennessine = Ts = elements.symbol("Ts") oganesson = Og = elements.symbol("Og") +deuterium = D = elements.symbol("D") +tritium = T = elements.symbol("T") +neutron = n = elements.symbol("n") # Add element name and symbol (e.g. nickel and Ni) to the public attributes. __all__ += core.define_elements(elements, globals()) diff --git a/periodictable/core.py b/periodictable/core.py index e053b4b..e342b3b 100644 --- a/periodictable/core.py +++ b/periodictable/core.py @@ -651,7 +651,7 @@ def __init__(self, element: Union["Element", "Isotope"], charge: int): def __getattr__(self, attr: str) -> Any: return getattr(self.element, attr) @property - def mass(self) -> float: + def mass(self) -> float: # type: ignore[override] return getattr(self.element, 'mass') - constants.electron_mass*self.charge def __str__(self) -> str: sign = '+' if self.charge > 0 else '-' diff --git a/periodictable/crystal_structure.py b/periodictable/crystal_structure.py index f076ef3..67001d8 100644 --- a/periodictable/crystal_structure.py +++ b/periodictable/crystal_structure.py @@ -40,9 +40,10 @@ This data is from Ashcroft and Mermin. ''' +from typing import Any, Dict, List, Union from .core import PeriodicTable -crystal_structures = [ +crystal_structures: List[Union[Dict[str, Any], None]] = [ {'symmetry': 'diatom', 'd': 0.74}, #H {'symmetry': 'atom'}, #He {'symmetry': 'BCC', 'a': 3.49}, #Li diff --git a/periodictable/density.py b/periodictable/density.py index 9ad69f7..8318b6b 100644 --- a/periodictable/density.py +++ b/periodictable/density.py @@ -143,18 +143,18 @@ def init(table: PeriodicTable, reload: bool=False) -> None: return table.properties.append('density') Isotope.density \ - = property(density, "density using inter-atomic spacing from naturally occurring form") + = property(density, doc="density using inter-atomic spacing from naturally occurring form") # type: ignore[assignment] Element.density \ - = property(density, "density using inter-atomic spacing from naturally occurring form") + = property(density, doc="density using inter-atomic spacing from naturally occurring form") # type: ignore[assignment] Element.density_units = "g/cm^3" Element.interatomic_distance \ = property(interatomic_distance, - "interatomic distance estimated from density") + doc="interatomic distance estimated from density") # type: ignore[assignment] Element.interatomic_distance_units = "angstrom" Element.number_density \ = property(number_density, - "number density estimated from mass and density") + doc="number density estimated from mass and density") # type: ignore[assignment] Element.number_density_units = "1/cm^3" for k, v in element_densities.items(): diff --git a/periodictable/formulas.py b/periodictable/formulas.py index 561db4c..eb3ffcf 100644 --- a/periodictable/formulas.py +++ b/periodictable/formulas.py @@ -108,7 +108,7 @@ def _mix_by_weight_pairs(pairs: list[tuple["Formula", float]]) -> "Formula": result += ((q/f.mass)/scale) * f if all(f.density for f, _ in pairs): # Tested that densities are not None, so the following will work - volume = sum(q/f.density for f, q in pairs)/scale + volume = sum(q/cast(float, f.density) for f, q in pairs)/scale result.density = result.mass/volume return result @@ -193,9 +193,9 @@ def _mix_by_volume_pairs(pairs: list[tuple["Formula", float]]) -> "Formula": # = q / (mass/density) # = q * density / mass # scale this so that n = 1 for the smallest quantity - scale = min(q*f.density/f.mass for f, q in pairs) + scale = min(q*cast(float, f.density)/f.mass for f, q in pairs) for f, q in pairs: - result += ((q*f.density/f.mass)/scale) * f + result += ((q*cast(float, f.density)/f.mass)/scale) * f volume = sum(q for _, q in pairs)/scale result.density = result.mass/volume @@ -298,7 +298,7 @@ def formula( structure = ((1, cast(Atom, compound)), ) elif isinstance(compound, dict): structure = _convert_to_hill_notation(cast(dict[Atom, float], compound)) - elif _is_string_like(compound): + elif isinstance(compound, str): try: chem = parse_formula(compound, table=table) if name: @@ -403,7 +403,7 @@ def natural_mass_ratio(self) -> float: return (total_natural_mass + charge_correction)/(total_isotope_mass + charge_correction) @property - def natural_density(self) -> float: + def natural_density(self) -> float | None: """ |g/cm^3| @@ -411,6 +411,8 @@ def natural_density(self) -> float: replaced by the naturally occurring abundance of the element without changing the cell volume. """ + if self.density is None: + return None return self.density*self.natural_mass_ratio() @natural_density.setter @@ -455,7 +457,7 @@ def mass_fraction(self) -> dict[Atom, float]: return dict((a, m*a.mass/total_mass) for a, m in self.atoms.items()) # TODO: Remove compound._pf. It is unused and wrong. - def _pf(self) -> float: + def _pf(self) -> float | None: """ packing factor | unitless @@ -531,6 +533,7 @@ def volume(self, *args, **kw) -> float: # Compute atomic volume V = 0. for atom, count in self.atoms.items(): + assert atom.covalent_radius is not None, "Need covalent radius for "+str(atom) radius = atom.covalent_radius #if el.number == 1 and H_radius is not None: # radius = H_radius @@ -607,7 +610,7 @@ def change_table(self, table: PeriodicTable) -> "Formula": self.structure = _change_table(self.structure, table) return self - def replace(self, source, target, portion=1): + def replace(self, source, target, portion=1.0): """ Create a new formula with one atom/isotope substituted for another. @@ -688,6 +691,7 @@ def _isotope_substitution(compound: "Formula", source: Atom, target: Atom, porti """ # TODO: fails if density is not defined atoms = compound.atoms + assert compound.density is not None, "Need density for isotope substitution" if source in atoms: mass = compound.mass mass_reduction = atoms[source]*portion*(source.mass - target.mass) @@ -1099,13 +1103,6 @@ def _str_atoms(seq) -> str: return ret -def _is_string_like(val: Any) -> bool: - """Returns True if val acts like a string""" - try: - val+'' - except Exception: - return False - return True def from_subscript(value: str) -> str: """ diff --git a/periodictable/mass.py b/periodictable/mass.py index 1ad7b73..478481e 100644 --- a/periodictable/mass.py +++ b/periodictable/mass.py @@ -94,10 +94,10 @@ def init(table: PeriodicTable, reload: bool=False) -> None: if 'mass' in table.properties and not reload: return table.properties.append('mass') - Element.mass = property(mass, doc=mass.__doc__) + Element.mass = property(mass, doc=mass.__doc__) # type: ignore[assignment] Element.mass_units = "u" - Isotope.mass = property(mass, doc=mass.__doc__) - Isotope.abundance = property(abundance, doc=abundance.__doc__) + Isotope.mass = property(mass, doc=mass.__doc__) # type: ignore[assignment] + Isotope.abundance = property(abundance, doc=abundance.__doc__) # type: ignore[assignment] Isotope.abundance_units = "%" # Parse isotope mass table where each line looks like: diff --git a/pyproject.toml b/pyproject.toml index b647cfa..d5edf55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ "pytest-cov", "matplotlib", "sphinx", + "pytest-mypy", ] [project.urls] @@ -48,10 +49,14 @@ packages = ["periodictable"] [tool.pytest.ini_options] - addopts = ["--doctest-modules", "--doctest-glob=*.rst", "--cov=periodictable"] + addopts = ["--doctest-modules", "--doctest-glob=*.rst", "--cov=periodictable", "--mypy"] doctest_optionflags = "ELLIPSIS" pythonpath = ["doc/sphinx"] testpaths = ["periodictable", "test", "doc/sphinx/guide"] python_files = "*.py" python_classes = "NoClassTestsWillMatch" python_functions = ["test", "*_test", "test_*"] + +[tool.mypy] + files = ["test/test_element_types.py"] + follow_imports = "silent" # Check imports but don't report errors in them From f000b32654aa65fe4b3382d3eb516e3339dfb1ff Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 17 Feb 2026 16:39:17 -0500 Subject: [PATCH 02/19] adding test for element types --- test/test_element_types.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/test_element_types.py diff --git a/test/test_element_types.py b/test/test_element_types.py new file mode 100644 index 0000000..bb86338 --- /dev/null +++ b/test/test_element_types.py @@ -0,0 +1,22 @@ +import periodictable +from periodictable.core import Element, Isotope + +# Test symbol access +c_element: Element = periodictable.C +fe_element: Element = periodictable.Fe +u_element: Element = periodictable.U + +# Test name access +carbon: Element = periodictable.carbon +iron: Element = periodictable.iron +uranium: Element = periodictable.uranium + +# Test that we can access element properties +mass: float = periodictable.C.mass +symbol: str = periodictable.Fe.symbol +number: int = periodictable.U.number + +# Test special cases +deuterium: Element = periodictable.D +tritium: Isotope = periodictable.T +neutron: Element = periodictable.n From d11786734484a82b17c8e2a9c90c7a3e2f7da64c Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 17 Feb 2026 17:37:24 -0500 Subject: [PATCH 03/19] remaining fixes --- periodictable/fasta.py | 9 +++++---- periodictable/mass.py | 17 +++++++++-------- periodictable/mass_2001.py | 13 +++++++------ periodictable/nsf.py | 19 ++++++++++--------- pyproject.toml | 4 ++-- test/test_element_types.py | 2 +- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/periodictable/fasta.py b/periodictable/fasta.py index 43365d4..b378477 100644 --- a/periodictable/fasta.py +++ b/periodictable/fasta.py @@ -73,7 +73,7 @@ from pathlib import Path # Warning: name clash with Sequence from collections.abc import Iterator -from typing import IO +from typing import IO, cast from .formulas import formula as parse_formula, Formula, FormulaInput from .nsf import neutron_sld @@ -82,7 +82,7 @@ from .constants import avogadro_number # CRUFT 1.5.2: retaining fasta.isotope_substitution for compatibility -def isotope_substitution(formula: Formula, source: Atom, target: Atom, portion: float=1): +def isotope_substitution(formula: Formula, source: Atom, target: Atom, portion: float=1.0): """ Substitute one atom/isotope in a formula with another in some proportion. @@ -188,6 +188,7 @@ def __init__( M.density = 1e24*M.molecular_mass/cell_volume if cell_volume > 0 else 0 #print name, M.molecular_mass, cell_volume, M.density else: + assert M.density is not None, "Need density to compute cell volume" cell_volume = 1e24*M.molecular_mass/M.density H = M.replace(elements.H[1], elements.H) @@ -590,9 +591,9 @@ def fasta_table() -> None: for v in rows: protons = sum(num*el.number for el, num in v.natural_formula.atoms.items()) electrons = protons - v.charge - Xsld = xray_sld(v.formula, wavelength=elements.Cu.K_alpha) + Xsld = xray_sld(v.formula, wavelength=cast(float, elements.Cu.K_alpha)) print("%25s %7.1f %7.1f %7.1f %5.2f %5d %5.2f %5.2f %5.2f %5.1f"%( - v.name, v.mass, v.Dmass, v.cell_volume, v.natural_formula.density, + v.name, v.mass, v.Dmass, v.cell_volume, v.natural_formula.density or 0., electrons, Xsld[0], v.sld, v.Dsld, v.D2Omatch)) beta_casein = "RELEELNVPGEIVESLSSSEESITRINKKIEKFQSEEQQQTEDELQDKIHPFAQTQSLVYPFPGPIPNSLPQNIPPLTQTPVVVPPFLQPEVMGVSKVKEAMAPKHKEMPFPKYPVEPFTESQSLTLTDVENLHLPLPLLQSWMHQPHQPLPPTVMFPPQSVLSLSQSKVLPVPQKAVPYPQRDMPIQAFLLYQEPVLGPVRGPFPIIV" diff --git a/periodictable/mass.py b/periodictable/mass.py index 478481e..dbc640a 100644 --- a/periodictable/mass.py +++ b/periodictable/mass.py @@ -61,6 +61,7 @@ materials (IUPAC Technical Report). Pure and Applied Chemistry, 93(1), 155-166. https://doi.org/10.1515/pac-2018-0916 """ +from typing import cast from .core import Element, Isotope, PeriodicTable, default_table from .util import parse_uncertainty @@ -113,9 +114,9 @@ def init(table: PeriodicTable, reload: bool=False) -> None: iso = el.add_isotope(int(astr)) # Note: new mass table doesn't include nominal values for transuranics # so use old masses here and override later with new masses. - el._mass, el._mass_unc = parse_uncertainty(el_mass) + el._mass, el._mass_unc = cast(tuple[float, float], parse_uncertainty(el_mass)) #el._mass, el._mass_unc = None, None - iso._mass, iso._mass_unc = parse_uncertainty(iso_mass) + iso._mass, iso._mass_unc = cast(tuple[float, float], parse_uncertainty(iso_mass)) #iso._abundance, iso._abundance_unc = parse_uncertainty(p) iso._abundance, iso._abundance_unc = 0, 0 @@ -139,7 +140,7 @@ def init(table: PeriodicTable, reload: bool=False) -> None: #from uncertainties import ufloat as U #if delta > 0.01: # print(f"{el.number}-{el.symbol} mass changed by {delta:.2f}% to {U(v,dv):fS} from {U(el._mass,el._mass_unc):fS}") - el._mass, el._mass_unc = parse_uncertainty(valstr) + el._mass, el._mass_unc = cast(tuple[float, float], parse_uncertainty(valstr)) #Li_ratio = table.Li[7]._abundance/table.Li[6]._abundance @@ -176,25 +177,25 @@ def init(table: PeriodicTable, reload: bool=False) -> None: #print(line) parts = line.strip().split() #print(parts) - value[int(parts[0])] = parse_uncertainty(parts[1]) + value[int(parts[0])] = cast(tuple[float, float], parse_uncertainty(parts[1])) #new_Li_ratio = table.Li[7]._abundance/table.Li[6]._abundance #print(f"Li6:Li7 ratio changed from {Li_ratio:.1f} to {new_Li_ratio:.1f}") def print_natural_mass(table: PeriodicTable|None=None) -> None: - from uncertainties import ufloat as U + from uncertainties import ufloat as U # type: ignore[import-untyped] table = default_table(table) for el in table: iso_mass = [ - U(iso.abundance, iso._abundance_unc)/100*U(iso.mass, iso._mass_unc) + U(iso.abundance, iso._abundance_unc)/100*U(iso.mass, iso._mass_unc) # type: ignore[operator] for iso in el if iso.abundance>0] if iso_mass: el_mass = U(el.mass, el._mass_unc) iso_sum = sum(iso_mass) - delta = el_mass - iso_sum + delta = el_mass - iso_sum # type: ignore[operator] # python 3.6 and above only - if abs(delta.n) > 1e-3 or delta.s/iso_sum.n > 0.01: + if abs(delta.n) > 1e-3 or delta.s/iso_sum.n > 0.01: # type: ignore[operator,union-attr] print(f"{el.number}-{el}: {el_mass:fS}, sum: {iso_sum:fS}, Δ={delta:fS}") #print(f"{el.number}-{el}: {delta:fS}") #print(f"{el.number}-{el}: {el_mass:fS}, sum: {iso_sum:fS}, Δ=") diff --git a/periodictable/mass_2001.py b/periodictable/mass_2001.py index 48d8227..395d9e7 100644 --- a/periodictable/mass_2001.py +++ b/periodictable/mass_2001.py @@ -55,6 +55,7 @@ and High-Energy Physics, Amsterdam, The Netherlands. """ +from typing import cast from .core import Element, Isotope, PeriodicTable from .mass import mass, abundance from .util import parse_uncertainty @@ -64,10 +65,10 @@ def init(table: PeriodicTable, reload: bool=False) -> None: if 'mass' in table.properties and not reload: return table.properties.append('mass') - Element.mass = property(mass, doc=mass.__doc__) + Element.mass = property(mass, doc=mass.__doc__) # type: ignore Element.mass_units = "u" - Isotope.mass = property(mass, doc=mass.__doc__) - Isotope.abundance = property(abundance, doc=abundance.__doc__) + Isotope.mass = property(mass, doc=mass.__doc__) # type: ignore + Isotope.abundance = property(abundance, doc=abundance.__doc__) # type: ignore Isotope.abundance_units = "%" for line in massdata.split('\n'): @@ -77,9 +78,9 @@ def init(table: PeriodicTable, reload: bool=False) -> None: assert el.symbol == sym, \ "Symbol %s does not match %s"%(sym, el.symbol) iso = el.add_isotope(int(a)) - el._mass, el._mass_unc = parse_uncertainty(avg) - iso._mass, iso._mass_unc = parse_uncertainty(m) - iso._abundance,iso._abundance_unc = parse_uncertainty(p) if p else (0,0) + el._mass, el._mass_unc = cast(tuple[float, float], parse_uncertainty(avg)) + iso._mass, iso._mass_unc = cast(tuple[float, float], parse_uncertainty(m)) + iso._abundance, iso._abundance_unc = cast(tuple[float, float], parse_uncertainty(p)) if p else (0, 0) # # A single neutron is an isotope of element 0 # from .constants import neutron_mass diff --git a/periodictable/nsf.py b/periodictable/nsf.py index 68b0f89..bd10e12 100644 --- a/periodictable/nsf.py +++ b/periodictable/nsf.py @@ -464,7 +464,7 @@ def has_sld(self) -> bool: return self.b_c is not None and self._number_density is not None # PAK 2021-04-05: allow energy dependent b_c - def scattering_by_wavelength(self, wavelength: ArrayLike) -> tuple[ArrayLike, ArrayLike]|tuple[None, None]: + def scattering_by_wavelength(self, wavelength: float | NDArray) -> tuple[float | NDArray, float | NDArray]|tuple[None, None]: r""" Return scattering length and total cross section for each wavelength. @@ -497,7 +497,7 @@ def scattering_by_wavelength(self, wavelength: ArrayLike) -> tuple[ArrayLike, Ar sigma_s = _4PI_100*abs(b_c)**2 # 1 barn = 1 fm^2 1e-2 barn/fm^2 return b_c, sigma_s - def sld(self, *, wavelength: ArrayLike=ABSORPTION_WAVELENGTH) -> NDArray|None: + def sld(self, *, wavelength: float | NDArray =ABSORPTION_WAVELENGTH) -> NDArray|None: r""" Returns scattering length density for the element at natural abundance and density. @@ -518,7 +518,7 @@ def sld(self, *, wavelength: ArrayLike=ABSORPTION_WAVELENGTH) -> NDArray|None: return None return self.scattering(wavelength=wavelength)[0] - def scattering(self, *, wavelength: ArrayLike=ABSORPTION_WAVELENGTH) -> tuple[NDArray, NDArray, NDArray]|tuple[None, None, None]: + def scattering(self, *, wavelength: float | NDArray=ABSORPTION_WAVELENGTH) -> tuple[NDArray, NDArray, NDArray]|tuple[None, None, None]: r""" Returns neutron scattering information for the element at natural abundance and density. @@ -567,8 +567,8 @@ def energy_dependent_init(table: PeriodicTable) -> None: # Lu nat missing from Lynn and Seeger, so mix Lu[175] and Lu[176] Lu175 = table.Lu[175] Lu176 = table.Lu[176] - bc_175 = Lu175.neutron.b_c_complex - wavelength, bc_176 = Lu176.neutron.nsf_table + bc_175 = cast(complex, Lu175.neutron.b_c_complex) + wavelength, bc_176 = cast(tuple[DoubleArray, DoubleArray], Lu176.neutron.nsf_table) bc_nat = (bc_175*Lu175.abundance + bc_176*Lu176.abundance)/100.0 # 1 fm = 1fm * %/100 table.Lu.neutron.nsf_table = wavelength, cast(DoubleArray, bc_nat) #table.Lu.neutron.total = 0. # zap total cross section @@ -680,7 +680,7 @@ def init(table: PeriodicTable, reload: bool=False) -> None: def neutron_scattering( compound: "FormulaInput", *, density: float|None=None, - wavelength: ArrayLike|None=None, + wavelength: float|NDArray|None=None, energy: ArrayLike|None=None, natural_density: float|None=None, table: PeriodicTable|None=None, @@ -940,7 +940,8 @@ def neutron_scattering( # Sum over the quantities (note: formulas can have fractional atoms) molar_mass, num_atoms = 0., 0. - b_c, sigma_s = 0., 0. + b_c: float|NDArray = 0. + sigma_s: float|NDArray = 0. is_energy_dependent = False for element, quantity in compound.atoms.items(): # TODO: use NaN rather than None @@ -949,7 +950,7 @@ def neutron_scattering( molar_mass += element.mass*quantity num_atoms += quantity # PAK 2021-04-05: allow energy dependent b_c, b'' - b_ck, sigma_sk = element.neutron.scattering_by_wavelength(wavelength) + b_ck, sigma_sk = cast(tuple[float|NDArray, float|NDArray], element.neutron.scattering_by_wavelength(wavelength)) #print(f"{element=}; {b_ck=}; {sigma_sk=}") b_c += quantity * b_ck sigma_s += quantity * sigma_sk @@ -958,7 +959,7 @@ def neutron_scattering( # If nothing to sum, return values for a vacuum. This might be because # the material has no atoms or it might be because the density is zero. if molar_mass*compound.density == 0: - return (0, 0, 0), (0, 0, 0), inf + return (0, 0, 0), (0, 0, 0), inf # type: ignore[return-value] # Turn weighted sums into scattering factors b_c /= num_atoms diff --git a/pyproject.toml b/pyproject.toml index d5edf55..c916118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,5 +58,5 @@ python_functions = ["test", "*_test", "test_*"] [tool.mypy] - files = ["test/test_element_types.py"] - follow_imports = "silent" # Check imports but don't report errors in them +#files = ["test/test_element_types.py"] +follow_imports = "silent" # Check imports but don't report errors in them diff --git a/test/test_element_types.py b/test/test_element_types.py index bb86338..e04d9f5 100644 --- a/test/test_element_types.py +++ b/test/test_element_types.py @@ -17,6 +17,6 @@ number: int = periodictable.U.number # Test special cases -deuterium: Element = periodictable.D +deuterium: Isotope = periodictable.D tritium: Isotope = periodictable.T neutron: Element = periodictable.n From 00893ff07408026a4ead6aba034c4f072318d932 Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 17 Feb 2026 17:40:41 -0500 Subject: [PATCH 04/19] use isinstance instead of try/except treating as Isotope --- periodictable/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/periodictable/core.py b/periodictable/core.py index e342b3b..a816d01 100644 --- a/periodictable/core.py +++ b/periodictable/core.py @@ -661,12 +661,12 @@ def __str__(self) -> str: def __repr__(self) -> str: return repr(self.element)+'.ion[%d]'%self.charge def __reduce__(self): - try: + if isinstance(self.element, Isotope): return _make_isotope_ion, (self.element.table, self.element.number, self.element.isotope, self.charge) - except Exception: + else: return _make_ion, (self.element.table, self.element.number, self.charge) From d1b492c44a7dda11aa8a802eabbcb26407bd04a6 Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 17 Feb 2026 18:03:08 -0500 Subject: [PATCH 05/19] add pytest-mypy to test env --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 165acdc..af1e185 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,7 +77,7 @@ jobs: - name: Install Python dependencies run: | - python -m pip install pytest pytest-cov + python -m pip install pytest pytest-cov pytest-mypy # Change into the test directory to test the wheel so that the # source directory is not on the path. Full tests with coverage are From ab63b48f45c72f1344a49caf59fd87a5193a8a95 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 21:22:33 -0500 Subject: [PATCH 06/19] Make sure that mypy knows that numpy.abs is in use in nsf.py --- periodictable/nsf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/periodictable/nsf.py b/periodictable/nsf.py index bd10e12..4d404ec 100644 --- a/periodictable/nsf.py +++ b/periodictable/nsf.py @@ -494,7 +494,7 @@ def scattering_by_wavelength(self, wavelength: float | NDArray) -> tuple[float | #b_c = np.interp(energy, self.nsf_table[0], self.nsf_table[1]) b_c = np.interp(wavelength, self.nsf_table[0], self.nsf_table[1]) # TODO: sigma_s should include an incoherent contribution - sigma_s = _4PI_100*abs(b_c)**2 # 1 barn = 1 fm^2 1e-2 barn/fm^2 + sigma_s = _4PI_100*np.abs(b_c)**2 # 1 barn = 1 fm^2 1e-2 barn/fm^2 return b_c, sigma_s def sld(self, *, wavelength: float | NDArray =ABSORPTION_WAVELENGTH) -> NDArray|None: From 649b9962e13865a07e4048665d74c0209c4d9bb9 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 22:12:12 -0500 Subject: [PATCH 07/19] include pytest-mypy dependency in CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af1e185..d375274 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: Run the tests run: | - python -m pip install numpy pyparsing pytest pytest-cov + python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy pytest -v - name: Build the docs From e9ee7b48469d7bb7655b9f69af50ac65aaa3cbb3 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 22:15:33 -0500 Subject: [PATCH 08/19] run CI even if base branch is not master --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d375274..1b7cbe4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + # branches: [ master ] release: types: [published] From c5cb0834ba28ae61ed7aa00d24351bb777ed65ae Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 22:23:00 -0500 Subject: [PATCH 09/19] attempt to fix mypy stubs error for matplotlib --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b7cbe4..ad6b6b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: Run the tests run: | - python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy + python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy matplotlib-stubs pytest -v - name: Build the docs @@ -77,7 +77,7 @@ jobs: - name: Install Python dependencies run: | - python -m pip install pytest pytest-cov pytest-mypy + python -m pip install pytest pytest-cov pytest-mypy matplotlib-stubs # Change into the test directory to test the wheel so that the # source directory is not on the path. Full tests with coverage are From c98714bc464d9165042d5cee1203ac75f70d1895 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 22:43:40 -0500 Subject: [PATCH 10/19] attempt to fix mypy stubs error for matplotlib and uncertainties --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad6b6b5..25e7f95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,9 +31,10 @@ jobs: name: wheel path: dist/*.whl + # uncertainties and matplotlib are optional packages, but they are needed for mypy tests - name: Run the tests run: | - python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy matplotlib-stubs + python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy uncertainties matplotlib matplotlib-stubs pytest -v - name: Build the docs @@ -75,9 +76,10 @@ jobs: run: python -m pip install dist/periodictable*.whl shell: bash + # uncertainties and matplotlib are optional packages, but they are needed for mypy tests - name: Install Python dependencies run: | - python -m pip install pytest pytest-cov pytest-mypy matplotlib-stubs + python -m pip install pytest pytest-cov pytest-mypy matplotlib-stubs matplotlib uncertainties # Change into the test directory to test the wheel so that the # source directory is not on the path. Full tests with coverage are From 5d19c8a9b7141d419b1916fc36f1b5800d7a27fa Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 23:30:32 -0500 Subject: [PATCH 11/19] fix tests against installed wheels --- .github/workflows/test.yml | 2 +- MANIFEST.in | 1 + pyproject.toml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 25e7f95..8f32418 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,7 +87,7 @@ jobs: - name: Test wheel with pytest run: | cd test - pytest -v --pyargs --import-mode=append periodictable . ../doc/sphinx/guide + pytest -v . ../doc/sphinx/guide # Upload wheel to PyPI only when a tag is pushed, and its name begins with 'v' upload-to-pypi: diff --git a/MANIFEST.in b/MANIFEST.in index e53b68d..12574b4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include README.rst graft periodictable/xsf include periodictable/f0_WaasKirf.dat include periodictable/activation.dat +include periodictable/py.typed prune doc prune doc/sphinx/_build prune doc/sphinx/build diff --git a/pyproject.toml b/pyproject.toml index c916118..0463d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,10 @@ "build", "pytest", "pytest-cov", + "pytest-mypy", "matplotlib", "sphinx", - "pytest-mypy", + "uncertainties", ] [project.urls] From 7e5e4ebffae1147e78bac452ca8f36103b10ba13 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 17 Feb 2026 23:34:16 -0500 Subject: [PATCH 12/19] fix tests against installed wheels --- periodictable/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 periodictable/py.typed diff --git a/periodictable/py.typed b/periodictable/py.typed new file mode 100644 index 0000000..e69de29 From a44e7232e80944d695b6e38e1a4404092ac97914 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 00:21:56 -0500 Subject: [PATCH 13/19] fix tests against installed wheels --- .github/workflows/test.yml | 6 +++++- test/test_activation.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f32418..b069bd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,10 +84,14 @@ jobs: # Change into the test directory to test the wheel so that the # source directory is not on the path. Full tests with coverage are # run before building the wheel. + # Use --pyargs --import-mode=append to target the installed package. + # The mypy plugin fails when testing the installed package. It doesn't + # support --no-mypy so we erase the addopts configuration from pyproject.toml + # and put the remaining options directly on the pytest command line. - name: Test wheel with pytest run: | cd test - pytest -v . ../doc/sphinx/guide + pytest -v --override-ini addopts= --doctest-modules --doctest-glob=*.rst --pyargs --import-mode=append periodictable . ../doc/sphinx/guide # Upload wheel to PyPI only when a tag is pushed, and its name begins with 'v' upload-to-pypi: diff --git a/test/test_activation.py b/test/test_activation.py index 7e2bba6..d53c3ab 100644 --- a/test/test_activation.py +++ b/test/test_activation.py @@ -4,6 +4,10 @@ from periodictable.activation import Sample, ActivationEnvironment from periodictable.activation import IAEA1987_isotopic_abundance, table_abundance +# Verify that periodictable is being imported from the site-packages directory +# If that is the case we should see the following test failure on the CI +if "site-packages" in pt.__file__: raise BogusException + def test(): # This is not a very complete test of the activation calculator. # Mostly just a smoke test to see that things run and produce the From d6b841410be9595abd20dd04a4dc7a64a6bcd1fa Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 00:24:17 -0500 Subject: [PATCH 14/19] fix tests against installed wheels --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b069bd4..7fb73a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Run the tests run: | python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy uncertainties matplotlib matplotlib-stubs - pytest -v + #pytest -v - name: Build the docs run: | From 781672c68a4934a39f3e9ad649bf23a346992194 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 00:26:29 -0500 Subject: [PATCH 15/19] fix tests against installed wheels --- .github/workflows/test.yml | 2 +- test/test_activation.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fb73a2..b069bd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Run the tests run: | python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy uncertainties matplotlib matplotlib-stubs - #pytest -v + pytest -v - name: Build the docs run: | diff --git a/test/test_activation.py b/test/test_activation.py index d53c3ab..7e2bba6 100644 --- a/test/test_activation.py +++ b/test/test_activation.py @@ -4,10 +4,6 @@ from periodictable.activation import Sample, ActivationEnvironment from periodictable.activation import IAEA1987_isotopic_abundance, table_abundance -# Verify that periodictable is being imported from the site-packages directory -# If that is the case we should see the following test failure on the CI -if "site-packages" in pt.__file__: raise BogusException - def test(): # This is not a very complete test of the activation calculator. # Mostly just a smoke test to see that things run and produce the From 7dc020759e085bf8ce86ca6cf84be63c80006df0 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 00:30:41 -0500 Subject: [PATCH 16/19] fix tests against installed wheels --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b069bd4..cc33dc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: - name: Test wheel with pytest run: | cd test - pytest -v --override-ini addopts= --doctest-modules --doctest-glob=*.rst --pyargs --import-mode=append periodictable . ../doc/sphinx/guide + pytest -v --pyargs --import-mode=append periodictable --override-ini addopts= --doctest-modules --doctest-glob=*.rst --cov=periodictable . ../doc/sphinx/guide # Upload wheel to PyPI only when a tag is pushed, and its name begins with 'v' upload-to-pypi: From 75ff761e24264acff1c04528798f5ac2300df03d Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 00:44:35 -0500 Subject: [PATCH 17/19] added isotope attribute tests to elements and isotopes, and ions of each --- test/test_element_types.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_element_types.py b/test/test_element_types.py index e04d9f5..24140b8 100644 --- a/test/test_element_types.py +++ b/test/test_element_types.py @@ -20,3 +20,12 @@ deuterium: Isotope = periodictable.D tritium: Isotope = periodictable.T neutron: Element = periodictable.n + +# Test isotopes have isotope attribute +D_iso: int = periodictable.D.isotope +Dion_iso: int = periodictable.D.ion[1].isotope +Fe56_iso: int = periodictable.Fe[56].isotope + +# Test that elements do not +assert not hasattr(periodictable.H, "isotope") +assert not hasattr(periodictable.H.ion[1], "isotope") From 7630b2ae68f5dac8637faca5b75acee25aa2bcfd Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 12:04:12 -0500 Subject: [PATCH 18/19] use dependency groups so test dependency can be installed without installing the package --- .github/workflows/test.yml | 22 +++++++++------------- pyproject.toml | 22 ++++++++++++++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc33dc9..fbd9a8e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: - name: Build the wheel run: | - python -m pip install build + python -m pip install --group dev python -m build - name: Upload wheel @@ -31,16 +31,12 @@ jobs: name: wheel path: dist/*.whl - # uncertainties and matplotlib are optional packages, but they are needed for mypy tests - name: Run the tests run: | - python -m pip install numpy pyparsing pytest pytest-cov pytest-mypy uncertainties matplotlib matplotlib-stubs pytest -v - name: Build the docs run: | - python -m pip install matplotlib sphinx - python -m pip install dist/periodictable*.whl make -j 4 -C doc/sphinx SPHINXOPTS="-W --keep-going" html # Test the wheel on different platforms @@ -77,17 +73,17 @@ jobs: shell: bash # uncertainties and matplotlib are optional packages, but they are needed for mypy tests - - name: Install Python dependencies + - name: Install test dependencies run: | - python -m pip install pytest pytest-cov pytest-mypy matplotlib-stubs matplotlib uncertainties + python -m pip install --group test - # Change into the test directory to test the wheel so that the - # source directory is not on the path. Full tests with coverage are - # run before building the wheel. # Use --pyargs --import-mode=append to target the installed package. - # The mypy plugin fails when testing the installed package. It doesn't - # support --no-mypy so we erase the addopts configuration from pyproject.toml - # and put the remaining options directly on the pytest command line. + # Change into the test directory to test the wheel so that the + # source directory is not on the path. We are running coverage tests + # because it shows that it is indeed the installed package that is tested. + # The mypy plugin fails when testing the installed package. Since there is no + # way to turn the option off, so we instead erase the addopts configuration + # and put them explicitly on the pytest command line below without --mypy. - name: Test wheel with pytest run: | cd test diff --git a/pyproject.toml b/pyproject.toml index 0463d1c..fecc903 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,15 +24,18 @@ ] requires-python = ">=3.8" -[project.optional-dependencies] +[dependency-groups] + build = ["build"] + # Matplotlib and uncertainties are optional packages, used for making + # plots in the docs and generating neutron data tables for the web. + # mypy checks all code so they are needed for testing as well. + optional = ["uncertainties", "matplotlib"] + docs = ["sphinx", {include-group = "optional"}] + test = ["pytest", "pytest-cov", "pytest-mypy", {include-group = "optional"}] dev = [ - "build", - "pytest", - "pytest-cov", - "pytest-mypy", - "matplotlib", - "sphinx", - "uncertainties", + {include-group = "build"}, + {include-group = "docs"}, + {include-group = "test"}, ] [project.urls] @@ -50,6 +53,9 @@ packages = ["periodictable"] [tool.pytest.ini_options] + # mypy doesn't work on the wheel tests, so these options are repeated in + # .github/workflows/test.yml. We could use pytest-enabler with "pytest -p no:mypy" + # but that introduces yet another dependency. addopts = ["--doctest-modules", "--doctest-glob=*.rst", "--cov=periodictable", "--mypy"] doctest_optionflags = "ELLIPSIS" pythonpath = ["doc/sphinx"] From 1d061535af12de2dbccf5f49877b3738f4307722 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Wed, 18 Feb 2026 12:10:33 -0500 Subject: [PATCH 19/19] We are using liat[float], etc., so push min python version to 3.10 --- periodictable/crystal_structure.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/periodictable/crystal_structure.py b/periodictable/crystal_structure.py index 67001d8..a701fc6 100644 --- a/periodictable/crystal_structure.py +++ b/periodictable/crystal_structure.py @@ -40,10 +40,10 @@ This data is from Ashcroft and Mermin. ''' -from typing import Any, Dict, List, Union +from typing import Any from .core import PeriodicTable -crystal_structures: List[Union[Dict[str, Any], None]] = [ +crystal_structures: list[dict[str, Any]|None] = [ {'symmetry': 'diatom', 'd': 0.74}, #H {'symmetry': 'atom'}, #He {'symmetry': 'BCC', 'a': 3.49}, #Li diff --git a/pyproject.toml b/pyproject.toml index fecc903..36ec19b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ "Topic :: Scientific/Engineering :: Chemistry", "Topic :: Scientific/Engineering :: Physics", ] - requires-python = ">=3.8" + requires-python = ">=3.10" [dependency-groups] build = ["build"]