Modernize packaging, make ELSEPA configurable, add basedpyright #1

Merged
drawblank merged 8 commits from modernize-packaging-and-fixes into master 2026-08-17 12:06:41 +00:00
Contributor

Eight commits, grouped so the whitespace churn stays out of the code changes.

Packaging

setup.py used from distutils.core import setup. distutils was removed from the stdlib in Python 3.12 (PEP 632), so it only worked via the setuptools shim. Replaced with pyproject.toml: dynamic version from cstool.__version__, requires-python = ">=3.12", SPDX license, and setuptools + importlib_resources dropped from runtime dependencies (the latter was declared but never imported).

ELSEPA configuration

The ELSEPA distribution directory was only configurable by putting it on PATH, which is the wrong mechanism — ELSEPA reads its z_nnn.den database from the directory holding the binary, so it is a data directory, not a binary search path. Now resolved via --elsepa-dir, then $ELSEPA_DIR, then PATH, with validation up front.

This also fixes error reporting: shutil.which() returns None rather than raising, so the old try/except never fired and the code silently copied the entire working directory to a temp dir before failing with a confusing FileNotFoundError.

Bug fixes found by basedpyright

  • Two NameErrors in ashley.py (L_ashley_wo_ex vs L_Ashley_wo_ex) on functions exported from dielectric_function/__init__.py — public API that raised on first call.
  • dimfp_table.IMFP_ICDF referenced an undefined P_omega.
  • parse_photoat / parse_atomic_relax / parse_electrons returned None for an unknown element, surfacing as AttributeError: 'NoneType' object has no attribute 'reactions' several frames later.
  • endf_reader hit a bare ValueError on EOF.
  • band_structure's "Unknown band structure model {}" was never .format()ed.
  • optical.py's bare except: reported every failure as "No optical data provided".

Also fixed the .. parent traversal in importlib.resources, which is not supported by the Traversable protocol and would break on a zipped install.

basedpyright

Configured at typeCheckingMode = "standard" with eight rules disabled, each verified to fire only on pint and numba stub deficiencies rather than on project code. Reports 0 errors, 0 warnings.

Verification

uv build succeeds, basedpyright is clean, and a full cstool ./silicon.yaml compile runs to completion (exit 0) with ELSEPA_DIR set.

Not addressed

  • interpolate.py:17 still emits UserWarning: 'where' used without 'out', expect uninitialized memory in output twice per run.
  • obtain_endf_files still downloads into the installed package directory; a user cache directory would be the durable fix.
  • band_structure.__init__ validates model after loading fields, so an unknown model with missing fields still dies with KeyError first.
Eight commits, grouped so the whitespace churn stays out of the code changes. ## Packaging `setup.py` used `from distutils.core import setup`. distutils was removed from the stdlib in Python 3.12 (PEP 632), so it only worked via the setuptools shim. Replaced with `pyproject.toml`: dynamic version from `cstool.__version__`, `requires-python = ">=3.12"`, SPDX license, and `setuptools` + `importlib_resources` dropped from runtime dependencies (the latter was declared but never imported). ## ELSEPA configuration The ELSEPA distribution directory was only configurable by putting it on `PATH`, which is the wrong mechanism — ELSEPA reads its `z_nnn.den` database from the directory holding the binary, so it is a data directory, not a binary search path. Now resolved via `--elsepa-dir`, then `$ELSEPA_DIR`, then `PATH`, with validation up front. This also fixes error reporting: `shutil.which()` returns `None` rather than raising, so the old `try/except` never fired and the code silently copied the entire working directory to a temp dir before failing with a confusing `FileNotFoundError`. ## Bug fixes found by basedpyright - Two `NameError`s in `ashley.py` (`L_ashley_wo_ex` vs `L_Ashley_wo_ex`) on functions exported from `dielectric_function/__init__.py` — public API that raised on first call. - `dimfp_table.IMFP_ICDF` referenced an undefined `P_omega`. - `parse_photoat` / `parse_atomic_relax` / `parse_electrons` returned `None` for an unknown element, surfacing as `AttributeError: 'NoneType' object has no attribute 'reactions'` several frames later. - `endf_reader` hit a bare `ValueError` on EOF. - `band_structure`'s "Unknown band structure model {}" was never `.format()`ed. - `optical.py`'s bare `except:` reported every failure as "No optical data provided". Also fixed the `..` parent traversal in `importlib.resources`, which is not supported by the Traversable protocol and would break on a zipped install. ## basedpyright Configured at `typeCheckingMode = "standard"` with eight rules disabled, each verified to fire only on pint and numba stub deficiencies rather than on project code. Reports **0 errors, 0 warnings**. ## Verification `uv build` succeeds, basedpyright is clean, and a full `cstool ./silicon.yaml` compile runs to completion (exit 0) with `ELSEPA_DIR` set. ## Not addressed - `interpolate.py:17` still emits `UserWarning: 'where' used without 'out', expect uninitialized memory in output` twice per run. - `obtain_endf_files` still downloads into the installed package directory; a user cache directory would be the durable fix. - `band_structure.__init__` validates `model` after loading fields, so an unknown model with missing fields still dies with `KeyError` first.
The repository had no .gitignore, so build/, *.egg-info/, __pycache__/
and .venv/ all showed as untracked. Two further entries matter here:

- cstool/data/endf_data/ holds the ENDF archives that
  cstool.endf.obtain_endf_files downloads at runtime into the package
  directory (~16 MB).
- *.mat are the compiled material files cstool produces; a single
  silicon.mat is ~550 MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These files mixed tab and space indentation. The content is unchanged:
