diff --git a/src/hrflow_connectors/core/connector.py b/src/hrflow_connectors/core/connector.py index e6c2fd8d..39e33459 100644 --- a/src/hrflow_connectors/core/connector.py +++ b/src/hrflow_connectors/core/connector.py @@ -2,6 +2,7 @@ import copy import enum +import importlib import inspect import json import logging @@ -10,6 +11,7 @@ import uuid import warnings from collections import Counter +from contextvars import ContextVar from datetime import datetime from functools import partial from pathlib import Path @@ -1099,21 +1101,26 @@ class AmbiguousConnectorImportName(Exception): pass +MAIN_IMPORT_NAME: ContextVar[str] = ContextVar( + "MAIN_IMPORT_NAME", default="hrflow_connectors" +) + + def get_import_name(connector: Connector) -> str: - import hrflow_connectors + main_module = importlib.import_module(MAIN_IMPORT_NAME.get()) - members = inspect.getmembers(hrflow_connectors, lambda s: s is connector) + members = inspect.getmembers(main_module, lambda s: s is connector) if len(members) == 0: raise ConnectorImportNameNotFound( "Failed to find import name for" - f" Connector(name={connector.model.name})={connector}\nNot match found for" + f" Connector(name={connector.model.name})={connector}\nNo match found for" " below members" - f" {[symbol for symbol, _ in inspect.getmembers(hrflow_connectors)]}" + f" {[symbol for symbol, _ in inspect.getmembers(main_module)]}" ) if len(members) > 1: raise AmbiguousConnectorImportName( "Found multiple import names for" - f" Connector(name={connector.model.name})={connector}={connector}\n" + f" Connector(name={connector.model.name})={connector}\n" f" {[symbol for symbol, _ in members]}" ) return members[0][0] diff --git a/tests/core/test_documentation.py b/tests/core/test_documentation.py index 1fe8d521..4bff063c 100644 --- a/tests/core/test_documentation.py +++ b/tests/core/test_documentation.py @@ -34,7 +34,7 @@ from tests.core.src.hrflow_connectors.connectors.smartleads.warehouse import ( LeadsWarehouse, ) -from tests.core.utils import added_connectors +from tests.core.utils import added_connectors, main_import_name_as DUMMY_ROOT_README = """ # Test README used for documentation tests @@ -228,7 +228,7 @@ def test_documentation(connectors_directory): connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -243,6 +243,72 @@ def test_documentation(connectors_directory): assert action_documentation.exists() is True +def test_documentation_works_with_parameterized_main_module_name(connectors_directory): + readme = connectors_directory / SmartLeads.model.subtype / "README.md" + notebooks_directory = connectors_directory / SmartLeads.model.subtype / "notebooks" + keep_empty_notebooks_file = ( + connectors_directory + / SmartLeads.model.subtype + / "notebooks" + / KEEP_EMPTY_FOLDER + ) + format_mappings_directory = ( + connectors_directory / SmartLeads.model.subtype / "mappings" / "format" + ) + keep_empty_format_file = ( + connectors_directory + / SmartLeads.model.subtype + / "mappings" + / "format" + / KEEP_EMPTY_FOLDER + ) + action_documentation = ( + connectors_directory + / SmartLeads.model.subtype + / "docs" + / "{}.md".format(SmartLeads.model.actions[0].name.value) + ) + + assert readme.exists() is False + assert notebooks_directory.exists() is False + assert keep_empty_notebooks_file.exists() is False + assert format_mappings_directory.exists() is False + assert keep_empty_format_file.exists() is False + assert action_documentation.exists() is False + + connectors = [SmartLeads] + + parameterized_name = "third-party" + with main_import_name_as(parameterized_name): + # Should fail because by default add_connectors adds names to + # hrflow_connectors default import name + with pytest.raises(ModuleNotFoundError): + with patched_subprocess(): + with added_connectors([("SmartLeads", SmartLeads)]): + generate_docs( + connectors=connectors, + target_connectors=ALL_TARGET_CONNECTORS, + connectors_directory=connectors_directory, + ) + + with patched_subprocess(): + with added_connectors( + [("SmartLeads", SmartLeads)], parameterized_name, create_module=True + ): + generate_docs( + connectors=connectors, + target_connectors=ALL_TARGET_CONNECTORS, + connectors_directory=connectors_directory, + ) + + assert readme.exists() is True + assert notebooks_directory.exists() is True + assert keep_empty_notebooks_file.exists() is True + assert format_mappings_directory.exists() is True + assert keep_empty_format_file.exists() is True + assert action_documentation.exists() is True + + def test_documentation_adds_keep_empty_notebooks_file_if_folder_is_empty( connectors_directory, ): @@ -261,7 +327,7 @@ def test_documentation_adds_keep_empty_notebooks_file_if_folder_is_empty( connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -303,7 +369,7 @@ def test_documentation_does_not_add_keep_empty_notebooks_file_if_folder_has_othe connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -347,7 +413,7 @@ def test_documentation_removes_keep_empty_notebooks_file_if_folder_has_other_fil connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -390,7 +456,7 @@ def test_documentation_adds_keep_empty_format_file_if_folder_is_empty( connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -423,7 +489,7 @@ def test_documentation_does_not_add_keep_empty_format_file_if_folder_has_other_f connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -460,7 +526,7 @@ def test_documentation_removes_keep_empty_format_file_if_folder_has_other_files( connectors = [SmartLeads] with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -557,7 +623,7 @@ def test_documentation_fails_if_connector_misconfigured(connectors_directory): with pytest.raises(AmbiguousConnectorImportName): with patched_subprocess(): with added_connectors( - ("SmartLeads", SmartLeads), ("Duplicated", SmartLeads) + [("SmartLeads", SmartLeads), ("Duplicated", SmartLeads)] ): generate_docs( connectors=connectors, connectors_directory=connectors_directory @@ -573,9 +639,9 @@ def test_documentation_fails_if_connector_misconfigured(connectors_directory): def test_documentation_fails_if_actions_section_not_found(connectors_directory): readme = connectors_directory / SmartLeads.model.subtype / "README.md" - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=[SmartLeads], target_connectors=ALL_TARGET_CONNECTORS, @@ -590,7 +656,7 @@ def test_documentation_fails_if_actions_section_not_found(connectors_directory): with pytest.raises(InvalidConnectorReadmeFormat): with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=[SmartLeads], connectors_directory=connectors_directory, @@ -623,7 +689,7 @@ def test_main_readme_update_at_expected_value(root_readme, connectors_directory) connectors = [SmartLeads] with patched_subprocess(stdout=stdout): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -689,7 +755,7 @@ def test_ignored_path_are_not_taken_into_account_for_main_readme_updated_at( connectors = [SmartLeads] with patched_subprocess(stdout=base_stdout + "\n" + should_be_ignored): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -708,7 +774,7 @@ def test_ignored_path_are_not_taken_into_account_for_main_readme_updated_at( with patched_subprocess( stdout=base_stdout + greater_than_max_of_dates_with_regular_file ): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -741,7 +807,7 @@ def test_documentation_with_remote_code_links(connectors_directory): ), ): with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -764,7 +830,7 @@ def test_documentation_with_remote_code_links(connectors_directory): ), ): with patched_subprocess(): - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -816,7 +882,7 @@ def test_documentation_connector_directory_not_found(caplog, connectors_director connectors = [NameMismatchSmartLeads] with patched_subprocess(): - with added_connectors(("NameMismatchSmartLeads", NameMismatchSmartLeads)): + with added_connectors([("NameMismatchSmartLeads", NameMismatchSmartLeads)]): generate_docs( connectors=connectors, target_connectors=ALL_TARGET_CONNECTORS, @@ -836,7 +902,7 @@ def test_documentation_fails_if_subprocess_has_stderr(connectors_directory): stderr = "FATAL ERROR" with patched_subprocess(stderr=stderr): with pytest.raises(Exception) as excinfo: - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): generate_docs( connectors=[SmartLeads], target_connectors=ALL_TARGET_CONNECTORS, diff --git a/tests/core/test_manifest.py b/tests/core/test_manifest.py index a6e3b01c..32dbb6d6 100644 --- a/tests/core/test_manifest.py +++ b/tests/core/test_manifest.py @@ -15,7 +15,7 @@ ConnectorImportNameNotFound, ) from tests.core.test_connector import SmartLeadsF -from tests.core.utils import added_connectors +from tests.core.utils import added_connectors, main_import_name_as @pytest.fixture @@ -31,10 +31,29 @@ def manifest_directory(): def test_connector_manifest(test_connectors_directory): SmartLeads = SmartLeadsF() - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): SmartLeads.manifest(test_connectors_directory) +def test_connector_manifest_works_with_parameterized_main_module_name( + test_connectors_directory, +): + parameterized_name = "third-party" + + SmartLeads = SmartLeadsF() + with main_import_name_as(parameterized_name): + # Should fail because by default add_connectors adds names to + # hrflow_connectors default import name + with pytest.raises(ModuleNotFoundError): + with added_connectors([("SmartLeads", SmartLeads)]): + SmartLeads.manifest(test_connectors_directory) + + with added_connectors( + [("SmartLeads", SmartLeads)], parameterized_name, create_module=True + ): + SmartLeads.manifest(test_connectors_directory) + + def test_hrflow_connectors_manifest(manifest_directory, test_connectors_directory): manifest = Path(__file__).parent / "manifest.json" assert manifest.exists() is False @@ -52,7 +71,7 @@ def test_hrflow_connectors_manifest(manifest_directory, test_connectors_director dict(name="WrongConnector", type=None, subtype="wrongconnector"), ] with added_connectors( - ("SmartLeads", connector), + [("SmartLeads", connector)], ): hrflow_connectors_manifest( connectors=[connector], @@ -74,7 +93,7 @@ def test_connector_manifest_fails_if_cannot_find_import_name(test_connectors_dir def test_connector_manifest_fails_if_connector_misconfigured(test_connectors_directory): SmartLeads = SmartLeadsF() with pytest.raises(AmbiguousConnectorImportName): - with added_connectors(("SmartLeads", SmartLeads), ("Duplicated", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads), ("Duplicated", SmartLeads)]): SmartLeads.manifest(test_connectors_directory) @@ -83,7 +102,7 @@ def test_manifest_connector_directory_not_found(test_connectors_directory): SmartLeads.model.name = "SmartLeadsX" SmartLeads.model.subtype = "smartleadsx" with pytest.raises(ValueError) as excinfo: - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): SmartLeads.manifest(test_connectors_directory) assert "No directory found for connector SmartLeadsX" in excinfo.value.args[0] @@ -95,7 +114,7 @@ def test_manifest_logo_is_missing(test_connectors_directory): LocalUsers.model.name = "LocalUsers" LocalUsers.model.subtype = "localusers" with pytest.raises(ValueError) as excinfo: - with added_connectors(("LocalUsers", LocalUsers)): + with added_connectors([("LocalUsers", LocalUsers)]): LocalUsers.manifest(test_connectors_directory) assert "Missing logo for connector LocalUsers" in excinfo.value.args[0] @@ -109,7 +128,7 @@ def test_manifest_more_than_one_logo(test_connectors_directory): prefix="logo.", ): with pytest.raises(ValueError) as excinfo: - with added_connectors(("SmartLeads", SmartLeads)): + with added_connectors([("SmartLeads", SmartLeads)]): SmartLeads.manifest(test_connectors_directory) assert "Found multiple logos for connector SmartLeads" in excinfo.value.args[0] @@ -128,7 +147,7 @@ def test_manifest_logo_above_size_limit(test_connectors_directory): LocalUsers.model.name = "LocalUsers" LocalUsers.model.subtype = "localusers" with pytest.raises(ValueError) as excinfo: - with added_connectors(("LocalUsers", LocalUsers)): + with added_connectors([("LocalUsers", LocalUsers)]): LocalUsers.manifest(test_connectors_directory) assert ( @@ -149,7 +168,7 @@ def test_manifest_logo_not_valid_image(test_connectors_directory): LocalUsers.model.name = "LocalUsers" LocalUsers.model.subtype = "localusers" with pytest.raises(ValueError) as excinfo: - with added_connectors(("LocalUsers", LocalUsers)): + with added_connectors([("LocalUsers", LocalUsers)]): LocalUsers.manifest(test_connectors_directory) assert "Logo file for connector LocalUsers" in excinfo.value.args[0] @@ -188,7 +207,7 @@ def test_manifest_logo_bad_dimension(test_connectors_directory, shape): LocalUsers = SmartLeadsF() LocalUsers.model.subtype = "localusers" with pytest.raises(ValueError) as excinfo: - with added_connectors(("LocalUsers", LocalUsers)): + with added_connectors([("LocalUsers", LocalUsers)]): LocalUsers.manifest(test_connectors_directory) assert "Bad logo dimensions" in excinfo.value.args[0] @@ -206,7 +225,7 @@ def test_manifest_includes_jsonmap_when_file_exists(test_connectors_directory): jsonmap_content = {"key": "value"} jsonmap_file.write_text(json.dumps(jsonmap_content)) - with added_connectors(("SmartLeads", connector)): + with added_connectors([("SmartLeads", connector)]): manifest = connector.manifest(connectors_directory=test_connectors_directory) for action_manifest in manifest["actions"]: @@ -224,7 +243,7 @@ def test_manifest_includes_empty_jsonmap_when_file_missing(test_connectors_direc format_mappings_directory.mkdir(parents=True, exist_ok=True) - with added_connectors(("SmartLeads", connector)): + with added_connectors([("SmartLeads", connector)]): manifest = connector.manifest(connectors_directory=test_connectors_directory) for action_manifest in manifest["actions"]: diff --git a/tests/core/utils.py b/tests/core/utils.py index 039af975..d8884262 100644 --- a/tests/core/utils.py +++ b/tests/core/utils.py @@ -3,13 +3,30 @@ from unittest import mock from hrflow_connectors.core import Connector +from hrflow_connectors.core.connector import MAIN_IMPORT_NAME @contextmanager -def added_connectors(*symbols: t.Tuple[str, Connector]): +def added_connectors( + symbols: t.Iterable[t.Tuple[str, Connector]], + module: str = "hrflow_connectors", + *, + create_module=False, +): with ExitStack() as stack: - for name, connector in symbols: + if create_module: stack.enter_context( - mock.patch(f"hrflow_connectors.{name}", connector, create=True) + mock.patch.dict("sys.modules", **{module: mock.MagicMock()}) ) + for name, connector in symbols: + stack.enter_context(mock.patch(f"{module}.{name}", connector, create=True)) + yield + + +@contextmanager +def main_import_name_as(name: str): + reset_token = MAIN_IMPORT_NAME.set(name) + try: yield + finally: + MAIN_IMPORT_NAME.reset(reset_token)