Repository: conan-io/conan

Issue:
Generators : Add first-class mechanism for using templates
Conan Generators write text into files.  This is a very common programming task, and there are two general programmatic approaches.  First, is to embed the text to be generated as string variables in the source code.  Second, is to include template files with the program, and do read-replace-write on the templates at runtime. 

This "embedded" approach has advantages, and is currently the standard procedure for Generators.  However, there are numerous cases where it's preferable to use the "templates" approach.  Both are possible with Conan, the "templates" approach just requires an undesirable hack shown below.  Thus, this request is add a first-class feature to use a "template" with a single line.  

To my knowledge, within a Generator body, the only way to read a "template" file which has been `export`ed along with the Generator's `conanfile.py` is the hack below: 

```python
    exports = "*.xyz"
    ...
    template_filename = "some_template_file.xyz"
    generator_root_path = self.conanfile.deps_cpp_info["this_generator_packag_name"].rootpath
    template_path = os.path.join(root_path, os.pardir, os.pardir, "export", template_filename)
    template_content = tools.load(template_path)
```
This feels bad.  Besides the cosmetic element of being verbose, the bigger issue is that it's tightly coupling to Conan's current internal directory structure. 

`export`ing the templates still seems fine, but the hope is to add a simple feature to access them which works in generator body's. Something like `self.export_path` shown below: 

```python
    exports = "*.xyz"
    ...
    template_path = os.path.join(self.export_path, "some_template_file.xyz")
    template_content = tools.load(template_path)
```

We do this hack to use templates both in the `boost_generator` for modular boost, and in in my companies private generators. 

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
Possible implementation to discuss here: 
https://github.com/conan-io/conan/compare/develop...lasote:feature/custom_generators_dir?expand=1

Feedback welcome @memsharded 

Resolving patch:
diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py
--- a/conans/client/generators/__init__.py
+++ b/conans/client/generators/__init__.py
@@ -34,8 +34,8 @@ class _GeneratorManager(object):
     def __init__(self):
         self._generators = {}
 
-    def add(self, name, generator_class):
-        if name not in self._generators:
+    def add(self, name, generator_class, custom=False):
+        if name not in self._generators or custom:
             self._generators[name] = generator_class
 
     @property
diff --git a/conans/client/loader.py b/conans/client/loader.py
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -209,7 +209,7 @@ class defining the Recipe, but also process possible existing generators
             else:
                 raise ConanException("More than 1 conanfile in the file")
         elif issubclass(attr, Generator) and attr != Generator:
-            registered_generators.add(attr.__name__, attr)
+            registered_generators.add(attr.__name__, attr, custom=True)
 
     if result is None:
         raise ConanException("No subclass of ConanFile")


Test patch:
diff --git a/conans/test/functional/generators/custom_generator_test.py b/conans/test/functional/generators/custom_generator_test.py
--- a/conans/test/functional/generators/custom_generator_test.py
+++ b/conans/test/functional/generators/custom_generator_test.py
@@ -122,3 +122,28 @@ def multifile_test(self):
         for i in (1, 2):
             generated = load(os.path.join(client.current_folder, "file%d.gen" % i))
             self.assertEqual(generated, "CustomContent%d" % i)
+
+    def export_template_generator_test(self):
+        templated_generator = """
+import os
+from conans import ConanFile, load
+from conans.model import Generator
+class MyCustomTemplateGenerator(Generator):
+    @property
+    def filename(self):
+        return "customfile.gen"
+    @property
+    def content(self):
+        template = load(os.path.join(os.path.dirname(__file__), "mytemplate.txt"))
+        return template % "Hello"
+
+class MyCustomGeneratorWithTemplatePackage(ConanFile):
+    exports = "mytemplate.txt"
+"""
+        client = TestClient()
+        client.save({CONANFILE: templated_generator, "mytemplate.txt": "Template: %s"})
+        client.run("create . gen/0.1@user/stable")
+
+        client.run("install gen/0.1@user/stable -g=MyCustomTemplateGenerator")
+        generated = load(os.path.join(client.current_folder, "customfile.gen"))
+        self.assertEqual(generated, "Template: Hello")