diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4a69ec7..dad8dcd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -27,3 +27,5 @@ - [ ] New/changed functions and methods are covered in the test suite where possible - [ ] Test suite passes locally - [ ] Test suite passes on GitHub Actions +- [ ] Ran ``docs/pre-release-notes.sh`` and created a pre-release documentation page +- [ ] Pre-release docs include context, functional descriptions, and contributors as appropriate diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index a0e8793..15228dd 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -21,12 +21,19 @@ requirements: - setuptools_scm run: - python >=3.9 + - pcdsutils + - pyqt + - qtpy test: imports: - {{ import_name }} requires: + - coverage - pytest + - pytest-qt + - sphinx + - sphinx_rtd_theme about: home: https://github.com/pcdshub/superscore diff --git a/dev-requirements.txt b/dev-requirements.txt index f0fe66d..ca01160 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,9 @@ # These are required for developing the package (running the tests) but not # necessarily required for _using_ it. +coverage pytest pytest-cov +pytest-qt + +sphinx +sphinx_rtd_theme diff --git a/docs/pre-release-notes.sh b/docs/pre-release-notes.sh new file mode 100755 index 0000000..6e1e12b --- /dev/null +++ b/docs/pre-release-notes.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +ISSUE=$1 +shift +DESCRIPTION=$* + +if [[ -z "$ISSUE" || -z "$DESCRIPTION" ]]; then + echo "Usage: $0 (ISSUE NUMBER) (DESCRIPTION)" + exit 1 +fi + +re_issue_number='^[1-9][0-9]*$' + +if ! [[ "$ISSUE" =~ $re_issue_number ]]; then + echo "Error: Issue number is not a number: $ISSUE" + echo + echo "This should preferably be the issue number that this pull request solves." + echo "We may also accept the Pull Request number in place of the issue." + exit 1 +fi + +echo "Issue: $ISSUE" +echo "Description: $DESCRIPTION" + +FILENAME=source/upcoming_release_notes/${ISSUE}-${DESCRIPTION// /_}.rst + +pushd "$(dirname "$0")" || exit 1 + +sed -e "s/IssueNumber Title/${ISSUE} ${DESCRIPTION}/" \ + "source/upcoming_release_notes/template-short.rst" > "${FILENAME}" + +if ${EDITOR} "${FILENAME}"; then + echo "Adding ${FILENAME} to the git repository..." + git add "${FILENAME}" +fi + +popd || exit 0 diff --git a/docs/release_notes.py b/docs/release_notes.py new file mode 100644 index 0000000..4b5d0ee --- /dev/null +++ b/docs/release_notes.py @@ -0,0 +1,114 @@ +import sys +import time +from collections import defaultdict +from pathlib import Path + +# find the pre-release directory and release notes file +THIS_DIR = Path(__file__).resolve().parent +PRE_RELEASE = THIS_DIR / 'source' / 'upcoming_release_notes' +TEMPLATE = PRE_RELEASE / 'template-short.rst' +RELEASE_NOTES = THIS_DIR / 'source' / 'releases.rst' + +# Set up underline constants +TITLE_UNDER = '#' +RELEASE_UNDER = '=' +SECTION_UNDER = '-' + + +def parse_pre_release_file(path): + """ + Return dict mapping of release notes section to lines. + + Uses empty list when no info was entered for the section. + """ + print(f'Checking {path} for release notes.') + with path.open('r') as fd: + lines = fd.readlines() + + section_dict = defaultdict(list) + prev = None + section = None + + for line in lines: + if prev is not None: + if line.startswith(SECTION_UNDER * 2): + section = prev.strip() + continue + if section is not None and line[0] in ' -': + notes = section_dict[section] + if len(line) > 6: + notes.append(line) + section_dict[section] = notes + prev = line + + return section_dict + + +def extend_release_notes(path, version, release_notes): + """ + Given dict mapping of section to lines, extend the release notes file. + """ + with path.open('r') as fd: + lines = fd.readlines() + + new_lines = ['\n', '\n'] + date = time.strftime('%Y-%m-%d') + release_title = f'{version} ({date})' + new_lines.append(release_title + '\n') + new_lines.append(len(release_title) * RELEASE_UNDER + '\n') + new_lines.append('\n') + for section, section_lines in release_notes.items(): + if section == 'Contributors': + section_lines = sorted(list(set(section_lines))) + if len(section_lines) > 0: + new_lines.append(section + '\n') + new_lines.append(SECTION_UNDER * len(section) + '\n') + new_lines.extend(section_lines) + new_lines.append('\n') + + output_lines = lines[:2] + new_lines + lines[2:] + + print('Writing out release notes file') + for line in new_lines: + print(line.strip('\n')) + with path.open('w') as fd: + fd.writelines(output_lines) + + +def main(version_number: str): + section_notes = parse_pre_release_file(TEMPLATE) + to_delete = [] + for path in PRE_RELEASE.iterdir(): + if path.name[0] in '1234567890': + to_delete.append(path) + extra_notes = parse_pre_release_file(path) + for section, notes in section_notes.items(): + notes.extend(extra_notes[section]) + section_notes[section] = notes + + extend_release_notes(RELEASE_NOTES, version_number, section_notes) + + print( + "* Wrote release notes. Please perform the following manually:", + file=sys.stderr, + ) + for path in to_delete: + print(f" git rm {path}", file=sys.stderr) + print(f" git add {RELEASE_NOTES}", file=sys.stderr) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} VERSION_NUMBER", file=sys.stderr) + sys.exit(1) + + version_number = sys.argv[1] + + if not version_number.startswith("v"): + print( + f"Version number should start with 'v': {version_number}", + file=sys.stderr + ) + sys.exit(1) + + main(version_number) diff --git a/docs/source/upcoming_changes.rst b/docs/source/upcoming_changes.rst new file mode 100644 index 0000000..1323597 --- /dev/null +++ b/docs/source/upcoming_changes.rst @@ -0,0 +1,8 @@ +Upcoming Changes +################ + +.. toctree:: + :maxdepth: 1 + :glob: + + upcoming_release_notes/[0-9]* diff --git a/docs/source/upcoming_release_notes/2-enh_main_window.rst b/docs/source/upcoming_release_notes/2-enh_main_window.rst new file mode 100644 index 0000000..215ff39 --- /dev/null +++ b/docs/source/upcoming_release_notes/2-enh_main_window.rst @@ -0,0 +1,22 @@ +2 enh_main_window +################# + +API Breaks +---------- +- N/A + +Features +-------- +- Adds main window widget and cli entrypoint skeleton + +Bugfixes +-------- +- N/A + +Maintenance +----------- +- Updates dependencies, adds pre-release notes framework + +Contributors +------------ +- tangkong diff --git a/docs/source/upcoming_release_notes/template-full.rst b/docs/source/upcoming_release_notes/template-full.rst new file mode 100644 index 0000000..31657f7 --- /dev/null +++ b/docs/source/upcoming_release_notes/template-full.rst @@ -0,0 +1,36 @@ +IssueNumber Title +################# + +Update the title above with your issue number and a 1-2 word title. +Your filename should be issuenumber-title.rst, substituting appropriately. + +Make sure to fill out any section that represents changes you have made, +or replace the default bullet point with N/A. + +API Breaks +---------- +- List backwards-incompatible changes here. + Changes to PVs don't count as API changes for this library, + but changing method and component names or changing default behavior does. + +Features +-------- +- List new updates that add utility to many classes, + provide a new base classes, add options to helper methods, etc. + +Bugfixes +-------- +- List bug fixes that are not covered in the above sections. + +Maintenance +----------- +- List anything else. The intent is to accumulate changes + that the average user does not need to worry about. + +Contributors +------------ +- List your github username and anyone else who made significant + code or conceptual contributions to the PR. You don't need to + add reviewers unless their suggestions lead to large rewrites. + These will be used in the release notes to give credit and to + notify you when your code is being tagged. diff --git a/docs/source/upcoming_release_notes/template-short.rst b/docs/source/upcoming_release_notes/template-short.rst new file mode 100644 index 0000000..8349cd8 --- /dev/null +++ b/docs/source/upcoming_release_notes/template-short.rst @@ -0,0 +1,22 @@ +IssueNumber Title +################# + +API Breaks +---------- +- N/A + +Features +-------- +- N/A + +Bugfixes +-------- +- N/A + +Maintenance +----------- +- N/A + +Contributors +------------ +- N/A diff --git a/pyproject.toml b/pyproject.toml index 36ed58c..c021d5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,4 +53,3 @@ file = "docs-requirements.txt" [tool.pytest.ini_options] addopts = "--cov=." - diff --git a/requirements.txt b/requirements.txt index 49c7afc..7c41b91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ # List requirements here. +pcdsutils +PyQt5 +qtpy diff --git a/superscore/__main__.py b/superscore/__main__.py new file mode 100644 index 0000000..46dcf80 --- /dev/null +++ b/superscore/__main__.py @@ -0,0 +1,3 @@ +from superscore.bin.main import main + +main() diff --git a/superscore/bin/main.py b/superscore/bin/main.py index 62ee231..52d9f17 100644 --- a/superscore/bin/main.py +++ b/superscore/bin/main.py @@ -16,7 +16,7 @@ DESCRIPTION = __doc__ -MODULES = ("help", ) +MODULES = ("help", "ui") def _try_import(module): diff --git a/superscore/bin/ui.py b/superscore/bin/ui.py new file mode 100644 index 0000000..a174bd2 --- /dev/null +++ b/superscore/bin/ui.py @@ -0,0 +1,24 @@ +""" +`superscore ui` opens up the main application window +""" +import argparse +import sys + +from qtpy.QtWidgets import QApplication + +from superscore.widgets.window import Window + + +def build_arg_parser(argparser=None): + if argparser is None: + argparser = argparse.ArgumentParser() + + return argparser + + +def main(*args, **kwargs): + app = QApplication(sys.argv) + main_window = Window() + + main_window.show() + app.exec() diff --git a/superscore/tests/test_blank.py b/superscore/tests/test_blank.py deleted file mode 100644 index 4465468..0000000 --- a/superscore/tests/test_blank.py +++ /dev/null @@ -1,8 +0,0 @@ -def test_blank(): - """ - This is a placeholder for some testing and to verify your continuous - integration works as expected. - - Remove this function and write your own tests! - """ - raise ZeroDivisionError() diff --git a/superscore/tests/test_window.py b/superscore/tests/test_window.py new file mode 100644 index 0000000..3925cf3 --- /dev/null +++ b/superscore/tests/test_window.py @@ -0,0 +1,9 @@ +from pytestqt.qtbot import QtBot + +from superscore.widgets.window import Window + + +def test_main_window(qtbot: QtBot): + """Pass if main window opens successfully""" + window = Window() + qtbot.addWidget(window) diff --git a/superscore/ui/main_window.ui b/superscore/ui/main_window.ui new file mode 100644 index 0000000..b80812e --- /dev/null +++ b/superscore/ui/main_window.ui @@ -0,0 +1,127 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + true + + + + + + + + + 0 + 0 + 800 + 22 + + + + + File + + + + + + + + + + + + + Debug + + + + + Utilities + + + + + + + + + + New File + + + + + Open File + + + + + Save + + + + + Save As... + + + + + Save All + + + + + Print DataClass + + + + + Print Serialized + + + + + Open Archive Viewer + + + + + Print Report + + + + + Clear Results + + + + + Find / Replace + + + + + Welcome Tab + + + + + + diff --git a/superscore/utils.py b/superscore/utils.py new file mode 100644 index 0000000..f66b5c1 --- /dev/null +++ b/superscore/utils.py @@ -0,0 +1,3 @@ +from pathlib import Path + +SUPERSCORE_SOURCE_PATH = Path(__file__).parent diff --git a/superscore/widgets/__init__.py b/superscore/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/superscore/widgets/core.py b/superscore/widgets/core.py new file mode 100644 index 0000000..fd6927c --- /dev/null +++ b/superscore/widgets/core.py @@ -0,0 +1,14 @@ +""" +Core classes for qt-based GUIs. +""" +from pathlib import Path + +from pcdsutils.qt.designer_display import DesignerDisplay + +from superscore.utils import SUPERSCORE_SOURCE_PATH + + +class Display(DesignerDisplay): + """Helper class for loading designer .ui files and adding logic""" + + ui_dir: Path = SUPERSCORE_SOURCE_PATH / 'ui' diff --git a/superscore/widgets/window.py b/superscore/widgets/window.py new file mode 100644 index 0000000..e1dd1f0 --- /dev/null +++ b/superscore/widgets/window.py @@ -0,0 +1,14 @@ +""" +Top-level window widget that contains other widgets +""" +from __future__ import annotations + +from qtpy.QtWidgets import QMainWindow + +from superscore.widgets.core import Display + + +class Window(Display, QMainWindow): + """Main superscore window""" + + filename = 'main_window.ui'