refactor(braille): split braille.py into a package#20252
Conversation
Adds the brainstorming design for issue nvaccess#12772: split source/braille.py into a braille/ package following the controlTypes precedent, preserving the public braille.X API. Module relocations and shims are deferred to a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task-by-task TDD plan for issue nvaccess#12772: anchor with a public-surface regression test, convert the module to a package, then extract labels/regions/buffer/display/_handler one submodule at a time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…__all__ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gions Move the Region class hierarchy, braille field/property helpers, and speak-on-navigation helpers from braille.__init__ into the new braille.regions submodule. Re-export public symbols from __init__ for backwards compatibility. Update tests to patch the canonical location. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…opyright headers - Rename braille/_handler.py → braille/brailleHandler.py (avoids shadowing braille.handler global) - Rename braille/buffer.py → braille/buffers.py - Extract 9 extension points into new braille/extensions.py - Update all braille submodule copyright headers to new 4-line GPL format - Update test_handlerExtensionPoints.py header and year to 2022-2026 - Fix patch() targets in tests to match new submodule names Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
@LeonarddeR you write:
Wouldn't getter/setter functions be more suitable here, with deprecation of |
|
@CyrilleB79 wrote:
I thought about this yes and I think the current approach is a method to keep the pr as clear as possible.
|
|
I reconsidered hiding |
…age facade The package split left several symbols in submodules that did not match their responsibility. Re-home them: - New braille/constants.py (leaf, no package imports) holds the value constants previously dumped in labels.py: cursor/selection shapes, input indicators, port constants, the text separator and focus-context presentation constants. - labels.py now contains only the role/state/landmark label dictionaries. - The formatting-marker types (FormatTagDelimiter, FormattingMarker, fontAttributeFormattingMarkers), DisplayDimensions, the focus-region helpers (getFocusContextRegions/getFocusRegions plus the shared _cachedFocusAncestorsEnd cache and invalidateCachedFocusAncestors) and FALLBACK_TABLE move into the package __init__, in original braille.py order. - formatCellsForLog moves to brailleHandler, its only consumer. Public surface (braille.* and __all__) is unchanged; test_publicSurface passes without edits. The interleaved submodule imports in __init__ carry noqa: E402 as they must follow the definitions they depend on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1500c61 to
ef799a8
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR converts the braille module into a package while preserving the existing braille.X public API surface, updating internal imports/patch targets, and adding regression tests to prevent accidental API drift.
Changes:
- Split
braille.pyinto asource/braille/package with submodules (handler, buffers, regions, display, etc.). - Updated unit tests to patch the new module paths and added a regression test to guard the public symbol surface.
- Documented the packaging change in user-facing changelog notes.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| user_docs/en/changes.md | Notes that braille is now a package and claims API compatibility. |
| source/braille/init.py | Re-exports the public API and defines __all__ to preserve braille.X access. |
| source/braille/regions.py | Houses region classes and braille text generation previously in braille.py. |
| source/braille/buffers.py | Contains BrailleBuffer and window/word-wrapping logic moved out of the monolith. |
| source/braille/brailleHandler.py | Core handler moved into the package; wires buffers/display/extensions together. |
| source/braille/display.py | Display drivers and gesture logic moved into the package. |
| source/braille/extensions.py | Defines extension points for braille output/display changes. |
| source/braille/constants.py | Centralizes constants previously in braille.py. |
| source/braille/labels.py | Centralizes label dictionaries previously in braille.py. |
| tests/unit/test_braille/test_publicSurface.py | New regression test ensuring braille.X and braille.__all__ stay stable. |
| tests/unit/test_braille/test_regionLanguageIndexes.py | Updates patch(...) targets to new submodule paths. |
| tests/unit/test_braille/test_calculateWindowRowBufferOffsets.py | Updates patch(...) targets to new submodule paths. |
| errorMessage = None | ||
| if errorMessage and State.INVALID_ENTRY in states: | ||
| errorMessage = field.get("errorMessage", None) |
There was a problem hiding this comment.
@SaschaCowley You introduced this in #16411.
I think it must be something like:
| errorMessage = None | |
| if errorMessage and State.INVALID_ENTRY in states: | |
| errorMessage = field.get("errorMessage", None) | |
| errorMessage = field.get("errorMessage", None) if State.INVALID_ENTRY in states else None |
| region, regionStart, regionEnd = list(self.regionsWithPositions)[-1] | ||
| # Show paragraph start indicator if it is now at the left of the current braille window | ||
| if startPos <= len(paragraphStartMarker) + 1: | ||
| startPos = self.regionPosToBufferPos(region, regionStart) |
There was a problem hiding this comment.
| region, regionStart, regionEnd = list(self.regionsWithPositions)[-1] | |
| # Show paragraph start indicator if it is now at the left of the current braille window | |
| if startPos <= len(paragraphStartMarker) + 1: | |
| startPos = self.regionPosToBufferPos(region, regionStart) | |
| region, regionStart, regionEnd = list(self.regionsWithPositions)[-1] | |
| # Show paragraph start indicator if it is now at the left of the current braille window | |
| if startPos <= len(paragraphStartMarker) + 1: | |
| startPos = regionStart |
| if self.handler.displaySize == 0: | ||
| return 0 | ||
| windowPos = max(min(windowPos, self.handler.displaySize), 0) | ||
| row, col = divmod(windowPos, self.handler.displayDimensions.numCols) | ||
| if row < len(self._windowRowBufferOffsets): | ||
| rowPositions = self._windowRowBufferOffsets[row] | ||
| return max(min(rowPositions.start + col, rowPositions.end - 1), 0) | ||
| raise ValueError("Position outside window") |
| msg = f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}" | ||
| if i + 1 >= maxIterations or (exceeded := time.time() - startTime > 0.5): | ||
| logFunc = log.exception | ||
| curPos = pos | ||
| if exceeded: | ||
| msg += ", exceeded time limit of 0.5 seconds" | ||
| else: | ||
| logFunc = log.debug | ||
| logFunc(msg) |
| def _getFormattingTags( | ||
| field: dict[str, str], | ||
| fieldCache: dict[str, str], | ||
| ) -> str | None: | ||
| """Get the formatting tags for the given field and cache. | ||
|
|
||
| Formatting tags are calculated according to the preferences passed in formatConfig. | ||
|
|
||
| :param field: The format current field. | ||
| :param fieldCache: The previous format field. | ||
| :param formatConfig: The user's formatting preferences. | ||
| :return: The braille formatting tag as a string, or None if no pertinant formatting is applied. | ||
| """ |
| @param currentCellCount: The current number of cells | ||
| @type currentCellCount: bool |
| Filter that allows components or add-ons to change the number of rows and columns used for braille output. | ||
| For example, when a system has a display with 10 rows and 20 columns, but is being controlled by a remote system with a display of 5 rows and 40 coluns, the display number of rows should be lowered to 5. |
|
Not sure what to do with the copilot comments here. Some of them look serious enough to fix, but I don't think this pr is the right place to do so. |
| @@ -0,0 +1,1485 @@ | |||
| # A part of NonVisual Desktop Access (NVDA) | |||
There was a problem hiding this comment.
can this file be split up further? e.g. turn regions into it's own submodule?
There was a problem hiding this comment.
Both display and regions are now split even more.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… directly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NVDAObject.py and textInfo.py now match the casing of the NVDAObjects and textInfos packages respectively. Update patch paths in tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Link to issue number:
Closes #12772
Summary of the issue:
source/braille.pyhad grown to ~4256 lines, making it difficult to navigate, review, and maintain.Description of user facing changes:
None. This is a pure refactor.
Description of developer facing changes:
The
braillemodule is now a package (source/braille/), split into focused submodules:braille.constants— value constants: cursor/selection shapes, untranslated-input indicators, port constants, the text separator, and focus-context presentation constants (leaf module, no intra-package imports)braille.labels— the role / state / landmark label dictionariesbraille.formatting— the font-formatting markers (FormatTagDelimiter,FormattingMarker,fontAttributeFormattingMarkers),getParagraphStartMarker, and the private tag-rendering helpers (_getFormattingTags,_appendFormattingMarker) (leaf module)braille.regions— a sub-package (source/braille/regions/) containing the fullRegionhierarchy and focus-region generators, split into focused modules:regions.base— the baseRegionclassregions.NVDAObject— theNVDAObjectRegionregions.properties— braille field/property helpersregions.textInfo—TextInfoRegionregions.focus—getFocusContextRegions,getFocusRegions,invalidateCachedFocusAncestors, and the_cachedFocusAncestorsEndcacheregions._routing— internal routing helpersbrailleHandler,buffers) import directly from the relevant submodule rather than through theregions/__init__.pyfacade.braille.buffers—BrailleBuffer(and_WindowRowPositions)braille.display— a sub-package (source/braille/display/) split into:display.driver—BrailleDisplayDriver,DisplayDimensions, and driver discoverydisplay.gesture—BrailleDisplayGesturebraille.brailleHandler—BrailleHandler,formatCellsForLog, andFALLBACK_TABLEbraille.extensions— all extension points (pre_writeCells,filter_displaySize,filter_displayDimensions,displaySizeChanged,displayChanged,decide_enabled, and the private Remote Access points)braille/__init__.pydefines only thehandlerglobal plusinitialize/pumpAll/terminate; everything else is re-exported from the submodules. Because every symbol now lives in a real submodule (no submodule imports names back from the package facade), all of__init__.py's imports sit at the top of the file with no# noqa: E402.The public API is unchanged: every symbol previously accessed as
braille.Xremains available through thebraille/__init__.pyfacade. The__all__list is now explicit. Private symbols stay private to their submodules: external callers import them from where they live (braille.display._getDisplayDriver,braille.extensions._pre_showBrailleMessage/_post_dismissBrailleMessage/_decide_disabledIncludesMessages,braille.buffers._WindowRowPositions) rather than through the package facade.All copyright headers in the package have been updated to the current 4-line GPL format.
Description of development approach:
tests/unit/test_braille/suite.test_publicSurface.py) as an anchor before any extraction.labelsholds only label dictionaries; pure value constants live in a new leafconstantsmodule; the font-formatting markers and helpers live in a new leafformattingmodule;DisplayDimensionslives with the display code indisplay;FALLBACK_TABLElives with its only consumer inbrailleHandler; the focus-region helpers live with theRegionclasses they build inregions.__init__.pyis a pure facade: re-exports,__all__, thehandlerglobal, andinitialize/pumpAll/terminate.braille.regionsandbraille.displaywere further split into sub-packages.regionsis divided by responsibility (base region, NVDAObject region, property helpers, TextInfo region, focus-region generators, routing helpers);displayis divided into driver-side (driver) and gesture-side (gesture) concerns. Internal consumers import directly from the relevant submodule rather than through each sub-package's__init__.py.handlerglobal reference it asbraille.handlerat runtime (neverfrom braille import handler, which would capture a staleNone).braille.extensionsso add-on authors have a clear, named home separate from implementation.brailleHandler.pyis named with thebrailleprefix to avoid shadowing thebraille.handlermodule-level global.Testing strategy:
test_publicSurface.py— characterizes the fullbraille.Xpublic surface and__all__; guards against silent symbol loss across all tasks.tests/unit/test_braille/suite (routing, buffers, regions, display drivers, extension points, language indexes).handler→braille.handlerqualification where submodules reference the module-level global. Verified via diff.Known issues with pull request:
Submodules that access the
handlerglobal (regions/focus.py,display/driver.py,brailleHandler.py) useimport braille+braille.handlerat call time to avoid capturing theNonevalue at import time. This circular reference is intentional and safe, but could be eliminated in a follow-up by e.g. passing the handler explicitly or using a module-level accessor.Code Review Checklist: