Repository: conan-io/conan
Pull request: templates for conan new
Changelog: Feature: New ``conan new --template=mytemplate`` to initialize recipes with your own templates
Docs: https://github.com/conan-io/docs/pull/1286


This has been improved with Jinja templates in https://github.com/conan-io/conan/pull/5267

Close #5192

Problem statement:
conan new template recipes
A use case where a team is creating many packages for their own libraries, and the recipes are very similar, it is useful to have the possibility to use a pre-defined template for the ``conan new`` command.

Hints from issue discussion:

Commit message: templates for conan new

Patch to review:
diff --git a/conans/client/cmd/new.py b/conans/client/cmd/new.py
index cc7bafc9d7b..70c73f1513b 100644
--- a/conans/client/cmd/new.py
+++ b/conans/client/cmd/new.py
@@ -1,8 +1,11 @@
+import os
 import re
 
 from conans.client.cmd.new_ci import ci_get_files
 from conans.errors import ConanException
 from conans.model.ref import ConanFileReference
+from conans.util.files import load
+
 
 conanfile = """from conans import ConanFile, CMake, tools
 
@@ -233,9 +236,11 @@ def test(self):
 
 
 def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False, bare=False,
-            visual_versions=None, linux_gcc_versions=None, linux_clang_versions=None, osx_clang_versions=None,
-            shared=None, upload_url=None, gitignore=None, gitlab_gcc_versions=None, gitlab_clang_versions=None,
-            circleci_gcc_versions=None, circleci_clang_versions=None, circleci_osx_versions=None):
+            visual_versions=None, linux_gcc_versions=None, linux_clang_versions=None,
+            osx_clang_versions=None, shared=None, upload_url=None, gitignore=None,
+            gitlab_gcc_versions=None, gitlab_clang_versions=None,
+            circleci_gcc_versions=None, circleci_clang_versions=None, circleci_osx_versions=None,
+            template=None, cache=None):
     try:
         tokens = ref.split("@")
         name, version = tokens[0].split("/")
@@ -259,6 +264,8 @@ def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False,
         raise ConanException("'pure_c' is incompatible with 'header' and 'sources'")
     if bare and (header or exports_sources):
         raise ConanException("'bare' is incompatible with 'header' and 'sources'")
+    if template and (header or exports_sources or bare):
+        raise ConanException("'template' argument incompatible with 'header', 'sources', and 'bare'")
 
     if header:
         files = {"conanfile.py": conanfile_header.format(name=name, version=version,
@@ -272,6 +279,13 @@ def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False,
     elif bare:
         files = {"conanfile.py": conanfile_bare.format(name=name, version=version,
                                                        package_name=package_name)}
+    elif template:
+        path_template = os.path.join(cache.conan_folder, template)
+        if not os.path.isfile(path_template):
+            raise ConanException("Template doesn't exist: %s" % path_template)
+        conanfile_template = load(path_template)
+        files = {"conanfile.py": conanfile_template.format(name=name, version=version,
+                                                           package_name=package_name)}
     else:
         files = {"conanfile.py": conanfile.format(name=name, version=version,
                                                   package_name=package_name)}
diff --git a/conans/client/command.py b/conans/client/command.py
index 2d0670c7e83..802bb40224c 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -143,6 +143,8 @@ def new(self, *args):
         parser.add_argument("-b", "--bare", action='store_true', default=False,
                             help='Create the minimum package recipe, without build() method. '
                             'Useful in combination with "export-pkg" command')
+        parser.add_argument("-m", "--template",
+                            help='Use the given template from the local cache for conanfile.py')
         parser.add_argument("-cis", "--ci-shared", action='store_true',
                             default=False,
                             help='Package will have a "shared" option to be used in CI')
@@ -192,7 +194,8 @@ def new(self, *args):
                         gitlab_clang_versions=args.ci_gitlab_clang,
                         circleci_gcc_versions=args.ci_circleci_gcc,
                         circleci_clang_versions=args.ci_circleci_clang,
-                        circleci_osx_versions=args.ci_circleci_osx)
+                        circleci_osx_versions=args.ci_circleci_osx,
+                        template=args.template)
 
     def inspect(self, *args):
         """Displays conanfile attributes, like name, version, options
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index f431515a242..d7d34f69a98 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -258,7 +258,8 @@ def new(self, name, header=False, pure_c=False, test=False, exports_sources=Fals
             cwd=None, visual_versions=None, linux_gcc_versions=None, linux_clang_versions=None,
             osx_clang_versions=None, shared=None, upload_url=None, gitignore=None,
             gitlab_gcc_versions=None, gitlab_clang_versions=None,
-            circleci_gcc_versions=None, circleci_clang_versions=None, circleci_osx_versions=None):
+            circleci_gcc_versions=None, circleci_clang_versions=None, circleci_osx_versions=None,
+            template=None):
         from conans.client.cmd.new import cmd_new
         cwd = os.path.abspath(cwd or get_cwd())
         files = cmd_new(name, header=header, pure_c=pure_c, test=test,
@@ -272,7 +273,8 @@ def new(self, name, header=False, pure_c=False, test=False, exports_sources=Fals
                         gitlab_clang_versions=gitlab_clang_versions,
                         circleci_gcc_versions=circleci_gcc_versions,
                         circleci_clang_versions=circleci_clang_versions,
-                        circleci_osx_versions=circleci_osx_versions)
+                        circleci_osx_versions=circleci_osx_versions,
+                        template=template, cache=self._cache)
 
         save_files(cwd, files)
         for f in sorted(files):
