From 3a58e1e0f46a0b856e092a9a09888f004ae192e6 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 8 Mar 2022 17:49:53 -0800 Subject: [PATCH 01/24] Uncap dependency versions, bump test dependencies For all install_requires, remove the major version pins. This concretely fixes an incompatibility between markupsafe and the old 2.x version of Jinja2, and more generally matches our practices for other library packages. --- setup.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 6ad0334..d462143 100644 --- a/setup.py +++ b/setup.py @@ -18,20 +18,20 @@ # Core dependencies install_requires = [ - 'cookiecutter>=1.6.0,<2.0.0', - 'Jinja2>=2.10,<3.0.0', - 'scons>=3.0.1,<3.1.0', + 'cookiecutter>=1.6.0', + 'Jinja2>=2.10', + 'scons>=3.0.1', 'click', - 'pyperclip>=1.6.0,<1.7.0', + 'pyperclip>=1.6.0', 'PyYAML>=5.1', - 'Cerberus>=1.2,<2.0', + 'Cerberus>=1.2', 'GitPython>=3.0.0', ] # Test dependencies tests_require = [ - 'pytest==5.2.1', - 'pytest-flake8==1.0.4', + 'pytest==7.0.1', + 'pytest-flake8==1.1.0', ] tests_require += install_requires From 82f4cc63ffbe9132978f41b910edba8d5cb62825 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 11 Mar 2022 16:50:40 -0800 Subject: [PATCH 02/24] Run black on all source code --- docs/conf.py | 71 +++++----- templatekit/__init__.py | 4 +- templatekit/builder.py | 63 +++++---- templatekit/filerender.py | 33 ++--- templatekit/jinjaext.py | 61 +++++---- templatekit/repo.py | 192 ++++++++++++++------------- templatekit/scripts/check.py | 40 +++--- templatekit/scripts/listtemplates.py | 32 ++--- templatekit/scripts/main.py | 35 ++--- templatekit/scripts/make.py | 57 ++++---- templatekit/textutils.py | 6 +- tests/conftest.py | 7 +- tests/test_base_template.py | 41 +++--- tests/test_file_template.py | 7 +- tests/test_filerender.py | 8 +- tests/test_jinjaext.py | 79 +++++------ tests/test_repo.py | 68 +++++----- tests/test_repo_templateconfig.py | 53 ++++---- tests/test_textutils.py | 55 ++------ 19 files changed, 469 insertions(+), 443 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 14b493c..9ec634b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,8 @@ import os import sys -from documenteer.sphinxconfig.utils import form_ltd_edition_name import lsst_sphinx_bootstrap_theme - +from documenteer.sphinxconfig.utils import form_ltd_edition_name # Work around Sphinx bug related to large and highly-nested source files sys.setrecursionlimit(2000) @@ -17,42 +16,43 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.ifconfig', - 'sphinx_click.ext', - 'numpydoc', - 'sphinx_automodapi.automodapi', - 'sphinx_automodapi.smart_resolver', - 'documenteer.sphinxext' + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.ifconfig", + "sphinx_click.ext", + "numpydoc", + "sphinx_automodapi.automodapi", + "sphinx_automodapi.smart_resolver", + "documenteer.sphinxext", ] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Templatekit' -copyright = '2018-2019 Association of Universities for Research in Astronomy' -author = 'LSST Data Management' +project = "Templatekit" +copyright = "2018-2019 Association of Universities for Research in Astronomy" +author = "LSST Data Management" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -if os.getenv('TRAVIS_BRANCH', default='master') == 'master': +if os.getenv("TRAVIS_BRANCH", default="master") == "master": # Use the current release as the version tag if on master - version = 'Current' + version = "Current" release = version else: # Use branch name as the version tag version = form_ltd_edition_name( - git_ref_name=os.getenv('TRAVIS_BRANCH', default='master')) + git_ref_name=os.getenv("TRAVIS_BRANCH", default="master") + ) release = version # The language for content autogenerated by Sphinx. Refer to documentation @@ -70,22 +70,19 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = [ - '_build', - 'README.rst' -] +exclude_patterns = ["_build", "README.rst"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # The reST default role cross-links Python (used for this markup: `text`) -default_role = 'py:obj' +default_role = "py:obj" # Intersphinx intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'cookiecutter': ('https://cookiecutter.readthedocs.io/en/latest/', None), + "python": ("https://docs.python.org/3/", None), + "cookiecutter": ("https://cookiecutter.readthedocs.io/en/latest/", None), } rst_epilog = """ @@ -102,31 +99,31 @@ # -- Options for HTML output ---------------------------------------------- templates_path = [ - '_templates', - lsst_sphinx_bootstrap_theme.get_html_templates_path() + "_templates", + lsst_sphinx_bootstrap_theme.get_html_templates_path(), ] -html_theme = 'lsst_sphinx_bootstrap_theme' +html_theme = "lsst_sphinx_bootstrap_theme" html_theme_path = [lsst_sphinx_bootstrap_theme.get_html_theme_path()] html_context = { # Enable "Edit in GitHub" link - 'display_github': True, + "display_github": True, # https://{{ github_host|default("github.com") }}/{{ github_user }}/ # {{ github_repo }}/blob/ # {{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} - 'github_user': 'lsst-sqre', - 'github_repo': 'templatekit', - 'conf_py_path': 'docs/', + "github_user": "lsst-sqre", + "github_repo": "templatekit", + "conf_py_path": "docs/", # TRAVIS_BRANCH is available in CI, but master is a safe default - 'github_version': os.getenv('TRAVIS_BRANCH', default='master') + '/' + "github_version": os.getenv("TRAVIS_BRANCH", default="master") + "/", } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = {'logotext': project} +html_theme_options = {"logotext": project} # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". diff --git a/templatekit/__init__.py b/templatekit/__init__.py index 9b5ced2..2b6264e 100644 --- a/templatekit/__init__.py +++ b/templatekit/__init__.py @@ -1,7 +1,7 @@ -from pkg_resources import get_distribution, DistributionNotFound +from pkg_resources import DistributionNotFound, get_distribution try: - __version__ = get_distribution('templatekit').version + __version__ = get_distribution("templatekit").version except DistributionNotFound: # package is not installed pass diff --git a/templatekit/builder.py b/templatekit/builder.py index 1c5a6b9..2a74b85 100644 --- a/templatekit/builder.py +++ b/templatekit/builder.py @@ -16,13 +16,16 @@ new projects from a template. """ -__all__ = ('file_template_builder', 'cookiecutter_project_builder', - 'line_format_builder') +__all__ = ( + "file_template_builder", + "cookiecutter_project_builder", + "line_format_builder", +) import os -from cookiecutter.main import cookiecutter from cookiecutter.find import find_template +from cookiecutter.main import cookiecutter from SCons.Script import Builder from .filerender import render_and_write_file_template @@ -45,18 +48,19 @@ def build_file_template(target, source, env): source_path = str(source[0]) construction_vars = env.Dictionary() - if 'cookiecutter_context' in construction_vars: - context_overrides = construction_vars['cookiecutter_context'] + if "cookiecutter_context" in construction_vars: + context_overrides = construction_vars["cookiecutter_context"] else: context_overrides = None - render_and_write_file_template(source_path, target_path, - extra_context=context_overrides) + render_and_write_file_template( + source_path, target_path, extra_context=context_overrides + ) -file_template_builder = Builder(action=build_file_template, - suffix='', - src_suffix='.jinja') +file_template_builder = Builder( + action=build_file_template, suffix="", src_suffix=".jinja" +) """Scons builder for rendering a single-file template examples. """ @@ -85,8 +89,8 @@ def build_project_template(target, source, env): template_dir = os.path.dirname(cookiecutter_json_source) construction_vars = env.Dictionary() - if 'cookiecutter_context' in construction_vars: - context_overrides = construction_vars['cookiecutter_context'] + if "cookiecutter_context" in construction_vars: + context_overrides = construction_vars["cookiecutter_context"] else: context_overrides = None @@ -95,7 +99,8 @@ def build_project_template(target, source, env): output_dir=template_dir, overwrite_if_exists=True, no_input=True, - extra_context=context_overrides) + extra_context=context_overrides, + ) def emit_cookiecutter_sources(target, source, env): @@ -107,16 +112,18 @@ def emit_cookiecutter_sources(target, source, env): source list. """ # Get the template directory (i.e., "{{ cookiecutter.project_name }}/") - template_dir = find_template('.') + template_dir = find_template(".") # Get all the template files and add them to the sources for (_base_path, _dir_names, _file_names) in os.walk(template_dir): - source.extend([os.path.join(_base_path, file_name) - for file_name in _file_names]) + source.extend( + [os.path.join(_base_path, file_name) for file_name in _file_names] + ) return target, source -cookiecutter_project_builder = Builder(action=build_project_template, - emitter=emit_cookiecutter_sources) +cookiecutter_project_builder = Builder( + action=build_project_template, emitter=emit_cookiecutter_sources +) """Scons builder for rendering a cookiecutter project template. The action is `build_project_template` and the emitter is @@ -125,8 +132,9 @@ def emit_cookiecutter_sources(target, source, env): """ -def format_content(target, source, env, line_format=None, - header=None, footer=None): +def format_content( + target, source, env, line_format=None, header=None, footer=None +): """Scons builder action for rendering a Python comment from a plain text file. @@ -143,25 +151,26 @@ def format_content(target, source, env, line_format=None, source_path = str(source[0]) try: - line_format = env['line_format'] + line_format = env["line_format"] except KeyError: - line_format = '{}' + line_format = "{}" try: - header = env['header'] + header = env["header"] except KeyError: header = None try: - footer = env['footer'] + footer = env["footer"] except KeyError: footer = None with open(source_path) as fh: content = fh.read() - formatted_content = reformat_content_lines(content, line_format, - header=header, footer=footer) - with open(target_path, 'w') as fh: + formatted_content = reformat_content_lines( + content, line_format, header=header, footer=footer + ) + with open(target_path, "w") as fh: fh.write(formatted_content) diff --git a/templatekit/filerender.py b/templatekit/filerender.py index c49d57f..bfd0d0c 100644 --- a/templatekit/filerender.py +++ b/templatekit/filerender.py @@ -1,22 +1,23 @@ """Rendering file templates with Cookiecutter. """ -__all__ = ('render_file_template', 'render_and_write_file_template') +__all__ = ("render_file_template", "render_and_write_file_template") -import logging import io +import logging import os import shutil +from cookiecutter.environment import StrictEnvironment from cookiecutter.generate import generate_context from cookiecutter.prompt import prompt_for_config -from cookiecutter.environment import StrictEnvironment from jinja2 import FileSystemLoader from jinja2.exceptions import TemplateSyntaxError -def render_file_template(template_path, use_defaults=False, - extra_context=None): +def render_file_template( + template_path, use_defaults=False, extra_context=None +): """Render a single-file template with Cookiecutter. Currently this function only renders a file using default values defined @@ -42,16 +43,16 @@ def render_file_template(template_path, use_defaults=False, Content rendered from the template and ``cookiecutter.json`` defaults. """ logger = logging.getLogger(__name__) - logger.debug('Rendering file template %s', template_path) + logger.debug("Rendering file template %s", template_path) # Get variables for rendering the template template_dir = os.path.dirname(template_path) - context_file = os.path.join(template_dir, 'cookiecutter.json') + context_file = os.path.join(template_dir, "cookiecutter.json") context = generate_context(context_file=context_file) - context['cookiecutter'] = prompt_for_config(context, use_defaults) + context["cookiecutter"] = prompt_for_config(context, use_defaults) if extra_context is not None: - context['cookiecutter'].update(extra_context) + context["cookiecutter"].update(extra_context) # Jinja2 template rendering environment env = StrictEnvironment( @@ -72,8 +73,9 @@ def render_file_template(template_path, use_defaults=False, return rendered_text -def render_and_write_file_template(template_path, output_path, - extra_context=None): +def render_and_write_file_template( + template_path, output_path, extra_context=None +): """Render a single-file template and write it to the filesystem. Parameters @@ -92,11 +94,12 @@ def render_and_write_file_template(template_path, output_path, """ logger = logging.getLogger(__name__) - rendered_text = render_file_template(template_path, use_defaults=True, - extra_context=extra_context) + rendered_text = render_file_template( + template_path, use_defaults=True, extra_context=extra_context + ) - logger.debug('Writing rendered file to {}'.format(output_path)) - with io.open(output_path, 'w', encoding='utf-8') as fh: + logger.debug("Writing rendered file to {}".format(output_path)) + with io.open(output_path, "w", encoding="utf-8") as fh: fh.write(rendered_text) # Apply file permissions to output file diff --git a/templatekit/jinjaext.py b/templatekit/jinjaext.py index 4a28a63..7ec1a85 100644 --- a/templatekit/jinjaext.py +++ b/templatekit/jinjaext.py @@ -1,14 +1,17 @@ """Custom Jinja2 filters and tags. """ -__all__ = ('convert_py_to_cpp_namespace_code', - 'convert_py_namespace_to_cpp_header_def', - 'convert_py_to_cpp_namespace', - 'convert_py_namespace_to_includes_dir', - 'convert_py_namespace_to_header_filename', - 'escape_yaml_doublequoted') +__all__ = ( + "convert_py_to_cpp_namespace_code", + "convert_py_namespace_to_cpp_header_def", + "convert_py_to_cpp_namespace", + "convert_py_namespace_to_includes_dir", + "convert_py_namespace_to_header_filename", + "escape_yaml_doublequoted", +) import os + from jinja2.ext import Extension @@ -51,12 +54,24 @@ class TemplatekitExtension(Extension): def __init__(self, environment): super().__init__(environment) - environment.filters['convert_py_to_cpp_namespace_code'] = convert_py_to_cpp_namespace_code # noqa: E501 - environment.filters['convert_py_namespace_to_cpp_header_def'] = convert_py_namespace_to_cpp_header_def # noqa: E501 - environment.filters['convert_py_to_cpp_namespace'] = convert_py_to_cpp_namespace # noqa: E501 - environment.filters['convert_py_namespace_to_includes_dir'] = convert_py_namespace_to_includes_dir # noqa: E501 - environment.filters['convert_py_namespace_to_header_filename'] = convert_py_namespace_to_header_filename # noqa: E501 - environment.filters['escape_yaml_doublequoted'] = escape_yaml_doublequoted # noqa: E501 + environment.filters[ + "convert_py_to_cpp_namespace_code" + ] = convert_py_to_cpp_namespace_code # noqa: E501 + environment.filters[ + "convert_py_namespace_to_cpp_header_def" + ] = convert_py_namespace_to_cpp_header_def # noqa: E501 + environment.filters[ + "convert_py_to_cpp_namespace" + ] = convert_py_to_cpp_namespace # noqa: E501 + environment.filters[ + "convert_py_namespace_to_includes_dir" + ] = convert_py_namespace_to_includes_dir # noqa: E501 + environment.filters[ + "convert_py_namespace_to_header_filename" + ] = convert_py_namespace_to_header_filename # noqa: E501 + environment.filters[ + "escape_yaml_doublequoted" + ] = escape_yaml_doublequoted # noqa: E501 def convert_py_to_cpp_namespace_code(python_namespace): @@ -85,11 +100,11 @@ def convert_py_to_cpp_namespace_code(python_namespace): {{ 'lsst.example' | convert_py_to_cpp_namespace_code }} """ - name = python_namespace.replace('.', '::') - namespace_parts = python_namespace.split('.') - opening = 'namespace ' + ' { '.join(namespace_parts) + ' {\n' - closing = '}' * len(namespace_parts) + ' // {}'.format(name) - return '\n'.join((opening, closing)) + name = python_namespace.replace(".", "::") + namespace_parts = python_namespace.split(".") + opening = "namespace " + " { ".join(namespace_parts) + " {\n" + closing = "}" * len(namespace_parts) + " // {}".format(name) + return "\n".join((opening, closing)) def convert_py_namespace_to_cpp_header_def(python_namespace): @@ -106,7 +121,7 @@ def convert_py_namespace_to_cpp_header_def(python_namespace): cpp_header_def : `str` C++ header def, such as '`'LSST_EXAMPLE_H'``. """ - return python_namespace.upper().replace('.', '_') + '_H' + return python_namespace.upper().replace(".", "_") + "_H" def convert_py_to_cpp_namespace(python_namespace): @@ -123,7 +138,7 @@ def convert_py_to_cpp_namespace(python_namespace): cpp_namespace : `str` A C++ namespace. For example: ``'lsst::example'``. """ - return python_namespace.replace('.', '::') + return python_namespace.replace(".", "::") def convert_py_namespace_to_includes_dir(python_namespace): @@ -140,7 +155,7 @@ def convert_py_namespace_to_includes_dir(python_namespace): includes_dir : `str` The includes directory. """ - parts = python_namespace.split('.') + parts = python_namespace.split(".") return os.path.join(*parts[:-1]) @@ -158,8 +173,8 @@ def convert_py_namespace_to_header_filename(python_namespace): header_filename : `str` Filename of the root header file. """ - parts = python_namespace.split('.') - return parts[-1] + '.h' + parts = python_namespace.split(".") + return parts[-1] + ".h" def escape_yaml_doublequoted(string): @@ -183,4 +198,4 @@ def escape_yaml_doublequoted(string): - Replace ``\`` with ``\\``. - Replace ``"`` with ``"\``. """ - return string.replace('\\', '\\\\').replace('"', '\\"') + return string.replace("\\", "\\\\").replace('"', '\\"') diff --git a/templatekit/repo.py b/templatekit/repo.py index d990f3b..ce58dfc 100644 --- a/templatekit/repo.py +++ b/templatekit/repo.py @@ -1,22 +1,26 @@ -"""Template repository APIs. -""" +"""Template repository APIs.""" -__all__ = ('Repo', 'FileTemplate', 'ProjectTemplate', 'BaseTemplate', - 'TemplateConfig') +__all__ = ( + "Repo", + "FileTemplate", + "ProjectTemplate", + "BaseTemplate", + "TemplateConfig", +) -from copy import deepcopy import collections.abc -import os import functools import itertools -import logging -from pathlib import Path import json +import logging +import os import subprocess +from copy import deepcopy +from pathlib import Path +import cerberus import git import yaml -import cerberus class Repo(object): @@ -36,7 +40,7 @@ def __init__(self, root): self.root = root @classmethod - def discover_repo(cls, dirname='.'): + def discover_repo(cls, dirname="."): """Create a Repo instance by discovering the template repo's root directory. @@ -61,35 +65,37 @@ def discover_repo(cls, dirname='.'): original_dirname = dirname dirname = os.path.abspath(dirname) - while dirname != '/': + while dirname != "/": if cls._is_repo_dir(dirname): return cls(dirname) # Repeat with the parent directory dirname = os.path.split(dirname)[0] - message = ('The directory {0!r} is not contained by a recognizable ' - 'template repository.') + message = ( + "The directory {0!r} is not contained by a recognizable " + "template repository." + ) raise OSError(message.format(original_dirname)) @staticmethod def _is_repo_dir(dirname): - if not os.path.isdir(os.path.join(dirname, 'file_templates')): + if not os.path.isdir(os.path.join(dirname, "file_templates")): return False - if not os.path.isdir(os.path.join(dirname, 'project_templates')): + if not os.path.isdir(os.path.join(dirname, "project_templates")): return False return True def __repr__(self): - return 'Repo({0!r})'.format(self.root) + return "Repo({0!r})".format(self.root) def __str__(self): - return '{0!r}\nProject templates: {1!s}\nFile templates: {2!s}'.format( + return "{0!r}\nProject templates: {1!s}\nFile templates: {2!s}".format( self, - ', '.join([t.name for t in self.iter_project_templates()]), - ', '.join([t.name for t in self.iter_file_templates()]) + ", ".join([t.name for t in self.iter_project_templates()]), + ", ".join([t.name for t in self.iter_file_templates()]), ) def __iter__(self): @@ -101,27 +107,24 @@ def __iter__(self): yield template.name def __getitem__(self, key): - """Get either a file or project template by name. - """ + """Get either a file or project template by name.""" for template in self.iter_templates(): if template.name == key: return template - message = 'Template {0!r} not found'.format(key) + message = "Template {0!r} not found".format(key) raise KeyError(message) @property def file_templates_dirname(self): - """Path of the ``file_templates`` directory in the repository (`str`). - """ - return os.path.join(self.root, 'file_templates') + """Path of the ``file_templates`` directory in the repository (`str`).""" + return os.path.join(self.root, "file_templates") @property def project_templates_dirname(self): """Path of the ``project_templates`` directory in the repository - (`str`). - """ - return os.path.join(self.root, 'project_templates') + (`str`).""" + return os.path.join(self.root, "project_templates") def iter_templates(self): """Iterate over all templates in the repository (both file and @@ -154,8 +157,10 @@ def iter_file_templates(self): template = FileTemplate(template_dir) except (OSError, ValueError) as err: # Not a template directory - message = ('Found file_template directory {0!r} but it is not ' - 'a recognizable template. {1!s}') + message = ( + "Found file_template directory {0!r} but it is not " + "a recognizable template. {1!s}" + ) logging.warning(message.format(template_dir, err)) continue yield template @@ -177,8 +182,10 @@ def iter_project_templates(self): template = ProjectTemplate(template_dir) except (OSError, ValueError) as err: # Not a template directory - message = ('Found project_template directory {0!r} but it is ' - 'not a recognizable template. {1!s}') + message = ( + "Found project_template directory {0!r} but it is " + "not a recognizable template. {1!s}" + ) logging.warning(message.format(template_dir, err)) continue yield template @@ -201,12 +208,11 @@ def build(self): The result of the ``scons`` execution. See `subprocess.CompletedProcess` for details. """ - return subprocess.run(['scons'], shell=True, cwd=self.root) + return subprocess.run(["scons"], shell=True, cwd=self.root) @property def gitrepo(self): - """The template repository's Git repository (`git.Repo`). - """ + """The template repository's Git repository (`git.Repo`).""" if self._gitrepo is None: self._gitrepo = git.repo.base.Repo(path=self.root) return self._gitrepo @@ -224,7 +230,8 @@ def is_git_dirty(self): index=True, working_tree=True, untracked_files=True, - submodules=True) + submodules=True, + ) @property def untracked_files(self): @@ -270,7 +277,7 @@ def __init__(self, path): self._validate_template_dir() - with open(self.templatekit_yaml_path, 'r') as f: + with open(self.templatekit_yaml_path, "r") as f: config_data = yaml.safe_load(f) config = TemplateConfig(config_data) # Add default from cookiecutter.json @@ -281,45 +288,41 @@ def _validate_template_dir(self): repository, with a cookiecutter.json directory, etc. """ if not os.path.isdir(self.path): - message = 'File template directory {} not found.'.format(self.path) + message = "File template directory {} not found.".format(self.path) raise ValueError(message) if not os.path.isfile(self.cookiecutter_json_path): - message = 'cookiecutter.json not found in {}'.format(self.path) + message = "cookiecutter.json not found in {}".format(self.path) raise ValueError(message) if not os.path.isfile(self.templatekit_yaml_path): - message = 'templatekit.yaml not found in {}'.format(self.path) + message = "templatekit.yaml not found in {}".format(self.path) raise ValueError(message) def __str__(self): - return '{0!s}({1!r})'.format(self.__class__.__name__, self.name) + return "{0!s}({1!r})".format(self.__class__.__name__, self.name) def __repr__(self): - return '{0!s}({1!r})'.format(self.__class__.__name__, self.name) + return "{0!s}({1!r})".format(self.__class__.__name__, self.name) @property def name(self): - """Name of the template (`str`). - """ + """Name of the template (`str`).""" return os.path.split(self.path)[-1] @property def templatekit_yaml_path(self): - """Path of the templatekit.yaml file (`str`). - """ - return os.path.join(self.path, 'templatekit.yaml') + """Path of the templatekit.yaml file (`str`).""" + return os.path.join(self.path, "templatekit.yaml") @property def cookiecutter_json_path(self): - """Path of the cookiecutter.json file (`str`). - """ - return os.path.join(self.path, 'cookiecutter.json') + """Path of the cookiecutter.json file (`str`).""" + return os.path.join(self.path, "cookiecutter.json") @property def cookiecutter(self): - """The data from the ``cookiecutter.json`` file. - """ + """The data from the ``cookiecutter.json`` file.""" if self._cookiecutter_data is None: with open(self.cookiecutter_json_path) as f: self._cookiecutter_data = json.load(f) @@ -345,11 +348,10 @@ class FileTemplate(BaseTemplate): @property def source_path(self): - """Path to the template source file (a .jinja extension) (`str`). - """ + """Path to the template source file (a .jinja extension) (`str`).""" items = os.listdir(self.path) for item in items: - if os.path.splitext(item)[-1] == '.jinja': + if os.path.splitext(item)[-1] == ".jinja": return os.path.join(self.path, item) @@ -382,7 +384,7 @@ def get_config_validator(): validator : `cerberus.Validator` A Cerberus validator based on the ``configschema.yaml`` schema. """ - configpath = Path(__file__).parent / 'configschema.yaml' + configpath = Path(__file__).parent / "configschema.yaml" schema = yaml.safe_load(configpath.read_text()) validator = cerberus.Validator(schema, purge_unknown=True) return validator @@ -408,11 +410,11 @@ def __init__(self, data): self._validator = get_config_validator() if self._validator.validate(data) is False: - print('Validation errors:') + print("Validation errors:") print(json.dumps(self._validator.errors, sort_keys=True, indent=2)) - print('Data:') + print("Data:") print(json.dumps(data, sort_keys=True, indent=2)) - raise RuntimeError('Configuration syntax error') + raise RuntimeError("Configuration syntax error") # Apply Cereberus's schema-based normalization self.data = self._validator.normalized(self.data) @@ -443,36 +445,40 @@ def normalize(self, template): """ data = deepcopy(self.data) - if 'name' not in data: - data['name'] = template.name + if "name" not in data: + data["name"] = template.name - if 'group' not in data: - data['group'] = 'General' + if "group" not in data: + data["group"] = "General" - if 'dialog_fields' not in data: + if "dialog_fields" not in data: # Need to get the dialog fields from the cookiecutter.json file - data['dialog_fields'] = [] + data["dialog_fields"] = [] for key in template.cookiecutter: - if key.startswith('_'): + if key.startswith("_"): # skip things like "_extensions" continue elif isinstance(template.cookiecutter[key], str): - data['dialog_fields'].append({ - 'key': key, - 'label': self._truncate(key, 75), - 'component': 'text' - }) + data["dialog_fields"].append( + { + "key": key, + "label": self._truncate(key, 75), + "component": "text", + } + ) elif isinstance(template.cookiecutter[key], list): - data['dialog_fields'].append({ - 'key': key, - 'label': self._truncate(key, 75), - 'component': 'select' - }) - - for field in data['dialog_fields']: - if field['component'] == 'select': + data["dialog_fields"].append( + { + "key": key, + "label": self._truncate(key, 75), + "component": "select", + } + ) + + for field in data["dialog_fields"]: + if field["component"] == "select": self._normalize_select_field(field, template) - elif field['component'] == 'text': + elif field["component"] == "text": self._normalize_text_field(field, template) return TemplateConfig(data) @@ -483,20 +489,22 @@ def _normalize_select_field(self, field, template): - Add options that exist in the cookiecutter.json file if the options aren't explicitly set. """ - if 'preset_options' in field or 'preset_groups' in field: + if "preset_options" in field or "preset_groups" in field: # The schemas force these to be fully specified in templatekit.yaml return - elif 'options' not in field: + elif "options" not in field: # Add options from cookiecutter.json - field['options'] = [] - for option_value in template.cookiecutter[field['key']]: + field["options"] = [] + for option_value in template.cookiecutter[field["key"]]: # Enforce Slack length limit on the label option_label = self._truncate(option_value, 75) - field['options'].append({ - 'label': option_label, - 'value': option_label, # also needs truncation - 'template_value': option_value, - }) + field["options"].append( + { + "label": option_label, + "value": option_label, # also needs truncation + "template_value": option_value, + } + ) def _normalize_text_field(self, field, template): """Normalize text field components. @@ -504,12 +512,12 @@ def _normalize_text_field(self, field, template): - Add placeholder information found in the cookiecutter.json file if an explicit placeholder isn't set. """ - if 'placeholder' not in field or len(field['placeholder']) == 0: - field['placeholder'] = template.cookiecutter[field['key']] + if "placeholder" not in field or len(field["placeholder"]) == 0: + field["placeholder"] = template.cookiecutter[field["key"]] return field def _truncate(self, text, length): if isinstance(text, str) and len(text) > length: - return text[:length - 1] + "…" + return text[: length - 1] + "…" else: return text diff --git a/templatekit/scripts/check.py b/templatekit/scripts/check.py index ac9c677..49c24ad 100644 --- a/templatekit/scripts/check.py +++ b/templatekit/scripts/check.py @@ -2,14 +2,14 @@ operation. """ -__all__ = ('check',) +__all__ = ("check",) import sys import click -@click.command(short_help='Check the template repository') +@click.command(short_help="Check the template repository") @click.pass_obj def check(state): """Check the template repository for valid structure and operation. @@ -25,15 +25,15 @@ def check(state): - This command always recompiles the examples by running the scons command. """ - repo = state['repo'] - print('Testing template repository {0!s}'.format(repo.root)) + repo = state["repo"] + print("Testing template repository {0!s}".format(repo.root)) scons_result = repo.build() if scons_result.returncode > 0: message = ( '"scons" failed with status {0!d}\n\nThis means that the examples ' - 'could not be successfully generated because of an issue with the ' - 'Cookiecutter templates. Check the scons output, above, for ' - 'debugging hints.' + "could not be successfully generated because of an issue with the " + "Cookiecutter templates. Check the scons output, above, for " + "debugging hints." ) sys.exit(message.format(scons_result.returncode)) @@ -41,13 +41,17 @@ def check(state): error_count += _test_git_state(repo) if error_count == 1: - sys.exit('\n❌ The template repository checks failed with ' - '{0:d} error'.format(error_count)) + sys.exit( + "\n❌ The template repository checks failed with " + "{0:d} error".format(error_count) + ) elif error_count > 1: - sys.exit('\n❌ The template repository checks failed with ' - '{0:d} errors'.format(error_count)) + sys.exit( + "\n❌ The template repository checks failed with " + "{0:d} errors".format(error_count) + ) else: - print('✅ Passed!') + print("✅ Passed!") def _test_git_state(repo): @@ -67,9 +71,9 @@ def _test_untracked_files(repo): untracked_paths = repo.untracked_files error_count = 0 if len(untracked_paths) > 0: - print('\n🔴 Untracked files:') + print("\n🔴 Untracked files:") for p in untracked_paths: - print(' {}'.format(p)) + print(" {}".format(p)) error_count += 1 return error_count @@ -84,17 +88,17 @@ def _test_uncommitted_changes(repo): for change in diffindex.iter_change_type(changetype): # For deleted files, we want to use the original ("a") path. # Otherwise, we tend to want to show the user the new ("b") path - if changetype in ('D',): + if changetype in ("D",): uncommitted_changes.append( - '{0} {1}'.format(changetype, change.a_path) + "{0} {1}".format(changetype, change.a_path) ) else: uncommitted_changes.append( - '{0} {1}'.format(changetype, change.b_path) + "{0} {1}".format(changetype, change.b_path) ) error_count += 1 if error_count > 0: - print('\n🔴 Uncommitted changes:') + print("\n🔴 Uncommitted changes:") for change in uncommitted_changes: print(change) return error_count diff --git a/templatekit/scripts/listtemplates.py b/templatekit/scripts/listtemplates.py index a4972ef..c4e49bb 100644 --- a/templatekit/scripts/listtemplates.py +++ b/templatekit/scripts/listtemplates.py @@ -1,30 +1,32 @@ """Subcommand for listing available file or project templates. """ -__all__ = ('list_templates',) +__all__ = ("list_templates",) import click @click.command() @click.option( - '-t', '--type', 'template_type', - type=click.Choice(['all', 'file', 'project']), - default='all', - help='The type of templates to show. File templates are single files or ' - 'snippets. Project templates create whole project directories.') + "-t", + "--type", + "template_type", + type=click.Choice(["all", "file", "project"]), + default="all", + help="The type of templates to show. File templates are single files or " + "snippets. Project templates create whole project directories.", +) @click.pass_obj def list_templates(state, template_type): - """List available templates in the repository. - """ - repo = state['repo'] + """List available templates in the repository.""" + repo = state["repo"] - if template_type in ('all', 'file'): - click.echo(click.style('File templates:', bold=True)) + if template_type in ("all", "file"): + click.echo(click.style("File templates:", bold=True)) for template in repo.iter_file_templates(): - click.echo(' {}'.format(template.name)) + click.echo(" {}".format(template.name)) - if template_type in ('all', 'project'): - click.echo(click.style('Project templates:', bold=True)) + if template_type in ("all", "project"): + click.echo(click.style("Project templates:", bold=True)) for template in repo.iter_project_templates(): - click.echo(' {}'.format(template.name)) + click.echo(" {}".format(template.name)) diff --git a/templatekit/scripts/main.py b/templatekit/scripts/main.py index 5eceb7e..a939400 100644 --- a/templatekit/scripts/main.py +++ b/templatekit/scripts/main.py @@ -1,29 +1,32 @@ """Main command-line interface for templatekit. """ -__all__ = ('main',) +__all__ = ("main",) import click from ..repo import Repo +from .check import check from .listtemplates import list_templates from .make import make -from .check import check - # Add -h as a help shortcut option -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @click.group(context_settings=CONTEXT_SETTINGS) @click.option( - '-r', '--template-repo', 'template_repo', - type=click.Path(exists=True, file_okay=False, dir_okay=True, - resolve_path=True), - default='.', - help='Path to the cloned templates Git repository, or a sub directory ' - 'within the clone templates repository. Default is \'.\', the ' - 'current working directory.') + "-r", + "--template-repo", + "template_repo", + type=click.Path( + exists=True, file_okay=False, dir_okay=True, resolve_path=True + ), + default=".", + help="Path to the cloned templates Git repository, or a sub directory " + "within the clone templates repository. Default is '.', the " + "current working directory.", +) @click.pass_context def main(ctx, template_repo): """templatekit is a CLI for lsst/templates, LSST's project template @@ -35,18 +38,18 @@ def main(ctx, template_repo): # Subcommands should use the click.pass_obj decorator to get this # ctx.obj object as the first argument. Subcommands shouldn't create their # own Repo instance. - ctx.obj = {'repo': Repo.discover_repo(dirname=template_repo)} + ctx.obj = {"repo": Repo.discover_repo(dirname=template_repo)} # The help command implementation is taken from # https://www.burgundywall.com/post/having-click-help-subcommand + @main.command() -@click.argument('topic', default=None, required=False, nargs=1) +@click.argument("topic", default=None, required=False, nargs=1) @click.pass_context def help(ctx, topic, **kw): - """Show help for any command. - """ + """Show help for any command.""" if topic is None: click.echo(ctx.parent.get_help()) else: @@ -54,6 +57,6 @@ def help(ctx, topic, **kw): # Add subcommands from other modules -main.add_command(list_templates, name='list') +main.add_command(list_templates, name="list") main.add_command(make) main.add_command(check) diff --git a/templatekit/scripts/make.py b/templatekit/scripts/make.py index 8d26e54..aed0c76 100644 --- a/templatekit/scripts/make.py +++ b/templatekit/scripts/make.py @@ -1,26 +1,35 @@ """Subcommand for making something from a template. """ -__all__ = ('make',) +__all__ = ("make",) import os + import click -from cookiecutter.main import cookiecutter import pyperclip +from cookiecutter.main import cookiecutter from ..filerender import render_file_template from ..repo import FileTemplate -@click.command(short_help='Make a file or project from a template.') -@click.argument('name', metavar='