diff --git a/docs/source/user_guide/installation/index.rst b/docs/source/user_guide/installation/index.rst index 18e62c5dfa..9225f1ee98 100644 --- a/docs/source/user_guide/installation/index.rst +++ b/docs/source/user_guide/installation/index.rst @@ -71,6 +71,7 @@ Package Minimum supp `typing-extensions `__ 4.10.0 `pandas `__ 1.5.0 `pooch `__ 1.8.1 +`posthog `__ 3.6.5 =================================================================== ========================== .. _install.optional_dependencies: diff --git a/noxfile.py b/noxfile.py index 5ab32f463f..d65812b8ed 100644 --- a/noxfile.py +++ b/noxfile.py @@ -49,6 +49,7 @@ def set_iree_state(): "IREE_INDEX_URL": os.getenv( "IREE_INDEX_URL", "https://iree.dev/pip-release-links.html" ), + "PYBAMM_DISABLE_TELEMETRY": "true", } VENV_DIR = Path("./venv").resolve() diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index c53d1a0e40..3eb267860a 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -203,7 +203,8 @@ import os import pathlib import sysconfig -os.environ["CASADIPATH"] = str(pathlib.Path(sysconfig.get_path('purelib')) / 'casadi') + +os.environ["CASADIPATH"] = str(pathlib.Path(sysconfig.get_path("purelib")) / "casadi") __all__ = [ "batch_study", @@ -232,5 +233,4 @@ "pybamm_data", ] - pybamm.config.generate() diff --git a/src/pybamm/config.py b/src/pybamm/config.py index ee08ef6774..abe775ec97 100644 --- a/src/pybamm/config.py +++ b/src/pybamm/config.py @@ -3,8 +3,9 @@ import platformdirs from pathlib import Path import pybamm -import select import sys +import threading +import time def is_running_tests(): # pragma: no cover @@ -14,8 +15,6 @@ def is_running_tests(): # pragma: no cover Returns: bool: True if running tests or building docs, False otherwise. """ - import sys - # Check if pytest or unittest is running if any( test_module in sys.modules for test_module in ["pytest", "unittest", "nose"] @@ -33,11 +32,14 @@ def is_running_tests(): # pragma: no cover # Check for common test runner names in command-line arguments test_runners = ["pytest", "unittest", "nose", "trial", "nox", "tox"] - if any(runner in sys.argv[0].lower() for runner in test_runners): + if any(runner in arg.lower() for arg in sys.argv for runner in test_runners): return True # Check if building docs with Sphinx - if "sphinx" in sys.modules: + if any(mod == "sphinx" or mod.startswith("sphinx.") for mod in sys.modules): + print( + f"Found Sphinx module: {[mod for mod in sys.modules if mod.startswith('sphinx')]}" + ) return True return False @@ -67,24 +69,47 @@ def ask_user_opt_in(timeout=10): "For more information, see https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry" ) + def get_input(): + try: + user_input = ( + input("Do you want to enable telemetry? (Y/n): ").strip().lower() + ) + answer.append(user_input) + except Exception: + # Handle any input errors + pass + + time_start = time.time() + while True: - print("Do you want to enable telemetry? (Y/n): ", end="", flush=True) + if time.time() - time_start > timeout: + print("\nTimeout reached. Defaulting to not enabling telemetry.") + return False + + answer = [] + # Create and start input thread + input_thread = threading.Thread(target=get_input) + input_thread.daemon = True + input_thread.start() + + # Wait for either timeout or input + input_thread.join(timeout) - rlist, _, _ = select.select([sys.stdin], [], [], timeout) - if rlist: - user_input = sys.stdin.readline().strip().lower() - if user_input in ["yes", "y", ""]: + if answer: + if answer[0] in ["yes", "y", ""]: + print("\nTelemetry enabled.\n") return True - elif user_input in ["no", "n"]: + elif answer[0] in ["no", "n"]: + print("\nTelemetry disabled.\n") return False else: - print("Invalid input. Please enter 'yes/y' for yes or 'no/n' for no.") + print("\nInvalid input. Please enter 'yes/y' for yes or 'no/n' for no.") else: print("\nTimeout reached. Defaulting to not enabling telemetry.") return False -def generate(): # pragma: no cover +def generate(): if is_running_tests(): return @@ -101,7 +126,7 @@ def generate(): # pragma: no cover pybamm.telemetry.capture("user-opted-in") -def read(): # pragma: no cover +def read(): config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml" return read_uuid_from_file(config_file) @@ -121,7 +146,7 @@ def write_uuid_to_file(config_file, opt_in): def read_uuid_from_file(config_file): # Check if the config file exists - if not config_file.exists(): # pragma: no cover + if not config_file.exists(): return None # Read the UUID from the config file @@ -134,5 +159,5 @@ def read_uuid_from_file(config_file): config = yaml.safe_load(content) return config["pybamm"] - except (yaml.YAMLError, ValueError): # pragma: no cover + except (yaml.YAMLError, ValueError): return None diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 65d42bf168..62906b348d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -4,6 +4,8 @@ import pybamm import uuid +from pathlib import Path +import platformdirs class TestConfig: @@ -104,3 +106,52 @@ def mock_select(*args, **kwargs): captured = capsys.readouterr() assert "Do you want to enable telemetry? (Y/n):" in captured.out assert "Timeout reached. Defaulting to not enabling telemetry." in captured.out + + def test_generate_and_read(self, monkeypatch, tmp_path): + # Mock is_running_tests to return False + monkeypatch.setattr(pybamm.config, "is_running_tests", lambda: False) + + # Mock ask_user_opt_in to return True + monkeypatch.setattr(pybamm.config, "ask_user_opt_in", lambda: True) + + # Mock telemetry capture + capture_called = False + + def mock_capture(event): + nonlocal capture_called + assert event == "user-opted-in" + capture_called = True + + monkeypatch.setattr(pybamm.telemetry, "capture", mock_capture) + + # Mock config directory + monkeypatch.setattr(platformdirs, "user_config_dir", lambda x: str(tmp_path)) + + # Test generate() creates new config + pybamm.config.generate() + + # Verify config was created + config = pybamm.config.read() + assert config is not None + assert config["enable_telemetry"] is True + assert "uuid" in config + assert capture_called is True + + # Test generate() does nothing if config exists + capture_called = False + pybamm.config.generate() + assert capture_called is False + + def test_read_uuid_from_file_no_file(self): + config_dict = pybamm.config.read_uuid_from_file(Path("nonexistent_file.yml")) + assert config_dict is None + + def test_read_uuid_from_file_invalid_yaml(self, tmp_path): + # Create a temporary directory and file with invalid YAML content + invalid_yaml = tmp_path / "invalid_yaml.yml" + with open(invalid_yaml, "w") as f: + f.write("invalid: yaml: content:") + + config_dict = pybamm.config.read_uuid_from_file(invalid_yaml) + + assert config_dict is None