git diff --ignore-all-space is empty for every file in this commit.
Tab-separated columns become space-separated. The numeric content is
unchanged: git diff --ignore-all-space is empty for all 37 files.
pkg_resources is removed in setuptools 81+, so the ENDF resource lookup
had to move to importlib.resources.

Two corrections to that migration:

- files(__name__).joinpath('../data/...') relied on the Traversable being
  a real pathlib.Path so the OS could resolve the '..'. The Traversable
  protocol does not support parent traversal, so this breaks for a zipped
  install, where files() returns a zipfile.Path and '..' is looked up as a
  literal entry. Addressed by anchoring at files('cstool') instead.
- download_file swallowed the underlying network error; it now chains it
  with 'raise ... from e'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setup.py used `from distutils.core import setup`. distutils was removed
from the stdlib in Python 3.12 (PEP 632), so this only worked because
setuptools injects a shim via distutils-precedence.pth:

    $ python3.14 -c "import distutils; print(distutils.__file__)"
    /usr/lib/python3.14/site-packages/setuptools/_distutils/__init__.py

That shim is also why setuptools was listed as a runtime dependency. With
a proper [build-system] table it goes back to being a build dependency.

Other changes:

- Version is now dynamic from cstool.__version__, instead of being
  duplicated in setup.py and cstool/__init__.py.
- importlib_resources dropped: the backport was declared but never
  imported; the code uses the stdlib importlib.resources.
- requires-python = ">=3.12", which is what the code actually needs.
  cstool.endf.obtain_endf_files calls importlib.resources.as_file() on a
  directory, and directory support landed in 3.12 (see the _temp_dir /
  _temp_file dispatch in importlib/resources/_common.py).
- license declared as an SPDX expression per PEP 639.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_elscata located ELSEPA by taking dirname(shutil.which('elscata')),
which meant the only way to configure it was to put the ELSEPA
distribution directory on PATH.

PATH is the wrong mechanism here. ELSEPA reads its z_nnn.den and
z_nnn.dfs atomic database from the directory holding the binary
("These files must be placed in the same directory as the executable
binary file." - ELSEPA readme.txt), which is why run_elscata copies the
whole directory rather than just the binary. So the setting is really a
data directory, and the obvious tidy-up of symlinking elscata into
~/.local/bin silently breaks: the copy then contains no database.

find_elsepa_dir() now resolves, in order: an explicit argument, the
ELSEPA_DIR environment variable, then elscata on PATH. It validates that
the directory exists, holds elscata, and holds the z_nnn.den files.
mott_dimfp and run_elscata_parallel take an elsepa_dir argument, and
cstool grows a --elsepa-dir flag. Resolution happens once in
run_elscata_parallel so a bad configuration produces one clear error
rather than one per thread-pool worker.

This also fixes the error reporting. shutil.which() returns None rather
than raising, so the previous try/except never fired; str(None) became
the literal "None", Path("None").parent became ".", and "." exists, so
the code silently copied the entire current working directory to a temp
dir before failing with a confusing FileNotFoundError on the missing
binary.

Also drops apps/cstool.py's use of cst.input_data.param_file, which only
resolved because an unrelated top-level import bound input_data as an
attribute of the cstool package. param_file was already imported directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by basedpyright (added in the next commit).

Three NameErrors:

- ashley.py called L_ashley_wo_ex and L_ashley_w_ex, but the functions
  are defined as L_Ashley_wo_ex and L_Ashley_w_ex. Both dimfp_ashley and
  dimfp_ashley_exchange are exported from dielectric_function/__init__.py,
  so this was public API that raised on first call.
- dimfp_table.IMFP_ICDF used len(P_omega), which does not exist; the
  parameter is P. The method has no callers, so it was never reached.

Three silent failures:

- parse_photoat, parse_atomic_relax and parse_electrons fell off the end
  returning None when the archive held no file for that element, so an
  unsupported Z surfaced several frames later as
  "AttributeError: 'NoneType' object has no attribute 'reactions'".
  They now raise, e.g. "No electron ionization data for Z=999 in
  .../electrons.zip". The three near-identical bodies share a helper.
- endf_reader called int(line[70:72]) on the empty string returned at
  EOF, giving a bare ValueError. It now reports reaching end of file
  while looking for the first header.
- band_structure's "Unknown band structure model {}" was never
  .format()ed, so it printed a literal {}.

optical.py's bare `except:` reported every failure as "No optical data
provided"; it now checks for the missing key explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
basedpyright defaults to typeCheckingMode = "recommended", which on this
codebase reports 137 errors and 2616 warnings. Almost all of it is the
"nothing is annotated" family: reportUnknownMemberType (625),
reportUnknownArgumentType (592), reportUnknownVariableType (426),
reportMissingParameterType (291), reportAny (108).

"standard" drops that family and leaves 76 diagnostics. Of those, eight
rules are disabled here, each verified to fire only on third-party stub
deficiencies rather than on our code:

- pint (reportIndexIssue, reportOperatorIssue, reportOptionalOperand,
  reportAttributeAccessIssue) types units(...) as a union including Unit,
  and PlainQuantity declares neither __getitem__ nor __setitem__, so
  `np.zeros(n) * units('nm^-1')` followed by `imfp[i] = x` looks illegal
  despite working.
- numba (reportPrivateImportUsage, reportFunctionMemberAccess,
  reportCallIssue, reportArgumentType) does not re-export types/carray in
  its stubs and attaches .ctypes to jitted functions at runtime.

The remaining rules stay enabled; the diagnostics they reported were real
and are fixed in the preceding commit. Result is 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drawblank merged commit 5fd60f82a0 into master 2026-08-17 12:06:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
science/cstool!1
No description provided.