diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..08bcf97 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,47 @@ +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@v3 + - uses: conda-incubator/setup-miniconda@v2 + 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 diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..edbd240 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,44 @@ +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@v3 + - uses: conda-incubator/setup-miniconda@v2 + 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@v1 + - 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..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# 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/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..01b9e75 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.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.2.2 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + exclude: "tests/cis_tests/.*" + - id: ruff-format + exclude: "tests/cis_tests/.*" diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..6862e41 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "mambaforge-4.10" + +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..1f821f8 --- /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, socio-economic 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/README.md b/README.md new file mode 100644 index 0000000..0697b77 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# 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 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..bb624f7 --- /dev/null +++ b/conda.recipe/meta.yaml @@ -0,0 +1,44 @@ +# 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 + + build: + - setuptools + - versioningit + + run: + - python + +about: + home: {{ url }} + license: {{ license }} + license_family: MIT + license_file: ../LICENSE + summary: {{ description }} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..203bf5e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,72 @@ +# 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" +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", +] +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 = "alabaster" # "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 + +html_static_path = ["_static"] + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..248292d --- /dev/null +++ b/environment.yml @@ -0,0 +1,39 @@ +name: mypythonapp +channels: + - conda-forge +dependencies: + # -- Runtime dependencies + # base: list all base dependencies here + - python>=3.8 # please specify the mimimum 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 + # pacakge 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 + # test: list all test dependencies here + - pytest + - pytest-cov + - pytest-xdist + # -------------------------------------------------- + # add additional sections such as Qt, etc. if needed + # -------------------------------------------------- + # if pakcages are not available on conda, list them here + - pip + - pip: + - bm3d-streak-removal # example + - pytest-playwright diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a192ea1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[project] +name = "examplepyapp" +description = "Example Python repo for neutrons" +dynamic = ["version"] +requires-python = ">=3.10" +dependencies = [ + # list all runtime dependencies here +] +license = { text = "MIT" } + +[project.urls] +homepage = "https://github.com/neutrons/python_project_template/" # if no homepage, use repo url + +[build-system] +requires = [ + "setuptools >= 40.6.0", + "wheel", + "toml", + "versioningit" +] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 119 + +[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.pylint] +max-line-length = 120 +disable = ["too-many-locals", + "too-many-statements", + "too-many-instance-attributes", + "too-many-arguments", + "duplicate-code" +] + +# Add additional 3rd party tool configuration here as needed diff --git a/src/packagenamepy/__init__.py b/src/packagenamepy/__init__.py new file mode 100644 index 0000000..88d1a76 --- /dev/null +++ b/src/packagenamepy/__init__.py @@ -0,0 +1,15 @@ +""" +Contains the entry point for the application +""" + +try: + from ._version import __version__ # noqa: F401 +except ImportError: + __version__ = "unknown" + + +def PackageName(): # pylint: disable=invalid-name + """This is needed for backward compatibility because mantid workbench does "from shiver import Shiver" """ + from .packagenamepy import PackageName as packagename # pylint: disable=import-outside-toplevel + + return packagename() diff --git a/src/packagenamepy/configuration.py b/src/packagenamepy/configuration.py new file mode 100644 index 0000000..4a0baf5 --- /dev/null +++ b/src/packagenamepy/configuration.py @@ -0,0 +1,106 @@ +"""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..1ecc555 --- /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..e40810e --- /dev/null +++ b/src/packagenamepy/home/home_model.py @@ -0,0 +1,14 @@ +"""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..a710d34 --- /dev/null +++ b/src/packagenamepy/home/home_view.py @@ -0,0 +1,12 @@ +"""PyQt widget for the main tab""" +from qtpy.QtWidgets import QWidget, QHBoxLayout + + +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..351bd73 --- /dev/null +++ b/src/packagenamepy/mainwindow.py @@ -0,0 +1,59 @@ +""" +Main Qt window +""" + +from qtpy.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QPushButton + +from packagenamepy.home.home_view import Home +from packagenamepy.home.home_model import HomeModel +from packagenamepy.home.home_presenter import HomePresenter + +from packagenamepy.help.help_model import help_function + + +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..d30dde6 --- /dev/null +++ b/src/packagenamepy/packagename.py @@ -0,0 +1,66 @@ +""" +Main Qt application +""" + +import sys +from qtpy.QtWidgets import QApplication, QMainWindow + +from mantid.kernel import Logger +from mantidqt.gui_helper import set_matplotlib_backend + +# 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.configuration import Configuration # noqa: E402 pylint: disable=wrong-import-position +from packagenamepy.version import __version__ # 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/src/packagenamepy/version.py b/src/packagenamepy/version.py new file mode 100644 index 0000000..6f674ca --- /dev/null +++ b/src/packagenamepy/version.py @@ -0,0 +1,8 @@ +"""Module to load the version created by versioningit + +Will fall back to a default packagename is not installed""" + +try: + from ._version import __version__ +except ModuleNotFoundError: + __version__ = "0.0.1" 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"