diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2f7485..80fd1bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test cookiecutter +name: Test plugin template on: pull_request: @@ -33,7 +33,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pytest pytest-cookies tox + python -m pip install -r requirements.txt - name: Test uses: aganders3/headless-gui@v2 diff --git a/PROMPTS.md b/PROMPTS.md index 794aa3d..9fa6ab4 100644 --- a/PROMPTS.md +++ b/PROMPTS.md @@ -1,6 +1,6 @@ # napari Plugin Prompt Reference -When you first run cookiecutter to build a napari plugin, you will be prompted +When you first run the template to build a napari plugin, you will be prompted for some configuration options. Your answers to these prompts will determine some aspects of your plugin package including its name, versioning behaviour, license, etc. None of these configuration options are set in stone - you @@ -127,7 +127,7 @@ add `version = 0.0.1` to your `setup.cfg`. If you choose `"y"` for this prompt, your package will be set up to have [`setuptools_scm`](https://github.com/pypa/setuptools_scm) manage versions for you based on your git tags. See the -[readme](https://github.com/napari/cookiecutter-napari-plugin#automatic-deployment-and-version-management) +[readme](https://github.com/napari/napari-plugin-template#automatic-deployment-and-version-management) for details. This option typically requires the least effort to manage versioning for your diff --git a/README.md b/README.md index b08254e..5f34e90 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# cookiecutter-napari-plugin +# napari-plugin-template -[Cookiecutter] template for authoring ([npe2]-based) [napari] plugins. +[Copier](https://copier.readthedocs.io/en/stable/) template for authoring ([npe2](https://github.com/napari/npe2)-based) [napari](https://napari.org/) plugins. **NOTE: This repo is not meant to be cloned/forked directly! Please read "Getting Started" below** @@ -8,14 +8,18 @@ ### Create your plugin package -Install [Cookiecutter] and generate a new napari plugin project: +nstall [Copier](https://copier.readthedocs.io/en/stable/) and the [jinja2-time](https://pypi.org/project/jinja2-time/) extension. +Optionally install the napari plugin engine [npe2](https://github.com/napari/npe2), to help validate your new plugin is configured correctly. + +Then you can generate a new napari plugin project: ```bash -pip install cookiecutter -cookiecutter https://github.com/napari/cookiecutter-napari-plugin +python -m pip install copier jinja2-time +python -m pip install npe2 +copier copy --trust https://github.com/napari/napari-plugin-template new-plugin-name ``` -Cookiecutter prompts you for information regarding your plugin +Copier prompts you for information regarding your plugin (A new folder will be created in your current working directory): ```bash @@ -192,8 +196,7 @@ pytest ### Create your documentation Documentation generation is not included in this template. -We recommend following the getting started guides for one of the following -documentation generation tools: +We recommend following the getting started guides for one of the following documentation generation tools: 1. [Sphinx] 2. [MkDocs] @@ -242,27 +245,28 @@ Details on why this plugin template is using the `src` layout can be found [here ## Issues -If you encounter any problems with this cookiecutter template, please [file an -issue] along with a detailed description. +If you encounter any problems with this template, please +[file an issue](https://github.com/napari/napari-plugin-template/issues/new) +along with a detailed description. ## License -Distributed under the terms of the [BSD-3] license, `cookiecutter-napari-plugin` +Distributed under the terms of the [BSD-3] license, `napari-plugin-template` is free and open source software. [napari organization]: https://github.com/napari/ [gitter_badge]: https://badges.gitter.im/Join%20Chat.svg -[gitter]: https://gitter.im/napari/cookiecutter-napari-plugin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge "Join Chat on Gitter.im" -[travis_badge]: https://travis-ci.org/napari/cookiecutter-napari-plugin.svg?branch=main -[travis]: https://travis-ci.org/napari/cookiecutter-napari-plugin "See Build Status on Travis CI" -[docs_badge]: https://readthedocs.org/projects/cookiecutter-napari-plugin/badge/?version=latest -[documentation]: https://cookiecutter-napari-plugin.readthedocs.io/en/latest/ "Documentation" -[cookiecutter]: https://github.com/audreyr/cookiecutter +[gitter]: https://gitter.im/napari/napari-plugin-template?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge "Join Chat on Gitter.im" +[travis_badge]: https://travis-ci.org/napari/napari-plugin-template.svg?branch=main +[travis]: https://travis-ci.org/napari/napari-plugin-template "See Build Status on Travis CI" +[docs_badge]: https://readthedocs.org/projects/napari-plugin-template/badge/?version=latest +[documentation]: https://napari-plugin-template.readthedocs.io/en/latest/ "Documentation" +[copier]: https://github.com/copier-org/copier [napari]: https://github.com/napari/napari [npe2]: https://github.com/napari/npe2 [pypi]: https://pypi.org/ [tox]: https://tox.readthedocs.io/en/latest/ -[file an issue]: https://github.com/napari/cookiecutter-napari-plugin/issues +[file an issue]: https://github.com/napari/napari-plugin-template/issues [sphinx]: https://www.sphinx-doc.org/en/master/usage/quickstart.html [mkdocs]: https://www.mkdocs.org/getting-started/ [jupyterbook]: https://jupyterbook.org/en/stable/start/your-first-book.html @@ -275,7 +279,6 @@ is free and open source software. [travis ci]: https://travis-ci.com/ [appveyor]: http://www.appveyor.com/ [pypa code of conduct]: https://www.pypa.io/en/latest/code-of-conduct/ -[shortbread]: https://github.com/audreyr/cookiecutter/releases/tag/1.4.0 [osi_certified]: https://opensource.org/trademarks/osi-certified/web/osi-certified-120x100.png [osi]: https://opensource.org/ [github actions]: https://github.com/features/actions diff --git a/_tasks.py b/_tasks.py new file mode 100644 index 0000000..9fc2e5a --- /dev/null +++ b/_tasks.py @@ -0,0 +1,216 @@ +from argparse import ArgumentParser +import logging +import os +from pathlib import Path +import re +import subprocess +import sys + + +def module_name_pep8_compliance(module_name): + """Validate that the plugin module name is PEP8 compliant.""" + if not re.match(r"^[a-z][_a-z0-9]+$", module_name): + link = "https://www.python.org/dev/peps/pep-0008/#package-and-module-names" + logger.error("Module name should be pep-8 compliant.") + logger.error(f" More info: {link}") + sys.exit(1) + + +def pypi_package_name_compliance(plugin_name): + """Check there are no underscores in the plugin name""" + if re.search(r"_", plugin_name): + logger.error("PyPI.org and pip discourage package names with underscores.") + sys.exit(1) + + +def validate_manifest(module_name, project_directory): + """Validate the new plugin repository against napari requirements.""" + try: + from npe2 import PluginManifest + except ImportError as e: + logger.error("npe2 is not installed. Skipping manifest validation.") + return True + + current_directory = Path('.').absolute() + if (current_directory.match(project_directory) and not Path(project_directory).is_absolute()): + project_directory = current_directory + + path=Path(project_directory) / "src" / Path(module_name) / "napari.yaml" + + valid = False + try: + pm = PluginManifest.from_file(path) + msg = f"✔ Manifest for {(pm.display_name or pm.name)!r} valid!" + valid = True + except PluginManifest.ValidationError as err: + msg = f"🅇 Invalid! {err}" + logger.error(msg.encode("utf-8")) + sys.exit(1) + except Exception as err: + msg = f"🅇 Failed to read {path!r}. {type(err).__name__}: {err}" + logger.error(msg.encode("utf-8")) + sys.exit(1) + else: + logger.info(msg.encode("utf-8")) + return valid + + +def initialize_new_repository( + install_precommit=False, + plugin_name="napari-foobar", + github_repository_url="provide later", + github_username_or_organization="githubuser", + ): + """Initialize new plugin repository with git, and optionally pre-commit.""" + + msg = "" + + # Configure git line ending settings + # https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration + if os.name == 'nt': # if on Windows, configure git line ending characters + subprocess.run(["git", "config", "--global", "core.autocrlf", "true"]) + else: # for Linux and Mac + subprocess.run(["git", "config", "--global", "core.autocrlf", "input"]) + + # try to run git init + try: + subprocess.run(["git", "init", "-q"]) + subprocess.run(["git", "checkout", "-b", "main"]) + except Exception: + logger.error("Error in git initialization.") + + if install_precommit is True: + # try to install and update pre-commit + try: + print("install pre-commit ...") + subprocess.run(["python", "-m", "pip", "install", "pre-commit"], stdout=subprocess.DEVNULL) + print("updating pre-commit...") + subprocess.run(["pre-commit", "autoupdate"], stdout=subprocess.DEVNULL) + subprocess.run(["git", "add", "."]) + subprocess.run(["pre-commit", "run", "black", "-a"], capture_output=True) + except Exception: + logger.error("Error pip installing then running pre-commit.") + + try: + subprocess.run(["git", "add", "."]) + subprocess.run(["git", "commit", "-q", "-m", "initial commit"]) + except Exception: + logger.error("Error creating initial git commit.") + msg += f""" +Your plugin template is ready! Next steps: +1. `cd` into your new directory and initialize a git repo +(this is also important for version control!) + cd {plugin_name} + git init -b main + git add . + git commit -m 'initial commit' + # you probably want to install your new package into your environment + pip install -e . +""" + else: + msg +=f""" +Your plugin template is ready! Next steps: +1. `cd` into your new directory + cd {plugin_name} + # you probably want to install your new package into your env + pip install -e . +""" + # Ensure full reqd/write/execute permissions for .git files + if os.name == 'nt': # if on Windows OS + # Avoid permission denied errors on Github Actions CI + subprocess.run(["attrib", "-h", "rr", ".git", "/s", "/d"]) + + if install_precommit is True: + # try to install and update pre-commit + # installing after commit to avoid problem with comments in setup.cfg. + try: + print("install pre-commit hook...") + subprocess.run(["pre-commit", "install"]) + except Exception: + logger.error("Error at pre-commit install, skipping pre-commit") + + if github_repository_url != 'provide later': + msg += f""" + 2. Create a github repository with the name '{plugin_name}': + https://github.com/{github_username_or_organization}/{plugin_name}.git + 3. Add your newly created github repo as a remote and push: + git remote add origin https://github.com/{github_username_or_organization}/{plugin_name}.git + git push -u origin main + 4. The following default URLs have been added to `setup.cfg`: + Bug Tracker = https://github.com/{github_username_or_organization}/{plugin_name}/issues + Documentation = https://github.com/{github_username_or_organization}/{plugin_name}#README.md + Source Code = https://github.com/{github_username_or_organization}/{plugin_name} + User Support = https://github.com/{github_username_or_organization}/{plugin_name}/issues + These URLs will be displayed on your plugin's napari hub page. + You may wish to change these before publishing your plugin!""" + else: + msg += """ + 2. Create a github repository for your plugin: + https://github.com/new + 3. Add your newly created github repo as a remote and push: + git remote add origin https://github.com/your-repo-username/your-repo-name.git + git push -u origin main + Don't forget to add this url to setup.cfg! + [metadata] + url = https://github.com/your-repo-username/your-repo-name.git + 4. Consider adding additional links for documentation and user support to setup.cfg + using the project_urls key e.g. + [metadata] + project_urls = + Bug Tracker = https://github.com/your-repo-username/your-repo-name/issues + Documentation = https://github.com/your-repo-username/your-repo-name#README.md + Source Code = https://github.com/your-repo-username/your-repo-name + User Support = https://github.com/your-repo-username/your-repo-name/issues""" + + msg += """ + 5. Read the README for more info: https://github.com/napari/napari-plugin-template + 6. We've provided a template description for your plugin page on the napari hub at `.napari-hub/DESCRIPTION.md`. + You'll likely want to edit this before you publish your plugin. + 7. Consider customizing the rest of your plugin metadata for display on the napari hub: + https://github.com/chanzuckerberg/napari-hub/blob/main/docs/customizing-plugin-listing.md + """ + return msg + + +if __name__=="__main__": + logging.basicConfig(level=logging.DEBUG) + logger = logging.getLogger("pre_gen_project") + parser = ArgumentParser() + parser.add_argument("--plugin_name", + dest="plugin_name", + help="The name of your plugin") + parser.add_argument("--module_name", + dest="module_name", + help="Plugin module name") + parser.add_argument("--project_directory", + dest="project_directory", + help="Project directory") + parser.add_argument("--install_precommit", + dest="install_precommit", + help="Install pre-commit", + default="False") + parser.add_argument("--github_repository_url", + dest="github_repository_url", + help="Github repository URL", + default='provide later') + parser.add_argument("--github_username_or_organization", + dest="github_username_or_organization", + help="Github user or organisation name", + default='githubuser') + args = parser.parse_args() + + # Since bool("False") returns True, we need to check the actual string value + if str(args.install_precommit).lower() == "true": + install_precommit = True + else: + install_precommit = False + module_name_pep8_compliance(args.module_name) + pypi_package_name_compliance(args.plugin_name) + validate_manifest(args.module_name, args.project_directory) + msg = initialize_new_repository( + install_precommit=install_precommit, + plugin_name=args.plugin_name, + github_repository_url=args.github_repository_url, + github_username_or_organization=args.github_username_or_organization, + ) + print(msg) \ No newline at end of file diff --git a/cookiecutter.json b/cookiecutter.json deleted file mode 100644 index 4ffc834..0000000 --- a/cookiecutter.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "full_name": "Napari Developer", - "email": "yourname@example.com", - "github_username_or_organization": "githubuser", - "plugin_name": "napari-foobar", - "github_repository_url": ["https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}", "provide later"], - "module_name": "{{ cookiecutter.plugin_name|lower|replace('-', '_') }}", - "display_name": "FooBar Segmentation", - "short_description": "A simple plugin to use FooBar segmentation within napari", - "include_reader_plugin": "y", - "include_writer_plugin": "y", - "include_sample_data_plugin": "y", - "include_widget_plugin": "y", - "use_git_tags_for_versioning": "n", - "install_precommit": "n", - "install_dependabot": "n", - "license": [ - "BSD-3", - "MIT", - "Mozilla Public License 2.0", - "Apache Software License 2.0", - "GNU LGPL v3.0", - "GNU GPL v3.0" - ] -} diff --git a/copier.yaml b/copier.yaml new file mode 100644 index 0000000..c29c051 --- /dev/null +++ b/copier.yaml @@ -0,0 +1,117 @@ +plugin_name: + placeholder: napari-foobar + help: The name of your plugin + type: str + validator: >- + {% if not (plugin_name | regex_search('^[a-z][a-z0-9\-]+$')) %} + plugin_name must be lowercase, no spaces, and may contain hyphens. + {% endif %} +display_name: + placeholder: FooBar Segmentation + help: Display name for your plugin + type: str + validator: >- + {% if not (display_name | regex_search('(.|\s)*\S(.|\s)*')) %} + display_name must not be empty + {% endif %} +module_name: + placeholder: "{{ plugin_name|lower|replace('-', '_') }}" + help: Plugin module name + type: str + validator: >- + {% if not (module_name | regex_search("^[a-z][_a-z0-9]+$")) %} + module_name must be lowercase, and may contain underscores + {% endif %} +short_description: + placeholder: A simple plugin to use FooBar segmentation within napari + help: Short description of what your plugin does + type: str + validator: >- + {% if not (short_description | regex_search('(.|\s)*\S(.|\s)*')) %} + short_description must not be empty + {% endif %} +email: + placeholder: yourname@example.com + help: Email address + type: str +full_name: + placeholder: Napari Developer + help: Developer name + type: str +github_username_or_organization: + placeholder: githubuser + help: Github user or organisation name + type: str +github_repository_url: + default: provide later + help: Github repository URL + type: str + choices: + - provide later + - https://github.com/{{github_username_or_organization}}/{{plugin_name}} +include_reader_plugin: + default: true + help: Include reader plugin? + type: bool +include_writer_plugin: + default: true + help: Include writer plugin? + type: bool +include_sample_data_plugin: + default: true + help: Include sample data plugin? + type: bool +include_widget_plugin: + default: true + help: Include widget plugin? + type: bool +use_git_tags_for_versioning: + default: false + help: Use git tags for versioning? + type: bool +install_precommit: + default: false + help: Install pre-commit? (Code formatting checks) + type: bool +install_dependabot: + default: false + help: Install dependabot? (Automatic security updates of dependency versions) + type: bool +license: + default: BSD-3 + help: Which licence do you want your plugin code to have? + type: str + choices: + - BSD-3 + - MIT + - Mozilla Public License 2.0 + - Apache Software License 2.0 + - GNU LGPL v3.0 + - GNU GPL v3.0 +# copier configuration options +_subdirectory: template +_jinja_extensions: + - jinja2_time.TimeExtension +_exclude: + - "copier.yaml" + - "copier.yml" + - "~*" + - "*.py[co]" + - "__pycache__" + - ".git" + - ".DS_Store" + - ".svn" + - "*licenses*" + - "_tasks.py" +_tasks: + - [ + "{{ _copier_python }}", # which python + "{{ _copier_conf.src_path }}{{ _copier_conf.sep }}_tasks.py", # task script + # keyword arguments for python script + "--plugin_name={{ plugin_name }}", + "--module_name={{ module_name }}", + "--project_directory={{ _copier_conf.dst_path }}", + "--install_precommit={{ install_precommit }}", + "--github_repository_url={{ github_repository_url }}", + "--github_username_or_organization={{ github_username_or_organization }}", + ] \ No newline at end of file diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py deleted file mode 100644 index 218cb18..0000000 --- a/hooks/post_gen_project.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python - -import logging -import os -import shutil -import subprocess -from pathlib import Path - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("post_gen_project") - -PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) -ALL_TEMP_FOLDERS = ["licenses"] - - - -def remove_file(filepath): - os.remove(os.path.join(PROJECT_DIRECTORY, filepath)) - - -def remove_temp_folders(temp_folders): - for folder in temp_folders: - logger.debug("Remove temporary folder: %s", folder) - shutil.rmtree(folder, ignore_errors=True) - - -def remove_unrequested_plugin_examples(): - module = "{{ cookiecutter.module_name }}" - {% for key, value in cookiecutter.items() %} - {% if key.startswith('include_') and key.endswith("_plugin") and value != 'y' %} - name = "{{ key }}".replace("include_", "").replace("_plugin", "") - remove_file(f"src/{module}/_{name}.py") - remove_file(f"src/{module}/_tests/test_{name}.py") - logger.debug(f"removing {module}/_{name}.py") - # remove dependabot config if unrequested - {% elif key.startswith("install_dependabot") and value != 'y' %} - remove_file(".github/dependabot.yml") - logger.debug("removing .github/dependabot.yml") - {% endif %} - {% endfor %} - - -def validate_manifest(): - try: - from npe2 import PluginManifest - except ImportError as e: - print("npe2 is not installed. Skipping manifest validation.") - return True - - path=Path(PROJECT_DIRECTORY) / "src" / "{{cookiecutter.module_name}}" / "napari.yaml" - - valid = False - try: - pm = PluginManifest.from_file(path) - msg = f"✔ Manifest for {(pm.display_name or pm.name)!r} valid!" - valid = True - except PluginManifest.ValidationError as err: - msg = f"🅇 Invalid! {err}" - except Exception as err: - msg = f"🅇 Failed to read {path!r}. {type(err).__name__}: {err}" - - print(msg.encode("utf-8")) - return valid - - -if __name__ == "__main__": - remove_temp_folders(ALL_TEMP_FOLDERS) - remove_unrequested_plugin_examples() - valid=validate_manifest() - - msg = '' - # try to run git init - try: - subprocess.run(["git", "init", "-q"]) - subprocess.run(["git", "checkout", "-b", "main"]) - except Exception: - pass -{% if cookiecutter.install_precommit == 'y' %} - # try to install and update pre-commit - try: - print("install pre-commit ...") - subprocess.run(["pip", "install", "pre-commit"], stdout=subprocess.DEVNULL) - print("updating pre-commit...") - subprocess.run(["pre-commit", "autoupdate"], stdout=subprocess.DEVNULL) - subprocess.run(["git", "add", "."]) - subprocess.run(["pre-commit", "run", "black", "-a"], capture_output=True) - except Exception: - pass -{% endif %} - try: - subprocess.run(["git", "add", "."]) - subprocess.run(["git", "commit", "-q", "-m", "initial commit"]) - except Exception: - msg += """ -Your plugin template is ready! Next steps: - -1. `cd` into your new directory and initialize a git repo - (this is also important for version control!) - - cd {{ cookiecutter.plugin_name }} - git init -b main - git add . - git commit -m 'initial commit' - - # you probably want to install your new package into your environment - pip install -e .""" - else: - msg +=""" -Your plugin template is ready! Next steps: - -1. `cd` into your new directory - - cd {{ cookiecutter.plugin_name }} - # you probably want to install your new package into your env - pip install -e .""" - -{% if cookiecutter.install_precommit == 'y' %} - # try to install and update pre-commit - # installing after commit to avoid problem with comments in setup.cfg. - try: - print("install pre-commit hook...") - subprocess.run(["pre-commit", "install"]) - except Exception: - pass -{% endif %} - -{% if cookiecutter.github_repository_url != 'provide later' %} - msg += """ -2. Create a github repository with the name '{{ cookiecutter.plugin_name }}': - https://github.com/{{ cookiecutter.github_username_or_organization }}/{{ cookiecutter.plugin_name }}.git - -3. Add your newly created github repo as a remote and push: - - git remote add origin https://github.com/{{ cookiecutter.github_username_or_organization }}/{{ cookiecutter.plugin_name }}.git - git push -u origin main - -4. The following default URLs have been added to `setup.cfg`: - - Bug Tracker = https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/issues - Documentation = https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}#README.md - Source Code = https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}} - User Support = https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/issues - - These URLs will be displayed on your plugin's napari hub page. - You may wish to change these before publishing your plugin!""" - -{% else %} - msg += """ -2. Create a github repository for your plugin: - https://github.com/new - -3. Add your newly created github repo as a remote and push: - - git remote add origin https://github.com/your-repo-username/your-repo-name.git - git push -u origin main - - Don't forget to add this url to setup.cfg! - - [metadata] - url = https://github.com/your-repo-username/your-repo-name.git - -4. Consider adding additional links for documentation and user support to setup.cfg - using the project_urls key e.g. - - [metadata] - project_urls = - Bug Tracker = https://github.com/your-repo-username/your-repo-name/issues - Documentation = https://github.com/your-repo-username/your-repo-name#README.md - Source Code = https://github.com/your-repo-username/your-repo-name - User Support = https://github.com/your-repo-username/your-repo-name/issues""" -{% endif %} - msg += """ -5. Read the README for more info: https://github.com/napari/cookiecutter-napari-plugin - -6. We've provided a template description for your plugin page on the napari hub at `.napari-hub/DESCRIPTION.md`. - You'll likely want to edit this before you publish your plugin. - -7. Consider customizing the rest of your plugin metadata for display on the napari hub: - https://github.com/chanzuckerberg/napari-hub/blob/main/docs/customizing-plugin-listing.md -""" - - print(msg) diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py deleted file mode 100644 index 0757f4e..0000000 --- a/hooks/pre_gen_project.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python - -import logging -import re -import sys - -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger("pre_gen_project") - -if not re.match(r"^[a-z][_a-z0-9]+$", "{{cookiecutter.module_name}}"): - link = "https://www.python.org/dev/peps/pep-0008/#package-and-module-names" - logger.error("Module name should be pep-8 compliant.") - logger.error(f" More info: {link}") - sys.exit(1) - -if re.search(r"_", "{{cookiecutter.plugin_name}}"): - logger.error("PyPI.org and pip discourage package names with underscores.") - sys.exit(1) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..66be14a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +copier +jinja2-time +npe2 +pytest +pytest-copie +tox \ No newline at end of file diff --git a/{{cookiecutter.plugin_name}}/.github/workflows/test_and_deploy.yml b/template/.github/workflows/test_and_deploy.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/.github/workflows/test_and_deploy.yml rename to template/.github/workflows/test_and_deploy.jinja diff --git a/{{cookiecutter.plugin_name}}/.github/dependabot.yml b/template/.github/{% if install_dependabot %}dependabot.yml{% endif %}.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/.github/dependabot.yml rename to template/.github/{% if install_dependabot %}dependabot.yml{% endif %}.jinja diff --git a/{{cookiecutter.plugin_name}}/.gitignore b/template/.gitignore similarity index 100% rename from {{cookiecutter.plugin_name}}/.gitignore rename to template/.gitignore diff --git a/{{cookiecutter.plugin_name}}/.napari-hub/DESCRIPTION.md b/template/.napari-hub/DESCRIPTION.md.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/.napari-hub/DESCRIPTION.md rename to template/.napari-hub/DESCRIPTION.md.jinja diff --git a/template/.napari-hub/config-yml.jinja b/template/.napari-hub/config-yml.jinja new file mode 100644 index 0000000..904c76f --- /dev/null +++ b/template/.napari-hub/config-yml.jinja @@ -0,0 +1,9 @@ +# You may use this file to customize how your plugin page appears +# on the napari hub: https://www.napari-hub.org/ +# See their wiki for details https://github.com/chanzuckerberg/napari-hub/wiki + +# Please note that this file should only be used IN ADDITION to entering +# metadata fields (such as summary, description, authors, and various URLS) +# in your standard python package metadata (e.g. setup.cfg, setup.py, or +# pyproject.toml), when you would like those fields to be displayed +# differently on the hub than in the napari application. diff --git a/template/LICENSE.jinja b/template/LICENSE.jinja new file mode 100644 index 0000000..aae6224 --- /dev/null +++ b/template/LICENSE.jinja @@ -0,0 +1,13 @@ +{%- if license == "MIT" -%} +{%- include 'template/licenses/MIT.jinja' %} +{%- elif license == "BSD-3" -%} +{%- include 'template/licenses/BSD-3.jinja' %} +{%- elif license == "GNU GPL v3.0" -%} +{%- include 'template/licenses/GPL-3.jinja' %} +{%- elif license == "GNU LGPL v3.0" -%} +{%- include 'template/licenses/LGPL-3.jinja' %} +{%- elif license == "Apache Software License 2.0" -%} +{%- include 'template/licenses/Apache-2.jinja' %} +{%- elif license == "Mozilla Public License 2.0" -%} +{%- include 'template/licenses/MPL-2.jinja' %} +{%- endif -%} diff --git a/{{cookiecutter.plugin_name}}/MANIFEST.in b/template/MANIFEST.in similarity index 100% rename from {{cookiecutter.plugin_name}}/MANIFEST.in rename to template/MANIFEST.in diff --git a/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 0000000..ef45849 --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,67 @@ +# {{plugin_name}} + +[![License {{license}}](https://img.shields.io/pypi/l/{{plugin_name}}.svg?color=green)](https://github.com/{{github_username_or_organization}}/{{plugin_name}}/raw/main/LICENSE) +[![PyPI](https://img.shields.io/pypi/v/{{plugin_name}}.svg?color=green)](https://pypi.org/project/{{plugin_name}}) +[![Python Version](https://img.shields.io/pypi/pyversions/{{plugin_name}}.svg?color=green)](https://python.org) +[![tests](https://github.com/{{github_username_or_organization}}/{{plugin_name}}/workflows/tests/badge.svg)](https://github.com/{{github_username_or_organization}}/{{plugin_name}}/actions) +[![codecov](https://codecov.io/gh/{{github_username_or_organization}}/{{plugin_name}}/branch/main/graph/badge.svg)](https://codecov.io/gh/{{github_username_or_organization}}/{{plugin_name}}) +[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/{{plugin_name}})](https://napari-hub.org/plugins/{{plugin_name}}) + +{{short_description}} + +---------------------------------- + +This [napari] plugin was generated with [copier] using the [napari-plugin-template]. + + + +## Installation + +You can install `{{plugin_name}}` via [pip]: + + pip install {{plugin_name}} + + +{% if github_repository_url != 'provide later' %} +To install latest development version : + + pip install git+https://github.com/{{github_username_or_organization}}/{{plugin_name}}.git +{% endif %} + +## Contributing + +Contributions are very welcome. Tests can be run with [tox], please ensure +the coverage at least stays the same before you submit a pull request. + +## License + +Distributed under the terms of the [{{license}}] license, +"{{plugin_name}}" is free and open source software + +## Issues + +If you encounter any problems, please [file an issue] along with a detailed description. + +[napari]: https://github.com/napari/napari +[copier]: https://copier.readthedocs.io/en/stable/ +[@napari]: https://github.com/napari +[MIT]: http://opensource.org/licenses/MIT +[BSD-3]: http://opensource.org/licenses/BSD-3-Clause +[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt +[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt +[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0 +[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt +[napari-plugin-template]: https://github.com/napari/napari-plugin-template +{% if github_repository_url != 'provide later' %} +[file an issue]: https://github.com/{{github_username_or_organization}}/{{plugin_name}}/issues +{% endif %} +[napari]: https://github.com/napari/napari +[tox]: https://tox.readthedocs.io/en/latest/ +[pip]: https://pypi.org/project/pip/ +[PyPI]: https://pypi.org/ diff --git a/{{cookiecutter.plugin_name}}/licenses/Apache-2 b/template/licenses/Apache-2.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/Apache-2 rename to template/licenses/Apache-2.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/BSD-3 b/template/licenses/BSD-3.jinja similarity index 95% rename from {{cookiecutter.plugin_name}}/licenses/BSD-3 rename to template/licenses/BSD-3.jinja index 9e68087..17d4fd7 100644 --- a/{{cookiecutter.plugin_name}}/licenses/BSD-3 +++ b/template/licenses/BSD-3.jinja @@ -1,5 +1,5 @@ {#- source: http://opensource.org/licenses/BSD-3-Clause #} -Copyright (c) {% now 'utc', '%Y' %}, {{cookiecutter.full_name}} +Copyright (c) {% now 'utc', '%Y' %}, {{full_name}} All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/{{cookiecutter.plugin_name}}/licenses/GPL-3 b/template/licenses/GPL-3.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/GPL-3 rename to template/licenses/GPL-3.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/LGPL-3 b/template/licenses/LGPL-3.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/LGPL-3 rename to template/licenses/LGPL-3.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/MIT b/template/licenses/MIT.jinja similarity index 94% rename from {{cookiecutter.plugin_name}}/licenses/MIT rename to template/licenses/MIT.jinja index c093d51..4e95cd7 100644 --- a/{{cookiecutter.plugin_name}}/licenses/MIT +++ b/template/licenses/MIT.jinja @@ -1,7 +1,7 @@ {#- source: http://opensource.org/licenses/MIT #} The MIT License (MIT) -Copyright (c) {% now 'utc', '%Y' %} {{cookiecutter.full_name}} +Copyright (c) {% now 'utc', '%Y' %} {{full_name}} Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/{{cookiecutter.plugin_name}}/pyproject.toml b/template/pyproject.toml.jinja similarity index 65% rename from {{cookiecutter.plugin_name}}/pyproject.toml rename to template/pyproject.toml.jinja index c53c9cf..0882387 100644 --- a/{{cookiecutter.plugin_name}}/pyproject.toml +++ b/template/pyproject.toml.jinja @@ -1,28 +1,28 @@ [project] -name = "{{cookiecutter.plugin_name}}" +name = "{{plugin_name}}" dynamic = ["version"] -description = "{{cookiecutter.short_description}}" +description = "{{short_description}}" readme = "README.md" license = {file = "LICENSE"} authors = [ - {name = "{{cookiecutter.full_name}}"}, - {email = "{{cookiecutter.email}}"}, + {name = "{{full_name}}"}, + {email = "{{email}}"}, ] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Framework :: napari", "Intended Audience :: Developers", - {% if cookiecutter.license == "MIT" -%} + {% if license == "MIT" -%} "License :: OSI Approved :: MIT License", - {%- elif cookiecutter.license == "BSD-3" -%} + {%- elif license == "BSD-3" -%} "License :: OSI Approved :: BSD License", - {%- elif cookiecutter.license == "GNU GPL v3.0" -%} + {%- elif license == "GNU GPL v3.0" -%} "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - {%- elif cookiecutter.license == "GNU LGPL v3.0" -%} + {%- elif license == "GNU LGPL v3.0" -%} "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - {%- elif cookiecutter.license == "Apache Software License 2.0" -%} + {%- elif license == "Apache Software License 2.0" -%} "License :: OSI Approved :: Apache Software License", - {%- elif cookiecutter.license == "Mozilla Public License 2.0" -%} + {%- elif license == "Mozilla Public License 2.0" -%} "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", {%- endif %} "Operating System :: OS Independent", @@ -38,7 +38,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "numpy", -{% if cookiecutter.include_widget_plugin == 'y' %} "magicgui", +{% if include_widget_plugin %} "magicgui", "qtpy", "scikit-image", {% endif %}] @@ -48,24 +48,24 @@ testing = [ "tox", "pytest", # https://docs.pytest.org/en/latest/contents.html "pytest-cov", # https://pytest-cov.readthedocs.io/en/latest/ -{% if cookiecutter.include_widget_plugin == 'y' %} "pytest-qt", # https://pytest-qt.readthedocs.io/en/latest/ +{% if include_widget_plugin %} "pytest-qt", # https://pytest-qt.readthedocs.io/en/latest/ "napari", "pyqt5", {% endif %}] [project.entry-points."napari.manifest"] -{{cookiecutter.plugin_name}} = "{{cookiecutter.module_name}}:napari.yaml" +{{plugin_name}} = "{{module_name}}:napari.yaml" -{% if cookiecutter.github_repository_url != 'provide later' -%} +{% if github_repository_url != 'provide later' -%} [project.urls] -"Bug Tracker" = "https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/issues" -"Documentation" = "https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}#README.md" -"Source Code" = "https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}" -"User Support" = "https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/issues" +"Bug Tracker" = "https://github.com/{{github_username_or_organization}}/{{plugin_name}}/issues" +"Documentation" = "https://github.com/{{github_username_or_organization}}/{{plugin_name}}#README.md" +"Source Code" = "https://github.com/{{github_username_or_organization}}/{{plugin_name}}" +"User Support" = "https://github.com/{{github_username_or_organization}}/{{plugin_name}}/issues" {%- endif %} [build-system] -{% if cookiecutter.use_git_tags_for_versioning == 'y' -%} +{% if use_git_tags_for_versioning -%} requires = ["setuptools>=42.0.0", "wheel", "setuptools_scm"] {%- else -%} requires = ["setuptools>=42.0.0", "wheel"] @@ -81,12 +81,12 @@ where = ["src"] [tool.setuptools.package-data] "*" = ["*.yaml"] -{% if cookiecutter.use_git_tags_for_versioning == 'y' %} +{% if use_git_tags_for_versioning %} [tool.setuptools_scm] -write_to = "src/{{cookiecutter.module_name}}/_version.py" +write_to = "src/{{module_name}}/_version.py" {% else %} [tool.setuptools.dynamic] -version = {attr = "{{cookiecutter.module_name}}.__init__.__version__"} +version = {attr = "{{module_name}}.__init__.__version__"} {%- endif %} [tool.black] diff --git a/{{cookiecutter.plugin_name}}/licenses/MPL-2 b/template/src/licenses/MPL-2.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/MPL-2 rename to template/src/licenses/MPL-2.jinja diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py b/template/src/{{module_name}}/__init__.py.jinja similarity index 53% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py rename to template/src/{{module_name}}/__init__.py.jinja index 39cc84c..707e3bc 100644 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py +++ b/template/src/{{module_name}}/__init__.py.jinja @@ -1,4 +1,4 @@ -{% if cookiecutter.use_git_tags_for_versioning == 'y' %} +{% if use_git_tags_for_versioning %} try: from ._version import version as __version__ except ImportError: @@ -7,24 +7,24 @@ __version__ = "0.0.1" {% endif -%} -{% if cookiecutter.include_reader_plugin == 'y' %} +{% if include_reader_plugin %} from ._reader import napari_get_reader -{% endif %}{% if cookiecutter.include_sample_data_plugin == 'y' -%} +{% endif %}{% if include_sample_data_plugin -%} from ._sample_data import make_sample_data -{% endif %}{% if cookiecutter.include_widget_plugin == 'y' -%} +{% endif %}{% if include_widget_plugin -%} from ._widget import ExampleQWidget, ImageThreshold, threshold_autogenerate_widget, threshold_magic_widget -{% endif %}{% if cookiecutter.include_writer_plugin == 'y' -%} +{% endif %}{% if include_writer_plugin -%} from ._writer import write_multiple, write_single_image {% endif %} __all__ = ( - {% if cookiecutter.include_reader_plugin == 'y' -%} + {% if include_reader_plugin -%} "napari_get_reader", - {% endif %}{% if cookiecutter.include_writer_plugin == 'y' -%} + {% endif %}{% if include_writer_plugin -%} "write_single_image", "write_multiple", - {% endif %}{% if cookiecutter.include_sample_data_plugin == 'y' -%} + {% endif %}{% if include_sample_data_plugin -%} "make_sample_data", - {% endif %}{% if cookiecutter.include_widget_plugin == 'y' -%} + {% endif %}{% if include_widget_plugin -%} "ExampleQWidget", "ImageThreshold", "threshold_autogenerate_widget", diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/__init__.py b/template/src/{{module_name}}/_tests/__init__.py.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/__init__.py rename to template/src/{{module_name}}/_tests/__init__.py.jinja diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_reader.py b/template/src/{{module_name}}/_tests/{% if include_reader_plugin %}test_reader.py{% endif %}.jinja similarity index 94% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_reader.py rename to template/src/{{module_name}}/_tests/{% if include_reader_plugin %}test_reader.py{% endif %}.jinja index 9dd155b..7bb13b7 100644 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_reader.py +++ b/template/src/{{module_name}}/_tests/{% if include_reader_plugin %}test_reader.py{% endif %}.jinja @@ -1,6 +1,6 @@ import numpy as np -from {{cookiecutter.module_name}} import napari_get_reader +from {{module_name}} import napari_get_reader # tmp_path is a pytest fixture diff --git a/template/src/{{module_name}}/_tests/{% if include_sample_data_plugin %}test_sample_data.py{% endif %}.jinja b/template/src/{{module_name}}/_tests/{% if include_sample_data_plugin %}test_sample_data.py{% endif %}.jinja new file mode 100644 index 0000000..86c3f54 --- /dev/null +++ b/template/src/{{module_name}}/_tests/{% if include_sample_data_plugin %}test_sample_data.py{% endif %}.jinja @@ -0,0 +1,7 @@ +# from {{module_name}} import make_sample_data + +# add your tests here... + + +def test_something(): + pass diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_widget.py b/template/src/{{module_name}}/_tests/{% if include_widget_plugin %}test_widget.py{% endif %}.jinja similarity index 97% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_widget.py rename to template/src/{{module_name}}/_tests/{% if include_widget_plugin %}test_widget.py{% endif %}.jinja index d83e713..79a0ad0 100644 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_widget.py +++ b/template/src/{{module_name}}/_tests/{% if include_widget_plugin %}test_widget.py{% endif %}.jinja @@ -1,6 +1,6 @@ import numpy as np -from {{cookiecutter.module_name}}._widget import ( +from {{module_name}}._widget import ( ExampleQWidget, ImageThreshold, threshold_autogenerate_widget, diff --git a/template/src/{{module_name}}/_tests/{% if include_writer_plugin %}test_writer.py{% endif %}.jinja b/template/src/{{module_name}}/_tests/{% if include_writer_plugin %}test_writer.py{% endif %}.jinja new file mode 100644 index 0000000..a6fd2fc --- /dev/null +++ b/template/src/{{module_name}}/_tests/{% if include_writer_plugin %}test_writer.py{% endif %}.jinja @@ -0,0 +1,7 @@ +# from {{module_name}} import write_single_image, write_multiple + +# add your tests here... + + +def test_something(): + pass diff --git a/template/src/{{module_name}}/napari.yaml.jinja b/template/src/{{module_name}}/napari.yaml.jinja new file mode 100644 index 0000000..0ca46fb --- /dev/null +++ b/template/src/{{module_name}}/napari.yaml.jinja @@ -0,0 +1,57 @@ +name: {{plugin_name}} +display_name: {{display_name}} +# use 'hidden' to remove plugin from napari hub search results +visibility: public +# see https://napari.org/stable/plugins/manifest.html for valid categories +categories: ["Annotation", "Segmentation", "Acquisition"] +contributions: + commands:{% if include_reader_plugin %} + - id: {{plugin_name}}.get_reader + python_name: {{module_name}}._reader:napari_get_reader + title: Open data with {{display_name}}{% endif %}{% if include_writer_plugin %} + - id: {{plugin_name}}.write_multiple + python_name: {{module_name}}._writer:write_multiple + title: Save multi-layer data with {{display_name}} + - id: {{plugin_name}}.write_single_image + python_name: {{module_name}}._writer:write_single_image + title: Save image data with {{display_name}}{% endif %}{% if include_sample_data_plugin %} + - id: {{plugin_name}}.make_sample_data + python_name: {{module_name}}._sample_data:make_sample_data + title: Load sample data from {{display_name}}{% endif %}{% if include_widget_plugin %} + - id: {{plugin_name}}.make_container_widget + python_name: {{module_name}}:ImageThreshold + title: Make threshold Container widget + - id: {{plugin_name}}.make_magic_widget + python_name: {{module_name}}:threshold_magic_widget + title: Make threshold magic widget + - id: {{plugin_name}}.make_function_widget + python_name: {{module_name}}:threshold_autogenerate_widget + title: Make threshold function widget + - id: {{plugin_name}}.make_qwidget + python_name: {{module_name}}:ExampleQWidget + title: Make example QWidget{% endif %}{% if include_reader_plugin %} + readers: + - command: {{plugin_name}}.get_reader + accepts_directories: false + filename_patterns: ['*.npy']{% endif %}{% if include_writer_plugin %} + writers: + - command: {{plugin_name}}.write_multiple + layer_types: ['image*','labels*'] + filename_extensions: [] + - command: {{plugin_name}}.write_single_image + layer_types: ['image'] + filename_extensions: ['.npy']{% endif %}{% if include_sample_data_plugin %} + sample_data: + - command: {{plugin_name}}.make_sample_data + display_name: {{display_name}} + key: unique_id.1{% endif %}{% if include_widget_plugin %} + widgets: + - command: {{plugin_name}}.make_container_widget + display_name: Container Threshold + - command: {{plugin_name}}.make_magic_widget + display_name: Magic Threshold + - command: {{plugin_name}}.make_function_widget + autogenerate: true + display_name: Autogenerate Threshold + - command: {{plugin_name}}.make_qwidget + display_name: Example QWidget{% endif %} diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_reader.py b/template/src/{{module_name}}/{% if include_reader_plugin %}_reader.py{% endif %}.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_reader.py rename to template/src/{{module_name}}/{% if include_reader_plugin %}_reader.py{% endif %}.jinja diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_sample_data.py b/template/src/{{module_name}}/{% if include_sample_data_plugin %}_sample_data.py{% endif %}.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_sample_data.py rename to template/src/{{module_name}}/{% if include_sample_data_plugin %}_sample_data.py{% endif %}.jinja diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_widget.py b/template/src/{{module_name}}/{% if include_widget_plugin %}_widget.py{% endif %}.jinja similarity index 99% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_widget.py rename to template/src/{{module_name}}/{% if include_widget_plugin %}_widget.py{% endif %}.jinja index ed8c358..99b2863 100644 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_widget.py +++ b/template/src/{{module_name}}/{% if include_widget_plugin %}_widget.py{% endif %}.jinja @@ -44,7 +44,7 @@ # a widget. def threshold_autogenerate_widget( img: "napari.types.ImageData", - threshold: "float", + threshold: "float", ) -> "napari.types.LabelsData": return img_as_float(img) > threshold diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_writer.py b/template/src/{{module_name}}/{% if include_writer_plugin %}_writer.py{% endif %}.jinja similarity index 86% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_writer.py rename to template/src/{{module_name}}/{% if include_writer_plugin %}_writer.py{% endif %}.jinja index e67fcc9..8beab89 100644 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_writer.py +++ b/template/src/{{module_name}}/{% if include_writer_plugin %}_writer.py{% endif %}.jinja @@ -8,16 +8,17 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, List, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Union if TYPE_CHECKING: DataType = Union[Any, Sequence[Any]] - FullLayerData = Tuple[DataType, dict, str] + FullLayerData = tuple[DataType, dict, str] -def write_single_image(path: str, data: Any, meta: dict) -> List[str]: +def write_single_image(path: str, data: Any, meta: dict) -> list[str]: """Writes a single image layer. - + Parameters ---------- path : str @@ -39,9 +40,9 @@ def write_single_image(path: str, data: Any, meta: dict) -> List[str]: return [path] -def write_multiple(path: str, data: List[FullLayerData]) -> List[str]: +def write_multiple(path: str, data: list[FullLayerData]) -> list[str]: """Writes multiple layers of different types. - + Parameters ---------- path : str diff --git a/{{cookiecutter.plugin_name}}/tox.ini b/template/tox.ini similarity index 86% rename from {{cookiecutter.plugin_name}}/tox.ini rename to template/tox.ini index 9d5b1ac..be3606c 100644 --- a/{{cookiecutter.plugin_name}}/tox.ini +++ b/template/tox.ini @@ -30,4 +30,4 @@ passenv = PYVISTA_OFF_SCREEN extras = testing -commands = pytest -v --color=yes --cov={{cookiecutter.module_name}} --cov-report=xml +commands = pytest -v --color=yes --cov={{module_name}} --cov-report=xml diff --git a/{{cookiecutter.plugin_name}}/.pre-commit-config.yaml b/template/{% if install_precommit %}.pre-commit-config.yaml{% endif %}.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/.pre-commit-config.yaml rename to template/{% if install_precommit %}.pre-commit-config.yaml{% endif %}.jinja diff --git a/tests/test_create_template.py b/tests/test_create_template.py index 09005fb..3968f04 100644 --- a/tests/test_create_template.py +++ b/tests/test_create_template.py @@ -19,61 +19,85 @@ def run_tox(plugin): except subprocess.CalledProcessError as e: pytest.fail("Subprocess fail", pytrace=True) -@pytest.mark.parametrize("include_reader_plugin", ["y", "n"]) -@pytest.mark.parametrize("include_writer_plugin", ["y", "n"]) -@pytest.mark.parametrize("include_sample_data_plugin", ["y", "n"]) -@pytest.mark.parametrize("include_widget_plugin", ["y", "n"]) -def test_run_cookiecutter_and_plugin_tests(cookies, capsys, include_reader_plugin, include_writer_plugin, include_sample_data_plugin, include_widget_plugin): - """Create a new plugin via cookiecutter and run its tests.""" - result = cookies.bake(extra_context={ - "plugin_name": "foo-bar", - "include_reader_plugin": include_reader_plugin, - "include_writer_plugin": include_writer_plugin, - "include_sample_data_plugin": include_sample_data_plugin, - "include_widget_plugin": include_widget_plugin, - } - ) + +@pytest.mark.parametrize("include_reader_plugin", [True, False]) +@pytest.mark.parametrize("include_writer_plugin", [True, False]) +@pytest.mark.parametrize("include_sample_data_plugin", [True, False]) +@pytest.mark.parametrize("include_widget_plugin", [True, False]) +def test_run_plugin_tests(copie, capsys, include_reader_plugin, include_writer_plugin, include_sample_data_plugin, include_widget_plugin): + """Create a new plugin with the napari plugin template and run its tests.""" + result = copie.copy(extra_answers={ + "plugin_name": "foo-bar", + "display_name": "Foo Bar", + "module_name": "foo_bar", + "short_description": "Super fast foo for all the bars", + "full_name": "napari bot", + "email": "etal@example.com", + "github_username_or_organization": "napari", + "include_reader_plugin": include_reader_plugin, + "include_writer_plugin": include_writer_plugin, + "include_sample_data_plugin": include_sample_data_plugin, + "include_widget_plugin": include_widget_plugin, + }) assert result.exit_code == 0 assert result.exception is None - assert result.project_path.name == "foo-bar" - assert result.project_path.is_dir() - assert result.project_path.joinpath("src").is_dir() - assert result.project_path.joinpath("src", "foo_bar", "__init__.py").is_file() - - test_path = result.project_path.joinpath("src", "foo_bar", "_tests") - if include_reader_plugin == "y": + assert result.project_dir.is_dir() + with open(result.project_dir/"README.md") as f: + assert f.readline() == "# foo-bar\n" + assert result.project_dir.joinpath("src").is_dir() + assert result.project_dir.joinpath("src", "foo_bar", "__init__.py").is_file() + + test_path = result.project_dir.joinpath("src", "foo_bar", "_tests") + if include_reader_plugin is True: assert (test_path / "test_reader.py").is_file() - if include_writer_plugin == "y": + if include_writer_plugin is True: assert (test_path / "test_writer.py").is_file() - if include_sample_data_plugin == "y": + if include_sample_data_plugin is True: assert (test_path / "test_sample_data.py").is_file() - if include_widget_plugin == "y": + if include_widget_plugin is True: assert (test_path / "test_widget.py").is_file() - # if all are `n` there are no modules or tests - if "y" in {include_reader_plugin, include_writer_plugin, include_sample_data_plugin, include_widget_plugin}: - run_tox(str(result.project_path)) + # if all are False there are no modules or tests + if True in {include_reader_plugin, include_writer_plugin, include_sample_data_plugin, include_widget_plugin}: + run_tox(str(result.project_dir)) -def test_run_cookiecutter_and_plugin_tests_with_napari_prefix(cookies, capsys): +def test_run_plugin_tests_with_napari_prefix(copie, capsys): """make sure it's also ok to use napari prefix.""" - result = cookies.bake(extra_context={"plugin_name": "napari-foo"}) + name = "napari-foo" + result = copie.copy(extra_answers={ + "plugin_name": name, + "display_name": "napari Foo", + "module_name": "napari_foo", + "short_description": "Super fast foo for all the bars", + "full_name": "napari bot", + "email": "etal@example.com", + "github_username_or_organization": "napari", + }) assert result.exit_code == 0 assert result.exception is None - assert result.project_path.name == "napari-foo" - assert result.project_path.is_dir() - assert result.project_path.joinpath("src").is_dir() - assert result.project_path.joinpath("src", "napari_foo", "__init__.py").is_file() - assert result.project_path.joinpath("src", "napari_foo", "_tests", "test_reader.py").is_file() + assert result.project_dir.is_dir() + with open(result.project_dir/"README.md") as f: + assert f.readline() == f"# {name}\n" + assert result.project_dir.joinpath("src").is_dir() + assert result.project_dir.joinpath("src", "napari_foo", "__init__.py").is_file() + assert result.project_dir.joinpath("src", "napari_foo", "_tests", "test_reader.py").is_file() -def test_run_cookiecutter_select_plugins(cookies, capsys): +def test_run_select_plugins(copie, capsys): """make sure it's also ok to use napari prefix.""" - result = cookies.bake( - extra_context={ - "plugin_name": "anything", + name = "anything" + result = copie.copy( + extra_answers={ + "plugin_name": name, + "display_name": "Foo Bar", + "module_name": "anything", + "short_description": "Super fast foo for all the bars", + "full_name": "napari bot", + "email": "etal@example.com", + "github_username_or_organization": "napari", "include_widget_plugin": "n", "include_writer_plugin": "n", } @@ -81,39 +105,46 @@ def test_run_cookiecutter_select_plugins(cookies, capsys): assert result.exit_code == 0 assert result.exception is None - assert result.project_path.name == "anything" - assert result.project_path.is_dir() - assert result.project_path.joinpath("src").is_dir() - assert result.project_path.joinpath("src", "anything", "__init__.py").is_file() - assert result.project_path.joinpath("src", "anything", "_tests", "test_reader.py").is_file() - - assert not result.project_path.joinpath("src", "anything", "_widget.py").is_file() - assert not result.project_path.joinpath( + assert result.project_dir.is_dir() + with open(result.project_dir/"README.md") as f: + assert f.readline() == f"# {name}\n" + assert result.project_dir.joinpath("src").is_dir() + assert result.project_dir.joinpath("src", name, "__init__.py").is_file() + assert result.project_dir.joinpath("src", name, "_tests", "test_reader.py").is_file() + + assert not result.project_dir.joinpath("src", "anything", "_widget.py").is_file() + assert not result.project_dir.joinpath( "src", "anything", "_tests", "test_widget.py" ).is_file() - assert not result.project_path.joinpath("src", "anything", "_writer.py").is_file() - assert not result.project_path.joinpath( + assert not result.project_dir.joinpath("src", "anything", "_writer.py").is_file() + assert not result.project_dir.joinpath( "src", "anything", "_tests", "test_writer.py" ).is_file() -@pytest.mark.parametrize("include_reader_plugin", ["y", "n"]) -@pytest.mark.parametrize("include_writer_plugin", ["y", "n"]) -@pytest.mark.parametrize("include_sample_data_plugin", ["y", "n"]) -@pytest.mark.parametrize("include_widget_plugin", ["y", "n"]) -def test_pre_commit_validity(cookies, include_reader_plugin, include_writer_plugin, include_sample_data_plugin, include_widget_plugin): - result = cookies.bake( - extra_context={ +@pytest.mark.parametrize("include_reader_plugin", [True, False]) +@pytest.mark.parametrize("include_writer_plugin", [True, False]) +@pytest.mark.parametrize("include_sample_data_plugin", [True, False]) +@pytest.mark.parametrize("include_widget_plugin", [True, False]) +def test_pre_commit_validity(copie, include_reader_plugin, include_writer_plugin, include_sample_data_plugin, include_widget_plugin): + result = copie.copy( + extra_answers={ "plugin_name": "anything", + "display_name": "Foo Bar", + "module_name": "anything", + "short_description": "Super fast foo for all the bars", + "full_name": "napari bot", + "email": "etal@example.com", + "github_username_or_organization": "napari", "include_reader_plugin": include_reader_plugin, "include_writer_plugin": include_writer_plugin, "include_sample_data_plugin": include_sample_data_plugin, "include_widget_plugin": include_widget_plugin, - "install_precommit": "y", + "install_precommit": True, } ) - result.project_path.joinpath("setup.cfg").is_file() + result.project_dir.joinpath("setup.cfg").is_file() try: - subprocess.run(["pre-commit", "run", "--all", "--show-diff-on-failure"], cwd=str(result.project_path), check=True, capture_output=True) + subprocess.run(["pre-commit", "run", "--all", "--show-diff-on-failure"], cwd=str(result.project_dir), check=True, capture_output=True) except subprocess.CalledProcessError as e: pytest.fail(f"pre-commit failed with output:\n{e.stdout.decode()}\nerror:\n{e.stderr.decode()}") diff --git a/tox.ini b/tox.ini index 1ffa866..ff7dce3 100644 --- a/tox.ini +++ b/tox.ini @@ -5,11 +5,13 @@ envlist = py{39,310,311,312} toxworkdir = /tmp/.tox [testenv] -deps = npe2 +deps = copier + jinja2-time + npe2 pytest - pytest-cookies + pytest-copie tox commands = pytest -v {posargs:tests} [pytest] -norecursedirs = "{{cookiecutter.plugin_name}}" +norecursedirs = "{{plugin_name}}" diff --git a/{{cookiecutter.plugin_name}}/LICENSE b/{{cookiecutter.plugin_name}}/LICENSE deleted file mode 100644 index 9355b92..0000000 --- a/{{cookiecutter.plugin_name}}/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -{%- if cookiecutter.license == "MIT" -%} -{%- include 'licenses/MIT' %} -{%- elif cookiecutter.license == "BSD-3" -%} -{%- include 'licenses/BSD-3' %} -{%- elif cookiecutter.license == "GNU GPL v3.0" -%} -{%- include 'licenses/GPL-3' %} -{%- elif cookiecutter.license == "GNU LGPL v3.0" -%} -{%- include 'licenses/LGPL-3' %} -{%- elif cookiecutter.license == "Apache Software License 2.0" -%} -{%- include 'licenses/Apache-2' %} -{%- elif cookiecutter.license == "Mozilla Public License 2.0" -%} -{%- include 'licenses/MPL-2' %} -{%- endif -%} diff --git a/{{cookiecutter.plugin_name}}/README.md b/{{cookiecutter.plugin_name}}/README.md deleted file mode 100644 index c362129..0000000 --- a/{{cookiecutter.plugin_name}}/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# {{cookiecutter.plugin_name}} - -[![License {{cookiecutter.license}}](https://img.shields.io/pypi/l/{{cookiecutter.plugin_name}}.svg?color=green)](https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/raw/main/LICENSE) -[![PyPI](https://img.shields.io/pypi/v/{{cookiecutter.plugin_name}}.svg?color=green)](https://pypi.org/project/{{cookiecutter.plugin_name}}) -[![Python Version](https://img.shields.io/pypi/pyversions/{{cookiecutter.plugin_name}}.svg?color=green)](https://python.org) -[![tests](https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/workflows/tests/badge.svg)](https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/actions) -[![codecov](https://codecov.io/gh/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/branch/main/graph/badge.svg)](https://codecov.io/gh/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}) -[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/{{cookiecutter.plugin_name}})](https://napari-hub.org/plugins/{{cookiecutter.plugin_name}}) - -{{cookiecutter.short_description}} - ----------------------------------- - -This [napari] plugin was generated with [Cookiecutter] using [@napari]'s [cookiecutter-napari-plugin] template. - - - -## Installation - -You can install `{{cookiecutter.plugin_name}}` via [pip]: - - pip install {{cookiecutter.plugin_name}} - - -{% if cookiecutter.github_repository_url != 'provide later' %} -To install latest development version : - - pip install git+https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}.git -{% endif %} - -## Contributing - -Contributions are very welcome. Tests can be run with [tox], please ensure -the coverage at least stays the same before you submit a pull request. - -## License - -Distributed under the terms of the [{{cookiecutter.license}}] license, -"{{cookiecutter.plugin_name}}" is free and open source software - -## Issues - -If you encounter any problems, please [file an issue] along with a detailed description. - -[napari]: https://github.com/napari/napari -[Cookiecutter]: https://github.com/audreyr/cookiecutter -[@napari]: https://github.com/napari -[MIT]: http://opensource.org/licenses/MIT -[BSD-3]: http://opensource.org/licenses/BSD-3-Clause -[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt -[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt -[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0 -[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt -[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin -{% if cookiecutter.github_repository_url != 'provide later' %} -[file an issue]: https://github.com/{{cookiecutter.github_username_or_organization}}/{{cookiecutter.plugin_name}}/issues -{% endif %} -[napari]: https://github.com/napari/napari -[tox]: https://tox.readthedocs.io/en/latest/ -[pip]: https://pypi.org/project/pip/ -[PyPI]: https://pypi.org/ diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_sample_data.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_sample_data.py deleted file mode 100644 index d953416..0000000 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_sample_data.py +++ /dev/null @@ -1,7 +0,0 @@ -# from {{cookiecutter.module_name}} import make_sample_data - -# add your tests here... - - -def test_something(): - pass diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_writer.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_writer.py deleted file mode 100644 index cce6950..0000000 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_writer.py +++ /dev/null @@ -1,7 +0,0 @@ -# from {{cookiecutter.module_name}} import write_single_image, write_multiple - -# add your tests here... - - -def test_something(): - pass diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/napari.yaml b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/napari.yaml deleted file mode 100644 index 10cda15..0000000 --- a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/napari.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: {{cookiecutter.plugin_name}} -display_name: {{cookiecutter.display_name}} -# use 'hidden' to remove plugin from napari hub search results -visibility: public -# see https://napari.org/stable/plugins/manifest.html for valid categories -categories: ["Annotation", "Segmentation", "Acquisition"] -contributions: - commands:{% if cookiecutter.include_reader_plugin == 'y' %} - - id: {{cookiecutter.plugin_name}}.get_reader - python_name: {{cookiecutter.module_name}}._reader:napari_get_reader - title: Open data with {{cookiecutter.display_name}}{% endif %}{% if cookiecutter.include_writer_plugin == 'y' %} - - id: {{cookiecutter.plugin_name}}.write_multiple - python_name: {{cookiecutter.module_name}}._writer:write_multiple - title: Save multi-layer data with {{cookiecutter.display_name}} - - id: {{cookiecutter.plugin_name}}.write_single_image - python_name: {{cookiecutter.module_name}}._writer:write_single_image - title: Save image data with {{cookiecutter.display_name}}{% endif %}{% if cookiecutter.include_sample_data_plugin == 'y' %} - - id: {{cookiecutter.plugin_name}}.make_sample_data - python_name: {{cookiecutter.module_name}}._sample_data:make_sample_data - title: Load sample data from {{cookiecutter.display_name}}{% endif %}{% if cookiecutter.include_widget_plugin == 'y' %} - - id: {{cookiecutter.plugin_name}}.make_container_widget - python_name: {{cookiecutter.module_name}}:ImageThreshold - title: Make threshold Container widget - - id: {{cookiecutter.plugin_name}}.make_magic_widget - python_name: {{cookiecutter.module_name}}:threshold_magic_widget - title: Make threshold magic widget - - id: {{cookiecutter.plugin_name}}.make_function_widget - python_name: {{cookiecutter.module_name}}:threshold_autogenerate_widget - title: Make threshold function widget - - id: {{cookiecutter.plugin_name}}.make_qwidget - python_name: {{cookiecutter.module_name}}:ExampleQWidget - title: Make example QWidget{% endif %}{% if cookiecutter.include_reader_plugin == 'y' %} - readers: - - command: {{cookiecutter.plugin_name}}.get_reader - accepts_directories: false - filename_patterns: ['*.npy']{% endif %}{% if cookiecutter.include_writer_plugin == 'y' %} - writers: - - command: {{cookiecutter.plugin_name}}.write_multiple - layer_types: ['image*','labels*'] - filename_extensions: [] - - command: {{cookiecutter.plugin_name}}.write_single_image - layer_types: ['image'] - filename_extensions: ['.npy']{% endif %}{% if cookiecutter.include_sample_data_plugin == 'y' %} - sample_data: - - command: {{cookiecutter.plugin_name}}.make_sample_data - display_name: {{cookiecutter.display_name}} - key: unique_id.1{% endif %}{% if cookiecutter.include_widget_plugin == 'y' %} - widgets: - - command: {{cookiecutter.plugin_name}}.make_container_widget - display_name: Container Threshold - - command: {{cookiecutter.plugin_name}}.make_magic_widget - display_name: Magic Threshold - - command: {{cookiecutter.plugin_name}}.make_function_widget - autogenerate: true - display_name: Autogenerate Threshold - - command: {{cookiecutter.plugin_name}}.make_qwidget - display_name: Example QWidget{% endif %}