diff --git a/conans/test/functional/command/new_test.py b/conans/test/functional/command/new_test.py
index 95c65a368f1..e29a32bc528 100644
--- a/conans/test/functional/command/new_test.py
+++ b/conans/test/functional/command/new_test.py
@@ -3,10 +3,47 @@
 
 from conans.test.utils.tools import TestClient
 from conans.util.files import load
+from conans.tools import save
+import textwrap
 
 
 class NewTest(unittest.TestCase):
 
+    def template_test(self):
+        client = TestClient()
+        template1 = textwrap.dedent("""
+            class {package_name}Conan(ConanFile):
+                name = "{name}"
+                version = "{version}"
+        """)
+        save(os.path.join(client.base_folder, ".conan", "mytemplate.py"), template1)
+        client.run("new hello/0.1 --template=mytemplate.py")
+        conanfile = load(os.path.join(client.current_folder, "conanfile.py"))
+        self.assertIn("class HelloConan(ConanFile):", conanfile)
+        self.assertIn('name = "hello"', conanfile)
+        self.assertIn('version = "0.1"', conanfile)
+
+    def template_test_package_test(self):
+        client = TestClient()
+        template2 = textwrap.dedent("""
+            class {package_name}Conan(ConanFile):
+                version = "fixed"
+        """)
+        save(os.path.join(client.base_folder, ".conan", "subfolder", "mytemplate.py"), template2)
+        client.run("new hello/0.1 --template=subfolder/mytemplate.py")
+        conanfile = load(os.path.join(client.current_folder, "conanfile.py"))
+        self.assertIn("class HelloConan(ConanFile):", conanfile)
+        self.assertIn('version = "fixed"', conanfile)
+
+    def template_errors_test(self):
+        client = TestClient()
+        client.run("new hello/0.1 --template=mytemplate.py", assert_error=True)
+        self.assertIn("ERROR: Template doesn't exist", client.out)
+        client.run("new hello/0.1 --template=mytemplate.py --bare", assert_error=True)
+        self.assertIn("ERROR: 'template' argument incompatible", client.out)
+        client.run("new hello/0.1 --template", assert_error=True)
+        self.assertIn("ERROR: Exiting with code: 2", client.out)
+
     def new_test(self):
         client = TestClient()
         client.run('new MyPackage/1.3@myuser/testing -t')


Code review comments:
diff_hunk: @@ -143,6 +143,8 @@ def new(self, *args):
         parser.add_argument("-b", "--bare", action='store_true', default=False,
                             help='Create the minimum package recipe, without build() method. '
                             'Useful in combination with "export-pkg" command')
+        parser.add_argument("-m", "--template",
path: conans/client/command.py
text: @user1:
`-m`? `-f` meaning file maybe?