From 4ecc7901b83e57575fd05555548f711ce21ebac0 Mon Sep 17 00:00:00 2001 From: Pavel Baksy Date: Tue, 6 Jan 2026 01:45:36 +0100 Subject: [PATCH] Fix variable indicators and add project caching --- src/project_manager.py | 30 ++++++++++++-- src/request_tab_widget.py | 76 +++++++++++++++++++++++++----------- src/variable_substitution.py | 7 +++- src/window.py | 3 +- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/project_manager.py b/src/project_manager.py index 231d979..d8ef260 100644 --- a/src/project_manager.py +++ b/src/project_manager.py @@ -42,25 +42,44 @@ class ProjectManager: self.projects_file = self.data_dir / 'requests.json' self._ensure_data_dir() + # In-memory cache for projects to reduce disk I/O + self._projects_cache: Optional[List[Project]] = None + def _ensure_data_dir(self): """Create data directory if it doesn't exist.""" self.data_dir.mkdir(parents=True, exist_ok=True) - def load_projects(self) -> List[Project]: - """Load projects from JSON file.""" + def load_projects(self, force_reload: bool = False) -> List[Project]: + """ + Load projects from JSON file with in-memory caching. + + Args: + force_reload: If True, bypass cache and reload from disk + + Returns: + List of Project objects + """ + # Return cached data if available and not forcing reload + if self._projects_cache is not None and not force_reload: + return self._projects_cache + + # Load from disk if not self.projects_file.exists(): + self._projects_cache = [] return [] try: with open(self.projects_file, 'r') as f: data = json.load(f) - return [Project.from_dict(p) for p in data.get('projects', [])] + self._projects_cache = [Project.from_dict(p) for p in data.get('projects', [])] + return self._projects_cache except Exception as e: logger.error(f"Error loading projects: {e}") + self._projects_cache = [] return [] def save_projects(self, projects: List[Project]): - """Save projects to JSON file.""" + """Save projects to JSON file and update cache.""" try: data = { 'version': 1, @@ -68,6 +87,9 @@ class ProjectManager: } with open(self.projects_file, 'w') as f: json.dump(data, f, indent=2) + + # Update cache with the saved data + self._projects_cache = projects except Exception as e: logger.error(f"Error saving projects: {e}") diff --git a/src/request_tab_widget.py b/src/request_tab_widget.py index 469ee55..49003ba 100644 --- a/src/request_tab_widget.py +++ b/src/request_tab_widget.py @@ -60,6 +60,7 @@ class RequestTabWidget(Gtk.Box): self.env_separator: Optional[Gtk.Separator] = None self.undefined_variables: Set[str] = set() self._update_indicators_timeout_id: Optional[int] = None + self._is_programmatically_changing_environment: bool = False # Build the UI self._build_ui() @@ -819,6 +820,10 @@ class RequestTabWidget(Gtk.Box): if request.syntax in syntax_options: self.body_language_dropdown.set_selected(syntax_options.index(request.syntax)) + # Update variable indicators after loading the request (if project is set) + if self.project_id: + self._update_variable_indicators() + def get_request(self) -> HttpRequest: """Build and return HttpRequest from current UI state.""" method = self.method_dropdown.get_selected_item().get_string() @@ -1152,8 +1157,8 @@ class RequestTabWidget(Gtk.Box): self.environment_dropdown.set_enable_search(False) self.environment_dropdown.set_tooltip_text("Select environment for variable substitution") - # Connect signal - self.environment_dropdown.connect("notify::selected", self._on_environment_changed) + # Connect signal handler + self._env_change_handler_id = self.environment_dropdown.connect("notify::selected", self._on_environment_changed) # Add to container at the beginning (left side) container.prepend(self.environment_dropdown) @@ -1178,31 +1183,40 @@ class RequestTabWidget(Gtk.Box): if not project: return - # Build string list with "None" + environment names - string_list = Gtk.StringList() - string_list.append("None") + # Set flag to indicate we're programmatically changing the dropdown + # This prevents the signal handler from triggering indicator updates during initialization + self._is_programmatically_changing_environment = True - # Track environment IDs (index 0 is None) - self.environment_ids = [None] + try: + # Build string list with "None" + environment names + string_list = Gtk.StringList() + string_list.append("None") - for env in project.environments: - string_list.append(env.name) - self.environment_ids.append(env.id) + # Track environment IDs (index 0 is None) + self.environment_ids = [None] - self.environment_dropdown.set_model(string_list) + for env in project.environments: + string_list.append(env.name) + self.environment_ids.append(env.id) - # Select current environment - if self.selected_environment_id: - try: - index = self.environment_ids.index(self.selected_environment_id) - self.environment_dropdown.set_selected(index) - except ValueError: + self.environment_dropdown.set_model(string_list) + + # Select current environment + if self.selected_environment_id: + try: + index = self.environment_ids.index(self.selected_environment_id) + self.environment_dropdown.set_selected(index) + except ValueError: + self.environment_dropdown.set_selected(0) # Default to "None" + else: self.environment_dropdown.set_selected(0) # Default to "None" - else: - self.environment_dropdown.set_selected(0) # Default to "None" - # Update indicators after environment is set - self._update_variable_indicators() + finally: + # Always clear the flag + self._is_programmatically_changing_environment = False + + # Note: Don't update indicators here as the request might not be loaded yet + # Indicators will be updated when environment changes or request is loaded def _show_environment_selector(self): """Show environment selector (create if it doesn't exist).""" @@ -1226,17 +1240,29 @@ class RequestTabWidget(Gtk.Box): # Populate if project_manager is available if self.project_manager: self._populate_environment_dropdown() + # Update indicators after environment selector is shown + self._update_variable_indicators() def _on_environment_changed(self, dropdown, _param): """Handle environment selection change.""" + # If we're programmatically changing the environment during population, skip this + if self._is_programmatically_changing_environment: + return + if not hasattr(self, 'environment_ids'): + logger.warning("Environment changed but environment_ids not initialized") return selected_index = dropdown.get_selected() + if selected_index < len(self.environment_ids): self.selected_environment_id = self.environment_ids[selected_index] - # Update visual indicators when environment changes - self._update_variable_indicators() + else: + logger.warning(f"Environment index {selected_index} out of range (max {len(self.environment_ids)-1})") + self.selected_environment_id = None + + # Always update visual indicators when environment changes + self._update_variable_indicators() def get_selected_environment(self): """Get the currently selected environment object.""" @@ -1303,6 +1329,10 @@ class RequestTabWidget(Gtk.Box): def _update_variable_indicators(self): """Update visual indicators for undefined variables.""" + # Only update if we have a project (variables only make sense in project context) + if not self.project_id: + return + # Detect undefined variables self.undefined_variables = self._detect_undefined_variables() diff --git a/src/variable_substitution.py b/src/variable_substitution.py index 257dbfd..c1e3282 100644 --- a/src/variable_substitution.py +++ b/src/variable_substitution.py @@ -64,8 +64,11 @@ class VariableSubstitution: var_name = match.group(1) if var_name in variables: value = variables[var_name] - # Replace with value or empty string if value is None/empty - return value if value else "" + # Check if value is empty/None - treat as undefined for warning purposes + if not value: + undefined_vars.append(var_name) + return "" + return value else: # Variable not defined - track it and replace with empty string undefined_vars.append(var_name) diff --git a/src/window.py b/src/window.py index 3de4bc0..f7fe8fb 100644 --- a/src/window.py +++ b/src/window.py @@ -950,7 +950,8 @@ class RosterWindow(Adw.ApplicationWindow): def _on_manage_environments(self, widget, project): """Show environments management dialog.""" - # Reload project from disk to get latest data (including variables created by scripts) + # Get latest project data (includes variables created by scripts) + # Note: load_projects() uses cached data that's kept up-to-date by save operations projects = self.project_manager.load_projects() fresh_project = None for p in projects: