Repository: conan-io/conan

Issue:
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.



Discussion:

Patch:
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
index 250bee4ab..9a8df9a5e 100644
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -208,11 +208,16 @@ def _replace_scm_data_in_conanfile(conanfile_path, scm_data):
     tree = ast.parse(content)
     to_replace = []
     comments = []
+    class_line = None
+    tab_size = 4
     for i_body, item in enumerate(tree.body):
         if isinstance(item, ast.ClassDef):
             statements = item.body
+            class_line = item.lineno
             for i, stmt in enumerate(item.body):
                 if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1:
+                    line = lines[stmt.lineno - 1]
+                    tab_size = len(line) - len(line.lstrip())
                     if isinstance(stmt.targets[0], ast.Name) and stmt.targets[0].id == "scm":
                         try:
                             if i + 1 == len(statements):  # Last statement in my ClassDef
@@ -233,14 +238,24 @@ def _replace_scm_data_in_conanfile(conanfile_path, scm_data):
                         comments = [line.strip('\n') for line in replace
                                     if line.strip().startswith("#") or not line.strip()]
                         break
-    if len(to_replace) != 1:
+
+    if len(to_replace) > 1:
         raise ConanException("The conanfile.py defines more than one class level 'scm' attribute")
 
     new_text = "scm = " + ",\n          ".join(str(scm_data).split(",")) + "\n"
-    if comments:
-        new_text += '\n'.join(comments) + "\n"
-    content = content.replace(to_replace[0], new_text)
-    content = content if not headers else ''.join(headers) + content
+
+    if len(to_replace) == 0:
+        # SCM exists, but not found in the conanfile, probably inherited from superclass
+        # FIXME: This will inject the lines only the latest class declared in the conanfile
+        tmp = lines[0:class_line]
+        tmp.append("{}{}".format(" " * tab_size, new_text))
+        tmp.extend(lines[class_line:])
+        content = ''.join(tmp)
+    else:
+        if comments:
+            new_text += '\n'.join(comments) + "\n"
+        content = content.replace(to_replace[0], new_text)
+        content = content if not headers else ''.join(headers) + content
 
     remove(conanfile_path)
     save(conanfile_path, content)
diff --git a/conans/client/cmd/new.py b/conans/client/cmd/new.py
index cc7bafc9d..70c73f151 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 @@ test_package/build
 
 
 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 2d0670c7e..37505cb36 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -72,6 +72,14 @@ class OnceArgument(argparse.Action):
         setattr(namespace, self.dest, values)
 
 
+class SmartFormatter(argparse.HelpFormatter):
+
+    def _fill_text(self, text, width, indent):
+        import textwrap
+        text = textwrap.dedent(text)
+        return ''.join(indent + line for line in text.splitlines(True))
+
+
 _QUERY_EXAMPLE = ("os=Windows AND (arch=x86 OR compiler=gcc)")
 _PATTERN_EXAMPLE = ("boost/*")
 _REFERENCE_EXAMPLE = ("MyPackage/1.2@user/channel")
@@ -105,9 +113,12 @@ class Command(object):
         self._outputer = outputer
 
     def help(self, *args):
-        """Show help of a specific command.
         """
-        parser = argparse.ArgumentParser(description=self.help.__doc__, prog="conan help")
+        Shows help for a specific command.
+        """
+        parser = argparse.ArgumentParser(description=self.help.__doc__,
+                                         prog="conan help",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("command", help='command', nargs="?")
         args = parser.parse_args(*args)
         if not args.command:
@@ -122,10 +133,13 @@ class Command(object):
             raise ConanException("Unknown command '%s'" % args.command)
 
     def new(self, *args):
-        """Creates a new package recipe template with a 'conanfile.py' and optionally,
+        """
+        Creates a new package recipe template with a 'conanfile.py' and optionally,
         'test_package' testing files.
         """
