Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Resource: fix a name when the resource is created, and refuse to change it later - #1228

Merged
BioCam merged 2 commits into
mainfrom
resource-immutable-name
Aug 31, 2026
Merged

Resource: fix a name when the resource is created, and refuse to change it later#1228
BioCam merged 2 commits into
mainfrom
resource-immutable-name

Conversation

@BioCam

@BioCamBioCam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

A resource's name is its identifier: it is unique across the tree, it is what get_resource looks a resource up by, and it is the key serialize_all_state writes state under. Until now it could still be reassigned after the resource existed, and anything already named after it was left behind.

The problem

A well is named three times before it is usable - well_0_0 when it is constructed, well_A1 by create_ordered_items_2d, and finally plate_well_A1 by ItemizedResource.__init__, which prefixes every item it is handed with its own name. The last two are renames, and the last one writes the plate's name into all 96 of its wells.

Because the plate itself can still be renamed, those wells can be left describing a plate that no longer exists:

plate=Cor_96_wellplate_360ul_Fb(name="plate") # wells: plate_well_A1 ...plate.name="sample_plate"# wells: plate_well_A1 ...

Nothing raises, but get_resource("sample_plate_well_A1") finds nothing and the plate's state is serialized under keys that no longer correspond to anything in the tree.

This PR

  • the name setter now refuses, raising AttributeError rather than RuntimeError - that is what Python raises for a read-only property, so setattr and anything catching attribute errors behave as they would for any other immutable attribute.
  • create_equally_spaced_2d takes a name_for callable, so each item is constructed with the name it will keep rather than renamed afterwards.
  • create_ordered_items_2d takes name_prefix, the name of the resource that will hold the items, and passes it down. This matches create_homogeneous_resources, which already takes a name_prefix and already constructs each site with its final name. Carriers have named their children this way all along; this brings the plate side onto the same convention. The two differ only in separator - carriers join with -, items with _, which is what ItemizedResource used and what keeps every name unchanged.
  • ItemizedResource.__init__ no longer renames what it is given.
  • the 167 call sites pass name_prefix: 155 pass name_prefix=name, the name their holder is being created with, and 12 in tests pass a literal. Four sites that build items by hand - two Opentrons loaders, the STAR teaching rack, and one test - name them after their holder directly.
  • named() writes the field rather than going through the setter, since the copy it returns did not exist a moment ago and belongs to no tree; it is being built, not renamed.

Behaviour: names are unchanged, verified byte for byte across 342 resources on a loaded STARlet deck, and named() returns what it always did. The only change a caller can see is that assigning to name now raises.

BioCamand others added 2 commits August 31, 2026 15:10
…ange it later
A name is what a resource is identified by - it is unique across a tree, it is
what `get_resource` finds a resource by, and it is the key its state is
serialized under. Anything named after it takes that name at construction, so a
name that changed afterwards left those disagreeing with it. The setter now
refuses, with `AttributeError` rather than `RuntimeError`, since that is what
Python raises for a read-only property.
`create_ordered_items_2d` built its items and then renamed them from
`well_0_0` to `well_A1`. It now passes the name it wants to the constructor, via
a `name_for` argument on `create_equally_spaced_2d`. No call site changes: the
naming was internal to both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming them
`ItemizedResource.__init__` renamed every item it was given, prefixing it with
its own name, so a well became `plate_well_A1` only after the plate existed.
That is the one rename a fixed name cannot allow. `create_ordered_items_2d`
takes the holder's name as `name_prefix` and passes it to each item's
constructor instead.
The 169 call sites pass `name_prefix=name`, the name the holder is being created
with. Four sites that build their items by hand - two Opentrons loaders, the STAR
teaching rack, and one test - name them after their holder directly.
`named()` writes the field rather than going through the setter: its copy did not
exist a moment ago and belongs to no tree, so it is being built rather than
renamed, and the setter cannot tell the two apart.
Names are unchanged: 342 resources on a loaded STARlet deck, byte for byte.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 833 to +837
def named(self, name: str) -> Self:
"""Return a copy of this resource with the given name."""
new_resource = self.copy()
new_resource.name = name
# The copy is new and in no tree, so this finishes building it rather than renaming anything.
new_resource._name = name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also good

@BioCam
BioCam merged commit 4b03d5e into mainAug 31, 2026
21 checks passed
@BioCam
BioCam deleted the resource-immutable-name branch August 31, 2026 21:42
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Sep 1, 2026
…gration
Brings in 17 commits, including PyLabRobot#1228, which moved item naming from
`ItemizedResource.__init__` into the item helpers. `n_channel_pipettes` builds
its mounting shafts through `create_ordered_items_2d` and did not pass
`name_prefix`, so the shafts came out as `tipmountingshaft_A1` rather than
`<pipette>_tipmountingshaft_A1`. Passing the pipette's name restores what they
were called before.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BioCam@rickwierenga