Skip to content

Fix span_range() crash when end is omitted (documented limit-only usage) - #1396

Open
simpleqt wants to merge 1 commit into
arrow-py:masterfrom
simpleqt:fix/span-range-limit-without-end
Open

simpleqt wants to merge 1 commit into
arrow-py:masterfrom
simpleqt:fix/span-range-limit-without-end

Conversation

@simpleqt

Copy link
Copy Markdown

Pull Request Checklist

  • 🧪 Added tests for changed code.
  • 🛠️ All tests pass when run locally (run tox or make test to find out!).
  • 🧹 All linting checks pass when run locally (run tox -e lint or make lint to find out!).
  • 📚 Updated documentation for changed code.
  • ⏩ Code is up-to-date with the master branch.

Description of Changes

Arrow.span_range() documents that end is optional and that limit may be passed on its own:

:param end: (optional) A datetime expression, the end of the range.
**NOTE**: The ``end`` or ``limit`` must be provided.  Call with ``end`` alone to return the
entire range.  Call with ``limit`` alone to return a maximum # of results from the start.

but both documented forms raise:

>>> from datetime import datetime
>>> list(arrow.Arrow.span_range("month", datetime(2013, 1, 1), limit=3))
TypeError: Arrow.span_range() missing 1 required positional argument: 'end'
>>> list(arrow.Arrow.span_range("month", datetime(2013, 1, 1), None, limit=3))
AttributeError: 'NoneType' object has no attribute 'year'

The second one fails at arrow/arrow.py:706 (end = cls.fromdatetime(end, tzinfo)), even though end is forwarded to Arrow.range(), which already implements the documented rule (_get_iteration_params() treats end=None as Arrow.max and raises ValueError("One of 'end' or 'limit' is required.") when both are omitted).

This makes end default to None and only converts/compares against it when it was actually provided, so the documented limit-only call returns timespans:

>>> list(arrow.Arrow.span_range("month", datetime(2013, 1, 1), limit=3))
[(<Arrow [2013-01-01T00:00:00+00:00]>, <Arrow [2013-01-31T23:59:59.999999+00:00]>),
 (<Arrow [2013-02-01T00:00:00+00:00]>, <Arrow [2013-02-28T23:59:59.999999+00:00]>),
 (<Arrow [2013-03-01T00:00:00+00:00]>, <Arrow [2013-03-31T23:59:59.999999+00:00]>)]

No existing call changes behaviour — every input affected by this patch raised before — and calling with neither end nor limit now raises the same ValueError as Arrow.range() instead of AttributeError.

Tests added in tests/test_arrow.py::TestArrowSpanRange: test_limit_without_end, test_limit_without_end_quarter_and_tz (frame + tz), test_limit_without_end_exact (exact=True), test_limit_or_end_is_required (ValueError). All four fail on master (TypeError/AttributeError) and pass with the fix; the full suite is green (1905 passed, coverage 99.93%) and black/flake8/isort/mypy (pre-commit pins) are clean.

Prepared with AI assistance; every line was verified by running the cases above.

Arrow.span_range()'s docstring documents end as optional and states that
calling with "limit alone" returns at most `limit` timespans from start,
which is how Arrow.range() behaves, but the parameter was mandatory and was
converted unconditionally, so both documented forms raised:

    span_range("month", start, limit=3)
        -> TypeError: missing 1 required positional argument: 'end'
    span_range("month", start, None, limit=3)
        -> AttributeError: 'NoneType' object has no attribute 'year'
           (arrow/arrow.py:706 -> fromdatetime())

Make end default to None, convert it only when it was provided, and guard the
exact=True truncation comparisons against a missing end. None is still
forwarded to Arrow.range(), which raises ValueError("One of 'end' or 'limit'
is required.") when neither end nor limit is given.

Affected calls all raised before, so no working call changes behaviour.

Adds regression tests in tests/test_arrow.py::TestArrowSpanRange covering
limit-only usage for month and quarter frames (with tz), exact=True, and the
ValueError when both end and limit are omitted.
Copilot AI balanced review requested due to automatic review settings October 24, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@codecov

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2224255) to head (745a41c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #1396   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        10           
  Lines         2315      2315           
  Branches       358       358           
=========================================
  Hits          2315      2315           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

This branch has not been deployed

No deployments
Sign up for free to 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