From b23c4bb1abafde1db59948c88e68f1cb0764aec8 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Wed, 19 Nov 2025 11:50:33 -0500 Subject: [PATCH 01/18] skpkg: skpkg update without manual edits --- .flake8 | 4 +- .github/ISSUE_TEMPLATE/release_checklist.md | 2 +- .../workflows/build-wheel-release-upload.yml | 2 +- .isort.cfg | 1 + .readthedocs.yaml | 2 +- CODE-OF-CONDUCT.rst | 133 ++++++++++++++++++ LICENSE.rst | 52 +++---- cookiecutter.json | 18 +++ docs/source/conf.py | 15 +- src/diffpy/pdffit2/pdffit2_app.py | 33 +++++ tests/test_version.py | 2 +- 11 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 CODE-OF-CONDUCT.rst create mode 100644 cookiecutter.json create mode 100644 src/diffpy/pdffit2/pdffit2_app.py diff --git a/.flake8 b/.flake8 index 2d2cb168..a5105116 100644 --- a/.flake8 +++ b/.flake8 @@ -1,10 +1,12 @@ +# As of now, flake8 does not natively support configuration via pyproject.toml +# https://github.com/microsoft/vscode-flake8/issues/135 [flake8] exclude = .git, __pycache__, build, dist, - doc/source/conf.py + docs/source/conf.py max-line-length = 115 # Ignore some style 'errors' produced while formatting by 'black' # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#labels-why-pycodestyle-warnings diff --git a/.github/ISSUE_TEMPLATE/release_checklist.md b/.github/ISSUE_TEMPLATE/release_checklist.md index 6107962c..56bcd015 100644 --- a/.github/ISSUE_TEMPLATE/release_checklist.md +++ b/.github/ISSUE_TEMPLATE/release_checklist.md @@ -34,7 +34,7 @@ Please let the maintainer know that all checks are done and the package is ready - [ ] Ensure that the full release has appeared on PyPI successfully. -- [ ] New package dependencies listed in `conda.txt` and `test.txt` are added to `meta.yaml` in the feedstock. +- [ ] New package dependencies listed in `conda.txt` and `tests.txt` are added to `meta.yaml` in the feedstock. - [ ] Close any open issues on the feedstock. Reach out to the maintainer if you have questions. - [ ] Tag the maintainer for conda-forge release. diff --git a/.github/workflows/build-wheel-release-upload.yml b/.github/workflows/build-wheel-release-upload.yml index 5d80dac2..5dec5f6e 100644 --- a/.github/workflows/build-wheel-release-upload.yml +++ b/.github/workflows/build-wheel-release-upload.yml @@ -7,7 +7,7 @@ on: - "*" # Trigger on all tags initially, but tag and release privilege are verified in _build-wheel-release-upload.yml jobs: - release: + build-release: uses: scikit-package/release-scripts/.github/workflows/_build-wheel-release-upload.yml@v0 with: project: diffpy.pdffit2 diff --git a/.isort.cfg b/.isort.cfg index e0926f42..7ce0fb1f 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,5 @@ [settings] +# Keep import statement below line_length character limit line_length = 115 multi_line_output = 3 include_trailing_comma = True diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 47f7a017..aaa88895 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,4 +10,4 @@ python: - requirements: requirements/docs.txt sphinx: - configuration: doc/source/conf.py + configuration: docs/source/conf.py diff --git a/CODE-OF-CONDUCT.rst b/CODE-OF-CONDUCT.rst new file mode 100644 index 00000000..e8199ca5 --- /dev/null +++ b/CODE-OF-CONDUCT.rst @@ -0,0 +1,133 @@ +===================================== + Contributor Covenant Code of Conduct +===================================== + +Our Pledge +---------- + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socioeconomic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +Our Standards +------------- + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +Enforcement Responsibilities +---------------------------- + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +Scope +----- + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +sb2896@columbia.edu. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +Enforcement Guidelines +---------------------- + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +1. Correction +**************** + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +2. Warning +************* + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +3. Temporary Ban +****************** + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +4. Permanent Ban +****************** + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor Covenant `_. + +Community Impact Guidelines were inspired by `Mozilla's code of conduct enforcement ladder `_. + +For answers to common questions about this code of conduct, see the `FAQ `_. `Translations are available `_ diff --git a/LICENSE.rst b/LICENSE.rst index 040c880d..092e90c5 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -21,32 +21,26 @@ For more information please visit the project web-page: or email Prof. Simon Billinge at sb2896@columbia.edu Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - * Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS". COPYRIGHT HOLDER -EXPRESSLY DISCLAIMS ANY AND ALL WARRANTIES AND CONDITIONS, EITHER -EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY, TITLE, FITNESS, ADEQUACY OR SUITABILITY -FOR A PARTICULAR PURPOSE, AND ANY WARRANTIES OF FREEDOM FROM -INFRINGEMENT OF ANY DOMESTIC OR FOREIGN PATENT, COPYRIGHTS, TRADE -SECRETS OR OTHER PROPRIETARY RIGHTS OF ANY PARTY. IN NO EVENT SHALL -COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE OR RELATING TO THIS AGREEMENT, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cookiecutter.json b/cookiecutter.json new file mode 100644 index 00000000..9fad4e41 --- /dev/null +++ b/cookiecutter.json @@ -0,0 +1,18 @@ +{ + "maintainer_name": "Simon Billinge", + "maintainer_email": "sb2896@columbia.edu", + "maintainer_github_username": "sbillinge", + "contributors": "Sangjoon Lee, Simon Billinge, Billinge Group members", + "license_holders": "The Trustees of Columbia University in the City of New York", + "project_name": "diffpy.pdffit2", + "github_username_or_orgname": "diffpy", + "github_repo_name": "diffpy.pdffit2", + "conda_pypi_package_dist_name": "diffpy.pdffit2", + "package_dir_name": "diffpy.pdffit2", + "project_short_description": "PDFfit2 - real space structure refinement program.", + "project_keywords": "PDF, structure refinement", + "minimum_supported_python_version": "3.11", + "maximum_supported_python_version": "3.13", + "project_needs_c_code_compiled": "Yes", + "project_has_gui_tests": "No" +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 58fb11f6..16935deb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# diffpy.pdffit2 documentation build configuration file, created by +# diffpy.pdffit2 documentation build configuration file, created by # noqa: E501 # sphinx-quickstart on Thu Jan 30 15:49:41 2014. # # This file is execfile()d with the current directory set to its @@ -22,17 +22,17 @@ try: fullversion = version("diffpy.pdffit2") except Exception: - fullversion = "No version found. The correct version will appear in the released version." + fullversion = "No version found. The correct version will appear in the released version." # noqa: E501 # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the -# documentation root, use Path().resolve() to make it absolute, like shown here. +# documentation root, use Path().resolve() to make it absolute, like shown here. # noqa: E501 # sys.path.insert(0, str(Path(".").resolve())) sys.path.insert(0, str(Path("../..").resolve())) sys.path.insert(0, str(Path("../../src").resolve())) # abbreviations -ab_authors = "Billinge group and community members." +ab_authors = "Sangjoon Lee, Simon Billinge, Billinge Group members" # -- General configuration ------------------------------------------------ @@ -53,10 +53,6 @@ "m2r", ] -autodoc_mock_imports = [ - "diffpy.pdffit2.pdffit2", -] - # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -79,7 +75,6 @@ # |version| and |release|, also used in various other places throughout the # built documents. -fullversion = version(project) # The short X.Y version. version = "".join(fullversion.split(".post")[:1]) # The full version, including alpha/beta/rc tags. @@ -144,7 +139,7 @@ "github_user": "diffpy", "github_repo": "diffpy.pdffit2", "github_version": "main", - "conf_py_path": "/doc/source/", + "conf_py_path": "/docs/source/", } # Theme options are theme-specific and customize the look and feel of a theme diff --git a/src/diffpy/pdffit2/pdffit2_app.py b/src/diffpy/pdffit2/pdffit2_app.py new file mode 100644 index 00000000..d8ff2fd0 --- /dev/null +++ b/src/diffpy/pdffit2/pdffit2_app.py @@ -0,0 +1,33 @@ +import argparse + +from diffpy.pdffit2.version import __version__ # noqa + + +def main(): + parser = argparse.ArgumentParser( + prog="diffpy.pdffit2", + description=( + "PDFfit2 - real space structure refinement program.\n\n" + "For more information, visit: " + "https://github.com/diffpy/diffpy.pdffit2/" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", + action="store_true", + help="Show the program's version number and exit", + ) + + args = parser.parse_args() + + if args.version: + print(f"diffpy.pdffit2 {__version__}") + else: + # Default behavior when no arguments are given + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/tests/test_version.py b/tests/test_version.py index b9b25b07..a232202f 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,6 +1,6 @@ """Unit tests for __version__.py.""" -import diffpy.pdffit2 +import diffpy.pdffit2 # noqa def test_package_version(): From 5ea24ed43edf6c7ac14f0cbf30f11eeca51f4373 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Wed, 19 Nov 2025 18:09:42 -0500 Subject: [PATCH 02/18] fix: fix author and required variable back. --- cookiecutter.json | 2 +- docs/source/conf.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index 9fad4e41..174c2873 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -2,7 +2,7 @@ "maintainer_name": "Simon Billinge", "maintainer_email": "sb2896@columbia.edu", "maintainer_github_username": "sbillinge", - "contributors": "Sangjoon Lee, Simon Billinge, Billinge Group members", + "contributors": "Pavol Juhas, Chris Farrow, Simon Billinge, Billinge Group members", "license_holders": "The Trustees of Columbia University in the City of New York", "project_name": "diffpy.pdffit2", "github_username_or_orgname": "diffpy", diff --git a/docs/source/conf.py b/docs/source/conf.py index 16935deb..56af88a3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,7 +32,7 @@ sys.path.insert(0, str(Path("../../src").resolve())) # abbreviations -ab_authors = "Sangjoon Lee, Simon Billinge, Billinge Group members" +ab_authors = "Pavol Juhas, Chris Farrow, Simon Billinge, Billinge Group members" # -- General configuration ------------------------------------------------ @@ -53,6 +53,9 @@ "m2r", ] +autodoc_mock_imports = [ + "diffpy.pdffit2.pdffit2", +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -75,6 +78,7 @@ # |version| and |release|, also used in various other places throughout the # built documents. +fullversion = version(project) # The short X.Y version. version = "".join(fullversion.split(".post")[:1]) # The full version, including alpha/beta/rc tags. From 1ab04aecec0393f9af96310f035188f65e7b4034 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 23:10:01 +0000 Subject: [PATCH 03/18] [pre-commit.ci] auto fixes from pre-commit hooks --- docs/source/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 56af88a3..cf1514d6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,7 +32,9 @@ sys.path.insert(0, str(Path("../../src").resolve())) # abbreviations -ab_authors = "Pavol Juhas, Chris Farrow, Simon Billinge, Billinge Group members" +ab_authors = ( + "Pavol Juhas, Chris Farrow, Simon Billinge, Billinge Group members" +) # -- General configuration ------------------------------------------------ From d6fc863acbeb524e3b94ec5fed3181e784699991 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 20 Nov 2025 12:36:53 -0500 Subject: [PATCH 04/18] skpkg: add CHAGELOG.rst, README.rst, license, update pyproject.toml --- CHANGELOG.rst | 68 ----------------------------------------- README.rst | 40 +++++++++++------------- docs/source/license.rst | 52 ++++++++++++++----------------- pyproject.toml | 3 ++ 4 files changed, 43 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1253f933..f29d3b53 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,71 +3,3 @@ Release notes ============= .. current developments - -1.5.1 -===== - -**Fixed:** - -* Fixed `SystemError` and `MemoryError` for `redirect_stdout` on Windows with Python 3.13. - -**Removed:** - -* Removed `restore_stdout` function and wrapper. - - -1.5.0 -===== - -**Added:** - -* Python 3.11, 3.12 support -* Option to skip printing of introductory information when initializing the `PdfFit` class. -* Added additional runtime linker flags in `CustomBuildExt.run` to embed the `RPATH` flags for the built extensions. -* Support for retrieving GSL configuration from `CONDA_PREFIX`/ `GSL_PATH` on all platforms. -* Separate installation instruction for macOS (Arm64) in READEM -* Added `restore_stdout` function and wrapper. -* Added Python 3.13 support. - -**Changed:** - -* Changed setup.py to lazy evaluate gsl installation. -* Documentation brought up to date -* Merged the GSL configuration logic in `setup.py`. -* Changed `pytest` `capture_output` fixture. Now automatically restores `sys.stdout`. - -**Fixed:** - -* remove older conda-recipe files -* moved the tests directory from src to the root using conftest.py. -* fixed a circular import bug during " pip install ." in GitHub CI. -* renamed .py files under tests to snake_case. -* add PyPI packages under pip.txt -* re-cookiecutter to group's package standard -* Fix missing `__date__`, use PyPI release date. -* Fixed `SystemError` when running `pytest` on Windows with Python 3.13. - -**Removed:** - -* Python <= 3.10 support -* Six dependency and py2 support - - -1.4.2 -===== - -**Added:** - -* Support for Python 3.11, 3.12 - -**Changed:** - -* No notable functional changes from 1.4.1 - -1.4.4rc0 -======== - -**Fixed:** - -* Code linted to group flake8 standards -* Package structure moved to diffpy standard structure diff --git a/README.rst b/README.rst index 4c90e849..dfbcd71c 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,7 @@ :target: https://anaconda.org/conda-forge/diffpy.pdffit2 .. |PR| image:: https://img.shields.io/badge/PR-Welcome-29ab47ff + :target: https://github.com/diffpy/diffpy.pdffit2/pulls .. |PyPI| image:: https://img.shields.io/pypi/v/diffpy.pdffit2 :target: https://pypi.org/project/diffpy.pdffit2/ @@ -78,11 +79,6 @@ If you use diffpy.pdffit2 in a scientific publication, we would like you to cite Installation ------------ -diffpy.pdffit2 supports Python 3.11, 3.12, and 3.13. - -Windows, macOS (non-Arm64), Linux -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The preferred method is to use `Miniconda Python `_ and install from the "conda-forge" channel of Conda packages. @@ -97,34 +93,32 @@ The following creates and activates a new environment named ``diffpy.pdffit2_env conda create -n diffpy.pdffit2_env diffpy.pdffit2 conda activate diffpy.pdffit2_env -To confirm that the installation was successful, type :: - - python -c "import diffpy.pdffit2; print(diffpy.pdffit2.__version__)" - -macOS (Arm64) -~~~~~~~~~~~~~ +The output should print the latest version displayed on the badges above. -Create a new conda environment ``diffpy.pdffit2_env``: :: +If the above does not work, you can use ``pip`` to download and install the latest release from +`Python Package Index `_. +To install using ``pip`` into your ``diffpy.pdffit2_env`` environment, type :: - conda create -n diffpy.pdffit2_env python=3.13 + pip install diffpy.pdffit2 -Activate the environment: :: +If you prefer to install from sources, after installing the dependencies, obtain the source archive from +`GitHub `_. Once installed, ``cd`` into your ``diffpy.pdffit2`` directory +and run the following :: - conda activate diffpy.pdffit2_env + pip install . -Install pdffit2 using ``pip`` to download and install the latest version from `Python Package Index `_: :: +This package also provides command-line utilities. To check the software has been installed correctly, type :: - pip install diffpy.pdffit2 + diffpy.pdffit2 --version -To confirm that the installation was successful, type :: +You can also type the following command to verify the installation. :: python -c "import diffpy.pdffit2; print(diffpy.pdffit2.__version__)" -If you prefer to install from sources, after installing the dependencies, obtain the source archive from -`GitHub `_. Once installed, ``cd`` into your ``diffpy.pdffit2`` directory -and run the following :: - pip install . +To view the basic usage and available commands, type :: + + diffpy.pdffit2 -h Getting Started --------------- @@ -170,7 +164,7 @@ trying to commit again. Improvements and fixes are always appreciated. -Before contributing, please read our `Code of Conduct `_. +Before contributing, please read our `Code of Conduct `_. Contact ------- diff --git a/docs/source/license.rst b/docs/source/license.rst index fbdd3edc..5cdf347a 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -25,32 +25,26 @@ For more information please visit the project web-page: or email Prof. Simon Billinge at sb2896@columbia.edu Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - * Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS". COPYRIGHT HOLDER -EXPRESSLY DISCLAIMS ANY AND ALL WARRANTIES AND CONDITIONS, EITHER -EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY, TITLE, FITNESS, ADEQUACY OR SUITABILITY -FOR A PARTICULAR PURPOSE, AND ANY WARRANTIES OF FREEDOM FROM -INFRINGEMENT OF ANY DOMESTIC OR FOREIGN PATENT, COPYRIGHTS, TRADE -SECRETS OR OTHER PROPRIETARY RIGHTS OF ANY PARTY. IN NO EVENT SHALL -COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE OR RELATING TO THIS AGREEMENT, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyproject.toml b/pyproject.toml index af52ce04..959054dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ include = ["*"] # package names should match these glob patterns (["*"] by defa exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) +[project.scripts] +diffpy-pdffit2 = "diffpy.pdffit2_app:main" + [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} From a0d4c2c4ebb3de19351167b7297cc41c2ccccddd Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 20 Nov 2025 13:24:55 -0500 Subject: [PATCH 05/18] chore: Add instructions back to README.rst and revert history of CHANGELOG.rst --- CHANGELOG.rst | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 25 ++++++++++++------- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f29d3b53..1253f933 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,3 +3,71 @@ Release notes ============= .. current developments + +1.5.1 +===== + +**Fixed:** + +* Fixed `SystemError` and `MemoryError` for `redirect_stdout` on Windows with Python 3.13. + +**Removed:** + +* Removed `restore_stdout` function and wrapper. + + +1.5.0 +===== + +**Added:** + +* Python 3.11, 3.12 support +* Option to skip printing of introductory information when initializing the `PdfFit` class. +* Added additional runtime linker flags in `CustomBuildExt.run` to embed the `RPATH` flags for the built extensions. +* Support for retrieving GSL configuration from `CONDA_PREFIX`/ `GSL_PATH` on all platforms. +* Separate installation instruction for macOS (Arm64) in READEM +* Added `restore_stdout` function and wrapper. +* Added Python 3.13 support. + +**Changed:** + +* Changed setup.py to lazy evaluate gsl installation. +* Documentation brought up to date +* Merged the GSL configuration logic in `setup.py`. +* Changed `pytest` `capture_output` fixture. Now automatically restores `sys.stdout`. + +**Fixed:** + +* remove older conda-recipe files +* moved the tests directory from src to the root using conftest.py. +* fixed a circular import bug during " pip install ." in GitHub CI. +* renamed .py files under tests to snake_case. +* add PyPI packages under pip.txt +* re-cookiecutter to group's package standard +* Fix missing `__date__`, use PyPI release date. +* Fixed `SystemError` when running `pytest` on Windows with Python 3.13. + +**Removed:** + +* Python <= 3.10 support +* Six dependency and py2 support + + +1.4.2 +===== + +**Added:** + +* Support for Python 3.11, 3.12 + +**Changed:** + +* No notable functional changes from 1.4.1 + +1.4.4rc0 +======== + +**Fixed:** + +* Code linted to group flake8 standards +* Package structure moved to diffpy standard structure diff --git a/README.rst b/README.rst index dfbcd71c..9ca91b8b 100644 --- a/README.rst +++ b/README.rst @@ -79,6 +79,11 @@ If you use diffpy.pdffit2 in a scientific publication, we would like you to cite Installation ------------ +diffpy.pdffit2 supports Python 3.11, 3.12, and 3.13. + +Windows, macOS (non-Arm64), Linux +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The preferred method is to use `Miniconda Python `_ and install from the "conda-forge" channel of Conda packages. @@ -93,7 +98,16 @@ The following creates and activates a new environment named ``diffpy.pdffit2_env conda create -n diffpy.pdffit2_env diffpy.pdffit2 conda activate diffpy.pdffit2_env -The output should print the latest version displayed on the badges above. +To confirm that the installation was successful, type :: + + python -c "import diffpy.pdffit2; print(diffpy.pdffit2.__version__)" + +macOS (Arm64) +~~~~~~~~~~~~~ + +Create a new conda environment ``diffpy.pdffit2_env``: :: + + conda create -n diffpy.pdffit2_env python=3.13 If the above does not work, you can use ``pip`` to download and install the latest release from `Python Package Index `_. @@ -107,19 +121,12 @@ and run the following :: pip install . -This package also provides command-line utilities. To check the software has been installed correctly, type :: - - diffpy.pdffit2 --version -You can also type the following command to verify the installation. :: +You can type the following command to verify the installation is successful. :: python -c "import diffpy.pdffit2; print(diffpy.pdffit2.__version__)" -To view the basic usage and available commands, type :: - - diffpy.pdffit2 -h - Getting Started --------------- From f795440d588e1d52f8c9e4153cd3730f8404c28f Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 20 Nov 2025 13:30:28 -0500 Subject: [PATCH 06/18] chore: add activate for env to make consistent. --- README.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9ca91b8b..0adcc4b1 100644 --- a/README.rst +++ b/README.rst @@ -109,9 +109,11 @@ Create a new conda environment ``diffpy.pdffit2_env``: :: conda create -n diffpy.pdffit2_env python=3.13 -If the above does not work, you can use ``pip`` to download and install the latest release from -`Python Package Index `_. -To install using ``pip`` into your ``diffpy.pdffit2_env`` environment, type :: +Activate the environment: :: + + conda activate diffpy.pdffit2_env + +Install pdffit2 using ``pip`` to download and install the latest version from `Python Package Index `_: :: pip install diffpy.pdffit2 From eae357eaf83c94fea2a1fa903b0b167a80a33820 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 20 Nov 2025 15:26:17 -0500 Subject: [PATCH 07/18] skpkg: change version build file. --- src/diffpy/__init__.py | 10 ------ src/diffpy/pdffit2/__init__.py | 20 ++---------- src/diffpy/pdffit2/version.py | 57 ++++++---------------------------- 3 files changed, 12 insertions(+), 75 deletions(-) diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 2efe9395..04d8ace2 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -14,13 +14,3 @@ # See LICENSE.rst for license information. # ############################################################################## -"""diffpy - tools for structure analysis by diffraction. - -Blank namespace package for module diffpy.""" - - -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) - -# End of file diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index 527477a5..fceee947 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -16,24 +16,10 @@ ############################################################################## """PDFfit2 - real space structure refinement program.""" -# WARNING: Do NOT remove the isort: off/on comments in this file. -# These tags are used to prevent isort from reordering imports in this file. -# __version__ must be initialized before importing C++ extensions. +# package version +from diffpy.pdffit2.version import __version__ # noqa -# isort: off -# Import the package version before C++ extensions are loaded. -from diffpy.pdffit2.output import redirect_stdout -from diffpy.pdffit2.version import __date__, __version__ - -# Import C++ related modules since the __version__ attribute is used. -from diffpy.pdffit2.pdffit import PdfFit -from diffpy.pdffit2.pdffit2 import is_element - -# isort: on - -# Ensure all necessary components are imported and initialized +# silence the pyflakes syntax checker assert __version__ or True -assert __date__ or True -assert all((PdfFit, redirect_stdout, is_element)) # End of file diff --git a/src/diffpy/pdffit2/version.py b/src/diffpy/pdffit2/version.py index 86159280..0ade3955 100644 --- a/src/diffpy/pdffit2/version.py +++ b/src/diffpy/pdffit2/version.py @@ -7,59 +7,20 @@ # File coded by: Billinge Group members and community contributors. # # See GitHub contributions for a more detailed list of contributors. -# https://github.com/diffpy/diffpy.pdffit2/graphs/contributors +# https://github.com/diffpy/diffpy.pdffit2/graphs/contributors # noqa: E501 # # See LICENSE.rst for license information. # ############################################################################## """Definition of __version__.""" -import datetime -import json -import urllib.request -from importlib.metadata import version -from pathlib import Path +# We do not use the other three variables, but can be added back if needed. +# __all__ = ["__date__", "__git_commit__", "__timestamp__", "__version__"] +# obtain version information +from importlib.metadata import PackageNotFoundError, version -def get_pypi_release_date(package_name, timeout=5): - package_file = Path(__file__).resolve() - - try: - with open(package_file, "r", encoding="utf-8") as f: - lines = f.readlines() - for line in reversed(lines): - if line.startswith("# Release date:"): - return line.split(":", 1)[1].strip() - - url = f"https://pypi.org/pypi/{package_name}/json" - with urllib.request.urlopen(url, timeout=timeout) as response: - data = json.loads(response.read().decode("utf-8")) - - installed_version = version(package_name) - release_data = data["releases"].get(installed_version, []) - if not release_data: - raise ValueError( - f"No release data found for version {installed_version}" - ) - - release_date_str = release_data[-1]["upload_time"] - release_date = datetime.datetime.fromisoformat(release_date_str).date() - - with open(package_file, "a", encoding="utf-8") as f: - f.write(f"\n# Release date: {release_date}") - - except (ValueError, OSError) as e: - print(f"Warning: Could not fetch release date from PyPI: {e}") - release_date = datetime.datetime.fromtimestamp( - package_file.stat().st_ctime - ).isoformat() - - return str(release_date) - - -__version__ = version("diffpy.pdffit2") -__date__ = get_pypi_release_date("diffpy.pdffit2") - -# End of file - -# Release date: 2025-02-07 +try: + __version__ = version("diffpy.pdffit2") +except PackageNotFoundError: + __version__ = "unknown" From 8b93472f93ce0f75a24a1f1539acae2eb3e8b23d Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 20 Nov 2025 20:26:12 -0500 Subject: [PATCH 08/18] skpkg: package update command with no manual edits. --- setup.py | 197 +++------------------------------------------- tests/conftest.py | 31 -------- 2 files changed, 12 insertions(+), 216 deletions(-) diff --git a/setup.py b/setup.py index 9a334a8e..9c1e69cf 100644 --- a/setup.py +++ b/setup.py @@ -1,199 +1,27 @@ #!/usr/bin/env python # Extensions script for diffpy.pdffit2 -"""PDFfit2 - real space structure refinement engine - -Packages: diffpy.pdffit2 -Scripts: pdffit2 -""" import glob -import os -import re -import shutil -import sys -import warnings -from pathlib import Path +import os # noqa +import re # noqa +import sys # noqa from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext - -# Use this version when git data are not available, like in git zip archive. -# Update when tagging a new release. -FALLBACK_VERSION = "1.4.3" - -MYDIR = str(Path(__file__).parent.resolve()) - -# Helper functions ----------------------------------------------------------- - - -def get_compiler_type(): - """Return the compiler type used during the build.""" - cc_arg = [a for a in sys.argv if a.startswith("--compiler=")] - if cc_arg: - return cc_arg[-1].split("=", 1)[1] - from distutils.ccompiler import new_compiler - - return new_compiler().compiler_type - - -def get_gsl_config(): - """ - Determine the GSL include and library directories by trying in order: - 1) CONDA_PREFIX, - 2) GSL_PATH, - 3) gsl-config (for Unix-like systems). - Raises EnvironmentError if none are found. - """ - rv = {"include_dirs": [], "library_dirs": []} - - # 1. Check using CONDA_PREFIX. - conda_prefix = os.environ.get("CONDA_PREFIX", "") - if conda_prefix: - if os.name == "nt": - inc = Path(conda_prefix) / "Library" / "include" - lib = Path(conda_prefix) / "Library" / "lib" - else: - inc = Path(conda_prefix) / "include" - lib = Path(conda_prefix) / "lib" - if inc.is_dir() and lib.is_dir(): - rv["include_dirs"].append(str(inc)) - rv["library_dirs"].append(str(lib)) - return rv - else: - warnings.warn( - f"CONDA_PREFIX is set to {conda_prefix}, " - "but GSL not found at those paths. Proceeding..." - ) - # 2. Check using GSL_PATH. - gsl_path = os.environ.get("GSL_PATH", "") - if gsl_path: - inc = Path(gsl_path) / "include" - lib = Path(gsl_path) / "lib" - if inc.is_dir() and lib.is_dir(): - rv["include_dirs"].append(str(inc)) - rv["library_dirs"].append(str(lib)) - return rv - else: - raise EnvironmentError( - f"GSL_PATH={gsl_path} is set, but {inc} or {lib} not found. " - "Please verify your GSL_PATH." - ) - - # 3. Try using the gsl-config executable (only on Unix-like systems). - if os.name != "nt": - path_dirs = os.environ.get("PATH", "").split(os.pathsep) - gslcfg_paths = [Path(p) / "gsl-config" for p in path_dirs if p] - gslcfg_paths = [p for p in gslcfg_paths if p.is_file()] - if gslcfg_paths: - gslcfg = gslcfg_paths[0] - txt = gslcfg.read_text() - prefix_match = re.search(r"(?m)^prefix=(.+)", txt) - include_match = re.search(r"(?m)^[^#]*\s-I(\S+)", txt) - lib_match = re.search(r"(?m)^[^#]*\s-L(\S+)", txt) - if prefix_match: - prefix_path = Path(prefix_match.group(1)) - inc_dir = ( - include_match.group(1) - if include_match - else (prefix_path / "include") - ) - lib_dir = ( - lib_match.group(1) if lib_match else (prefix_path / "lib") - ) - rv["include_dirs"].append(str(inc_dir)) - rv["library_dirs"].append(str(lib_dir)) - return rv - else: - raise RuntimeError(f"Cannot parse 'prefix=' from {gslcfg}.") - else: - warnings.warn( - "No gsl-config found in PATH. GSL may not be installed or not in PATH. " - "Proceeding without GSL configuration." - ) - - # 4. Nothing found: raise error. - raise EnvironmentError( - "Unable to locate GSL:\n" - "1) CONDA_PREFIX not set or no GSL there\n" - "2) GSL_PATH not set or invalid\n" - "3) gsl-config not available\n" - "Please set GSL_PATH or use a conda environment with GSL." - ) - - -class CustomBuildExt(build_ext): - def run(self): - # Retrieve the GSL library directories and append them to each extension. - gsl_cfg = get_gsl_config() - lib_dirs = gsl_cfg.get("library_dirs", []) - for ext in self.extensions: - # Add gsl lib for linking. - ext.library_dirs.extend(lib_dirs) - # Embed RPATH flags, runtime linking without LD_LIBRARY_PATH. - ext.extra_link_args = ext.extra_link_args or [] - for lib in lib_dirs: - ext.extra_link_args.append(f"-Wl,-rpath,{lib}") - super().run() - # Avoid dll error - gsl_path = ( - Path(os.environ.get("GSL_PATH")) - if os.environ.get("GSL_PATH") - else Path(os.environ.get("CONDA_PREFIX", "")) / "Library" - ) - bin_path = gsl_path / "bin" - dest_path = Path(self.build_lib) / "diffpy" / "pdffit2" - dest_path.mkdir(parents=True, exist_ok=True) - for dll_file in bin_path.glob("gsl*.dll"): - shutil.copy(str(dll_file), str(dest_path)) +# Define extension arguments here +ext_kws = { + "libraries": [], + "extra_compile_args": [], + "extra_link_args": [], + "include_dirs": [], +} def create_extensions(): - """Create the list of Extension objects for the build.""" - # lazy evaluation prevents build sdist failure - try: - gcfg = get_gsl_config() - except EnvironmentError: - return [] - - libraries = ["gsl"] - - include_dirs = [MYDIR] + gcfg["include_dirs"] - library_dirs = gcfg["library_dirs"] - define_macros = [] - extra_objects = [] - extra_compile_args = [] - extra_link_args = [] - - compiler_type = get_compiler_type() - if compiler_type in ("unix", "cygwin", "mingw32"): - extra_compile_args = [ - "-std=c++11", - "-Wall", - "-Wno-write-strings", - "-O3", - "-funroll-loops", - "-ffast-math", - ] - elif compiler_type == "msvc": - define_macros += [("_USE_MATH_DEFINES", None)] - extra_compile_args = ["/EHs"] - - # Extension keyword arguments. - ext_kws = { - "include_dirs": include_dirs, - "libraries": libraries, - "library_dirs": library_dirs, - "define_macros": define_macros, - "extra_compile_args": extra_compile_args, - "extra_link_args": extra_link_args, - "extra_objects": extra_objects, - } + "Initialize Extension objects for the setup function." ext = Extension( - "diffpy.pdffit2.pdffit2", - glob.glob("src/extensions/**/*.cc"), - **ext_kws, + "diffpy.pdffit2.pdffit2", glob.glob("src/extensions/*.cpp"), **ext_kws ) return [ext] @@ -201,7 +29,6 @@ def create_extensions(): # Extensions not included in pyproject.toml setup_args = dict( ext_modules=[], - cmdclass={"build_ext": CustomBuildExt}, ) diff --git a/tests/conftest.py b/tests/conftest.py index 6a142536..e3b63139 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,8 @@ -import io import json from pathlib import Path import pytest -import diffpy.pdffit2 -import diffpy.pdffit2.output # assuming this is the correct import path - @pytest.fixture def user_filesystem(tmp_path): @@ -21,30 +17,3 @@ def user_filesystem(tmp_path): json.dump(home_config_data, f) yield tmp_path - - -@pytest.fixture -def datafile(): - """Fixture to dynamically load any test file.""" - - def _load(filename): - return "tests/testdata/" + filename - - return _load - - -@pytest.fixture -def capture_output(): - """Capture output from pdffit2 engine produced in function call.""" - - def _capture(f, *args, **kwargs): - savestdout = diffpy.pdffit2.output.stdout - fp = io.StringIO() - diffpy.pdffit2.redirect_stdout(fp) - try: - f(*args, **kwargs) - finally: - diffpy.pdffit2.redirect_stdout(savestdout) - return fp.getvalue() - - return _capture From f7bc156cc7997be7f311034162ccd6328deb1cd4 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 13:12:09 -0500 Subject: [PATCH 09/18] fix: revert setup.py and conftest.py to original --- setup.py | 192 +++++++++++++++++++++++++++++++++++++++++++--- tests/conftest.py | 31 ++++++++ 2 files changed, 211 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 9c1e69cf..d808441b 100644 --- a/setup.py +++ b/setup.py @@ -3,25 +3,192 @@ # Extensions script for diffpy.pdffit2 import glob -import os # noqa -import re # noqa -import sys # noqa +import os +import re +import shutil +import sys +import warnings +from pathlib import Path from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext -# Define extension arguments here -ext_kws = { - "libraries": [], - "extra_compile_args": [], - "extra_link_args": [], - "include_dirs": [], -} +# Use this version when git data are not available, like in git zip archive. +# Update when tagging a new release. +FALLBACK_VERSION = "1.4.3" + +MYDIR = str(Path(__file__).parent.resolve()) + +# Helper functions ----------------------------------------------------------- + + +def get_compiler_type(): + """Return the compiler type used during the build.""" + cc_arg = [a for a in sys.argv if a.startswith("--compiler=")] + if cc_arg: + return cc_arg[-1].split("=", 1)[1] + from distutils.ccompiler import new_compiler + + return new_compiler().compiler_type + + +def get_gsl_config(): + """ + Determine the GSL include and library directories by trying in order: + 1) CONDA_PREFIX, + 2) GSL_PATH, + 3) gsl-config (for Unix-like systems). + Raises EnvironmentError if none are found. + """ + rv = {"include_dirs": [], "library_dirs": []} + + # 1. Check using CONDA_PREFIX. + conda_prefix = os.environ.get("CONDA_PREFIX", "") + if conda_prefix: + if os.name == "nt": + inc = Path(conda_prefix) / "Library" / "include" + lib = Path(conda_prefix) / "Library" / "lib" + else: + inc = Path(conda_prefix) / "include" + lib = Path(conda_prefix) / "lib" + if inc.is_dir() and lib.is_dir(): + rv["include_dirs"].append(str(inc)) + rv["library_dirs"].append(str(lib)) + return rv + else: + warnings.warn( + f"CONDA_PREFIX is set to {conda_prefix}, " + "but GSL not found at those paths. Proceeding..." + ) + + # 2. Check using GSL_PATH. + gsl_path = os.environ.get("GSL_PATH", "") + if gsl_path: + inc = Path(gsl_path) / "include" + lib = Path(gsl_path) / "lib" + if inc.is_dir() and lib.is_dir(): + rv["include_dirs"].append(str(inc)) + rv["library_dirs"].append(str(lib)) + return rv + else: + raise EnvironmentError( + f"GSL_PATH={gsl_path} is set, but {inc} or {lib} not found. " + "Please verify your GSL_PATH." + ) + + # 3. Try using the gsl-config executable (only on Unix-like systems). + if os.name != "nt": + path_dirs = os.environ.get("PATH", "").split(os.pathsep) + gslcfg_paths = [Path(p) / "gsl-config" for p in path_dirs if p] + gslcfg_paths = [p for p in gslcfg_paths if p.is_file()] + if gslcfg_paths: + gslcfg = gslcfg_paths[0] + txt = gslcfg.read_text() + prefix_match = re.search(r"(?m)^prefix=(.+)", txt) + include_match = re.search(r"(?m)^[^#]*\s-I(\S+)", txt) + lib_match = re.search(r"(?m)^[^#]*\s-L(\S+)", txt) + if prefix_match: + prefix_path = Path(prefix_match.group(1)) + inc_dir = ( + include_match.group(1) + if include_match + else (prefix_path / "include") + ) + lib_dir = ( + lib_match.group(1) if lib_match else (prefix_path / "lib") + ) + rv["include_dirs"].append(str(inc_dir)) + rv["library_dirs"].append(str(lib_dir)) + return rv + else: + raise RuntimeError(f"Cannot parse 'prefix=' from {gslcfg}.") + else: + warnings.warn( + "No gsl-config found in PATH. GSL may not be installed or not in PATH. " + "Proceeding without GSL configuration." + ) + + # 4. Nothing found: raise error. + raise EnvironmentError( + "Unable to locate GSL:\n" + "1) CONDA_PREFIX not set or no GSL there\n" + "2) GSL_PATH not set or invalid\n" + "3) gsl-config not available\n" + "Please set GSL_PATH or use a conda environment with GSL." + ) + + +class CustomBuildExt(build_ext): + def run(self): + # Retrieve the GSL library directories and append them to each extension. + gsl_cfg = get_gsl_config() + lib_dirs = gsl_cfg.get("library_dirs", []) + for ext in self.extensions: + # Add gsl lib for linking. + ext.library_dirs.extend(lib_dirs) + # Embed RPATH flags, runtime linking without LD_LIBRARY_PATH. + ext.extra_link_args = ext.extra_link_args or [] + for lib in lib_dirs: + ext.extra_link_args.append(f"-Wl,-rpath,{lib}") + super().run() + # Avoid dll error + gsl_path = ( + Path(os.environ.get("GSL_PATH")) + if os.environ.get("GSL_PATH") + else Path(os.environ.get("CONDA_PREFIX", "")) / "Library" + ) + bin_path = gsl_path / "bin" + dest_path = Path(self.build_lib) / "diffpy" / "pdffit2" + dest_path.mkdir(parents=True, exist_ok=True) + for dll_file in bin_path.glob("gsl*.dll"): + shutil.copy(str(dll_file), str(dest_path)) def create_extensions(): - "Initialize Extension objects for the setup function." + """Create the list of Extension objects for the build.""" + # lazy evaluation prevents build sdist failure + try: + gcfg = get_gsl_config() + except EnvironmentError: + return [] + + libraries = ["gsl"] + + include_dirs = [MYDIR] + gcfg["include_dirs"] + library_dirs = gcfg["library_dirs"] + define_macros = [] + extra_objects = [] + extra_compile_args = [] + extra_link_args = [] + + compiler_type = get_compiler_type() + if compiler_type in ("unix", "cygwin", "mingw32"): + extra_compile_args = [ + "-std=c++11", + "-Wall", + "-Wno-write-strings", + "-O3", + "-funroll-loops", + "-ffast-math", + ] + elif compiler_type == "msvc": + define_macros += [("_USE_MATH_DEFINES", None)] + extra_compile_args = ["/EHs"] + + # Extension keyword arguments. + ext_kws = { + "include_dirs": include_dirs, + "libraries": libraries, + "library_dirs": library_dirs, + "define_macros": define_macros, + "extra_compile_args": extra_compile_args, + "extra_link_args": extra_link_args, + "extra_objects": extra_objects, + } ext = Extension( - "diffpy.pdffit2.pdffit2", glob.glob("src/extensions/*.cpp"), **ext_kws + "diffpy.pdffit2.pdffit2", + glob.glob("src/extensions/**/*.cc"), + **ext_kws, ) return [ext] @@ -29,6 +196,7 @@ def create_extensions(): # Extensions not included in pyproject.toml setup_args = dict( ext_modules=[], + cmdclass={"build_ext": CustomBuildExt}, ) diff --git a/tests/conftest.py b/tests/conftest.py index e3b63139..6a142536 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,12 @@ +import io import json from pathlib import Path import pytest +import diffpy.pdffit2 +import diffpy.pdffit2.output # assuming this is the correct import path + @pytest.fixture def user_filesystem(tmp_path): @@ -17,3 +21,30 @@ def user_filesystem(tmp_path): json.dump(home_config_data, f) yield tmp_path + + +@pytest.fixture +def datafile(): + """Fixture to dynamically load any test file.""" + + def _load(filename): + return "tests/testdata/" + filename + + return _load + + +@pytest.fixture +def capture_output(): + """Capture output from pdffit2 engine produced in function call.""" + + def _capture(f, *args, **kwargs): + savestdout = diffpy.pdffit2.output.stdout + fp = io.StringIO() + diffpy.pdffit2.redirect_stdout(fp) + try: + f(*args, **kwargs) + finally: + diffpy.pdffit2.redirect_stdout(savestdout) + return fp.getvalue() + + return _capture From a8689bf450fcc6343a3958b30601c10c291bdc82 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 13:52:20 -0500 Subject: [PATCH 10/18] chore: add docstring back to setup.py --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index d808441b..9a334a8e 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,11 @@ #!/usr/bin/env python # Extensions script for diffpy.pdffit2 +"""PDFfit2 - real space structure refinement engine + +Packages: diffpy.pdffit2 +Scripts: pdffit2 +""" import glob import os From 7293c68d63c908fbc550f12700a18535f85c91a6 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 16:04:35 -0500 Subject: [PATCH 11/18] fix: change import directory --- tests/test_exceptions.py | 3 ++- tests/test_pdffit.py | 3 ++- tests/test_phase_fractions.py | 2 +- tests/test_shape_factors.py | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 83f2b375..07a4f4ea 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -18,7 +18,8 @@ import pytest -from diffpy.pdffit2 import PdfFit, pdffit2 +from diffpy.pdffit2 import pdffit2 +from diffpy.pdffit2.pdffit import PdfFit class read_structExceptions(unittest.TestCase): diff --git a/tests/test_pdffit.py b/tests/test_pdffit.py index 7336f0e6..54b1a1c8 100644 --- a/tests/test_pdffit.py +++ b/tests/test_pdffit.py @@ -6,7 +6,8 @@ import pytest -from diffpy.pdffit2 import PdfFit, pdffit2 +from diffpy.pdffit2 import pdffit2 +from diffpy.pdffit2.pdffit import PdfFit from diffpy.structure import loadStructure # ---------------------------------------------------------------------------- diff --git a/tests/test_phase_fractions.py b/tests/test_phase_fractions.py index ac33ec61..c86961e4 100644 --- a/tests/test_phase_fractions.py +++ b/tests/test_phase_fractions.py @@ -6,7 +6,7 @@ import pytest -from diffpy.pdffit2 import PdfFit +from diffpy.pdffit2.pdffit import PdfFit ############################################################################## diff --git a/tests/test_shape_factors.py b/tests/test_shape_factors.py index 110ce553..70424e58 100644 --- a/tests/test_shape_factors.py +++ b/tests/test_shape_factors.py @@ -8,7 +8,8 @@ import numpy import pytest -from diffpy.pdffit2 import PdfFit, pdffit2 +from diffpy.pdffit2 import pdffit2 +from diffpy.pdffit2.pdffit import PdfFit def spherefactor(r, d): From 3acf21ea80f8cb4d8ab450f4d4743ef42431c1da Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 17:34:12 -0500 Subject: [PATCH 12/18] fix: revert the get release date function. --- src/diffpy/pdffit2/__init__.py | 2 +- src/diffpy/pdffit2/version.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index fceee947..2e143ec8 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -17,7 +17,7 @@ """PDFfit2 - real space structure refinement program.""" # package version -from diffpy.pdffit2.version import __version__ # noqa +from diffpy.pdffit2.version import __date__, __version__ # noqa # silence the pyflakes syntax checker assert __version__ or True diff --git a/src/diffpy/pdffit2/version.py b/src/diffpy/pdffit2/version.py index 0ade3955..7a1eaaaa 100644 --- a/src/diffpy/pdffit2/version.py +++ b/src/diffpy/pdffit2/version.py @@ -17,8 +17,52 @@ # We do not use the other three variables, but can be added back if needed. # __all__ = ["__date__", "__git_commit__", "__timestamp__", "__version__"] +import datetime +import json +import urllib.request + # obtain version information from importlib.metadata import PackageNotFoundError, version +from pathlib import Path + + +def get_pypi_release_date(package_name, timeout=5): + package_file = Path(__file__).resolve() + + try: + with open(package_file, "r", encoding="utf-8") as f: + lines = f.readlines() + for line in reversed(lines): + if line.startswith("# Release date:"): + return line.split(":", 1)[1].strip() + + url = f"https://pypi.org/pypi/{package_name}/json" + with urllib.request.urlopen(url, timeout=timeout) as response: + data = json.loads(response.read().decode("utf-8")) + + installed_version = version(package_name) + release_data = data["releases"].get(installed_version, []) + if not release_data: + raise ValueError( + f"No release data found for version {installed_version}" + ) + + release_date_str = release_data[-1]["upload_time"] + release_date = datetime.datetime.fromisoformat(release_date_str).date() + + with open(package_file, "a", encoding="utf-8") as f: + f.write(f"\n# Release date: {release_date}") + + except (ValueError, OSError) as e: + print(f"Warning: Could not fetch release date from PyPI: {e}") + release_date = datetime.datetime.fromtimestamp( + package_file.stat().st_ctime + ).isoformat() + + return str(release_date) + + +__date__ = get_pypi_release_date("diffpy.pdffit2") try: __version__ = version("diffpy.pdffit2") From 3ec420e41009ed61fd8147c533d15ddce13b24d7 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 17:43:04 -0500 Subject: [PATCH 13/18] revert the import of redirect_stdout --- src/diffpy/pdffit2/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index 2e143ec8..048588ee 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -18,6 +18,7 @@ # package version from diffpy.pdffit2.version import __date__, __version__ # noqa +from diffpy.pdffit2.output import redirect_stdout # silence the pyflakes syntax checker assert __version__ or True From 07635d0d1581c4e207de965f46ea1e92b17ae973 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:43:22 +0000 Subject: [PATCH 14/18] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/pdffit2/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index 048588ee..f6f617fc 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -16,9 +16,10 @@ ############################################################################## """PDFfit2 - real space structure refinement program.""" +from diffpy.pdffit2.output import redirect_stdout + # package version from diffpy.pdffit2.version import __date__, __version__ # noqa -from diffpy.pdffit2.output import redirect_stdout # silence the pyflakes syntax checker assert __version__ or True From ce191940c74e614ff0d58b91fef23bc16c7cdcfd Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 17:47:38 -0500 Subject: [PATCH 15/18] chore: suppress flake8 since it is used in C++ --- src/diffpy/pdffit2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index f6f617fc..1b818238 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -16,7 +16,7 @@ ############################################################################## """PDFfit2 - real space structure refinement program.""" -from diffpy.pdffit2.output import redirect_stdout +from diffpy.pdffit2.output import redirect_stdout # noqa # package version from diffpy.pdffit2.version import __date__, __version__ # noqa From 29005904ac9793d53ddf78284f1ea0e56d2af9a2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:47:57 +0000 Subject: [PATCH 16/18] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/pdffit2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index 1b818238..f22d9cba 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -16,7 +16,7 @@ ############################################################################## """PDFfit2 - real space structure refinement program.""" -from diffpy.pdffit2.output import redirect_stdout # noqa +from diffpy.pdffit2.output import redirect_stdout # noqa # package version from diffpy.pdffit2.version import __date__, __version__ # noqa From d352c0964b621f964d50568099396a081acac24a Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 21 Nov 2025 23:56:02 -0500 Subject: [PATCH 17/18] fix: revert original code to import --- src/diffpy/pdffit2/__init__.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/diffpy/pdffit2/__init__.py b/src/diffpy/pdffit2/__init__.py index f22d9cba..527477a5 100644 --- a/src/diffpy/pdffit2/__init__.py +++ b/src/diffpy/pdffit2/__init__.py @@ -16,12 +16,24 @@ ############################################################################## """PDFfit2 - real space structure refinement program.""" -from diffpy.pdffit2.output import redirect_stdout # noqa +# WARNING: Do NOT remove the isort: off/on comments in this file. +# These tags are used to prevent isort from reordering imports in this file. +# __version__ must be initialized before importing C++ extensions. -# package version -from diffpy.pdffit2.version import __date__, __version__ # noqa +# isort: off +# Import the package version before C++ extensions are loaded. +from diffpy.pdffit2.output import redirect_stdout +from diffpy.pdffit2.version import __date__, __version__ -# silence the pyflakes syntax checker +# Import C++ related modules since the __version__ attribute is used. +from diffpy.pdffit2.pdffit import PdfFit +from diffpy.pdffit2.pdffit2 import is_element + +# isort: on + +# Ensure all necessary components are imported and initialized assert __version__ or True +assert __date__ or True +assert all((PdfFit, redirect_stdout, is_element)) # End of file From 122fecf4ed533fea6670c43afe3defd7d810c4e5 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 22 Nov 2025 13:34:59 -0500 Subject: [PATCH 18/18] chore: delete duplicated file --- CODE_OF_CONDUCT.rst | 133 -------------------------------------------- 1 file changed, 133 deletions(-) delete mode 100644 CODE_OF_CONDUCT.rst diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst deleted file mode 100644 index e8199ca5..00000000 --- a/CODE_OF_CONDUCT.rst +++ /dev/null @@ -1,133 +0,0 @@ -===================================== - Contributor Covenant Code of Conduct -===================================== - -Our Pledge ----------- - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socioeconomic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -Our Standards -------------- - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -Enforcement Responsibilities ----------------------------- - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -Scope ------ - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -Enforcement ------------ - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -sb2896@columbia.edu. All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -Enforcement Guidelines ----------------------- - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -1. Correction -**************** - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -2. Warning -************* - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -3. Temporary Ban -****************** - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -4. Permanent Ban -****************** - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -Attribution ------------ - -This Code of Conduct is adapted from the `Contributor Covenant `_. - -Community Impact Guidelines were inspired by `Mozilla's code of conduct enforcement ladder `_. - -For answers to common questions about this code of conduct, see the `FAQ `_. `Translations are available `_