') + ')', '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); } })(); })();
Summary
The current
State.Parallelconstruct requires all regions to be statically declared as uniquely named inner classes at class-definition time. This makes it impossible to express a common real-world pattern:This is a feature discussion, not a bug. Multiple implementation paths exist; their tradeoffs are documented below. The goal of this issue is to settle on the right abstraction before any code is written. This is first mentioned on issue #614.
Motivation
Consider a
Warstatechart that must track several simultaneousBattlesub-machines. The number of battles is not known until the orchestrating machine is instantiated. The desired pseudo-code would be:Today this must be hand-wired through an orchestration wrapper, pushing coordination logic out of the statechart and into imperative Python. The result is boilerplate that the library should be able to absorb.
Current behavior and workaround
The closest existing hook is
create_machine_class_from_definition, which can build aStateChartsubclass from a dict at runtime. A dynamic parallel state with N regions can be assembled via:This works but has serious ergonomic costs:
StateChartsubclass body.Proposed alternatives
Four designs are outlined below in ascending order of implementation complexity. They are not mutually exclusive — proposals A and B could ship as near-term conveniences while C and D are discussed separately.
A —
State.Parallel.replicate(n, template)class-level factoryComplexity: 🟢 Low lift · Spec-conformant
A class method on
State.Parallelthat returns a newState.Parallelsubclass withnuniquely-named copies oftemplatebaked in at metaclass time.nmust be a literal integer — resolved at class definition.Tradeoff: Hardcodes
nat class definition. Does not solve the runtime-count use case. Lowest risk.B —
count=andregion=parameters onState.ParallelComplexity: 🟢 Low lift · Spec-conformant
Same semantics as A, but expressed via
__init_subclass__.Tradeoff: More readable than A, still static. Mostly syntax sugar over dict-based construction.
C —
State.DynamicParallelwith runtime region countComplexity: 🔴 High lift · Beyond SCXML
A new state type that defers region instantiation to
__init__.Tradeoff: This actually solves the problem. But:
done.statesemantics.D —
invoke=accepting a list of sub-machine instancesComplexity: 🟡 Medium lift · Spec-aligned
Generalize
invoketo accept a runtime list ofStateChartinstances.Tradeoff: Most spec-aligned approach. However:
In(state)).Prior art and references
spawnChild)Questions for @fgmacedo
create_machine_class_from_definitionwith atemplate + countshorthand be the preferred minimal solution?invokesupport lists, or is one-to-one intentional?contribmodule until stabilized?Environment