DEPRECATED -- Rewritten and moved to https://github.com/NiklasRosenstein/shut/. 🌊 Shore is a distribution and release management tool for pure Python packages.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

147 lines
5.6 KiB

  1. # -*- coding: utf8 -*-
  2. # Copyright (c) 2020 Niklas Rosenstein
  3. #
  4. # Permission is hereby granted, free of charge, to any person obtaining a copy
  5. # of this software and associated documentation files (the "Software"), to
  6. # deal in the Software without restriction, including without limitation the
  7. # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. # sell copies of the Software, and to permit persons to whom the Software is
  9. # furnished to do so, subject to the following conditions:
  10. #
  11. # The above copyright notice and this permission notice shall be included in
  12. # all copies or substantial portions of the Software.
  13. #
  14. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. # IN THE SOFTWARE.
  21. from . import pkg
  22. from shore.core.plugins import FileToRender, write_to_disk
  23. from shore.util.resources import walk_package_resources
  24. from termcolor import colored
  25. from typing import Iterable, Optional
  26. import click
  27. import jinja2
  28. import os
  29. import pkg_resources
  30. import subprocess
  31. def load_author_from_git() -> Optional[str]:
  32. """
  33. Returns a string formatted as "name <mail>" from the Git `user.name` and `user.email`
  34. configuration values. Returns `None` if Git is not configured.
  35. """
  36. try:
  37. name = subprocess.getoutput('git config user.name')
  38. email = subprocess.getoutput('git config user.email')
  39. except FileNotFoundError:
  40. return None
  41. if not name and not email:
  42. return None
  43. return '{} <{}>'.format(name, email)
  44. @pkg.command()
  45. @click.argument('target_directory', required=False)
  46. @click.option('--project-name', metavar='name', required=True, help='The name of the project as it would appear on PyPI.')
  47. @click.option('--module-name', metavar='fqn', help='The name of the main Python module (this may be a dotted module name). Defaults to the package name (hyphens replaced with underscores).')
  48. @click.option('--author', metavar='"name <mail>"', help='The name of the author to write into the package configuration file. Defaults to the name and email from the Git config.')
  49. @click.option('--version', metavar='x.y.z', help='The version number to start counting from. Defaults to "0.0.0" (stands for "unreleased").')
  50. @click.option('--license', metavar='name', help='The name of the license to use for the project. A LICENSE.txt file will be created.')
  51. @click.option('--dry', is_flag=True, help='Do not write files to disk.')
  52. @click.option('-f', '--force', is_flag=True, help='Overwrite files if they already exist.')
  53. def bootstrap(
  54. target_directory,
  55. project_name,
  56. module_name,
  57. author,
  58. version,
  59. license,
  60. dry,
  61. force,
  62. ):
  63. """
  64. Create files for a new Python package. If the *target_directory* is specified, the files will
  65. be written to that directory. Otherwise the value of the --project-name argument will be used
  66. as the target directory.
  67. """
  68. if not target_directory:
  69. target_directory = project_name
  70. if not author:
  71. author = load_author_from_git()
  72. env_vars = {
  73. 'name': project_name,
  74. 'version': version,
  75. 'author': author,
  76. 'license': license,
  77. 'modulename': module_name,
  78. 'name_on_disk': module_name or project_name,
  79. }
  80. name_on_disk = module_name or project_name
  81. def _render_template(template_string, **kwargs):
  82. assert isinstance(template_string, str), type(template_string)
  83. return jinja2.Template(template_string).render(**(kwargs or env_vars))
  84. def _render_file(fp, filename):
  85. content = pkg_resources.resource_string('shore', filename).decode()
  86. fp.write(_render_template(content))
  87. def _render_namespace_file(fp):
  88. fp.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)\n")
  89. def _get_template_files(template_path) -> Iterable[FileToRender]:
  90. # Render the template files to the target directory.
  91. for source_filename in walk_package_resources('shore', template_path):
  92. # Expand variables in the filename.
  93. name = name_on_disk.replace('-', '_').replace('.', '/')
  94. filename = _render_template(source_filename, name=name)
  95. dest = os.path.join(target_directory, filename)
  96. yield FileToRender(
  97. None,
  98. os.path.normpath(dest),
  99. lambda _, fp: _render_file(fp, template_path + '/' + source_filename))
  100. def _get_package_files() -> Iterable[FileToRender]:
  101. yield from _get_template_files('templates/package')
  102. # Render namespace supporting files.
  103. parts = []
  104. for item in name_on_disk.replace('-', '_').split('.')[:-1]:
  105. parts.append(item)
  106. dest = os.path.join(target_directory, 'src', *parts, '__init__.py')
  107. yield FileToRender(
  108. None,
  109. os.path.normpath(dest),
  110. lambda _, fp: _render_namespace_file(fp))
  111. dest = os.path.join(target_directory, 'src', 'test', *parts, '__init__.py')
  112. yield FileToRender(
  113. None,
  114. os.path.normpath(dest),
  115. lambda _, fp: fp.write('pass\n'))
  116. # TODO (@NiklasRosenstein): Render the license file if it does not exist.
  117. def _get_monorepo_files() -> Iterable[FileToRender]:
  118. yield from _get_template_files('templates/monorepo')
  119. #if args['monorepo']:
  120. # files = _get_monorepo_files()
  121. files = _get_package_files()
  122. for file in files:
  123. if os.path.isfile(file.name) and not force:
  124. print(colored('Skip ' + file.name, 'yellow'))
  125. continue
  126. print(colored('Write ' + file.name, 'cyan'))
  127. if not dry:
  128. write_to_disk(file)