Repository navigation
Conversation
Strict ``YYYY-MM-DD[( |T)HH:MM[:SS[.ffffff]][Z|+HH[[:]MM]]]`` strings, the most common input, now skip the tokenizer: a regex builds the same ``_result`` the general path would, and the rest of parse() is unchanged. The fast path is only taken with the stock parserinfo, without dayfirst or fuzzy, and for str input; everything else falls back to the general path. parse() on ISO strings goes from ~21-27 us to ~6-8 us (3.5x). tests/test_parser_fastpath.py checks with hypothesis that parse() returns the same result, error or fuzzy tokens with and without the fast path, across dayfirst, yearfirst, ignoretz, fuzzy, fuzzy_with_tokens, default, tzinfos and parserinfo (including a subclass with different JUMP tokens). Ten deliberately injected bugs in the fast path are all caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- _timelex.split() runs the same get_token() state machine directly over the string, slicing tokens out, instead of one StringIO.read(1) and method call per character. Subclasses and streams keep the old path. Tokenizing is 2.5-2.9x faster. - parse() only calls datetime.now() for the default when the string does not give the full date. - _resultbase.__len__ no longer sums a generator. parse() vs upstream: ISO 4.2-4.8x, other formats 1.3-1.5x faster. tests/_reference_parser.py is a frozen copy of the upstream parser. tests/test_parser_equivalence.py checks with hypothesis that tokens and full parse() results (values, errors, fuzzy tokens) match it on real-world strftime formats, random fragments and Unicode edge cases. Fourteen deliberately injected bugs are all caught; a 100k-case run of each check found no difference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Apply the project's darker/black/isort formatting to the changed lines. - Compare exception args instead of str(e): on Python 2 str() of a ParserError holding non-ASCII input raises UnicodeEncodeError. - Compare tokenizer outcomes for bytes input: on Python 2 bytes.decode() is ASCII, and both parsers raise the same error. - Generate strftime inputs from 1900 on (Python 2's strftime limit). - Skip Hypothesis's target phase: on Hypothesis 4.x (the last release for Python 2) its Pareto optimiser ran for many minutes on these strategies. The tests do not call target(), so the phase added nothing. - Add Daniel Galperson to AUTHORS.md. Tested on CPython 2.7, 3.5, 3.6, 3.7, 3.12 and PyPy 2.7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
darker checks every line a PR adds, which includes this new file. The syntax tree is unchanged apart from merged imports and three docstrings that lost their leading and trailing spaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary of changes
Speeds up
parser.parse()without changing its behaviour, and adds tests that check this against a frozen copy of the current parser.YYYY-MM-DD[( |T)HH:MM[:SS[.ffffff]][Z|±HH[[:]MM]]]strings build the_resultthe general path would build, then go through the unchanged_build_naive/_build_tzaware. Out-of-range values are not checked separately;datetimerejects them with the same messages as before. Customparserinfosubclasses,dayfirst,fuzzyand non-strinput always take the general path._timelex.split()runs the existingget_token()state machine directly over the string._timelexsubclasses and streams keep the old path.datetime.now()is only called when the string doesn't give a full date, and_resultbase.__len__no longer sums a generator.This covers the ISO fast path and tokenizer work proposed in #1519. It overlaps with #1609 and #1613 (opened 14 Oct). The difference here is how the tests check that behaviour stays the same.
Evidence
parse(), µs per call (CPython 3.12, macOS arm64):2024-03-15T14:30:002024-03-15T14:30:00+02:002024-03-15T14:30:00.123456Z2024-03-1503/15/2024Fri, 15 Mar 2024 14:30:00 +0000March 15, 2024 2:30 PMBehaviour preservation:
tests/_reference_parser.pyis a verbatim copy of_parser.pyat 2642afa (only its two relative imports changed). It is test-only.tests/test_parser_equivalence.pyuses Hypothesis to check that_timelex.splittokens (str, bytes and streams) and fullparse()results match the reference. It compares values,utcoffset(), exception type and args, andfuzzy_with_tokensoutput. Inputs are real-worldstrftimeformats, random fragments and Unicode digits/letters. Options vary overdayfirst,yearfirst,ignoretz,fuzzy,fuzzy_with_tokens,default,tzinfosandparserinfo.tests/test_parser_fastpath.pychecksparse()with and without the fast path on inputs built around its grammar boundaries, including aparserinfosubclass with a differentJUMPlist. It also checks that common ISO strings skip the tokenizer.Full suite: CPython 2.7, 3.5, 3.6, 3.7, 3.12 and PyPy 2.7 all pass. Python 2.7 needed one test-only setting: Hypothesis 4.x (the last release for Python 2) can spend minutes in its
targetphase, so these tests skip that phase. They don't calltarget(), so nothing is lost.Merge Danger
Door: two-way. The changes are internal to
_parser.pywith no API change, and a revert is clean.Blast Radius: wide.
parse()is used throughout the ecosystem (pandas calls_timelex.splitdirectly), which is why the tests compare against the old implementation rather than against hand-picked cases.The reference copy adds about 1,600 lines to
tests/. If you would rather not carry it, the equivalence tests could compare against an installed release instead.Pull Request Checklist
🤖 Generated with Claude Code