Repository: conan-io/conan

Issue:
Conan_api incorrect caching. Create fresh instances for every call.
There are some components that are caching things: ConanFileLoader, Cache, PythonRequires. These components are instanced once in ConanAPI, but the api can be reused for multiple calls.

Consider:
- Invalidating mechanism, as in: https://github.com/conan-io/conan/pull/4362
- Creating fresh instances of all collaborators for every call.

Related to this: we might want to re-think how functional collaborators are passed around conan codebase.




Discussion:

Resolving patch:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -78,6 +78,7 @@ def get_basic_requester(cache):
 def api_method(f):
     def wrapper(*args, **kwargs):
         the_self = args[0]
+        the_self.invalidate_caches()
         try:
             curdir = get_cwd()
             log_command(f.__name__, kwargs)
@@ -236,6 +237,10 @@ def __init__(self, cache, user_io, runner, remote_manager, hook_manager, request
                                            resolver)
         self._hook_manager = hook_manager
 
+    def invalidate_caches(self):
+        self._loader.invalidate_caches()
+        self._cache.invalidate()
+
     def _init_manager(self, action_recorder):
         """Every api call gets a new recorder and new manager"""
         return ConanManager(self._cache, self._user_io,
diff --git a/conans/client/graph/python_requires.py b/conans/client/graph/python_requires.py
--- a/conans/client/graph/python_requires.py
+++ b/conans/client/graph/python_requires.py
@@ -18,6 +18,9 @@ def __init__(self, proxy, range_resolver):
         self._requires = None
         self.valid = True
 
+    def invalidate_caches(self):
+        self._cached_requires = {}
+
     @contextmanager
     def capture_requires(self):
         old_requires = self._requires
diff --git a/conans/client/loader.py b/conans/client/loader.py
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -36,6 +36,10 @@ def __init__(self, runner, output, python_requires):
         sys.modules["conans"].python_requires = python_requires
         self.cached_conanfiles = {}
 
+    def invalidate_caches(self):
+        self.cached_conanfiles = {}
+        self._python_requires.invalidate_caches()
+
     def load_class(self, conanfile_path):
         try:
             return self.cached_conanfiles[conanfile_path]


Test patch:
diff --git a/conans/test/functional/conan_api/two_conan_creates_test.py b/conans/test/functional/conan_api/two_conan_creates_test.py
--- a/conans/test/functional/conan_api/two_conan_creates_test.py
+++ b/conans/test/functional/conan_api/two_conan_creates_test.py
@@ -1,13 +1,18 @@
 import os
-import unittest
 import platform
+import sys
+import unittest
+
+from six import StringIO
+from textwrap import dedent
 
 from conans.client.conan_api import ConanAPIV1
 from conans.client.tools.env import environment_append
 from conans.client.tools.files import chdir
 from conans.model.ref import PackageReference
 from conans.test.utils.test_files import temp_folder
-from conans.util.files import load
+from conans.test.utils.tools import TestBufferConanOutput
+from conans.util.files import load, save
 
 
 class ConanCreateTest(unittest.TestCase):
@@ -38,3 +43,28 @@ def get_conaninfo(info):
                 settings = ["compiler=Visual Studio", "compiler.version=15", "build_type=Debug"]
                 info = api.create(".", user="conan", channel="stable", settings=settings)
                 self.assertIn("compiler.runtime=MDd", get_conaninfo(info))
+
+    def test_api_conanfile_loader_shouldnt_cache(self):
+        tmp = temp_folder()
+        with environment_append({"CONAN_USER_HOME": tmp}):
+            with chdir(tmp):
+                try:
+                    old_stdout = sys.stdout
+                    result = StringIO()
+                    sys.stdout = result
+                    api, _, _ = ConanAPIV1.factory()
+                    api._user_io.out = TestBufferConanOutput()
+                    conanfile = dedent("""
+                        from conans import ConanFile
+                        class Pkg(ConanFile):
+                            def build(self):
+                                self.output.info("NUMBER 42!!")
+                        """)
+                    save("conanfile.py", conanfile)
+                    api.create(".", "pkg", "version", "user", "channel")
+                    self.assertIn("pkg/version@user/channel: NUMBER 42!!", result.getvalue())
+                    save("conanfile.py", conanfile.replace("42", "123"))
+                    api.create(".", "pkg", "version", "user", "channel")
+                    self.assertIn("pkg/version@user/channel: NUMBER 123!!", result.getvalue())
+                finally:
+                    sys.stdout = old_stdout