From f30e353e5c2cd69239f31d74b06e04a04bfd7ab2 Mon Sep 17 00:00:00 2001 From: Nicholas Devenish Date: Tue, 29 Oct 2024 15:26:56 +0000 Subject: [PATCH] Revert "Migrate dxtbx build backend to hatchling (#766)" This reverts commit 2727313243042947eb13c24d48611697b1962f3a. This is causing failures in libtbx builds where the entry_points are not being picked up from the specific-installation location in the build/dxtbx/lib folder. Revert to fix main, so this can be investigated separately. --- build.py | 113 +++++++++++++++++++++++++++++++++++++++++ dependencies.yaml | 2 +- hatch_build.py | 98 ----------------------------------- newsfragments/766.misc | 1 - pyproject.toml | 42 +-------------- pytest.ini | 10 ++++ setup.cfg | 13 +++++ setup.py | 41 +++++++++++++++ 8 files changed, 180 insertions(+), 140 deletions(-) create mode 100644 build.py delete mode 100644 hatch_build.py delete mode 100644 newsfragments/766.misc create mode 100644 pytest.ini create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/build.py b/build.py new file mode 100644 index 000000000..66c4d1ca0 --- /dev/null +++ b/build.py @@ -0,0 +1,113 @@ +""" +Handle dynamic aspects of setup.py and building. + +This is separate because the non-dynamic stuff can generally be moved +out of a setup.py, but mainly because at the moment it's how poetry +offloads the unresolved build phases. +""" + +from __future__ import annotations + +import ast +import itertools +import re +import sys +from pathlib import Path +from typing import Any, Dict, List + + +def get_entry_point(filename: Path, prefix: str, import_path: str) -> List[str]: + """Returns the entry point string 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 [f"{name}={import_path}.{filename.stem}:run" for name in alternate_names] + + return [f"{prefix}.{filename.stem}={import_path}.{filename.stem}:run"] + + +def enumerate_format_classes(path: Path) -> List[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)} = dxtbx.format.{filename.stem}:{classname}" + ) + # print(" found", classname, " based on ", str(base_names)) + return format_classes + + +def build(setup_kwargs: Dict[str, Any]) -> None: + """Called by setup.py to inject any dynamic configuration""" + package_path = Path(__file__).parent / "src" / "dxtbx" + entry_points = setup_kwargs.setdefault("entry_points", {}) + console_scripts = entry_points.setdefault("console_scripts", []) + # Work out what dispatchers to add + all_dispatchers = sorted( + itertools.chain( + *[ + get_entry_point(f, "dxtbx", "dxtbx.command_line") + for f in (package_path / "command_line").glob("*.py") + ] + ) + ) + console_scripts.extend(x for x in all_dispatchers if x not in console_scripts) + libtbx_dispatchers = entry_points.setdefault("libtbx.dispatcher.script", []) + libtbx_dispatchers.extend( + "{name}={name}".format(name=x.split("=")[0]) for x in console_scripts + ) + + dxtbx_format = entry_points.setdefault("dxtbx.format", []) + format_classes = sorted(enumerate_format_classes(package_path / "format")) + dxtbx_format.extend([x for x in format_classes if x not in dxtbx_format]) + + print(f"Found {len(entry_points['console_scripts'])} dxtbx dispatchers") + print(f"Found {len(entry_points['dxtbx.format'])} Format classes") + + +if __name__ == "__main__": + sys.exit("Cannot call build.py directly, please use setup.py instead") diff --git a/dependencies.yaml b/dependencies.yaml index 42be0c169..3d0eb8790 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -86,7 +86,7 @@ run: test: - dials-data - pip - - pytest >6 + - pytest - pytest-mock - pytest-nunit # [win] - pytest-xdist diff --git a/hatch_build.py b/hatch_build.py deleted file mode 100644 index dc6b1d76e..000000000 --- a/hatch_build.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -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 diff --git a/newsfragments/766.misc b/newsfragments/766.misc deleted file mode 100644 index 2fd16713a..000000000 --- a/newsfragments/766.misc +++ /dev/null @@ -1 +0,0 @@ -Switch build backend to hatchling. This lets us avoid deprecated setuptools behaviour, and automatically generate metadata in a more future-proof way. diff --git a/pyproject.toml b/pyproject.toml index 0052c3cf0..2f5bf55a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,5 @@ -[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 = "dials-support@lists.sourceforge.net" }, -] -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.black] +include = '\.pyi?$|/SConscript$|/libtbx_config$' [tool.towncrier] package = "dxtbx" @@ -96,12 +67,3 @@ 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", -] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..5d6319d5e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +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 +junit_family = legacy +markers = + regression: dxtbx regression test diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..ae760f5f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +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 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..a77f90951 --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pathlib import Path + +import setuptools + +from build import build + +# Static version number which is updated by bump2version +# Do not change this manually - use 'bump2version ' +__version_tag__ = "3.23.dev" + +setup_kwargs = { + "name": "dxtbx", + "version": __version_tag__, + "long_description": Path(__file__).parent.joinpath("README.md").read_text(), + "description": "Diffraction Experiment Toolbox", + "author": "Diamond Light Source", + "license": "BSD-3-Clause", + "author_email": "dials-support@lists.sourceforge.net", + "project_urls": { + "homepage": "https://dials.github.io", + "repository": "https://github.com/cctbx/dxtbx", + }, + "packages": setuptools.find_packages(where="src"), + "package_dir": {"": "src"}, + "package_data": { + "": ["*"], + "dxtbx": ["array_family/*", "boost_python/*", "example/*", "py.typed"], + "dxtbx.format": ["boost_python/*"], + "dxtbx.masking": ["boost_python/*"], + "dxtbx.model": ["boost_python/*"], + }, + "entry_points": { + "libtbx.precommit": ["dxtbx=dxtbx"], + "libtbx.dispatcher.script": ["pytest=pytest"], + }, +} + +build(setup_kwargs) +setuptools.setup(**setup_kwargs)