I built this PySide6 popover because I wanted a compact date and range picker I could drop into desktop tools without rewriting the same scaffolding each time.
- Feature-complete snapshot that I keep around as a reference implementation.
- Not actively maintained—issues/PRs may sit, but the CI suite keeps the repo healthy.
- Releases follow semantic versioning; see CHANGELOG.md for history.
- Documentation lives under
docs/and mirrors the published MkDocs site.
DateRangePopoverexposes top-level Qt signals (date_selected,range_selected,cancelled) so you can wire business logic without spelunking through widgets.DatePickerConfigcentralizes options: picker mode, sizing, initial date or range, theming, and the newmin_date/max_datebounds.- A consistent style system driven by
ColorPalette,LayoutConfig, andStyleRegistry, plus keyboard-friendly focus handling and track animations. - A runnable demo under
examples/basic_popover_demo.pythat mirrors the code shown below.
Here are a few of the core views you get out of the box:
| Day view | Custom range |
|---|---|
![]() |
![]() |
| Month view | Year view |
|---|---|
![]() |
![]() |
# editable install while iterating
pip install -e .
# once you publish, pip can install it by name
pip install valgo-date-range-popoverEverything comes from the pyproject.toml (PEP 621 via hatchling), so pip, python -m build, or hatch build all work. The repo URL is https://github.com/Taitranz/PySide6-Date-Range-Popover.
python -m examples.basic_popover_demoThat script opens a centered popover, prints selected dates/ranges to stdout, and shows how the signals behave. I often duplicate that module when testing different configs.
pip install -e .[test]
pytest --maxfail=1 --disable-warnings --cov=date_range_popover --cov-report=term-missing --cov-report=xml --cov-report=htmlpytest.inikeeps discovery strict (testpaths, strict markers) so widget regressions surface quickly.tests/conftest.pyforcesQT_QPA_PLATFORM=offscreenbefore Qt spins up, so the suite runs fine without an X server.pytest-qtdrives widget-level tests via the built-inqtbotfixture while session-scopedQApplicationsetup also lives intests/conftest.py.- Coverage reports are generated in both XML (
coverage.xml) and HTML (htmlcov/index.html), so CI and humans can audit regressions quickly. - The coverage config enforces
fail_under=90, ensuring the suite exercises the vast majority of the codebase.
- The sanitizers that guard configs and runtime input live in
date_range_popover/validation/validators.pyand are re-exported via the public package so embedders can import them withfrom date_range_popover.validation import validate_qdate. - Behavioural tests for those helpers sit in
tests/unit/test_validation.py; they assert the happy-path conversions as well as failure cases for missing endpoints, reordered ranges, and type mismatches. - Picker lifecycle tests in
tests/unit/test_state_manager.pyexercise the same validation helpers in context so reviewers can confirm how out-of-bounds dates are clamped. - Run
pytest tests/unit/test_validation.py -vvif you want a focused view on the validation helpers without spinning up the rest of the suite.
from PySide6.QtWidgets import QApplication
from date_range_popover import DatePickerConfig, DateRangePopover, PickerMode
app = QApplication([])
config = DatePickerConfig(
mode=PickerMode.DATE,
initial_date=None,
min_date=None,
max_date=None,
time_step_minutes=15,
)
popover = DateRangePopover(config=config)
popover.date_selected.connect(lambda date: print(date.toString("yyyy-MM-dd")))
popover.show()
app.exec()The picker now exposes selected_date and selected_range properties for
quick inspections (instead of get_* methods). Each access returns a fresh
QDate/DateRange, so you can safely cache the value:
if popover.selected_range.end_date is not None:
print("Range locked in:", popover.selected_range)date_selected(QDate): fires when a single date is locked in.range_selected(DateRange): fires once both endpoints exist.cancelled(): fires when the user closes or cancels the popover.
DatePickerConfig lets me keep behavior predictable:
mode:PickerMode.DATEorPickerMode.CUSTOM_RANGE.initial_date/initial_range: seed selections on open.width/height: clamp popover bounds.theme: supply a customTheme,ColorPalette, orLayoutConfig.min_date/max_date: block out-of-range navigation and selection. If you don't pass amax_date, it defaults to today's date so future days stay disabled (you can still opt-in to future dates by setting a latermax_date).time_step_minutes: set the spacing for the time completer.
For heavier customization, import date_range_popover.styles.theme and build your own palette or layout before passing the theme into the config.
- Configuration objects validate themselves in
__post_init__, but you should still normalise user input before creating them. - Reuse helpers from
date_range_popover.validation(e.g.validate_qdate,validate_date_range,validate_dimension) to clamp untrusted values. - Wrap config construction inside
try/exceptblocks so you can surface informative errors or fall back to safe defaults. - The demo under
examples/basic_popover_demo.pymirrors the README examples and is safe to copy into your own app.
Need more detail? See
docs/embedding.mdfor a longer end-to-end sanitisation guide.
StyleManager pulls variants from StyleRegistry, so every component (calendar views, header, inputs, action buttons) calls into the same palette. Changing one palette entry gives you consistent colors, and swapping layout values adjusts spacing/geometry without touching the widgets.
DateRangePicker.get_selected_date()and.get_selected_range()were replaced with the property-basedselected_date/selected_range. Update call sites to drop the()suffix.- Docstrings and sanitisation guidance were expanded. Review the new
docs/embedding.mdfile if you embed the widget inside a larger host.
- Increment the version in
pyproject.toml. hatch buildorpython -m buildto create wheels/sdists.twine upload dist/*to push to PyPI or TestPyPI.
The metadata already lists the license, authorship, and dependencies, so those commands are all you need when you decide to publish.
This is a side project I built for my own tools, and my schedule is pretty full, so I do not expect to actively iterate on it. I may occasionally fix critical issues, but you should treat the API and behavior as "use at your own risk" and feel free to fork if you need faster changes. Some may recognise the design 😂.
The MkDocs site mirrors the architecture notes, embedding guide, API reference, and extension hooks.
pip install -e ".[docs]"
mkdocs serve # open http://127.0.0.1:8000Highlights:
docs/index.md– overview + quick startdocs/architecture.md– state manager, theming, performancedocs/extending.md– theming + callback hooksdocs/api/public_api.md– stability contractdocs/api/extension_points.md– supported hooksdocs/roadmap.md– upcoming work & design betsdocs/ARCHITECTURE_DECISIONS.md– key design trade-offs
Contributions are welcome! See CONTRIBUTING.md for setup
instructions, quality gates (ruff/black/mypy/pytest), and PR guidelines.
MIT License. See LICENSE for the exact terms.