-        parser = argparse.ArgumentParser(description=self.new.__doc__, prog="conan new")
+        parser = argparse.ArgumentParser(description=self.new.__doc__,
+                                         prog="conan new",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("name", help='Package name, e.g.: "Poco/1.7.3" or complete reference'
                                          ' for CI scripts: "Poco/1.7.3@conan/stable"')
         parser.add_argument("-t", "--test", action='store_true', default=False,
@@ -143,6 +157,8 @@ class Command(object):
         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("-f", "--file",
+                            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,13 +208,17 @@ class Command(object):
                         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.file)
 
     def inspect(self, *args):
-        """Displays conanfile attributes, like name, version, options
-        Works both locally, in local cache and remote
         """
-        parser = argparse.ArgumentParser(description=self.inspect.__doc__, prog="conan inspect")
+        Displays conanfile attributes, like name, version and options. Works locally,
+        in local cache and remote.
+        """
+        parser = argparse.ArgumentParser(description=self.inspect.__doc__,
+                                         prog="conan inspect",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path_or_reference", help="Path to a folder containing a recipe"
                             " (conanfile.py) or to a recipe file. e.g., "
                             "./my_project/conanfile.py. It could also be a reference")
@@ -232,7 +252,8 @@ class Command(object):
             save(json_output_file, json_output)
 
     def test(self, *args):
-        """Test a package consuming it from a conanfile.py with a test() method.
+        """
+        Tests a package consuming it from a conanfile.py with a test() method.
 
         This command installs the conanfile dependencies (including the tested
         package), calls a 'conan build' to build test apps and finally executes
@@ -240,7 +261,9 @@ class Command(object):
         neither definition of package() or package_info() methods. The package
         to be tested must exist in the local cache or in any configured remote.
         """
-        parser = argparse.ArgumentParser(description=self.test.__doc__, prog="conan test")
+        parser = argparse.ArgumentParser(description=self.test.__doc__,
+                                         prog="conan test",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help='Path to the "testing" folder containing a conanfile.py or'
                             ' to a recipe file with test() method'
                             'e.g. conan test_package/conanfile.py pkg/version@user/channel')
@@ -257,7 +280,8 @@ class Command(object):
                                 build_modes=args.build, test_build_folder=args.test_build_folder)
 
     def create(self, *args):
-        """Builds a binary package for a recipe (conanfile.py).
+        """
+        Builds a binary package for a recipe (conanfile.py).
 
         Uses the specified configuration in a profile or in -s settings, -o
         options etc. If a 'test_package' folder (the name can be configured
@@ -265,7 +289,9 @@ class Command(object):
         that the package has been created correctly. Check 'conan test' command
         to know more about 'test_folder' project.
         """
-        parser = argparse.ArgumentParser(description=self.create.__doc__, prog="conan create")
+        parser = argparse.ArgumentParser(description=self.create.__doc__,
+                                         prog="conan create",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help=_PATH_HELP)
         parser.add_argument("reference",
                             help='user/channel, version@user/channel or pkg/version@user/channel '
@@ -316,7 +342,8 @@ class Command(object):
                 self._outputer.json_output(info, args.json, cwd)
 
     def download(self, *args):
-        """Downloads recipe and binaries to the local cache, without using settings.
+        """
+        Downloads recipe and binaries to the local cache, without using settings.
 
         It works specifying the recipe reference and package ID to be
         installed. Not transitive, requirements of the specified reference will
@@ -326,7 +353,9 @@ class Command(object):
         If no remote is specified, it will use the default remote.
         """
 
-        parser = argparse.ArgumentParser(description=self.download.__doc__, prog="conan download")
+        parser = argparse.ArgumentParser(description=self.download.__doc__,
+                                         prog="conan download",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("reference",
                             help='pkg/version@user/channel')
         parser.add_argument("-p", "--package", nargs=1, action=Extender,
@@ -343,7 +372,8 @@ class Command(object):
                                     remote_name=args.remote, recipe=args.recipe)
 
     def install(self, *args):
-        """Installs the requirements specified in a recipe (conanfile.py or conanfile.txt).
+        """
+        Installs the requirements specified in a recipe (conanfile.py or conanfile.txt).
 
         It can also be used to install a concrete package specifying a
         reference. If any requirement is not found in the local cache, it will
@@ -355,7 +385,9 @@ class Command(object):
         the package is installed, Conan will write the files for the specified
         generators.
         """
-        parser = argparse.ArgumentParser(description=self.install.__doc__, prog="conan install")
+        parser = argparse.ArgumentParser(description=self.install.__doc__,
+                                         prog="conan install",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path_or_reference", help="Path to a folder containing a recipe"
                             " (conanfile.py or conanfile.txt) or to a recipe file. e.g., "
                             "./my_project/conanfile.txt. It could also be a reference")
@@ -423,11 +455,14 @@ class Command(object):
                 self._outputer.json_output(info, args.json, cwd)
 
     def config(self, *args):
-        """Manages Conan configuration.
+        """
+        Manages Conan configuration.
 
         Used to edit conan.conf, or install config files.
         """
-        parser = argparse.ArgumentParser(description=self.config.__doc__, prog="conan config")
+        parser = argparse.ArgumentParser(description=self.config.__doc__,
+                                         prog="conan config",
+                                         formatter_class=SmartFormatter)
 
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
         subparsers.required = True
@@ -478,7 +513,8 @@ class Command(object):
                                               target_folder=args.target_folder)
 
     def info(self, *args):
-        """Gets information about the dependency graph of a recipe.
+        """
+        Gets information about the dependency graph of a recipe.
 
         It can be used with a recipe or a reference for any existing package in
         your local cache.
@@ -490,7 +526,9 @@ class Command(object):
         str_path_only_options = ", ".join(['"%s"' % field for field in path_only_options])
         str_only_options = ", ".join(['"%s"' % field for field in info_only_options])
 
-        parser = argparse.ArgumentParser(description=self.info.__doc__, prog="conan info")
+        parser = argparse.ArgumentParser(description=self.info.__doc__,
+                                         prog="conan info",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path_or_reference", help="Path to a folder containing a recipe"
                             " (conanfile.py or conanfile.txt) or to a recipe file. e.g., "
                             "./my_project/conanfile.txt. It could also be a reference")
@@ -598,11 +636,14 @@ class Command(object):
                 self._outputer.info(deps_graph, only, args.package_filter, args.paths)
 
     def source(self, *args):
-        """ Calls your local conanfile.py 'source()' method.
+        """
+        Calls your local conanfile.py 'source()' method.
 
         Usually downloads and uncompresses the package sources.
         """
-        parser = argparse.ArgumentParser(description=self.source.__doc__, prog="conan source")
+        parser = argparse.ArgumentParser(description=self.source.__doc__,
+                                         prog="conan source",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help=_PATH_HELP)
         parser.add_argument("-sf", "--source-folder", action=OnceArgument,
                             help='Destination directory. Defaulted to current directory')
@@ -629,7 +670,8 @@ class Command(object):
         return self._conan.source(args.path, args.source_folder, args.install_folder)
 
     def build(self, *args):
-        """Calls your local conanfile.py 'build()' method.
+        """
+        Calls your local conanfile.py 'build()' method.
 
         The recipe will be built in the local directory specified by
         --build-folder, reading the sources from --source-folder. If you are
@@ -637,7 +679,9 @@ class Command(object):
         configured as destination folder for the install step.
         """
 
-        parser = argparse.ArgumentParser(description=self.build.__doc__, prog="conan build")
+        parser = argparse.ArgumentParser(description=self.build.__doc__,
+                                         prog="conan build",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help=_PATH_HELP)
         parser.add_argument("-b", "--build", default=None, action="store_true",
                             help="Execute the build step (variable should_build=True). When "
@@ -684,7 +728,8 @@ class Command(object):
                                  should_test=test)
 
     def package(self, *args):
-        """ Calls your local conanfile.py 'package()' method.
+        """
+        Calls your local conanfile.py 'package()' method.
 
         This command works in the user space and it will copy artifacts from
         the --build-folder and --source-folder folder to the --package-folder
@@ -692,7 +737,9 @@ class Command(object):
         do it, use 'conan create' or 'conan export-pkg' after a 'conan build'
         command.
         """
-        parser = argparse.ArgumentParser(description=self.package.__doc__, prog="conan package")
+        parser = argparse.ArgumentParser(description=self.package.__doc__,
+                                         prog="conan package",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help=_PATH_HELP)
         parser.add_argument("-bf", "--build-folder", action=OnceArgument, help=_BUILD_FOLDER_HELP)
         parser.add_argument("-if", "--install-folder", action=OnceArgument,
@@ -726,13 +773,16 @@ class Command(object):
                                    install_folder=args.install_folder)
 
     def imports(self, *args):
-        """ Calls your local conanfile.py or conanfile.txt 'imports' method.
+        """
+        Calls your local conanfile.py or conanfile.txt 'imports' method.
 
         It requires to have been previously installed and have a
         conanbuildinfo.txt generated file in the --install-folder (defaulted to
         current directory).
         """
-        parser = argparse.ArgumentParser(description=self.imports.__doc__, prog="conan imports")
+        parser = argparse.ArgumentParser(description=self.imports.__doc__,
+                                         prog="conan imports",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path",
                             help=_PATH_HELP + " With --undo option, this parameter is the folder "
                             "containing the conan_imports_manifest.txt file generated in a previous "
@@ -760,14 +810,16 @@ class Command(object):
 
     def export_pkg(self, *args):
         """
-        Exports a recipe and create a binary package from local files.
+        Exports a recipe, then creates a package from local source and build folders.
 
         If '--package-folder' is provided it will copy the files from there, otherwise it
         will execute package() method over '--source-folder' and '--build-folder' to create
         the binary package.
         """
+
         parser = argparse.ArgumentParser(description=self.export_pkg.__doc__,
-                                         prog="conan export-pkg")
+                                         prog="conan export-pkg",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help=_PATH_HELP)
         parser.add_argument("reference", help="user/channel or pkg/version@user/channel "
                                               "(if name and version are not declared in the "
@@ -826,13 +878,16 @@ class Command(object):
                 self._outputer.json_output(info, args.json, cwd)
 
     def export(self, *args):
-        """Copies the recipe (conanfile.py & associated files) to your local cache.
+        """
+        Copies the recipe (conanfile.py & associated files) to your local cache.
 
         Use the 'reference' param to specify a user and channel where to export
         it. Once the recipe is in the local cache it can be shared, reused and
         to any remote with the 'conan upload' command.
         """
-        parser = argparse.ArgumentParser(description=self.export.__doc__, prog="conan export")
+        parser = argparse.ArgumentParser(description=self.export.__doc__,
+                                         prog="conan export",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("path", help=_PATH_HELP)
         parser.add_argument("reference", help="user/channel, or Pkg/version@user/channel (if name "
                                               "and version are not declared in the conanfile.py")
@@ -847,13 +902,16 @@ class Command(object):
                                   keep_source=args.keep_source)
 
     def remove(self, *args):
-        """Removes packages or binaries matching pattern from local cache or remote.
+        """
+        Removes packages or binaries matching pattern from local cache or remote.
 
         It can also be used to remove temporary source or build folders in the
         local conan cache. If no remote is specified, the removal will be done
         by default in the local conan cache.
         """
-        parser = argparse.ArgumentParser(description=self.remove.__doc__, prog="conan remove")
+        parser = argparse.ArgumentParser(description=self.remove.__doc__,
+                                         prog="conan remove",
+                                         formatter_class=SmartFormatter)
         parser.add_argument('pattern_or_reference', nargs="?", help=_PATTERN_OR_REFERENCE_HELP)
         parser.add_argument('-b', '--builds', nargs="*", action=Extender,
                             help=("By default, remove all the build folders or select one, "
@@ -917,12 +975,15 @@ class Command(object):
                                   force=args.force, remote_name=args.remote, outdated=args.outdated)
 
     def copy(self, *args):
-        """Copies conan recipes and packages to another user/channel.
+        """
+        Copies conan recipes and packages to another user/channel.
 
         Useful to promote packages (e.g. from "beta" to "stable") or transfer
         them from one user to another.
         """
-        parser = argparse.ArgumentParser(description=self.copy.__doc__, prog="conan copy")
+        parser = argparse.ArgumentParser(description=self.copy.__doc__,
+                                         prog="conan copy",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("reference", default="",
                             help='package reference. e.g., MyPackage/1.2@user/channel')
         parser.add_argument("user_channel", default="",
@@ -944,7 +1005,8 @@ class Command(object):
                                 force=args.force, packages=args.package or args.all)
 
     def user(self, *args):
-        """Authenticates against a remote with user/pass, caching the auth token.
+        """
+        Authenticates against a remote with user/pass, caching the auth token.
 
         Useful to avoid the user and password being requested later. e.g. while
         you're uploading a package.  You can have one user for each remote.
@@ -956,7 +1018,9 @@ class Command(object):
         # - conan user list ('remote') -> list users (of a remote)
         # - conan user auth 'remote' ('user') ('password') -> login a remote (w/o user or pass)
         # - conan user set 'user' 'remote' -> set user for a remote (not login) necessary??
-        parser = argparse.ArgumentParser(description=self.user.__doc__, prog="conan user")
+        parser = argparse.ArgumentParser(description=self.user.__doc__,
+                                         prog="conan user",
+                                         formatter_class=SmartFormatter)
         parser.add_argument("name", nargs='?', default=None,
                             help='Username you want to use. If no name is provided it will show the'
                             ' current user')
@@ -1009,7 +1073,8 @@ class Command(object):
                 self._outputer.json_output(info, args.json, cwd)
 
     def search(self, *args):
-        """Searches package recipes and binaries in the local cache or in a remote.
+        """
+        Searches package recipes and binaries in the local cache or in a remote.
 
         If you provide a pattern, then it will search for existing package
         recipes matching it.  If a full reference is provided
@@ -1019,7 +1084,9 @@ class Command(object):
         has to be used. For case insensitive file systems, like Windows, case
         sensitive search can be forced with '--case-sensitive'.
         """
-        parser = argparse.ArgumentParser(description=self.search.__doc__, prog="conan search")
+        parser = argparse.ArgumentParser(description=self.search.__doc__,
+                                         prog="conan search",
+                                         formatter_class=SmartFormatter)
         parser.add_argument('pattern_or_reference', nargs='?', help=_PATTERN_OR_REFERENCE_HELP)
         parser.add_argument('-o', '--outdated', default=False, action='store_true',
                             help="Show only outdated from recipe packages. "
@@ -1123,13 +1190,15 @@ class Command(object):
                 self._outputer.json_output(info, args.json, cwd)
 
     def upload(self, *args):
-        """Uploads a recipe and binary packages to a remote.
+        """
+        Uploads a recipe and binary packages to a remote.
 
         If no remote is specified, the first configured remote (by default conan-center, use
         'conan remote list' to list the remotes) will be used.
         """
         parser = argparse.ArgumentParser(description=self.upload.__doc__,
-                                         prog="conan upload")
+                                         prog="conan upload",
+                                         formatter_class=SmartFormatter)
         parser.add_argument('pattern_or_reference', help=_PATTERN_OR_REFERENCE_HELP)
         parser.add_argument("-p", "--package", default=None, action=OnceArgument,
                             help='package ID to upload')
@@ -1201,9 +1270,12 @@ class Command(object):
                 self._outputer.json_output(info, args.json, cwd)
 
     def remote(self, *args):
-        """Manages the remote list and the package recipes associated to a remote.
         """
-        parser = argparse.ArgumentParser(description=self.remote.__doc__, prog="conan remote")
+        Manages the remote list and the package recipes associated to a remote.
+        """
+        parser = argparse.ArgumentParser(description=self.remote.__doc__,
+                                         prog="conan remote",
+                                         formatter_class=SmartFormatter)
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
         subparsers.required = True
 
@@ -1314,12 +1386,16 @@ class Command(object):
             return self._conan.remote_clean()
 
     def profile(self, *args):
-        """ Lists profiles in the '.conan/profiles' folder, or shows profile details.
+        """
+        Lists profiles in the '.conan/profiles' folder, or shows profile details.
+
         The 'list' subcommand will always use the default user 'conan/profiles' folder. But the
         'show' subcommand is able to resolve absolute and relative paths, as well as to map names to
         '.conan/profiles' folder, in the same way as the '--profile' install argument.
         """
-        parser = argparse.ArgumentParser(description=self.profile.__doc__, prog="conan profile")
+        parser = argparse.ArgumentParser(description=self.profile.__doc__,
+                                         prog="conan profile",
+                                         formatter_class=SmartFormatter)
         subparsers = parser.add_subparsers(dest='subcommand')
         subparsers.required = True
 
@@ -1378,10 +1454,12 @@ class Command(object):
             self._conan.delete_profile_key(profile, args.item)
 
     def get(self, *args):
-        """ Gets a file or list a directory of a given reference or package.
+        """
+        Gets a file or list a directory of a given reference or package.
         """
         parser = argparse.ArgumentParser(description=self.get.__doc__,
-                                         prog="conan get")
+                                         prog="conan get",
+                                         formatter_class=SmartFormatter)
         parser.add_argument('reference', help='package recipe reference')
         parser.add_argument('path',
                             help='Path to the file or directory. If not specified will get the '
@@ -1423,7 +1501,8 @@ class Command(object):
         return
 
     def alias(self, *args):
-        """Creates and exports an 'alias package recipe'.
+        """
+        Creates and exports an 'alias package recipe'.
 
         An "alias" package is a symbolic name (reference) for another package
         (target). When some package depends on an alias, the target one will be
@@ -1431,7 +1510,8 @@ class Command(object):
         does not appear in the final dependency graph.
         """
         parser = argparse.ArgumentParser(description=self.alias.__doc__,
-                                         prog="conan alias")
+                                         prog="conan alias",
+                                         formatter_class=SmartFormatter)
         parser.add_argument('reference', help='Alias reference. e.g.: mylib/1.X@user/channel')
         parser.add_argument('target', help='Target reference. e.g.: mylib/1.12@user/channel')
         args = parser.parse_args(*args)
@@ -1441,10 +1521,16 @@ class Command(object):
         self._conan.export_alias(args.reference, args.target)
 
     def workspace(self, *args):
-        """ command to manage workspaces
+        """
+        Manages a workspace (a set of packages consumed from the user workspace that
+        belongs to the same project).
+
+        Use this command to manage a Conan workspace, use the subcommand 'install' to
+        create the workspace from a file.
         """
         parser = argparse.ArgumentParser(description=self.workspace.__doc__,
-                                         prog="conan workspace")
+                                         prog="conan workspace",
+                                         formatter_class=SmartFormatter)
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
         subparsers.required = True
 
@@ -1470,10 +1556,16 @@ class Command(object):
                                           install_folder=args.install_folder)
 
     def editable(self, *args):
-        """ Manage editable packages
+        """
+        Manages editable packages (package that resides in the user workspace, but
+        are consumed as if they were in the cache).
+
+        Use the subcommands 'add', 'remove' and 'list' to create, remove an list
+        packages currently installed in this mode.
         """
         parser = argparse.ArgumentParser(description=self.editable.__doc__,
-                                         prog="conan editable")
+                                         prog="conan editable",
+                                         formatter_class=SmartFormatter)
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
         subparsers.required = True
 
@@ -1512,7 +1604,8 @@ class Command(object):
                 self._user_io.out.writeln("    Layout: %s" % v["layout"])
 
     def _show_help(self):
-        """Prints a summary of all commands
+        """
+        Prints a summary of all commands.
         """
         grps = [("Consumer commands", ("install", "config", "get", "info", "search")),
                 ("Creator commands", ("new", "create", "upload", "export", "export-pkg", "test")),
@@ -1539,7 +1632,23 @@ class Command(object):
             for name in comm_names:
                 # future-proof way to ensure tabular formatting
                 self._user_io.out.write(fmt % name, Color.GREEN)
-                self._user_io.out.writeln(commands[name].__doc__.split('\n', 1)[0].strip())
+
+                # Help will be all the lines up to the first empty one
+                docstring_lines = commands[name].__doc__.split('\n')
+                start = False
+                data = []
+                for line in docstring_lines:
+                    line = line.strip()
+                    if not line:
+                        if start:
+                            break
+                        start = True
+                        continue
+                    data.append(line)
+
+                import textwrap
+                txt = textwrap.fill(' '.join(data), 80, subsequent_indent=" "*(max_len+2))
+                self._user_io.out.writeln(txt)
 
         self._user_io.out.writeln("")
         self._user_io.out.writeln('Conan commands. Type "conan <command> -h" for help',
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index f431515a2..79760ad3c 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -258,7 +258,8 @@ class ConanAPIV1(object):
             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 @@ class ConanAPIV1(object):
                         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):
@@ -1137,11 +1139,12 @@ class ConanAPIV1(object):
 
     @api_method
     def editable_add(self, path, reference, layout, cwd):
-        remotes = self._cache.registry.load_remotes()
-        self.python_requires.enable_remotes(remotes=remotes)
         # Retrieve conanfile.py from target_path
         target_path = _get_conanfile_path(path=path, cwd=cwd, py=True)
 
+        remotes = self._cache.registry.load_remotes()
+        self.python_requires.enable_remotes(remotes=remotes)
+
         # Check the conanfile is there, and name/version matches
         ref = ConanFileReference.loads(reference, validate=True)
         target_conanfile = self._graph_manager._loader.load_class(target_path)
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index e7cf60859..09cf0e4d9 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -416,21 +416,9 @@ class ConanClientConfigParser(ConfigParser, object):
         try:
             proxies = self.get_conf("proxies")
             # If there is proxies section, but empty, it will try to use system proxy
-            if not proxies:
-                # We don't have evidences that this following line is necessary.
-                # If the proxies has been
-                # configured at system level, conan will use it, and shouldn't be necessary
-                # to return here the proxies read from the system.
-                # Furthermore, the urls excluded for use proxies at system level do not work in
-                # this case, then the only way is to remove the [proxies] section with
-                # conan config remote proxies, then this method will return None and the proxies
-                # dict passed to requests will be empty.
-                # We don't remove this line because we are afraid to break something, but maybe
-                # until now is working because no one is using system-wide proxies or those proxies
-                # rules don't contain excluded urls.c #1777
-                return urllib.request.getproxies()
-            result = {k: (None if v == "None" else v) for k, v in proxies}
-            return result
+            if proxies:
+                return {k: (None if v == "None" else v) for k, v in proxies}
+            return {}
         except:
             return None
 
diff --git a/conans/client/rest/conan_requester.py b/conans/client/rest/conan_requester.py
index 885fb5e51..9f39898db 100644
--- a/conans/client/rest/conan_requester.py
+++ b/conans/client/rest/conan_requester.py
@@ -87,8 +87,8 @@ class ConanRequester(object):
             old_env = dict(os.environ)
             # Clean the proxies from the environ and use the conan specified proxies
             for var_name in ("http_proxy", "https_proxy", "no_proxy"):
-                popped = popped or os.environ.pop(var_name, None)
-                popped = popped or os.environ.pop(var_name.upper(), None)
+                popped = True if os.environ.pop(var_name, None) else popped
+                popped = True if os.environ.pop(var_name.upper(), None) else popped
         try:
             t1 = time.time()
             all_kwargs = self._add_kwargs(url, kwargs)
diff --git a/conans/client/rest/uploader_downloader.py b/conans/client/rest/uploader_downloader.py
index 94c4729d9..c850c9cb8 100644
--- a/conans/client/rest/uploader_downloader.py
+++ b/conans/client/rest/uploader_downloader.py
@@ -63,6 +63,9 @@ class Uploader(object):
                 if auth.token is None:
                     raise AuthenticationException(response.content)
                 raise ForbiddenException(response.content)
+
+            response.raise_for_status()  # Raise HTTPError for bad http response status
+
         except ConanException:
             raise
         except Exception as exc:
diff --git a/conans/client/tools/scm.py b/conans/client/tools/scm.py
index 64241005d..d35e784ff 100644
--- a/conans/client/tools/scm.py
+++ b/conans/client/tools/scm.py
@@ -318,7 +318,7 @@ class SVN(SCMBase):
     def get_qualified_remote_url(self, remove_credentials=False):
         # Return url with peg revision
         url = self.get_remote_url(remove_credentials=remove_credentials)
-        revision = self.get_last_changed_revision()
+        revision = self.get_revision()
         return "{url}@{revision}".format(url=url, revision=revision)
 
     def is_local_repository(self):


Test patch:
diff --git a/conans/test/functional/command/new_test.py b/conans/test/functional/command/new_test.py
index 95c65a368..7a10981e8 100644
--- a/conans/test/functional/command/new_test.py
+++ b/conans/test/functional/command/new_test.py
@@ -3,10 +3,47 @@ import unittest
 
 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 --f=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 --file=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 --file=mytemplate.py", assert_error=True)
+        self.assertIn("ERROR: Template doesn't exist", client.out)
+        client.run("new hello/0.1 --f=mytemplate.py --bare", assert_error=True)
+        self.assertIn("ERROR: 'template' argument incompatible", client.out)
+        client.run("new hello/0.1 --file", 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')
diff --git a/conans/test/functional/remote/retry_test.py b/conans/test/functional/remote/retry_test.py
new file mode 100644
index 000000000..72388f5f3
--- /dev/null
+++ b/conans/test/functional/remote/retry_test.py
@@ -0,0 +1,104 @@
+# coding=utf-8
+
+import os
+import unittest
+from collections import namedtuple, Counter
+
+import six
+
+from conans.client.rest.uploader_downloader import Uploader
+from conans.errors import AuthenticationException, ForbiddenException
+from conans.test.utils.test_files import temp_folder
+from conans.test.utils.tools import TestBufferConanOutput
+from conans.util.files import save
+from requests.exceptions import HTTPError
+
+
+class response_type:
+    def __init__(self, status_code, content):
+        self.status_code = status_code
+        self.content = content
+
+    def raise_for_status(self):
+        """Raises stored :class:`HTTPError`, if one occurred."""
+
+        http_error_msg = ''
+        if 400 <= self.status_code < 500:
+            http_error_msg = u'%s Client Error: %s' % (self.status_code, self.content)
+
+        elif 500 <= self.status_code < 600:
+            http_error_msg = u'%s Server Error: %s' % (self.status_code, self.content)
+
+        if http_error_msg:
+            raise HTTPError(http_error_msg, response=self)
+
+
+class _RequesterMock:
+    def __init__(self, status_code, content):
+        self.response = response_type(status_code, content)
+
+    def put(self, *args, **kwargs):
+        return self.response
+
+
+class RetryDownloadTests(unittest.TestCase):
+
+    def setUp(self):
+        self.filename = os.path.join(temp_folder(), "anyfile")
+        save(self.filename, "anything")
+
+    def test_error_401(self):
+        output = TestBufferConanOutput()
+        uploader = Uploader(requester=_RequesterMock(401, "content"), output=output, verify=False)
+        with six.assertRaisesRegex(self, AuthenticationException, "content"):
+            uploader.upload(url="fake", abs_path=self.filename, retry=3)
+        output_lines = str(output).splitlines()
+        counter = Counter(output_lines)
+        self.assertEqual(counter["ERROR: content"], 2)
+        self.assertEqual(counter["Waiting 0 seconds to retry..."], 2)
+
+    def test_error_403_forbidden(self):
+        output = TestBufferConanOutput()
+        uploader = Uploader(requester=_RequesterMock(403, "content"), output=output, verify=False)
+        with six.assertRaisesRegex(self, ForbiddenException, "content"):
+            auth = namedtuple("auth", "token")
+            uploader.upload(url="fake", abs_path=self.filename, retry=3, auth=auth("token"))
+        output_lines = str(output).splitlines()
+        counter = Counter(output_lines)
+        self.assertEqual(counter["ERROR: content"], 2)
+        self.assertEqual(counter["Waiting 0 seconds to retry..."], 2)
+
+    def test_error_403_authentication(self):
+        output = TestBufferConanOutput()
+        uploader = Uploader(requester=_RequesterMock(403, "content"), output=output, verify=False)
+        with six.assertRaisesRegex(self, AuthenticationException, "content"):
+            auth = namedtuple("auth", "token")
+            uploader.upload(url="fake", abs_path=self.filename, retry=3, auth=auth(None))
+        output_lines = str(output).splitlines()
+        counter = Counter(output_lines)
+        self.assertEqual(counter["ERROR: content"], 2)
+        self.assertEqual(counter["Waiting 0 seconds to retry..."], 2)
+
+    def test_error_requests(self):
+        class _RequesterMock:
+            def put(self, *args, **kwargs):
+                raise Exception("any exception")
+
+        output = TestBufferConanOutput()
+        uploader = Uploader(requester=_RequesterMock(), output=output, verify=False)
+        with six.assertRaisesRegex(self, Exception, "any exception"):
+            uploader.upload(url="fake", abs_path=self.filename, retry=3)
+        output_lines = str(output).splitlines()
+        counter = Counter(output_lines)
+        self.assertEqual(counter["ERROR: any exception"], 2)
+        self.assertEqual(counter["Waiting 0 seconds to retry..."], 2)
+
+    def test_error_500(self):
+        output = TestBufferConanOutput()
+        uploader = Uploader(requester=_RequesterMock(500, "content"), output=output, verify=False)
+        with six.assertRaisesRegex(self, Exception, "500 Server Error: content"):
+            uploader.upload(url="fake", abs_path=self.filename, retry=3)
+        output_lines = str(output).splitlines()
+        counter = Counter(output_lines)
+        self.assertEqual(counter["ERROR: 500 Server Error: content"], 2)
+        self.assertEqual(counter["Waiting 0 seconds to retry..."], 2)
diff --git a/conans/test/functional/scm/issues/__init__.py b/conans/test/functional/scm/issues/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/test/functional/scm/issues/test_svn_tag.py b/conans/test/functional/scm/issues/test_svn_tag.py
new file mode 100644
index 000000000..ce90e8c10
--- /dev/null
+++ b/conans/test/functional/scm/issues/test_svn_tag.py
@@ -0,0 +1,61 @@
+# coding=utf-8
+
+import os
+import textwrap
+
+from nose.plugins.attrib import attr
+
+from conans.model.ref import ConanFileReference
+from conans.test.utils.tools import SVNLocalRepoTestCase, TestClient, \
+    load
+
+
+@attr('svn')
+class SVNTaggedComponentTest(SVNLocalRepoTestCase):
+    # Reproducing https://github.com/conan-io/conan/issues/5017
+
+    def setUp(self):
+        # Create a sample SVN repository
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile, tools
+            
+            class Lib(ConanFile):
+                scm = {"type": "svn", "url": "auto", "revision": "auto"}
+        """)
+        files = {'trunk/level0.txt': "level0",
+                 'trunk/level1/level1.txt': "level1",
+                 'trunk/level1/conanfile.py': "invalid content",
+                 'tags/sentinel': ""}
+        self.project_url, rev = self.create_project(files=files)
+        self.project_url = self.project_url.replace(" ", "%20")
+
+        # Modify the recipe file and commit in trunk
+        t = TestClient()
+        t.runner('svn co "{url}/trunk" "{path}"'.format(url=self.project_url, path=t.current_folder))
+        t.save({"level1/conanfile.py": conanfile})
+        t.runner('svn commit -m "created the conanfile"', cwd=t.current_folder)
+
+        # Create a tag for 'release 1.0'
+        t.runner('svn copy {url}/trunk {url}/tags/release-1.0'
+                 ' -m "Release 1.0"'.format(url=self.project_url), cwd=t.current_folder)
+
+    def test_auto_tag(self):
+        t = TestClient()
+        ref = ConanFileReference.loads("lib/version@issue/testing")
+
+        # Clone the tag to local folder
+        url = os.path.join(self.project_url, "tags/release-1.0/level1").replace('\\', '/')
+        t.runner('svn co "{url}" "{path}"'.format(url=url, path=t.current_folder))
+        
+        # Export the recipe (be sure sources are retrieved from the repository)
+        t.run("export . {ref}".format(ref=ref))
+        package_layout = t.cache.package_layout(ref)
+        exported_conanfile = load(package_layout.conanfile())
+        self.assertNotIn("auto", exported_conanfile)
+        self.assertIn('"revision": "3",', exported_conanfile)
+        self.assertIn('tags/release-1.0/level1@3', exported_conanfile)
+        os.remove(package_layout.scm_folder())  # Just in case, avoid scm_folder optimization
+
+        # Compile (it will clone the repo)
+        t.run("install {ref} --build=lib".format(ref=ref))
+        self.assertIn("lib/version@issue/testing: Getting sources from url:", t.out)
diff --git a/conans/test/unittests/client/cmd/export_test.py b/conans/test/unittests/client/cmd/export_test.py
index ec62d8f93..b336323fb 100644
--- a/conans/test/unittests/client/cmd/export_test.py
+++ b/conans/test/unittests/client/cmd/export_test.py
@@ -9,7 +9,7 @@ from conans.client.tools import chdir
 from conans.model.ref import ConanFileReference
 from conans.model.scm import SCMData
 from conans.test.utils.test_files import temp_folder
-from conans.test.utils.tools import TestClient, TestServer
+from conans.test.utils.tools import TestClient, TestServer, TurboTestClient
 from conans.util.files import load, save
 
 
@@ -82,6 +82,56 @@ class ConanLib(ConanFile):
         after_recipe = ''
         self._do_actual_test(scm_data=scm_data, after_scm=after_scm, after_recipe=after_recipe)
 
+    def scm_from_superclass_test(self):
+        client = TurboTestClient()
+        conanfile = '''from conans import ConanFile
+
+def get_conanfile():
+    
+    class BaseConanFile(ConanFile):
+        scm = {
+            "type": "git", 
+            "url": "auto",
+            "revision": "auto"
+        }
+    
+    return BaseConanFile
+
+class Baseline(ConanFile):
+    name = "Base"
+    version = "1.0.0"
+'''
+        client.init_git_repo({"conanfile.py": conanfile}, origin_url="http://whatever.com/c.git")
+        client.run("export . conan/stable")
+        conanfile1 = """from conans import ConanFile, python_requires, tools
+
+baseline = "Base/1.0.0@conan/stable"
+
+# recipe inherits properties from the conanfile defined in the baseline
+class ModuleConan(python_requires(baseline).get_conanfile()):
+    name = "module_name"
+    version = "1.0.0"
+"""
+        conanfile2 = """from conans import ConanFile, python_requires, tools
+
+baseline = "Base/1.0.0@conan/stable"
+
+# recipe inherits properties from the conanfile defined in the baseline
+class ModuleConan(python_requires(baseline).get_conanfile()):
+    pass
+"""
+
+        for conanfile in [conanfile1, conanfile2]:
+            client.save({"conanfile.py": conanfile})
+            client.run("export . module_name/1.0.0@conan/stable")
+            self.assertIn("module_name/1.0.0@conan/stable: "
+                          "A new conanfile.py version was exported", client.out)
+            ref = ConanFileReference.loads("module_name/1.0.0@conan/stable")
+            contents = load(os.path.join(client.cache.package_layout(ref).export(),
+                                         "conanfile.py"))
+            class_str = 'class ModuleConan(python_requires(baseline).get_conanfile()):\n'
+            self.assertIn('%s    scm = {"revision":' % class_str, contents)
+
 
 class SCMUpload(unittest.TestCase):
 
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index e5d2a4d7f..40628dcda 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -23,6 +23,7 @@ from mock import Mock
 from six import StringIO
 from six.moves.urllib.parse import quote, urlsplit, urlunsplit
 from webtest.app import TestApp
+from requests.exceptions import HTTPError
 
 from conans import tools, load
 from conans.client.cache.cache import ClientCache
@@ -109,6 +110,18 @@ class TestingResponse(object):
     def ok(self):
         return self.test_response.status_code == 200
 
+    def raise_for_status(self):
+        """Raises stored :class:`HTTPError`, if one occurred."""
+        http_error_msg = ''
+        if 400 <= self.status_code < 500:
+            http_error_msg = u'%s Client Error: %s' % (self.status_code, self.content)
+
+        elif 500 <= self.status_code < 600:
+            http_error_msg = u'%s Server Error: %s' % (self.status_code, self.content)
+
+        if http_error_msg:
+            raise HTTPError(http_error_msg, response=self)
+
     @property
     def content(self):
         return self.test_response.body