Skip to content
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
34 changes: 32 additions & 2 deletions Addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import datetime
import os
import re
from urllib.parse import urlparse
from urllib.parse import urlparse, urlunparse
from typing import Set, List, Optional
from threading import Lock
from enum import IntEnum, auto
Expand Down Expand Up @@ -314,7 +314,13 @@ def set_metadata(self, metadata: Metadata) -> None:
"""

self.metadata = metadata
self.display_name = metadata.name
self.display_name = (
str(metadata.name)
.replace("\n", " ")
.replace("\r", " ")
.replace("{", "")
.replace("}", "")
Comment on lines +317 to +322
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The display name sanitization removes curly braces unconditionally. This could potentially break legitimate display names that use braces for formatting. Consider a more targeted approach or document why these characters need to be removed.

Suggested change
self.display_name = (
str(metadata.name)
.replace("\n", " ")
.replace("\r", " ")
.replace("{", "")
.replace("}", "")
# Only sanitize newlines and carriage returns; allow curly braces in display names.
self.display_name = (
str(metadata.name)
.replace("\n", " ")
.replace("\r", " ")

Copilot uses AI. Check for mistakes.
)
self.repo_type = Addon.Kind.PACKAGE
self.description = metadata.description
for url in metadata.url:
Expand Down Expand Up @@ -633,6 +639,30 @@ def _find_classname_in_file(current_file) -> str:
pass
return ""

def get_zip_url(self) -> str:
if self.url.endswith(".zip"):
zip_url = self.url
else:
# The ZIP url is based on the location of the main cache file:
if self.relative_cache_path:
cache_file_url = fci.Preferences().get("addon_catalog_cache_url")
parsed_url = urlparse(cache_file_url)
path_parts = parsed_url.path.rpartition("/")
new_path = path_parts[0] + "/" + self.relative_cache_path
zip_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
new_path,
parsed_url.params,
parsed_url.query,
parsed_url.fragment,
)
)
else:
zip_url = utils.get_zip_url(self)
return zip_url


# @dataclass(frozen)
class MissingDependencies:
Expand Down
26 changes: 13 additions & 13 deletions AddonManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,7 @@ def launch(self) -> None:
self.composite_view = CompositeView(self.dialog)
self.button_bar = WidgetGlobalButtonBar(self.dialog)

# If we are checking for updates automatically, hide the Check for updates button:
autocheck = fci.Preferences().get("AutoCheck")
if autocheck:
self.button_bar.check_for_updates.hide()
else:
self.button_bar.update_all_addons.hide()
self.button_bar.check_for_updates.hide()

# Set up the listing of packages using the model-view-controller architecture
self.item_model = PackageListItemModel()
Expand Down Expand Up @@ -471,13 +466,6 @@ def select_addon(self) -> None:
def check_updates(self) -> None:
"""checks every installed addon for available updates"""

autocheck = fci.Preferences().get("AutoCheck")
if not autocheck:
fci.Console.PrintLog(
"Addon Manager: Skipping update check because AutoCheck user preference is False\n"
)
self.do_next_startup_phase()
return
if not self.packages_with_updates:
self.force_check_updates(standalone=False)
else:
Expand Down Expand Up @@ -538,6 +526,18 @@ def update_check_complete(self) -> None:

def check_python_updates(self) -> None:
# TODO: Run the checker to see if we need to do any Python updates as well

# Really, there are two different things to check here: first, run our normal dependency
# checker and display the dependency resolution dialog. This will handle addons that have
# disappeared/been uninstalled (but were required by other addons) as well as Python required
# and optional dependencies. The only catch is, if we ONLY have optional Python dependencies
# missing, we should ignore them.

# Second, if this is a version of Python we've used before, do any of our Python libraries
# installed into the custom directory, or the venv, need to be updated?

# To the user these are two quite different things, so their interface should reflect that.

self.do_next_startup_phase()

def show_python_updates_dialog(self) -> None:
Expand Down
165 changes: 10 additions & 155 deletions AddonManagerOptions.ui
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,18 @@
<string>Addon Manager Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxcheckupdates">
<property name="toolTip">
<string>Checks for updates of installed addons when launching the Addon Manager</string>
</property>
<property name="text">
<string>Automatically check for updates at start (requires Git)</string>
</property>
<property name="autoExclusive">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>AutoCheck</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhideunlicensed">
<property name="text">
Expand Down Expand Up @@ -84,54 +74,6 @@
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidepy2">
<property name="text">
<string>Hide addons marked Python 2 only</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>HidePy2</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhideobsolete">
<property name="text">
<string>Hide addons marked obsolete</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>HideObsolete</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidenewerfreecadrequired">
<property name="text">
<string>Hide addons that require a newer version of FreeCAD</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>HideNewerFreeCADRequired</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
Expand Down Expand Up @@ -286,83 +228,6 @@
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Path to Git executable (optional)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::PrefFileChooser" name="gui::preffilechooser" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>The path to the Git executable. Autodetected if needed and not specified.</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>GitExecutable</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="advanced">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Advanced Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxShowBranchSwitcher">
<property name="text">
<string>Show option to change branches (requires Git)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>ShowBranchSwitcher</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxDisableGit">
<property name="text">
<string>Disable Git (fall back to ZIP downloads only)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>disableGit</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Addons</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
Expand All @@ -384,11 +249,6 @@
<extends>QCheckBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefComboBox</class>
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefRadioButton</class>
<extends>QRadioButton</extends>
Expand All @@ -399,11 +259,6 @@
<extends>QLineEdit</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefFileChooser</class>
<extends>QWidget</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
Expand Down
3 changes: 3 additions & 0 deletions AddonManagerTest/app/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ def set_status(self, status):
def get_best_icon_relative_path():
return ""

def get_zip_url(self):
return self.url


class MockMacro:
"""Minimal Macro class"""
Expand Down
5 changes: 4 additions & 1 deletion AddonManagerTest/app/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def test_install_by_git(self):
mock_addon.url = os.path.join(temp_dir, "test_repo")
mock_addon.branch = "main"
installer = AddonInstaller(mock_addon, [])
installer.git_manager = git_manager # Make sure it's been created
installer.installation_path = os.path.join(temp_dir, "installed_addon")
installer._install_by_git()

Expand Down Expand Up @@ -247,6 +248,7 @@ def test_determine_install_method_local_path(self):
self.assertEqual(method, InstallationMethod.COPY)
git_manager = initialize_git()
if git_manager:
installer.git_manager = git_manager
method = installer._determine_install_method(temp_dir, InstallationMethod.GIT)
self.assertEqual(method, InstallationMethod.GIT)
method = installer._determine_install_method(temp_dir, InstallationMethod.ZIP)
Expand All @@ -264,6 +266,7 @@ def test_determine_install_method_file_url(self):
self.assertEqual(method, InstallationMethod.COPY)
git_manager = initialize_git()
if git_manager:
installer.git_manager = git_manager
method = installer._determine_install_method(temp_dir, InstallationMethod.GIT)
self.assertEqual(method, InstallationMethod.GIT)
method = installer._determine_install_method(temp_dir, InstallationMethod.ZIP)
Expand Down Expand Up @@ -358,7 +361,7 @@ def test_determine_install_method_https_known_sites_any_gm(self):
method = installer._determine_install_method(temp_file, InstallationMethod.ANY)
self.assertEqual(
method,
InstallationMethod.GIT,
InstallationMethod.ZIP,
f"Failed to allow git access to {site} URL",
)

Expand Down
Loading