Skip to content

Commit

Permalink
Migrate PEP517 backend to Hatchling
Browse files Browse the repository at this point in the history
Hatch/Hatchling (https://hatch.pypa.io/) Uses standard pyproject.toml
[project] table settings, like setuptools, but provides more places
for extensibility. That's used here to automatically generate the
`dxtbx.format` and `console_scripts` entry-points on build.

A change was needed because pip had started raising "pending removal"
noises about the way we deployed before.

Opportunity take to do some more pyproject.toml consolidations.
  • Loading branch information
ndevenish committed Oct 29, 2024
1 parent e6d53a7 commit 96dd64f
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 180 deletions.
113 changes: 0 additions & 113 deletions build.py

This file was deleted.

2 changes: 1 addition & 1 deletion dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ run:
test:
- dials-data
- pip
- pytest
- pytest >6
- pytest-mock
- pytest-nunit # [win]
- pytest-xdist
98 changes: 98 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Dynamically generate the list of console_scripts dxtbx.format entry-points.
"""

from __future__ import annotations

import ast
import re
from pathlib import Path

from hatchling.metadata.plugin.interface import MetadataHookInterface


def get_entry_point(
filename: Path, prefix: str, import_path: str
) -> list[tuple[str, str]]:
"""Returns any entry point strings for a given path.
This looks for LIBTBX_SET_DISPATCHER_NAME, and a root function
named 'run'. It can return multiple results for each file, if more
than one dispatcher name is bound.
Args:
filename:
The python file to parse. Will look for a run() function
and any number of LIBTBX_SET_DISPATCHER_NAME.
prefix: The prefix to output the entry point console script with
import_path: The import path to get to the package the file is in
Returns:
A list of entry_point specifications
"""
contents = filename.read_text()
tree = ast.parse(contents)
# Find root functions named "run"
has_run = any(
x
for x in tree.body
if (isinstance(x, ast.FunctionDef) and x.name == "run")
or (isinstance(x, ast.ImportFrom) and "run" in [a.name for a in x.names])
)
if not has_run:
return []
# Find if we need an alternate name via LIBTBX_SET_DISPATCHER_NAME
alternate_names = re.findall(
r"^#\s*LIBTBX_SET_DISPATCHER_NAME\s+(.*)$", contents, re.M
)
if alternate_names:
return [
(name, f"{import_path}.{filename.stem}:run") for name in alternate_names
]

return [(f"{prefix}.{filename.stem}", f"{import_path}.{filename.stem}:run")]


def enumerate_format_classes(path: Path) -> list[tuple(str, str)]:
"""Find all Format*.py files and contained Format classes in a path"""
format_classes = []
for filename in path.glob("Format*.py"):
content = filename.read_bytes()
try:
parsetree = ast.parse(content)
except SyntaxError:
print(f" *** Could not parse {filename.name}")
continue
for top_level_def in parsetree.body:
if not isinstance(top_level_def, ast.ClassDef):
continue
base_names = [
baseclass.id
for baseclass in top_level_def.bases
if isinstance(baseclass, ast.Name) and baseclass.id.startswith("Format")
]
if base_names:
classname = top_level_def.name
format_classes.append(
(
f"{classname}:{','.join(base_names)}",
f"dxtbx.format.{filename.stem}:{classname}",
)
)
return format_classes


class CustomMetadataHook(MetadataHookInterface):
def update(self, metadata):
scripts = metadata.setdefault("scripts", {})
package_path = Path(self.root) / "src" / "dxtbx"
for file in package_path.joinpath("command_line").glob("*.py"):
for name, symbol in get_entry_point(file, "dxtbx", "dxtbx.command_line"):
if name not in scripts:
scripts[name] = symbol

plugins = metadata.setdefault("entry-points", {})
formats = plugins.setdefault("dxtbx.format", {})
for name, symbol in sorted(enumerate_format_classes(package_path / "format")):
if name not in formats:
formats[name] = symbol
1 change: 1 addition & 0 deletions newsfragments/XXX.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Switch build backend to hatchling. This lets us avoid deprecated setuptools behaviour, and automatically generate metadata in a more future-proof way.
42 changes: 40 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
[tool.black]
include = '\.pyi?$|/SConscript$|/libtbx_config$'
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "dxtbx"
version = "3.23.dev"
description = "Diffraction Experiment Toolkit"
authors = [
{ name = "Diamond Light Source", email = "[email protected]" },
]
license = { file = "LICENSE.txt" }
readme = "README.md"
requires-python = ">=3.9, <3.13"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
]
dynamic = ["entry-points", "scripts"]

[project.urls]
Homepage = "https://dials.github.io"
Repository = "https://github.com/cctbx/dxtbx"

[tool.hatch.metadata.hooks.custom.entry-points]

[tool.towncrier]
package = "dxtbx"
Expand Down Expand Up @@ -67,3 +96,12 @@ section-order = [

[tool.mypy]
no_implicit_optional = true

[tool.pytest.ini_options]
addopts = "-rsxX"
filterwarnings = [
"ignore:the matrix subclass is not the recommended way:PendingDeprecationWarning",
"ignore:numpy.dtype size changed:RuntimeWarning",
"ignore:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning",
"ignore:`product` is deprecated as of NumPy:DeprecationWarning:h5py|numpy",
]
10 changes: 0 additions & 10 deletions pytest.ini

This file was deleted.

13 changes: 0 additions & 13 deletions setup.cfg

This file was deleted.

41 changes: 0 additions & 41 deletions setup.py

This file was deleted.

0 comments on commit 96dd64f

Please sign in to comment.