Add immutable Monitor model - #566
Conversation
Replace monitor dictionaries with a frozen, slotted Monitor dataclass while preserving temporary string-key access. Update platform enumeration, legacy grab inputs, filename formatting, typing, tests, examples, and migration documentation.
Some samples would not work correctly. Also prepared for eventually phasing out string like access.
jholveck
left a comment
There was a problem hiding this comment.
I'm not really comfortable with this idea of making synthetic Monitor objects to send down through _impl.grab, rather than sending capture regions as dicts or tuples. Can you elaborate on why you made that choice?
- MSS.grab() still accepts Monitor, dictionaries, and tuples, but normalizes them to CaptureRegion. - Platform backends and custom screenshot classes now receive regions, never synthetic monitors. - Added Monitor.as_capture_region(). - Applied the Linux **output_ids and comment cleanups. - Fixed the demo import grouping. - Documented why monitors are immutable. - Added test_wheel guidance to AGENTS.md.
|
One overall thing. I'm wondering if the name |
Yeah, I think that (Besides, Food for thought: should a |
|
This is ready to merge. |
- Change code to prefer the new attribute access for Monitor and Region. - Update docs - Update tests
- MSS.grab() now snapshots caller-owned Region objects before validation/backend use in src/mss/base.py:318. - Added a custom-image snapshot regression test and separated Region grab coverage. - Migrated five documentation examples to Region, retaining one explicit dictionary compatibility example.
|
/cc @jholveck I think you synced with @halldorfannar already, just checking :) |
jholveck
left a comment
There was a problem hiding this comment.
I think it's good as-is. Unlike the previous revision, I don't have any major structural issues here.
I would suggest we commit it as it stands, but I do have several suggestions. Many of these are documentation suggestions. Many others are for code that's just near the edits, and not really part of @halldorfannar 's proposed changes, but I just happened to notice them. I've put in suggestions for most of them, but most of these haven't been tested, or even run them through check.sh.
As I said, I suggest we commit this as it stands, but also consider these other suggestions for follow-up commits, or to split into new issues.
| @@ -224,7 +224,7 @@ def main() -> None: | |||
| monitor = sct.monitors[1] | |||
There was a problem hiding this comment.
Now that we have primary_monitor, we should use that.
| monitor = sct.monitors[1] | |
| monitor = sct.primary_monitor |
|
|
||
| :param value: The capture area string to validate. | ||
| :returns: Dict with 'left', 'top', 'width', 'height' keys. | ||
| :returns: Capture region. |
There was a problem hiding this comment.
I normally don't leave in :returns: if it's obvious from the context and return type. I do for dicts, to clarify the keys, but I don't think it's needed for Region.
| :returns: Capture region. |
|
|
||
| with mss.MSS() as sct: | ||
| sct.cls_image = SimpleScreenShot | ||
| image = sct.grab(sct.monitors[1]) |
There was a problem hiding this comment.
While we're in here...
| image = sct.grab(sct.primary_monitor) |
| To maintain compatibility: | ||
|
|
||
| - dictionary-style access will continue to work | ||
| - string-key access will temporarily continue to work |
There was a problem hiding this comment.
Just to make it clear even to new programmers:
| - string-key access will temporarily continue to work | |
| - string-key (dictionary-style) access will temporarily continue to work |
| {py:class}`mss.MSS` instance. Use attributes to read geometry and metadata: | ||
|
|
||
| ```python | ||
| monitor = sct.monitors[1] |
There was a problem hiding this comment.
Just to get people in the habit of thinking about primary_monitor instead of monitors[1], I suggest we use that.
| monitor = sct.monitors[1] | |
| monitor = sct.primary_monitor # or monitors[1], etc. |
| with mss_impl() as sct: | ||
| monitor = sct.monitors[1] | ||
| with pytest.raises(FormattingCompleteError): | ||
| next(sct.save(mon=1, output=fmt, callback=capture_filename)) |
There was a problem hiding this comment.
Seeing this next makes me wonder: is there a reason that we make save a generator, instead of saving all the screenshots immediately and returning a list? I looked at the commit history, and this actually goes back to the very first MSS code, in 237a744. But it does seem a bit surprising to me that we're doing it this way.
If we want to change it, I can open a new issue.
There was a problem hiding this comment.
Looking at that commit makes me nostalgic haha.
I do not remember why I did a generator here, maybe was I thinking about memory resources optimization.
Anyway, we can change it, yes!
| The compatibility access does not make `Monitor` a complete mapping. Migrate dictionary methods, membership tests, and | ||
| unpacking to attribute access: | ||
|
|
||
| ```python | ||
| monitor.left | ||
| monitor.top | ||
| ``` | ||
|
|
There was a problem hiding this comment.
I might suggest that we either list all the attributes, or even just the four coordinate ones, or if we want a brief list, just put them inline. I think that just a couple inline would be fine here.
| The compatibility access does not make `Monitor` a complete mapping. Migrate dictionary methods, membership tests, and | |
| unpacking to attribute access: | |
| ```python | |
| monitor.left | |
| monitor.top | |
| ``` | |
| The compatibility access does not make `Monitor` a complete mapping. Migrate dictionary methods, membership tests, and | |
| unpacking to attribute access, such as ``monitor.left`` and ``monitor.top``. |
| ``width``, and ``height`` attributes. The ``is_primary``, ``name``, ``unique_id``, and ``output`` metadata attributes | ||
| are ``None`` when unavailable:: | ||
|
|
||
| monitor = sct.monitors[1] |
There was a problem hiding this comment.
| monitor = sct.monitors[1] | |
| monitor = sct.primary_monitor |
| monitor = sct.monitors[1] | ||
| print(monitor.width, monitor.height) | ||
|
|
||
| Call ``monitor.as_region()`` when you need its geometry as a :py:class:`mss.models.Region`:: |
There was a problem hiding this comment.
This "Capturing Screenshots" section is meant to be a "how to get started" introduction to MSS. It tries to focus on the essentials, and avoid confusing new users.
With that in mind, most of this seems to be just an unnecessary distraction. It increases the cognitive load on new users. For instance, are new users who are just learning the basics likely to care that **monitor unpacking is not supported? It's just extra stuff that they don't have to care about, but still have to spend cognitive cycles to decide if they care.
For the purposes of this flow, I think that explaining all about a Monitor object may be a bit too much. I might suggest putting the details in a separate section or subsection, or even just using some cross-references into the API reference for some details. Putting some of this into a separate section can help with cognitive chunking, and let the user get a basic flow before trying to absorb a lot of the details.
| elif isinstance(region, dict): | ||
| grab_region = Region( | ||
| left=region["left"], | ||
| top=region["top"], | ||
| width=region["width"], | ||
| height=region["height"], | ||
| ) |
There was a problem hiding this comment.
May want to do type coercion here.
| elif isinstance(region, dict): | |
| grab_region = Region( | |
| left=region["left"], | |
| top=region["top"], | |
| width=region["width"], | |
| height=region["height"], | |
| ) | |
| elif isinstance(region, dict): | |
| grab_region = Region( | |
| left=int(region["left"]), | |
| top=int(region["top"]), | |
| width=int(region["width"]), | |
| height=int(region["height"]), | |
| ) |
Agreed. I merge, feel free to open a follow up PR. |
|
Thank you @halldorfannar 🥂 |
Changes proposed in this PR
Fixes #470
Replace heterogeneous monitor dictionaries with a frozen, slotted
Monitordataclass that exposes typed geometry andplatform metadata attributes.
Monitorobjects.grab().Regionclass andas_regionfactory function for Monitors; refactor code to use theseregions.
test_sdistwhen files are added, removed, or renamed.This gives callers a stable, read-only monitor representation without preventing existing applications from passing
dictionary-based capture regions.
./check.shpassedValidation:
uv run ./check.sh. The full pytest suite was not run during the final review.AI assistance disclosure
Codex assisted with reviewing the implementation, identifying and implementing follow-up fixes in the demos and
ScreenShotinitialization, updating the immutability test for static typing, running validation, and drafting this PRdescription. The contributor reviewed and directed the API and capture-region decisions.