From 3ff89d47e997105826a00bcad1a86156518c66b1 Mon Sep 17 00:00:00 2001 From: Mat Doucet Date: Mon, 9 Sep 2024 15:30:00 -0400 Subject: [PATCH] Initial commit --- .github/dependabot.yml | 11 ++ .github/workflows/package.yml | 75 +++++++++ .github/workflows/unittest.yml | 48 ++++++ .gitignore | 161 +++++++++++++++++++ .pre-commit-config.yaml | 26 +++ .readthedocs.yaml | 14 ++ CODE_OF_CONDUCT.md | 128 +++++++++++++++ LICENSE | 21 +++ README.md | 154 ++++++++++++++++++ codecov.yaml | 9 ++ conda.recipe/meta.yaml | 46 ++++++ docs/README.md | 1 + docs/conf.py | 76 +++++++++ docs/index.rst | 17 ++ environment.yml | 44 +++++ notebooks/example.ipynb | 27 ++++ pyproject.toml | 71 ++++++++ scripts/myscripts.py | 2 + src/packagenamepy/__init__.py | 13 ++ src/packagenamepy/configuration.py | 105 ++++++++++++ src/packagenamepy/configuration_template.ini | 2 + src/packagenamepy/help/help_model.py | 12 ++ src/packagenamepy/home/home_model.py | 12 ++ src/packagenamepy/home/home_presenter.py | 19 +++ src/packagenamepy/home/home_view.py | 13 ++ src/packagenamepy/mainwindow.py | 54 +++++++ src/packagenamepy/packagename.py | 62 +++++++ tests/test_version.py | 5 + 28 files changed, 1228 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/package.yml create mode 100644 .github/workflows/unittest.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 codecov.yaml create mode 100644 conda.recipe/meta.yaml create mode 120000 docs/README.md create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 environment.yml create mode 100644 notebooks/example.ipynb create mode 100644 pyproject.toml create mode 100644 scripts/myscripts.py create mode 100644 src/packagenamepy/__init__.py create mode 100644 src/packagenamepy/configuration.py create mode 100644 src/packagenamepy/configuration_template.ini create mode 100644 src/packagenamepy/help/help_model.py create mode 100644 src/packagenamepy/home/home_model.py create mode 100644 src/packagenamepy/home/home_presenter.py create mode 100644 src/packagenamepy/home/home_view.py create mode 100644 src/packagenamepy/mainwindow.py create mode 100644 src/packagenamepy/packagename.py create mode 100644 tests/test_version.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5cf92a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Workflow files stored in the default location of `.github/workflows` + schedule: + interval: "weekly" diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..836de4b --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,75 @@ +name: conda packaging and deployment + +on: + workflow_dispatch: + push: + branches: [qa, main] + tags: ['v*'] + +jobs: + linux: + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + steps: + - uses: actions/checkout@v4 + - uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + channels: conda-forge,defaults + mamba-version: "*" + environment-file: environment.yml + cache-environment-key: ${{ runner.os }}-env-${{ hashFiles('**/environment.yml') }} + cache-downloads-key: ${{ runner.os }}-downloads-${{ hashFiles('**/environment.yml') }} + - name: install additional dependencies + run: | + echo "installing additional dependencies from environment_development.yml" + - name: build conda package + run: | + # set up environment + cd conda.recipe + echo "versioningit $(versioningit ../)" + # build the package + VERSION=$(versioningit ../) conda mambabuild --channel conda-forge --output-folder . . + conda verify noarch/mypackagename*.tar.bz2 + - name: upload conda package to anaconda + shell: bash -l {0} + if: startsWith(github.ref, 'refs/tags/v') + env: + ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }} + IS_RC: ${{ contains(github.ref, 'rc') }} + run: | + # label is main or rc depending on the tag-name + CONDA_LABEL="main" + if [ "${IS_RC}" = "true" ]; then CONDA_LABEL="rc"; fi + echo pushing ${{ github.ref }} with label $CONDA_LABEL + anaconda upload --label $CONDA_LABEL conda.recipe/noarch/mypackagename*.tar.bz2 + + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + channels: conda-forge,defaults + mamba-version: "*" + environment-file: environment.yml + cache-environment-key: ${{ runner.os }}-env-${{ hashFiles('**/environment.yml') }} + cache-downloads-key: ${{ runner.os }}-downloads-${{ hashFiles('**/environment.yml') }} + - name: build pypi package + run: | + # build the package + VERSION=$(versioningit .) python -m build + # publish your distributions here (need to setup on PyPI first) + - name: Publish package distributions to PyPI + if: startsWith(github.ref, 'refs/tags/v') + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..902fa68 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,48 @@ +name: unit-test + +on: + workflow_dispatch: + pull_request: + push: + branches: [next, qa, main] + tags: ['v*'] + +jobs: + linux: + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + steps: + - uses: actions/checkout@v4 + - uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + channels: conda-forge,defaults + mamba-version: "*" + environment-file: environment.yml + cache-environment-key: ${{ runner.os }}-env-${{ hashFiles('**/environment.yml') }} + cache-downloads-key: ${{ runner.os }}-downloads-${{ hashFiles('**/environment.yml') }} + - name: install additional dependencies + run: | + echo "installing additional dependencies if cannot be installed from conda" + - name: run unit tests + run: | + echo "running unit tests" + python -m pytest --cov=src --cov-report=xml --cov-report=term-missing tests/ + - name: upload coverage to codecov + uses: codecov/codecov-action@v4 + if: + github.actor != 'dependabot[bot]' + with: + token: ${{ secrets.CODECOV_TOKEN }} + - name: build conda package + run: | + # test that the conda package builds + cd conda.recipe + echo "versioningit $(versioningit ../)" + # conda channels could have been defined in the conda-incubator, but you can copy/paste the lines + # below to build the conda package in your local machine + CHANNELS="--channel mantid/label/main --channel conda-forge" + VERSION=$(versioningit ../) conda mambabuild $CHANNELS --output-folder . . + conda verify noarch/mypackagename*.tar.bz2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e88523 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +.envrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..469e00f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + args: [--maxkb=8192] + - id: check-merge-conflict + - id: check-yaml + args: [--allow-multiple-documents] + exclude: "conda.recipe/meta.yaml" + - id: end-of-file-fixer + exclude: "tests/cis_tests/.*" + - id: trailing-whitespace + exclude: "tests/cis_tests/.*" +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.3 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + exclude: "tests/cis_tests/.*" + - id: ruff-format + exclude: "tests/cis_tests/.*" +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..5c7176e --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "mambaforge-22.9" + +sphinx: + builder: html + configuration: docs/conf.py + fail_on_warning: true + +conda: + environment: environment.yml diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c55f5e1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# 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, 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 e-mail 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 +petersonpf@ornl.gov, zhangc@ornl.gov, bilheuxjm@ornl.gov. +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][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..076db6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Neutron Scattering Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c51529 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +python_project_template +======================= + +This repository is a template repository for Python projects under neutrons. +After you create a new repository using this repo as template, please follow the following steps to adjust it for the new project. + +Codebase Adjustments +-------------------- + +1. Adjust the branch protection rules for the new repo. By default, we should protect the `main` (stable), `qa` (release candidate), and `next` (development) branches. + + 1.1 Go to the `Settings` tab of the new repo. + + 1.2 Click on `Branches` on the left side. + + 1.3 Click on `Add rule` button. + + 1.4 Follow the instructions from Github. + + +2. Change the License if MIT license is not suitable for you project. For more information about licenses, please +refer to [Choose an open source license](https://choosealicense.com/). + + +3. Update the environment dependency file `environment.yml`, which contain both runtime and development dependencies. +For more information about conda environment file, please refer to [Conda environment file](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually). + + 3.1 Specify environment 'name' field to match package name + + 3.2 We strongly recommended using a single `environment.yml` file to manage all the dependencies, including the runtime and development dependencies. + + 3.3 Please add comments to the `environment.yml` file to explain the dependencies. + + 3.4 Please prune the dependencies to the minimum when possible, we would like the solver to figure out the dependency tree for us. + + +4. Adjust pre-commit configuration file, `.pre-commit-config.yaml` to enable/disable the hooks you need. For more information about pre-commit, please refer to [pre-commit](https://pre-commit.com/). + + +5. Having code coverage, `codecov.yaml` is **strongly recommended**, please refer to [Code coverage](https://coverage.readthedocs.io/en/coverage-5.5/) for more information. + + +6. Adjust the demo Github action yaml files for CI/CD. For more information about Github action, please refer to [Github action](https://docs.github.com/en/actions). + + 6.1 Specify package name at: .github/workflows/package.yml#L34 + + 6.2 Specify package name at: .github/workflows/package.yml#L46 + + +7. Adjust the conda recipe, `conda-recipe/meta.yaml` to provide the meta information for the conda package. For more information about conda recipe, please refer to [Conda build](https://docs.conda.io/projects/conda-build/en/latest/). + + 7.1 Specify package name at: conda.recipe/meta.yaml#L15 + + 7.2 Update license family, if necessary: conda.recipe/meta.yaml#L42 + + +8. Adjust `pyproject.toml` to match your project. For more information about `pyproject.toml`, +please refer to [pyproject.toml](https://www.python.org/dev/peps/pep-0518/). + + 8.1 Specify package name at: pyproject.toml#L2 + + 8.2 Specify package description at: pyproject.toml#L3 + + 8.3 Specify package name at: pyproject.toml#L39 + + 8.4 Specify any terminal entry points (terminal commands) at: pyproject.toml#48. + +In the example, invoking `packagename-cli` in a terminal is equivalent to running the python script `from packagenamepy.packagename.import main; main()` + + 8.5 Projects will use a single `pyproject.toml` file to manage all the project metadata, including the project name, version, author, license, etc. + + 8.6 Python has moved away from `setup.cfg`/`setup.py`, and we would like to follow the trend for our new projects. + + +10. Specify package name at src/packagenamepy + + +11. Specify package name at: src/packagenamepy/packagename.py + +12. If a GUI isn't used, delete the MVP structure at src/packagenamepy: + 11.1: mainwindow.py + 11.2: home/ + 11.3: help/ + + +11. Clear the content of this file and add your own README.md as the project README file. +We recommend putting badges of the project status at the top of the README file. +For more information about badges, please refer to [shields.io](https://shields.io/). + +Repository Adjustments +---------------------- + +### Add an access token to anaconda + +Here we assume your intent is to upload the conda package to the [anaconda.org/neutrons](https://anaconda.org/neutrons) organization. +An administrator of `anaconda.org/neutrons` must create an access token for your repository in the [access settings](https://anaconda.org/neutrons/settings/access). + +After created, the token must be stored in a `repository secret`: +1. Navigate to the main page of the repository on GitHub.com. +2. Click on the "Settings" tab. +3. In the left sidebar, navigate to the "Security" section and select "Secrets and variables" followed by "Actions". +4. Click on the "New repository secret" button. +5. Enter `ANACONDA_TOKEN` for the secret name +6. Paste the Anaconda access token +7. Click on the "Add secret" button +8. Test the setup by creating a release candidate tag, +which will result in a package built and uploaded to https://anaconda.org/neutrons/mypackagename + +### Add an access token to codecov + +Follow the instructions in the [Confluence page](https://ornl-neutrons.atlassian.net/wiki/spaces/NDPD/pages/103546883/Coverage+reports) +to create the access token. + +Packaging building instructions +------------------------------- + +The default package publishing service is anaconda. +However, we also support PyPI publishing as well. + +### Instruction for publish to PyPI + +1. Make sure you have the correct access to the project on PyPI. +2. Make sure `git status` returns a clean state. +3. At the root of the repo, use `python -m build` to generate the wheel. +4. Check the wheel with `twine check dist/*`, everything should pass before we move to next step. +5. When doing manual upload test, make sure to use testpypi instead of pypi. +6. Use `twine upload --repository testpypi dist/*` to upload to testpypi, you will need to specify the testpipy url in your `~/.pypirc`, i.e. + +`````` +[distutils] +index-servers = pypi, testpypi + +[testpypi] + repository = https://test.pypi.org/legacy/ + username = __token__ + password = YOUR_TESTPYPI_TOKEN + +`````` + +7. Test the package on testpypi with `pip install --index-url https://test.pypi.org/simple/ mypackagename`. +8. If everything is good, use the Github workflow, `package.yml` to trigger the publishing to PyPI. + +### Instruction for publish to Anaconda + +Publishing to Anaconda is handled via workflow, `package.yml`. + +Development environment setup +----------------------------- + +### Build development environment + +1. By default, we recommend providing a single `environment.yml` that covers all necessary packages for development. +2. The runtime dependency should be in `meta.yaml` for anaconda packaging, and `pyproject.toml` for PyPI publishing. +3. When performing editable install for your feature branch, make sure to use `pip install --no-deps -e .` to ensure that `pip` does not install additional packages from `pyproject.toml` into development environment by accident. diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 0000000..1a88f41 --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,9 @@ +# Configuration file for codecov reporting code coverage + +# Percentage drop allowed +coverage: + status: + project: + default: + # base on last build, but allow drop of upto this percent + threshold: 0.5% diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml new file mode 100644 index 0000000..09e37d7 --- /dev/null +++ b/conda.recipe/meta.yaml @@ -0,0 +1,46 @@ +# load information from pyproject.toml +{% set pyproject = load_file_data('pyproject.toml') %} +{% set project = pyproject.get('project', {}) %} +{% set license = project.get('license').get('text') %} +{% set description = project.get('description') %} +{% set project_url = pyproject.get('project', {}).get('urls') %} +{% set url = project_url.get('homepage') %} +# this will get the version set by environment variable +{% set version = environ.get('VERSION') %} +{% set version_number = version.split('+')[0] %} +# change the build number by hand if you want to rebuild the package +{% set build_number = 0 %} + +package: + name: mypackagename + version: {{ version_number }} + +source: + path: .. + +build: + noarch: python + number: {{ build_number }} + string: py{{py}} + script: {{ PYTHON }} -m pip install . --no-deps --ignore-installed -vvv + +requirements: + host: + - python + - versioningit + - setuptools>=42 + - wheel + + build: + - setuptools>=42 + - versioningit + + run: + - python + +about: + home: {{ url }} + license: {{ license }} + license_family: MIT + license_file: ../LICENSE + summary: {{ description }} diff --git a/docs/README.md b/docs/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..904bceb --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,76 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys + +import versioningit + +sys.path.insert(0, os.path.abspath("../src")) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "Project Name" +copyright = "Copyright 2024" # noqa A001 +author = "Author Name" + +# The short X.Y version +# NOTE: need to specify the location of the pyproject.toml file instead of the +# location of the source tree +version = versioningit.get_version("..") +# The full version, including alpha/beta/rc tags +release = ".".join(version.split(".")[:-1]) + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "myst_parser", +] +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = [".rst", ".md"] + +# The master toctree document. +master_doc = "index" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" # please add corresponding package to environment.yml if you want to use it +autosummary_generate = True + +# Napoleon settings +napoleon_google_docstring = False +napoleon_numpy_docstring = True + +# "Static" resources (like *.css *.js files, or images) to be included when generating the HTML documentation. +if os.path.exists("_static"): + html_static_path = ["_static"] + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..b3000f3 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +========================================= +Welcome to python project template docs! +========================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + README + +========================================= +Indices and tables +========================================= + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..2a1abfd --- /dev/null +++ b/environment.yml @@ -0,0 +1,44 @@ +name: mypythonapp +channels: + - conda-forge +dependencies: + # -- Runtime dependencies + # base: list all base dependencies here + - python>=3.8 # please specify the minimum version of python here + - versioningit + # compute: list all compute dependencies here + - numpy + - pandas + # plot: list all plot dependencies here, if applicable + - matplotlib + # jupyter: list all jupyter dependencies here, if applicable + - jupyterlab + - ipympl + # -- Development dependencies + # utils: + - pre-commit + # package building: + - libmamba + - libarchive + - anaconda-client + - boa + - conda-build < 4 # conda-build 24.x has a bug, missing update_index from conda_build.index + - conda-verify + - python-build + - twine # for uploading to pypi and testpypi + # docs + - sphinx + - sphinx_rtd_theme + - myst-parser # required for parsing markdown files + # test: list all test dependencies here + - pytest + - pytest-cov + - pytest-xdist + # -------------------------------------------------- + # add additional sections such as Qt, etc. if needed + # -------------------------------------------------- + # if packages are not available on conda, list them here + - pip + - pip: + - bm3d-streak-removal # example + - pytest-playwright diff --git a/notebooks/example.ipynb b/notebooks/example.ipynb new file mode 100644 index 0000000..7fa8701 --- /dev/null +++ b/notebooks/example.ipynb @@ -0,0 +1,27 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Overview\n", + "\n", + "This folder is used to store notebooks that demonstrate how to use the library in an interactive environment like Jupyter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3943d8b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[project] +name = "examplepyapp" +description = "Example Python repo for neutrons" +dynamic = ["version"] +requires-python = ">=3.10" +dependencies = [ + # list all runtime dependencies here +] +license = { text = "MIT" } +keywords = ["neutrons", "example", "python"] +readme = "README.md" + +[project.urls] +homepage = "https://github.com/neutrons/python_project_template/" # if no homepage, use repo url +repository = "https://github.com/neutrons/python_project_template/" +# documentation = add_url_to_readthedoc_here +issues = "https://github.com/neutrons/python_project_template/issues" + +[build-system] +requires = [ + "setuptools>= 42", + "wheel", + "toml", + "versioningit" +] +build-backend = "setuptools.build_meta" + +[tool.versioningit.vcs] +method = "git" +default-tag = "0.0.1" + +[tool.versioningit.next-version] +method = "minor" + +[tool.versioningit.format] +distance = "{next_version}.dev{distance}" +dirty = "{version}+d{build_date:%Y%m%d}" +distance-dirty = "{next_version}.dev{distance}+d{build_date:%Y%m%d%H%M}" + +[tool.versioningit.write] +file = "src/packagenamepy/_version.py" + +[tool.setuptools.packages.find] +where = ["src"] +exclude = ["tests*", "scripts*", "docs*", "notebooks*"] + +[tool.setuptools.package-data] +"*" = ["*.yml","*.yaml","*.ini"] + +[project.scripts] +packagename-cli = "packagenamepy.packagename:main" + +[project.gui-scripts] +packagenamepy = "packagenamepy.packagename:gui" + +[tool.pytest.ini_options] +pythonpath = [ + ".", "src", "scripts" +] +testpaths = ["tests"] +python_files = ["test*.py"] +norecursedirs = [".git", "tmp*", "_tmp*", "__pycache__", "*dataset*", "*data_set*"] +markers = [ + "mymarker: example markers goes here" +] + +[tool.ruff] +line-length = 120 +select = ["A", "ARG","ASYNC","BLE","C90", "E", "F", "I", "N", "UP032", "W"] + +# Add additional 3rd party tool configuration here as needed diff --git a/scripts/myscripts.py b/scripts/myscripts.py new file mode 100644 index 0000000..fce7406 --- /dev/null +++ b/scripts/myscripts.py @@ -0,0 +1,2 @@ +"""This is a script that use the package as a module.""" +#!/usr/bin/env python diff --git a/src/packagenamepy/__init__.py b/src/packagenamepy/__init__.py new file mode 100644 index 0000000..96e1d74 --- /dev/null +++ b/src/packagenamepy/__init__.py @@ -0,0 +1,13 @@ +"""Contains the entry point for the application""" + +try: + from ._version import __version__ # noqa: F401 +except ImportError: + __version__ = "unknown" + + +def PackageName(): # noqa N802 + """This is needed for backward compatibility because mantid workbench does "from shiver import Shiver" """ + from .packagenamepy import PackageName as packagename # noqa N813 + + return packagename() diff --git a/src/packagenamepy/configuration.py b/src/packagenamepy/configuration.py new file mode 100644 index 0000000..2231ab6 --- /dev/null +++ b/src/packagenamepy/configuration.py @@ -0,0 +1,105 @@ +"""Module to load the the settings from SHOME/.packagename/configuration.ini file + +Will fall back to a default +""" + +import os +import shutil +from configparser import ConfigParser +from pathlib import Path + +from mantid.kernel import Logger + +logger = Logger("PACKAGENAME") + +# configuration settings file path +CONFIG_PATH_FILE = os.path.join(Path.home(), ".packagename", "configuration.ini") + + +class Configuration: + """Load and validate Configuration Data""" + + def __init__(self): + """Initialization of configuration mechanism""" + # capture the current state + self.valid = False + + # locate the template configuration file + project_directory = Path(__file__).resolve().parent + self.template_file_path = os.path.join(project_directory, "configuration_template.ini") + + # retrieve the file path of the file + self.config_file_path = CONFIG_PATH_FILE + logger.information(f"{self.config_file_path} will be used") + + # if template conf file path exists + if os.path.exists(self.template_file_path): + # file does not exist create it from template + if not os.path.exists(self.config_file_path): + # if directory structure does not exist create it + if not os.path.exists(os.path.dirname(self.config_file_path)): + os.makedirs(os.path.dirname(self.config_file_path)) + shutil.copy2(self.template_file_path, self.config_file_path) + + self.config = ConfigParser(allow_no_value=True, comment_prefixes="/") + # parse the file + try: + self.config.read(self.config_file_path) + # validate the file has the all the latest variables + self.validate() + except ValueError as err: + logger.error(str(err)) + logger.error(f"Problem with the file: {self.config_file_path}") + else: + logger.error(f"Template configuration file: {self.template_file_path} is missing!") + + def validate(self): + """Validates that the fields exist at the config_file_path and writes any missing fields/data + using the template configuration file: configuration_template.ini as a guide + """ + template_config = ConfigParser(allow_no_value=True, comment_prefixes="/") + template_config.read(self.template_file_path) + for section in template_config.sections(): + # if section is missing + if section not in self.config.sections(): + # copy the whole section + self.config.add_section(section) + + for item in template_config.items(section): + field, _ = item + if field not in self.config[section]: + # copy the field + self.config[section][field] = template_config[section][field] + with open(self.config_file_path, "w", encoding="utf8") as config_file: + self.config.write(config_file) + self.valid = True + + def is_valid(self): + """Returns the configuration state""" + return self.valid + + +def get_data(section, name=None): + """Retrieves the configuration data for a variable with name""" + # default file path location + config_file_path = CONFIG_PATH_FILE + if os.path.exists(config_file_path): + config = ConfigParser() + # parse the file + config.read(config_file_path) + try: + if name: + value = config[section][name] + # in case of boolean string value cast it to bool + if value in ("True", "False"): + return value == "True" + # in case of None + if value == "None": + return None + return value + return config[section] + except KeyError as err: + # requested section/field do not exist + logger.error(str(err)) + return None + return None diff --git a/src/packagenamepy/configuration_template.ini b/src/packagenamepy/configuration_template.ini new file mode 100644 index 0000000..c8ea100 --- /dev/null +++ b/src/packagenamepy/configuration_template.ini @@ -0,0 +1,2 @@ +[global.other] +help_url = https://github.com/neutrons/python_project_template/blob/main/README.md diff --git a/src/packagenamepy/help/help_model.py b/src/packagenamepy/help/help_model.py new file mode 100644 index 0000000..636472d --- /dev/null +++ b/src/packagenamepy/help/help_model.py @@ -0,0 +1,12 @@ +"""single help module""" + +import webbrowser + +from packagenamepy.configuration import get_data + + +def help_function(context): + """Open a browser with the appropriate help page""" + help_url = get_data("global.other", "help_url") + if context: + webbrowser.open(help_url) diff --git a/src/packagenamepy/home/home_model.py b/src/packagenamepy/home/home_model.py new file mode 100644 index 0000000..e574925 --- /dev/null +++ b/src/packagenamepy/home/home_model.py @@ -0,0 +1,12 @@ +"""Model for the Main tab""" + +from mantid.kernel import Logger + +logger = Logger("PACKAGENAME") + + +class HomeModel: # pylint: disable=too-many-public-methods + """Main model""" + + def __init__(self): + return diff --git a/src/packagenamepy/home/home_presenter.py b/src/packagenamepy/home/home_presenter.py new file mode 100644 index 0000000..1b55b34 --- /dev/null +++ b/src/packagenamepy/home/home_presenter.py @@ -0,0 +1,19 @@ +"""Presenter for the Main tab""" + + +class HomePresenter: # pylint: disable=too-many-public-methods + """Main presenter""" + + def __init__(self, view, model): + self._view = view + self._model = model + + @property + def view(self): + """Return the view for this presenter""" + return self._view + + @property + def model(self): + """Return the model for this presenter""" + return self._model diff --git a/src/packagenamepy/home/home_view.py b/src/packagenamepy/home/home_view.py new file mode 100644 index 0000000..61a8e71 --- /dev/null +++ b/src/packagenamepy/home/home_view.py @@ -0,0 +1,13 @@ +"""PyQt widget for the main tab""" + +from qtpy.QtWidgets import QHBoxLayout, QWidget + + +class Home(QWidget): # pylint: disable=too-many-public-methods + """Main widget""" + + def __init__(self, parent=None): + super().__init__(parent) + + layout = QHBoxLayout() + self.setLayout(layout) diff --git a/src/packagenamepy/mainwindow.py b/src/packagenamepy/mainwindow.py new file mode 100644 index 0000000..16c8aec --- /dev/null +++ b/src/packagenamepy/mainwindow.py @@ -0,0 +1,54 @@ +"""Main Qt window""" + +from qtpy.QtWidgets import QHBoxLayout, QPushButton, QTabWidget, QVBoxLayout, QWidget + +from packagenamepy.help.help_model import help_function +from packagenamepy.home.home_model import HomeModel +from packagenamepy.home.home_presenter import HomePresenter +from packagenamepy.home.home_view import Home + + +class MainWindow(QWidget): + """Main widget""" + + def __init__(self, parent=None): + super().__init__(parent) + + ### Create tabs here ### + + ### Main tab + self.tabs = QTabWidget() + home = Home(self) + home_model = HomeModel() + self.home_presenter = HomePresenter(home, home_model) + self.tabs.addTab(home, "Home") + + ### Set tab layout + layout = QVBoxLayout() + layout.addWidget(self.tabs) + + ### Create bottom interface here ### + + # Help button + help_button = QPushButton("Help") + help_button.clicked.connect(self.handle_help) + + # Set bottom interface layout + hor_layout = QHBoxLayout() + hor_layout.addWidget(help_button) + + layout.addLayout(hor_layout) + + self.setLayout(layout) + + # register child widgets to make testing easier + self.home = home + + def handle_help(self): + """Get current tab type and open the corresponding help page""" + open_tab = self.tabs.currentWidget() + if isinstance(open_tab, Home): + context = "home" + else: + context = "" + help_function(context=context) diff --git a/src/packagenamepy/packagename.py b/src/packagenamepy/packagename.py new file mode 100644 index 0000000..15db777 --- /dev/null +++ b/src/packagenamepy/packagename.py @@ -0,0 +1,62 @@ +"""Main Qt application""" + +import sys + +from mantid.kernel import Logger +from mantidqt.gui_helper import set_matplotlib_backend +from qtpy.QtWidgets import QApplication, QMainWindow + +# make sure matplotlib is correctly set before we import shiver +set_matplotlib_backend() + +# make sure the algorithms have been loaded so they are available to the AlgorithmManager +import mantid.simpleapi # noqa: F401, E402 pylint: disable=unused-import, wrong-import-position + +from packagenamepy import __version__ # noqa: E402 pylint: disable=wrong-import-position +from packagenamepy.configuration import Configuration # noqa: E402 pylint: disable=wrong-import-position +from packagenamepy.mainwindow import MainWindow # noqa: E402 pylint: disable=wrong-import-position + +logger = Logger("PACKAGENAME") + + +class PackageName(QMainWindow): + """Main Package window""" + + __instance = None + + def __new__(cls): + if PackageName.__instance is None: + PackageName.__instance = QMainWindow.__new__(cls) # pylint: disable=no-value-for-parameter + return PackageName.__instance + + def __init__(self, parent=None): + super().__init__(parent) + logger.information(f"PackageName version: {__version__}") + config = Configuration() + + if not config.is_valid(): + msg = ( + "Error with configuration settings!", + f"Check and update your file: {config.config_file_path}", + "with the latest settings found here:", + f"{config.template_file_path} and start the application again.", + ) + + print(" ".join(msg)) + sys.exit(-1) + self.setWindowTitle(f"PACKAGENAME - {__version__}") + self.main_window = MainWindow(self) + self.setCentralWidget(self.main_window) + + +def gui(): + """Main entry point for Qt application""" + input_flags = sys.argv[1::] + if "--v" in input_flags or "--version" in input_flags: + print(__version__) + sys.exit() + else: + app = QApplication(sys.argv) + window = PackageName() + window.show() + sys.exit(app.exec_()) diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..5b09eca --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,5 @@ +from packagenamepy import __version__ + + +def test_version(): + assert __version__ == "unknown"