Skip to content
This repository has been archived by the owner on Nov 6, 2024. It is now read-only.

Use copier instead of cookiecutter #165

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
# cookiecutter-napari-plugin

[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**

## Getting Started

### Create your plugin package

Install [Cookiecutter] and generate a new napari plugin project:
Install [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/cookiecutter-napari-plugin 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
Expand Down Expand Up @@ -190,8 +194,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]
Expand Down Expand Up @@ -234,8 +237,9 @@ 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/cookiecutter-napari-plugin/issues/new)
along with a detailed description.

## License

Expand Down
217 changes: 217 additions & 0 deletions _tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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 = ""
# 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 .
"""

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/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
"""
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()

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=bool(args.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)
24 changes: 0 additions & 24 deletions cookiecutter.json

This file was deleted.

97 changes: 97 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
plugin_name:
default: napari-foobar
help: The name of your plugin
type: str
display_name:
default: FooBar Segmentation
help: Display name for your plugin
type: str
short_description:
default: A simple plugin to use FooBar segmentation within napari
help: Short description of what your plugin does
type: str
full_name:
default: Napari Developer
help: Developer name
type: str
email:
default: yourname@example.com
help: Email address
type: str
github_username_or_organization:
default: githubuser
help: Github user or organisation name
type: str
github_repository_url:
default: https://github.com/{{github_username_or_organization}}/{{plugin_name}}
help: Github repository URL
type: str
choices:
- https://github.com/{{github_username_or_organization}}/{{plugin_name}}
- provide later
module_name:
default: "{{ plugin_name|lower|replace('-', '_') }}"
help: Plugin module name
type: str
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?
type: bool
license:
default: BSD-3
help: Which licence do you 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 }}",
]
Loading