Move version functionality to its own `helpers/version.py` file
What changed, and why it matters
This commit simply moves existing version-checking code from one file to a new dedicated helper file. It is a routine code cleanup with no visible security implications.
No security action required; treat as normal refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extracts get_version() and get_last_src_edit() from Controller into a new Version utility class in helpers/version.py. Callers in screensaver.py, settings_views.py, and settings_screens.py are updated accordingly. The logic remains functionally the same, with minor defensive improvements: get_last_src_edit() now catches exceptions and returns None instead of crashing, and VersionScreen only displays the last-edit timestamp when one is available. No security-sensitive behavior is introduced or removed.
Changed components
src/seedsigner/controller.pysrc/seedsigner/helpers/version.pysrc/seedsigner/gui/screens/settings_screens.pysrc/seedsigner/views/screensaver.pysrc/seedsigner/views/settings_views.pyInspect captured patch +141 / −81
diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py
index 877e348..c2ecf96 100644
--- a/src/seedsigner/controller.py
+++ b/src/seedsigner/controller.py
@@ -23,9 +23,6 @@ from seedsigner.views.view import Destination
logger = logging.getLogger(__name__)
-VERSION = "0.8.6"
-
-
class BackStack(list[Destination]):
def __repr__(self):
@@ -478,70 +475,3 @@ class Controller(Singleton):
exception_msg,
]
return Destination(UnhandledExceptionView, view_args={"error": error}, clear_history=True)
-
-
- def get_version(self) -> str:
- """
- Will attempt to read the current git branch name or commit hash from
- .git/HEAD. But if there's no git info available, it will fall back to the
- hard-coded Controller.VERSION.
- """
- name = f"v{VERSION}"
-
- # .git/HEAD will be in the project root, if it exists
- git_HEAD_dir = os.getcwd()
-
- # Main app runs from src/ dir, tests and screenshot generator from project root
- if "src" in git_HEAD_dir:
- git_HEAD_dir = os.path.join(git_HEAD_dir, "..")
- git_HEAD_path = os.path.join(git_HEAD_dir, ".git", "HEAD")
-
- if os.path.exists(git_HEAD_path):
- with open(git_HEAD_path, "r") as f:
- git_ref = f.read().strip()
- if git_ref.startswith("ref:"):
- # HEAD format: "ref: refs/heads/some_branch_name"
- name = git_ref.split("/")[-1]
- else:
- # If we're on a detached HEAD, the contents will just be the current
- # commit hash.
- name = git_ref[:7]
-
- # Check the .git/refs/tags dir for a tag matching this commit hash
- git_refs_tags_dir = os.path.join(git_HEAD_dir, ".git", "refs", "tags")
- if os.path.exists(git_refs_tags_dir):
- for tag_filename in os.listdir(git_refs_tags_dir):
- tag_path = os.path.join(git_refs_tags_dir, tag_filename)
- with open(tag_path, "r") as tag_file:
- # Tag files just contain their associated commit hash
- tag_commit_hash = tag_file.read().strip()
- if tag_commit_hash == git_ref:
- # Filename is the tag name
- name = f"v{tag_filename}"
- break
-
- return name
-
-
- def get_last_src_edit(self) -> datetime:
- """
- Recursively scan the src/ directory for the most recent python file edit time and
- return it.
- """
- src_path = os.getcwd()
- if "src" not in src_path:
- # Screenshot generator runs from the project root
- src_path = os.path.join(src_path, "src")
-
- latest_edit = 0
- for dirpath, dirnames, filenames in os.walk(src_path):
- if "__pycache__" in dirpath:
- continue
- for filename in filenames:
- if filename.endswith(".py"):
- filepath = os.path.join(dirpath, filename)
- mtime = os.path.getmtime(filepath)
- if mtime > latest_edit:
- latest_edit = mtime
-
- return datetime.fromtimestamp(latest_edit)
\ No newline at end of file
diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py
index 14e27d2..2c1ed82 100644
--- a/src/seedsigner/gui/screens/settings_screens.py
+++ b/src/seedsigner/gui/screens/settings_screens.py
@@ -353,12 +353,13 @@ class VersionScreen(BaseTopNavScreen):
screen_y=screen_y,
))
- last_edit_str = self.last_edit.strftime("%Y-%m-%d\n%H:%M:%S UTC")
- self.components.append(TextArea(
- text=last_edit_str,
- is_text_centered=True,
- screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
- ))
+ if self.last_edit:
+ last_edit_str = self.last_edit.strftime("%Y-%m-%d\n%H:%M:%S UTC")
+ self.components.append(TextArea(
+ text=last_edit_str,
+ is_text_centered=True,
+ screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
+ ))
diff --git a/src/seedsigner/helpers/version.py b/src/seedsigner/helpers/version.py
new file mode 100644
index 0000000..db1d4ab
--- /dev/null
+++ b/src/seedsigner/helpers/version.py
@@ -0,0 +1,129 @@
+import logging
+import os
+from datetime import datetime
+
+
+logger = logging.getLogger(__name__)
+
+
+
+class Version:
+ """
+ Utility class to detect the current version and the last edit time of the source code.
+
+ Version detection attempts to read the current git branch, commit hash, or tag but
+ will fall back to the hard-coded VERSION constant if no git info is available.
+
+ Internal utility functions are separated out as class methods for easier mocking in tests.
+ """
+
+ VERSION = "0.8.6"
+
+
+ @classmethod
+ def _get_dot_git_dir(cls) -> str:
+ # .git will be in the project root, if it exists
+ git_HEAD_dir = os.getcwd()
+
+ # Main app runs from src/ dir, tests and screenshot generator from project root
+ if "src" in git_HEAD_dir:
+ git_HEAD_dir = git_HEAD_dir.rsplit("src", 1)[0]
+
+ return os.path.join(git_HEAD_dir, ".git")
+
+
+ @classmethod
+ def _read_HEAD_file(cls) -> tuple[str,str]:
+ git_HEAD_path = os.path.join(cls._get_dot_git_dir(), "HEAD")
+
+ branch_name = None
+ commit_hash = None
+ if os.path.exists(git_HEAD_path):
+ with open(git_HEAD_path, "r") as f:
+ git_ref = f.read().strip()
+ if git_ref.startswith("ref:"):
+ # HEAD format: "ref: refs/heads/some_branch_name"
+ branch_name = git_ref.split("/")[-1]
+ else:
+ # If we're on a detached HEAD, the contents will just be the current
+ # commit hash.
+ commit_hash = git_ref
+ return (branch_name, commit_hash)
+
+
+ @classmethod
+ def _get_matching_tag(cls, commit_hash: str) -> str:
+ # Check the .git/refs/tags dir for a tag matching this commit hash
+ git_refs_tags_dir = os.path.join(cls._get_dot_git_dir(), "refs", "tags")
+ if os.path.exists(git_refs_tags_dir):
+ for tag_filename in os.listdir(git_refs_tags_dir):
+ tag_path = os.path.join(git_refs_tags_dir, tag_filename)
+ with open(tag_path, "r") as tag_file:
+ # Tag files just contain their associated commit hash
+ tag_commit_hash = tag_file.read().strip()
+ if tag_commit_hash == commit_hash:
+ # Filename is the tag name
+ return tag_filename
+ return None
+
+
+ @classmethod
+ def get_version(cls) -> str:
+ """
+ Will attempt to read the current git branch name or commit hash from
+ .git/HEAD. But if there's no git info available, it will fall back to the
+ hard-coded VERSION constant.
+ """
+ name = f"v{cls.VERSION}"
+
+ branch_name, commit_hash = cls._read_HEAD_file()
+ if branch_name:
+ name = branch_name
+ elif commit_hash:
+ # See if this commit_hash matches a tag
+ matching_tag = cls._get_matching_tag(commit_hash)
+ if matching_tag:
+ name = matching_tag
+ else:
+ name = commit_hash[:7] # short commit hash
+
+ return name
+
+
+ @classmethod
+ def get_last_src_edit(cls) -> datetime:
+ """
+ Recursively scan the src/ directory for the most recent python file edit time.
+ """
+ try:
+ src_path = os.getcwd()
+ if "src" not in src_path:
+ # Screenshot generator runs from the project root
+ src_path = os.path.join(src_path, "src")
+
+ last_modified = 0.0
+ num_files = 0
+ for dirpath, dirnames, filenames in os.walk(src_path):
+ if "__pycache__" in dirpath:
+ continue
+ for filename in filenames:
+ if filename.endswith(".py"):
+ num_files += 1
+ filepath = os.path.join(dirpath, filename)
+
+ # getmtime
+ file_mtime = os.path.getmtime(filepath)
+ last_modified = max(file_mtime, last_modified)
+
+ # Sanity check
+ if num_files == 0:
+ raise Exception(f"No python source files found in {src_path}")
+
+ return datetime.fromtimestamp(last_modified)
+
+ except Exception as e:
+ # Catch and log any unexpected errors but this isn't a mission-critical
+ # function so return gracefully.
+ import traceback
+ logger.error(traceback.format_exc())
+ return None
\ No newline at end of file
diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py
index 25be5cc..3160626 100644
--- a/src/seedsigner/views/screensaver.py
+++ b/src/seedsigner/views/screensaver.py
@@ -8,6 +8,7 @@ from gettext import gettext as _
from seedsigner.gui.components import Fonts, GUIConstants, load_image
from seedsigner.gui.screens.screen import BaseScreen
+from seedsigner.helpers.version import Version
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.views.view import View
@@ -97,7 +98,7 @@ class OpeningSplashScreen(LogoScreen):
# Display version num below SeedSigner logo
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
- version = controller.get_version()
+ version = Version.get_version()
# The logo png is 240x240, but the actual logo is 70px tall, vertically centered
logo_height = 70
diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py
index a35631d..85b446e 100644
--- a/src/seedsigner/views/settings_views.py
+++ b/src/seedsigner/views/settings_views.py
@@ -328,13 +328,12 @@ class DonateView(View):
class VersionView(View):
def run(self):
- from seedsigner.controller import Controller
- controller = Controller.get_instance()
+ from seedsigner.helpers.version import Version
self.run_screen(
settings_screens.VersionScreen,
- version=controller.get_version(),
- last_edit=controller.get_last_src_edit(),
+ version=Version.get_version(),
+ last_edit=Version.get_last_src_edit(),
)
return Destination(SettingsMenuView)
\ No newline at end of file
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.