Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom global generators in cache extensions/generators folder #13718

Merged
merged 2 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conan/api/subapi/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ def install_consumer(self, deps_graph, generators=None, source_folder=None, outp

conanfile.generators = list(set(conanfile.generators).union(generators or []))
app = ConanApp(self.conan_api.cache_folder)
write_generators(conanfile, app.hook_manager)
write_generators(conanfile, app)
4 changes: 4 additions & 0 deletions conans/client/cache/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ def settings_path(self):
def custom_commands_path(self):
return os.path.join(self.cache_folder, EXTENSIONS_FOLDER, CUSTOM_COMMANDS_FOLDER)

@property
def custom_generators_path(self):
return os.path.join(self.cache_folder, EXTENSIONS_FOLDER, "generators")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider defining GENERATORS_EXTENSION_FOLDER analogous to DEPLOYERS_EXTENSION_FOLDER at line 30. Also, if it is not too late, it would read better if DEPLOYERS_EXTENSION_FOLDER was defined to be "deployers" rather than "deploy". Lastly, this code might be more logically placed at line 211.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid that it would be better not to change the deploy folders, as we are aware of some users already using them, and it would be breaking, probably not worth it.

Regarding GENERATORS_EXTENSION_FOLDER I am tempted to go in the other direction, if no one else in the whole codebase uses the constant, then, tit is better encapsulated in the custom_generators_path, so I was tempted to remove the CUSTOM_COMMANDS_FOLDER global const, and make it an implementation detail over custom_commands_path, which is the abstraction to use, not the constant. I will double check that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, most of those consts, defined as public in the global module scope are unused in the rest of the codebase. I typically prefer to provide the less possible scope and indirection when it is artificial/unused/premature, so I'd probably propose an internal simplification PR for that (but I'll do it in another PR to keep this one minimal).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @memsharded,
Thanks for the additional insights. Regarding "deploy" vs. "deployers", would it be possible to look in both "deployers" and "deploy" but issues a deprecation warning for any files found in the older one? Thanks


@property
def plugins_path(self):
return os.path.join(self.cache_folder, EXTENSIONS_FOLDER, PLUGINS_FOLDER)
Expand Down
26 changes: 24 additions & 2 deletions conans/client/generators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
import os
import traceback
import importlib
Expand Down Expand Up @@ -45,10 +46,30 @@ def _get_generator_class(generator_name):
f"inside module {generator_class}") from e


def write_generators(conanfile, hook_manager):
def load_cache_generators(path):
from conans.client.loader import load_python_file
result = {} # Name of the generator: Class
if not os.path.isdir(path):
return result
for f in os.listdir(path):
if not f.endswith(".py") or f.startswith("_"):
continue
full_path = os.path.join(path, f)
mod, _ = load_python_file(full_path)
for name, value in inspect.getmembers(mod):
if inspect.isclass(value) and not name.startswith("_"):
result = {name: value}
return result


def write_generators(conanfile, app):
new_gen_folder = conanfile.generators_folder
_receive_conf(conanfile)

hook_manager = app.hook_manager
cache = app.cache
# TODO: Optimize this, so the global generators are not loaded every call to write_generators
global_generators = load_cache_generators(cache.custom_generators_path)
hook_manager.execute("pre_generate", conanfile=conanfile)

if conanfile.generators:
Expand All @@ -60,7 +81,8 @@ def write_generators(conanfile, hook_manager):
conanfile.generators = []
try:
for generator_name in old_generators:
generator_class = _get_generator_class(generator_name)
global_generator = global_generators.get(generator_name)
generator_class = global_generator or _get_generator_class(generator_name)
if generator_class:
try:
generator = generator_class(conanfile)
Expand Down
4 changes: 2 additions & 2 deletions conans/client/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def _copy_sources(conanfile, source_folder, build_folder):
raise ConanException("%s\nError copying sources to build folder" % msg)

def _build(self, conanfile, pref):
write_generators(conanfile, self._hook_manager)
write_generators(conanfile, self._app)

try:
run_build_method(conanfile, self._hook_manager)
Expand Down Expand Up @@ -351,7 +351,7 @@ def _handle_node_editable(self, install_node):
output = conanfile.output
output.info("Rewriting files of editable package "
"'{}' at '{}'".format(conanfile.name, conanfile.generators_folder))
write_generators(conanfile, self._hook_manager)
write_generators(conanfile, self._app)

if node.binary == BINARY_EDITABLE_BUILD:
run_build_method(conanfile, self._hook_manager)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import os
import textwrap

from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.tools import TestClient
from conans.util.files import save


def test_custom_global_generator():
c = TestClient()
generator = textwrap.dedent("""
class MyCustomGenerator:
def __init__(self, conanfile):
self._conanfile = conanfile
def generate(self):
pkg = self._conanfile.dependencies["pkg"].ref
self._conanfile.output.info(f"DEP: {pkg}!!")
""")
save(os.path.join(c.cache.custom_generators_path, "mygen.py"), generator)
conanfile = textwrap.dedent("""
[requires]
pkg/0.1

[generators]
MyCustomGenerator
""")
c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"),
"conanfile.txt": conanfile})
c.run("create pkg")
c.run("install .")
assert "conanfile.txt: Generator 'MyCustomGenerator' calling 'generate()'" in c.out
assert "conanfile.txt: DEP: pkg/0.1!!" in c.out

# By CLI also works
conanfile = textwrap.dedent("""
[requires]
pkg/0.1
""")
c.save({"conanfile.txt": conanfile})
c.run("install . -g MyCustomGenerator")
assert "conanfile.txt: Generator 'MyCustomGenerator' calling 'generate()'" in c.out
assert "conanfile.txt: DEP: pkg/0.1!!" in c.out

# In conanfile.py also works
conanfile = textwrap.dedent("""
from conan import ConanFile
class MyPkg(ConanFile):
requires = "pkg/0.1"
generators = "MyCustomGenerator"
""")
c.save({"conanfile.py": conanfile}, clean_first=True)
c.run("install . ")
assert "conanfile.py: Generator 'MyCustomGenerator' calling 'generate()'" in c.out
assert "conanfile.py: DEP: pkg/0.1!!" in c.out


def test_custom_global_generator_imports():
"""
our custom generator can use python imports
"""
c = TestClient()
generator = textwrap.dedent("""
from _myfunc import mygenerate

class MyCustomGenerator:
def __init__(self, conanfile):
self._conanfile = conanfile
def generate(self):
mygenerate(self._conanfile)
""")
myaux = textwrap.dedent("""
def mygenerate(conanfile):
conanfile.output.info("MYGENERATE WORKS!!")
""")
save(os.path.join(c.cache.custom_generators_path, "mygen.py"), generator)
save(os.path.join(c.cache.custom_generators_path, "_myfunc.py"), myaux)

c.save({"conanfile.txt": ""})
c.run("install . -g MyCustomGenerator")
assert "conanfile.txt: Generator 'MyCustomGenerator' calling 'generate()'" in c.out
assert "conanfile.txt: MYGENERATE WORKS!!" in c.out