diff --git a/.github/workflows/deploypypi.yml b/.github/workflows/deploypypi.yml index 5108d0602..c2daeeb08 100644 --- a/.github/workflows/deploypypi.yml +++ b/.github/workflows/deploypypi.yml @@ -22,7 +22,6 @@ jobs: - name: Install dependencies run: | python3 -m pip install --upgrade pip - python3 -m pip install -r requirements.txt python3 -m pip install --upgrade setuptools setuptools_scm[toml] wheel twine build - name: Build diff --git a/.github/workflows/deploytestpypi.yml b/.github/workflows/deploytestpypi.yml index 06e8bd650..8d96d8a21 100644 --- a/.github/workflows/deploytestpypi.yml +++ b/.github/workflows/deploytestpypi.yml @@ -27,7 +27,6 @@ jobs: - name: Install dependencies run: | python3 -m pip install --upgrade pip - python3 -m pip install -r requirements.txt python3 -m pip install --upgrade setuptools setuptools_scm[toml] wheel twine build - name: Build diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d196944f0..f394e7c9d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -37,11 +37,18 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Cache venv + uses: actions/cache@v4 + with: + path: .venv + key: py${{ matrix.python-version }}-venv + - name: Install artistools run: | python3 -m pip install --upgrade uv - uv venv + if [ ! -d ".venv" ]; then uv venv .venv; fi source .venv/bin/activate + echo "PATH=$PWD/.venv/bin:$PATH" >> $GITHUB_ENV uv pip install -e . - name: Cache test data @@ -54,6 +61,11 @@ jobs: working-directory: tests/data/ run: source ./setuptestdata.sh + - name: Check artistools command line tool + run: | + .venv/bin/artistools --help + .venv/bin/artistools setupcompletions + - name: Test with pytest run: | source .venv/bin/activate diff --git a/MANIFEST.in b/MANIFEST.in index 156d59124..89b788a24 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,15 @@ recursive-include artistools *.csv recursive-include artistools *.txt recursive-include artistools matplotlibrc -global-include *.pyi -global-include *.typed global-include *.md global-include *.cff -include LICENSE.txt \ No newline at end of file +include LICENSE.txt +recursive-exclude dist * +recursive-exclude build * +prune **/.* +prune **/*.egg* +prune tests +prune build +prune lib +prune dist +prune images \ No newline at end of file diff --git a/artistools/__main__.py b/artistools/__main__.py index 9a4ed7eac..fa78da574 100644 --- a/artistools/__main__.py +++ b/artistools/__main__.py @@ -8,12 +8,12 @@ import argcomplete from artistools.commands import CommandType -from artistools.commands import dictcommands as atdictcommands +from artistools.commands import subcommandtree as atdictcommands from artistools.misc import CustomArgHelpFormatter def addsubparsers( - parser: argparse.ArgumentParser, parentcommand: str, dictcommands: CommandType, depth: int = 1 + parser: argparse.ArgumentParser, parentcommand: str, subcommandtree: CommandType, depth: int = 1 ) -> None: def func(args: t.Any) -> None: parser.print_help() @@ -21,7 +21,7 @@ def func(args: t.Any) -> None: parser.set_defaults(func=func) subparsers = parser.add_subparsers(dest=f"{parentcommand} command", required=False) - for subcommand, subcommands in dictcommands.items(): + for subcommand, subcommands in subcommandtree.items(): strhelp: str | None if isinstance(subcommands, dict): strhelp = "command group" @@ -40,7 +40,7 @@ def func(args: t.Any) -> None: subparser.set_defaults(func=func) else: assert not isinstance(subcommands, tuple) - addsubparsers(parser=subparser, parentcommand=subcommand, dictcommands=subcommands, depth=depth + 1) + addsubparsers(parser=subparser, parentcommand=subcommand, subcommandtree=subcommands, depth=depth + 1) def addargs(parser: argparse.ArgumentParser) -> None: diff --git a/artistools/commands.py b/artistools/commands.py index 44e074a01..c1243c470 100644 --- a/artistools/commands.py +++ b/artistools/commands.py @@ -3,10 +3,32 @@ import typing as t from pathlib import Path +# top-level commands (one file installed per command) +# we generally should phase this out except for a couple of main ones like at and artistools +COMMANDS = [ + "at", + "artistools", + "makeartismodel1dslicefromcone", + "makeartismodel", + "plotartisdensity", + "plotartisdeposition", + "plotartisestimators", + "plotartislightcurve", + "plotartislinefluxes", + "plotartismacroatom", + "plotartisnltepops", + "plotartisnonthermal", + "plotartisradfield", + "plotartisspectrum", + "plotartistransitions", + "plotartisinitialcomposition", + "plotartisviewingangles", +] + CommandType: t.TypeAlias = dict[str, t.Union[tuple[str, str], "CommandType"]] # new subparser based list -dictcommands: CommandType = { +subcommandtree: CommandType = { "comparetogsinetwork": ("gsinetwork", "main"), "deposition": ("deposition", "main_analytical"), "describeinputmodel": ("inputmodel.describeinputmodel", "main"), @@ -52,36 +74,6 @@ } -def get_commandlist() -> dict[str, tuple[str, str]]: - # direct commands (one file installed per command) - # we generally should phase this out except for a couple of main ones like at and artistools - return { - "at": ("artistools", "main"), - "artistools": ("artistools", "main"), - "makeartismodel1dslicefromcone": ("artistools.inputmodel.slice1dfromconein3dmodel", "main"), - "makeartismodel": ("artistools.inputmodel.makeartismodel", "main"), - "plotartisdensity": ("artistools.inputmodel.plotdensity", "main"), - "plotartisdeposition": ("artistools.deposition", "main"), - "plotartisestimators": ("artistools.estimators.plotestimators", "main"), - "plotartislightcurve": ("artistools.lightcurve.plotlightcurve", "main"), - "plotartislinefluxes": ("artistools.linefluxes", "main"), - "plotartismacroatom": ("artistools.macroatom", "main"), - "plotartisnltepops": ("artistools.nltepops.plotnltepops", "main"), - "plotartisnonthermal": ("artistools.nonthermal", "main"), - "plotartisradfield": ("artistools.radfield", "main"), - "plotartisspectrum": ("artistools.spectra.plotspectra", "main"), - "plotartistransitions": ("artistools.transitions", "main"), - "plotartisinitialcomposition": ("artistools.inputmodel.plotinitialcomposition", "main"), - "plotartisviewingangles": ("artistools.viewing_angles_visualization", "main"), - } - - -def get_console_scripts() -> list[str]: - return [ - f"{command} = {submodulename}:{funcname}" for command, (submodulename, funcname) in get_commandlist().items() - ] - - def setup_completions(*args: t.Any, **kwargs: t.Any) -> None: # Add the following lines to your .zshrc file to get command completion: # autoload -U bashcompinit @@ -105,7 +97,7 @@ def setup_completions(*args: t.Any, **kwargs: t.Any) -> None: f.write(strsplit) f.write("\n\n") - for command in get_commandlist(): + for command in COMMANDS: completecommand = strcommandregister.replace("__MY_COMMAND__", command) f.write(completecommand + "\n") diff --git a/artistools/nonthermal/_nonthermal_core.py b/artistools/nonthermal/_nonthermal_core.py index 15b6101ec..f0d95c83d 100755 --- a/artistools/nonthermal/_nonthermal_core.py +++ b/artistools/nonthermal/_nonthermal_core.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 - +import argparse import math from collections import namedtuple from math import atan @@ -284,7 +284,7 @@ def lossfunction(energy_ev, nne_cgs): return lossfunc / EV # return as [eV / cm] -def Psecondary(e_p, ionpot_ev, J, e_s=-1, epsilon=-1): +def Psecondary(e_p: float, ionpot_ev: float, J: float, e_s: float = -1, epsilon: float = -1) -> float: # probability distribution function for secondaries energy e_s [eV] (or equivalently the energy loss of # the primary electron epsilon [eV]) given a primary energy e_p [eV] for an impact ionisation event @@ -1257,7 +1257,7 @@ def calculate_Latom_ionisation(ions, ionpopdict, dfcollion, nntot, nne, en_ev, n return L_atom_sum -def workfunction_tests(modelpath, args): +def workfunction_tests(modelpath: str | Path, args: argparse.Namespace) -> None: electron_binding = read_binding_energies() dfcollion = at.nonthermal.read_colliondata() @@ -1266,14 +1266,11 @@ def workfunction_tests(modelpath, args): ) axes = [axes] - ionpopdict = { - # (16, 2): 3e5, + ionpopdict: dict[tuple[int, int], float] = { (26, 2): 1e5, } - # keep only the ion populations, not element or total populations - ions = [key for key in ionpopdict if isinstance(key, tuple) and len(key) == 2] - ions.sort() + ions = sorted(ionpopdict.keys()) nntot = get_nntot(ions=ions, ionpopdict=ionpopdict) nnetot = get_nnetot(ions=ions, ionpopdict=ionpopdict) # total electrons: free and bound included @@ -1454,7 +1451,9 @@ def workfunction_tests(modelpath, args): integrand = arr_xs / (L / EV) # arr_workfn_integrated[i] is the en_ev / (integral xs / L dE from EMIN to E[i]) with np.errstate(divide="ignore"): - arr_workfn_integrated = [arr_en_ev[i] / (sum((integrand * delta_en_ev)[:i])) for i in range(len(arr_en_ev))] + arr_workfn_integrated = np.array( + [arr_en_ev[i] / (sum((integrand * delta_en_ev)[:i])) for i in range(len(arr_en_ev))] + ) print(f"\n workfn_integral_Emin_Emax: {arr_workfn_integrated[-1]:.2f} eV") print(f" eta_ion {ionpot_valence_ev / arr_workfn_integrated[-1]:.3f}") diff --git a/artistools/test_artistools.py b/artistools/test_artistools.py index b0bf9040b..c725b02f9 100755 --- a/artistools/test_artistools.py +++ b/artistools/test_artistools.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 +import contextlib import hashlib import importlib import inspect import math import typing as t +from pathlib import Path import artistools as at @@ -23,12 +25,21 @@ def funcname() -> str: def test_commands() -> None: - # ensure that the commands are pointing to valid submodule.function() targets - for _command, (submodulename, funcname) in sorted(at.commands.get_commandlist().items()): - submodule = importlib.import_module(submodulename) - assert hasattr(submodule, funcname) or ( - funcname == "main" and hasattr(importlib.import_module(f"{submodulename}.__main__"), funcname) - ) + commands: dict[str, tuple[str, str]] = {} + + with contextlib.suppress(ImportError): + import tomllib + + with Path("pyproject.toml").open("rb") as f: + pyproj = tomllib.load(f) + commands = {k: tuple(v.split(":")) for k, v in pyproj["project"]["scripts"].items()} + + # ensure that the commands are pointing to valid submodule.function() targets + for command, (submodulename, funcname) in commands.items(): + submodule = importlib.import_module(submodulename) + assert hasattr(submodule, funcname) or ( + funcname == "main" and hasattr(importlib.import_module(f"{submodulename}.__main__"), funcname) + ), f"{submodulename}.{funcname} not found for command {command}" def recursive_check(dictcmd: dict[str, t.Any]) -> None: for cmdtarget in dictcmd.values(): @@ -43,7 +54,7 @@ def recursive_check(dictcmd: dict[str, t.Any]) -> None: funcname == "main" and hasattr(importlib.import_module(f"{namestr}.__main__"), funcname) ) - recursive_check(at.commands.dictcommands) + recursive_check(at.commands.subcommandtree) def test_timestep_times() -> None: diff --git a/artistoolscompletions.sh b/artistoolscompletions.sh index e56f13424..430d14de8 100644 --- a/artistoolscompletions.sh +++ b/artistoolscompletions.sh @@ -36,7 +36,15 @@ _python_argcomplete() { _ARGCOMPLETE_SHELL="zsh" \ _ARGCOMPLETE_SUPPRESS_SPACE=1 \ __python_argcomplete_run ${script:-${words[1]}})) - _describe "${words[1]}" completions -o nosort + local nosort=() + local nospace=() + if is-at-least 5.8; then + nosort=(-o nosort) + fi + if [[ "${completions-}" =~ ([^\\]): && "${match[1]}" =~ [=/:] ]]; then + nospace=(-S '') + fi + _describe "${words[1]}" completions "${nosort[@]}" "${nospace[@]}" else local SUPPRESS_SPACE=0 if compopt +o nospace 2> /dev/null; then @@ -63,102 +71,119 @@ _python_argcomplete() { if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete at else + autoload is-at-least compdef _python_argcomplete at fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete artistools else + autoload is-at-least compdef _python_argcomplete artistools fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete makeartismodel1dslicefromcone else + autoload is-at-least compdef _python_argcomplete makeartismodel1dslicefromcone fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete makeartismodel else + autoload is-at-least compdef _python_argcomplete makeartismodel fi if [[ -z "${ZSH_VERSION-}" ]]; then - complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartismodeldensity + complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisdensity else - compdef _python_argcomplete plotartismodeldensity + autoload is-at-least + compdef _python_argcomplete plotartisdensity fi if [[ -z "${ZSH_VERSION-}" ]]; then - complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartismodeldeposition + complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisdeposition else - compdef _python_argcomplete plotartismodeldeposition + autoload is-at-least + compdef _python_argcomplete plotartisdeposition fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisestimators else + autoload is-at-least compdef _python_argcomplete plotartisestimators fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartislightcurve else + autoload is-at-least compdef _python_argcomplete plotartislightcurve fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartislinefluxes else + autoload is-at-least compdef _python_argcomplete plotartislinefluxes fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartismacroatom else + autoload is-at-least compdef _python_argcomplete plotartismacroatom fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisnltepops else + autoload is-at-least compdef _python_argcomplete plotartisnltepops fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisnonthermal else + autoload is-at-least compdef _python_argcomplete plotartisnonthermal fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisradfield else + autoload is-at-least compdef _python_argcomplete plotartisradfield fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisspectrum else + autoload is-at-least compdef _python_argcomplete plotartisspectrum fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartistransitions else + autoload is-at-least compdef _python_argcomplete plotartistransitions fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisinitialcomposition else + autoload is-at-least compdef _python_argcomplete plotartisinitialcomposition fi if [[ -z "${ZSH_VERSION-}" ]]; then complete -o nospace -o default -o bashdefault -F _python_argcomplete plotartisviewingangles else + autoload is-at-least compdef _python_argcomplete plotartisviewingangles fi diff --git a/pyproject.toml b/pyproject.toml index 18d667483..d163d6ae6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,15 +21,33 @@ classifiers = [ "Framework :: Matplotlib", "Intended Audience :: Science/Research", ] -dynamic = ["version", "dependencies", "entry-points", "readme", "scripts"] +dynamic = ["version", "dependencies"] requires-python = ">=3.10" -license = { file = "LICENSE.txt" } +license = {text = "MIT"} +readme = {file = "README.md", content-type='text/markdown'} [project.urls] Repository ="https://www.github.com/artis-mcrt/artistools/" -#[project.scripts] +[project.scripts] #atcompletions = "artistoolscompletions.sh" +at = 'artistools:main' +artistools = 'artistools:main' +makeartismodel1dslicefromcone = 'artistools.inputmodel.slice1dfromconein3dmodel:main' +makeartismodel = 'artistools.inputmodel.makeartismodel:main' +plotartisdensity = 'artistools.inputmodel.plotdensity:main' +plotartisdeposition = 'artistools.deposition:main' +plotartisestimators = 'artistools.estimators.plotestimators:main' +plotartislightcurve = 'artistools.lightcurve.plotlightcurve:main' +plotartislinefluxes = 'artistools.linefluxes:main' +plotartismacroatom = 'artistools.macroatom:main' +plotartisnltepops = 'artistools.nltepops.plotnltepops:main' +plotartisnonthermal = 'artistools.nonthermal:main' +plotartisradfield = 'artistools.radfield:main' +plotartisspectrum = 'artistools.spectra.plotspectra:main' +plotartistransitions = 'artistools.transitions:main' +plotartisinitialcomposition = 'artistools.inputmodel.plotinitialcomposition:main' +plotartisviewingangles = 'artistools.viewing_angles_visualization:main' [tool.black] line-length = 120 @@ -235,16 +253,17 @@ ban-relative-imports = "all" force-single-line = true order-by-type = false -#[tool.setuptools] -#packages = ["artistools"] -#include-package-data = true - -[tool.setuptools.dynamic] -readme = {file = ["README.md"]} -dependencies = {file = ["requirements.txt"]} +[tool.setuptools] +include-package-data = false +license-files = ["LICENSE.txt"] [tool.setuptools.packages.find] +namespaces = false where = ["."] +exclude = ["tests", "build", "**/dist", "lib", "**/build"] + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} [tool.setuptools_scm] write_to = "_version.py" diff --git a/setup.py b/setup.py deleted file mode 100755 index 1ff949a8f..000000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# mypy: ignore-errors -"""Plotting and analysis tools for the ARTIS 3D supernova radiative transfer code.""" -import importlib.util -from pathlib import Path - -from setuptools import find_namespace_packages -from setuptools import setup -from setuptools_scm import get_version - -spec = importlib.util.spec_from_file_location("commands", "./artistools/commands.py") -commands = importlib.util.module_from_spec(spec) -spec.loader.exec_module(commands) - -setup( - name="artistools", - version=get_version(local_scheme="no-local-version"), - author="ARTIS Collaboration", - author_email="luke.shingles@gmail.com", - packages=find_namespace_packages(where="."), - package_dir={"": "."}, - package_data={"artistools": ["**/*.txt", "**/*.csv", "**/*.md", "matplotlibrc"]}, - url="https://www.github.com/artis-mcrt/artistools/", - long_description=(Path(__file__).absolute().parent / "README.md").open().read(), - long_description_content_type="text/markdown", - entry_points={ - "console_scripts": commands.get_console_scripts(), - }, -)