') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix: allow lazy/translation proxy objects as State/Event name (#632) by fgmacedo · Pull Request #636 · fgmacedo/python-statemachine · GitHub
Skip to content

fix: allow lazy/translation proxy objects as State/Event name (#632) - #636

Merged
fgmacedo merged 2 commits into
developfrom
fix/632-lazy-translation-names
Jun 26, 2026
Merged

fix: allow lazy/translation proxy objects as State/Event name (#632)#636
fgmacedo merged 2 commits into
developfrom
fix/632-lazy-translation-names

Conversation

@fgmacedo

Copy link
Copy Markdown
Owner

Fixes#632.

Problem

Lazy translation strings (e.g. django.utils.translation.gettext_lazy) are proxy objects that are not real str instances, although castable via str(). Changes in 3.x stored a State/Eventname as-is and later assumed it was a real str, so a proxy broke:

  • the TransitionNotAllowed message (str.join requires real str elements) — statemachine/exceptions.py;
  • str(state)State.__str__ returned the proxy directly, raising TypeError: __str__ returned non-string.

Fix

Coerce to strat the point of use, keeping the lazy object untouched in storage. Coercing at assignment would resolve the translation at class-definition time, defeating the purpose of i18n (translation must resolve at display time, honoring the active locale).

  • exceptions.py: ", ".join(str(s.name) ...) in the TransitionNotAllowed message.
  • state.py: __str__ returns str(self.name); the hash uses str(self.name) for robust hashability regardless of locale.
  • contrib/diagram/extract.py: coerce names to str once at the domain→model boundary, so every renderer (dot/mermaid/table) receives real str.

Docs

  • Release notes entry in docs/releases/3.2.1.md.
  • Note in docs/states.md that name accepts any object castable to str, resolved at display time.

Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
@codecov

codecovBot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (aff3977) to head (bfadcb8).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@ Coverage Diff @@## develop #636 +/- ##
=========================================
Coverage 100.00% 100.00% =========================================
Files 52 52 Lines 5505 5505 Branches 869 869 =========================================
Hits 5505 5505 
FlagCoverage Δ
unittests100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@fgmacedofgmacedo self-assigned this Jun 26, 2026
@fgmacedo
fgmacedo merged commit 471fe17 into developJun 26, 2026
13 checks passed
@fgmacedo
fgmacedo deleted the fix/632-lazy-translation-names branch June 26, 2026 23:56
@sonarqubecloud

Copy link
Copy Markdown

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Please allow translation strings (ie lazy proxy objects) as name

1 participant

@fgmacedo