From 134ce29867767f06982791c0c0a538e1dd5aad2b Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:08:12 +1000 Subject: [PATCH 01/11] Use copier instead of cookiecutter --- .github/workflows/test.yml | 4 +- PROMPTS.md | 4 +- README.md | 41 ++-- _tasks.py | 216 ++++++++++++++++++ cookiecutter.json | 25 -- copier.yaml | 101 ++++++++ hooks/post_gen_project.py | 182 --------------- hooks/pre_gen_project.py | 18 -- requirements.txt | 6 + .../.github/workflows/test_and_deploy.jinja | 0 ...pendabot %}dependabot.yml{% endif %}.jinja | 0 .../.gitignore | 0 .../.napari-hub/DESCRIPTION.md.jinja | 0 template/.napari-hub/config-yml.jinja | 9 + .../LICENSE => template/src/LICENSE.jinja | 0 .../src}/MANIFEST.in | 0 .../README.md => template/src/README.md.jinja | 0 .../src/licenses/Apache-2.jinja | 0 .../src/licenses/BSD-3.jinja | 0 .../src/licenses/GPL-3.jinja | 0 .../src/licenses/LGPL-3.jinja | 0 .../MIT => template/src/licenses/MIT.jinja | 0 .../src/licenses/MPL-2.jinja | 0 .../src}/pyproject.toml | 0 .../src}/tox.ini | 0 .../src/{{module_name}}/__init__.py.jinja | 0 .../{{module_name}}/_tests/__init__.py.jinja | 0 ...r_plugin %}test_reader.py{% endif %}.jinja | 0 ...gin %}test_sample_data.py{% endif %}.jinja | 0 ...t_plugin %}test_widget.py{% endif %}.jinja | 0 ...r_plugin %}test_writer.py{% endif %}.jinja | 0 .../src/{{module_name}}/napari.yaml.jinja | 0 ...eader_plugin %}_reader.py{% endif %}.jinja | 0 ..._plugin %}_sample_data.py{% endif %}.jinja | 0 ...idget_plugin %}_widget.py{% endif %}.jinja | 2 +- ...riter_plugin %}_writer.py{% endif %}.jinja | 13 +- ...%}.pre-commit-config.yaml{% endif %}.jinja | 0 37 files changed, 366 insertions(+), 255 deletions(-) create mode 100644 _tasks.py delete mode 100644 cookiecutter.json create mode 100644 copier.yaml delete mode 100644 hooks/post_gen_project.py delete mode 100644 hooks/pre_gen_project.py create mode 100644 requirements.txt rename {{cookiecutter.plugin_name}}/.github/workflows/test_and_deploy.yml => template/.github/workflows/test_and_deploy.jinja (100%) rename {{cookiecutter.plugin_name}}/.github/dependabot.yml => template/.github/{% if install_dependabot %}dependabot.yml{% endif %}.jinja (100%) rename {{{cookiecutter.plugin_name}} => template}/.gitignore (100%) rename {{cookiecutter.plugin_name}}/.napari-hub/DESCRIPTION.md => template/.napari-hub/DESCRIPTION.md.jinja (100%) create mode 100644 template/.napari-hub/config-yml.jinja rename {{cookiecutter.plugin_name}}/LICENSE => template/src/LICENSE.jinja (100%) rename {{{cookiecutter.plugin_name}} => template/src}/MANIFEST.in (100%) rename {{cookiecutter.plugin_name}}/README.md => template/src/README.md.jinja (100%) rename {{cookiecutter.plugin_name}}/licenses/Apache-2 => template/src/licenses/Apache-2.jinja (100%) rename {{cookiecutter.plugin_name}}/licenses/BSD-3 => template/src/licenses/BSD-3.jinja (100%) rename {{cookiecutter.plugin_name}}/licenses/GPL-3 => template/src/licenses/GPL-3.jinja (100%) rename {{cookiecutter.plugin_name}}/licenses/LGPL-3 => template/src/licenses/LGPL-3.jinja (100%) rename {{cookiecutter.plugin_name}}/licenses/MIT => template/src/licenses/MIT.jinja (100%) rename {{cookiecutter.plugin_name}}/licenses/MPL-2 => template/src/licenses/MPL-2.jinja (100%) rename {{{cookiecutter.plugin_name}} => template/src}/pyproject.toml (100%) rename {{{cookiecutter.plugin_name}} => template/src}/tox.ini (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py => template/src/{{module_name}}/__init__.py.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/__init__.py => template/src/{{module_name}}/_tests/__init__.py.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_reader.py => template/src/{{module_name}}/_tests/{% if include_reader_plugin %}test_reader.py{% endif %}.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_sample_data.py => template/src/{{module_name}}/_tests/{% if include_sample_data_plugin %}test_sample_data.py{% endif %}.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_widget.py => template/src/{{module_name}}/_tests/{% if include_widget_plugin %}test_widget.py{% endif %}.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_writer.py => template/src/{{module_name}}/_tests/{% if include_writer_plugin %}test_writer.py{% endif %}.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/napari.yaml => template/src/{{module_name}}/napari.yaml.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_reader.py => template/src/{{module_name}}/{% if include_reader_plugin %}_reader.py{% endif %}.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_sample_data.py => template/src/{{module_name}}/{% if include_sample_data_plugin %}_sample_data.py{% endif %}.jinja (100%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_widget.py => template/src/{{module_name}}/{% if include_widget_plugin %}_widget.py{% endif %}.jinja (99%) rename {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_writer.py => template/src/{{module_name}}/{% if include_writer_plugin %}_writer.py{% endif %}.jinja (86%) rename {{cookiecutter.plugin_name}}/.pre-commit-config.yaml => template/{% if install_precommit %}.pre-commit-config.yaml{% endif %}.jinja (100%) 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..4769297 --- /dev/null +++ b/copier.yaml @@ -0,0 +1,101 @@ +plugin_name: + default: napari-foobar + help: The name of your plugin + type: str +display_name: + deafult: FooBar Segmentation + help: Display name for your plugin + type: str +module_name: + default: "{{ plugin_name|lower|replace('-', '_') }}" + help: Plugin module name + type: str +short_description: + placeholder: A simple plugin to use FooBar segmentation within napari + help: Short description of what your plugin does + type: str +full_name: + placeholder: Napari Developer + help: Developer name + type: str +email: + placeholder: yourname@example.com + help: Email address + 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/{{cookiecutter.plugin_name}}/LICENSE b/template/src/LICENSE.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/LICENSE rename to template/src/LICENSE.jinja diff --git a/{{cookiecutter.plugin_name}}/MANIFEST.in b/template/src/MANIFEST.in similarity index 100% rename from {{cookiecutter.plugin_name}}/MANIFEST.in rename to template/src/MANIFEST.in diff --git a/{{cookiecutter.plugin_name}}/README.md b/template/src/README.md.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/README.md rename to template/src/README.md.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/Apache-2 b/template/src/licenses/Apache-2.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/Apache-2 rename to template/src/licenses/Apache-2.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/BSD-3 b/template/src/licenses/BSD-3.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/BSD-3 rename to template/src/licenses/BSD-3.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/GPL-3 b/template/src/licenses/GPL-3.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/GPL-3 rename to template/src/licenses/GPL-3.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/LGPL-3 b/template/src/licenses/LGPL-3.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/LGPL-3 rename to template/src/licenses/LGPL-3.jinja diff --git a/{{cookiecutter.plugin_name}}/licenses/MIT b/template/src/licenses/MIT.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/licenses/MIT rename to template/src/licenses/MIT.jinja 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}}/pyproject.toml b/template/src/pyproject.toml similarity index 100% rename from {{cookiecutter.plugin_name}}/pyproject.toml rename to template/src/pyproject.toml diff --git a/{{cookiecutter.plugin_name}}/tox.ini b/template/src/tox.ini similarity index 100% rename from {{cookiecutter.plugin_name}}/tox.ini rename to template/src/tox.ini diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py b/template/src/{{module_name}}/__init__.py.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py rename to template/src/{{module_name}}/__init__.py.jinja 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 100% 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 diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_sample_data.py b/template/src/{{module_name}}/_tests/{% if include_sample_data_plugin %}test_sample_data.py{% endif %}.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_sample_data.py rename to template/src/{{module_name}}/_tests/{% if include_sample_data_plugin %}test_sample_data.py{% endif %}.jinja 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 100% 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 diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_writer.py b/template/src/{{module_name}}/_tests/{% if include_writer_plugin %}test_writer.py{% endif %}.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/_tests/test_writer.py rename to template/src/{{module_name}}/_tests/{% if include_writer_plugin %}test_writer.py{% endif %}.jinja diff --git a/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/napari.yaml b/template/src/{{module_name}}/napari.yaml.jinja similarity index 100% rename from {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/napari.yaml rename to template/src/{{module_name}}/napari.yaml.jinja 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}}/.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 From 80adc478edcba0e5202481ac391879b1dc04b653 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:11:38 +1000 Subject: [PATCH 02/11] Remove references to cookiecutter and pytest-cookies --- template/src/LICENSE.jinja | 12 +- template/src/README.md.jinja | 40 ++--- template/src/licenses/BSD-3.jinja | 2 +- template/src/licenses/MIT.jinja | 2 +- template/src/pyproject.toml | 44 +++--- template/src/tox.ini | 2 +- .../src/{{module_name}}/__init__.py.jinja | 18 +-- ...r_plugin %}test_reader.py{% endif %}.jinja | 2 +- ...gin %}test_sample_data.py{% endif %}.jinja | 2 +- ...t_plugin %}test_widget.py{% endif %}.jinja | 2 +- ...r_plugin %}test_writer.py{% endif %}.jinja | 2 +- .../src/{{module_name}}/napari.yaml.jinja | 72 ++++----- tests/test_create_template.py | 139 +++++++++++------- tox.ini | 8 +- 14 files changed, 188 insertions(+), 159 deletions(-) diff --git a/template/src/LICENSE.jinja b/template/src/LICENSE.jinja index 9355b92..8aa79e2 100644 --- a/template/src/LICENSE.jinja +++ b/template/src/LICENSE.jinja @@ -1,13 +1,13 @@ -{%- if cookiecutter.license == "MIT" -%} +{%- if license == "MIT" -%} {%- include 'licenses/MIT' %} -{%- elif cookiecutter.license == "BSD-3" -%} +{%- elif license == "BSD-3" -%} {%- include 'licenses/BSD-3' %} -{%- elif cookiecutter.license == "GNU GPL v3.0" -%} +{%- elif license == "GNU GPL v3.0" -%} {%- include 'licenses/GPL-3' %} -{%- elif cookiecutter.license == "GNU LGPL v3.0" -%} +{%- elif license == "GNU LGPL v3.0" -%} {%- include 'licenses/LGPL-3' %} -{%- elif cookiecutter.license == "Apache Software License 2.0" -%} +{%- elif license == "Apache Software License 2.0" -%} {%- include 'licenses/Apache-2' %} -{%- elif cookiecutter.license == "Mozilla Public License 2.0" -%} +{%- elif license == "Mozilla Public License 2.0" -%} {%- include 'licenses/MPL-2' %} {%- endif -%} diff --git a/template/src/README.md.jinja b/template/src/README.md.jinja index c362129..ef45849 100644 --- a/template/src/README.md.jinja +++ b/template/src/README.md.jinja @@ -1,21 +1,21 @@ -# {{cookiecutter.plugin_name}} +# {{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}}) +[![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}}) -{{cookiecutter.short_description}} +{{short_description}} ---------------------------------- -This [napari] plugin was generated with [Cookiecutter] using [@napari]'s [cookiecutter-napari-plugin] template. +This [napari] plugin was generated with [copier] using the [napari-plugin-template].