├── .flake8 ├── .github ├── ghstack_direct └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── expecttest ├── __init__.py └── py.typed ├── pyproject.toml ├── run_tests.sh ├── smoketests ├── accept_expected.py ├── accept_non_ascii.py ├── accept_twice.py ├── accept_twice_clobber.py ├── accept_twice_reload.py └── no_unittest.py ├── test_expecttest.py └── uv.lock /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = B,C,E,F,P,T4,W,B9 3 | max-line-length = 150 4 | ### DEFAULT IGNORES FOR 4-space INDENTED PROJECTS ### 5 | # E127, E128 are hard to silence in certain nested formatting situations. 6 | # E203 doesn't work for slicing 7 | # E265, E266 talk about comment formatting which is too opinionated. 8 | # E402 warns on imports coming after statements. There are important use cases 9 | # like demandimport (https://fburl.com/demandimport) that require statements 10 | # before imports. 11 | # E501 is not flexible enough, we're using B950 instead. 12 | # E722 is a duplicate of B001. 13 | # P207 is a duplicate of B003. 14 | # P208 is a duplicate of C403. 15 | # W503 talks about operator formatting which is too opinionated. 16 | ignore = E127, E128, E203, E265, E266, E402, E501, E722, P207, P208, W503 17 | exclude = 18 | .git, 19 | .hg, 20 | __pycache__, 21 | .venv, 22 | 23 | max-complexity = 12 24 | -------------------------------------------------------------------------------- /.github/ghstack_direct: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytorch/expecttest/65d09767d56b38e355634124ee55ab00b54fd1c4/.github/ghstack_direct -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | ci: 14 | strategy: 15 | fail-fast: false 16 | max-parallel: 5 17 | matrix: 18 | python-version: 19 | - 3.8 20 | - 3.9 21 | - "3.10" 22 | - 3.11 23 | - 3.12 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Install uv 29 | uses: astral-sh/setup-uv@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | enable-cache: true 33 | cache-suffix: "optional-suffix" 34 | - name: Run tests 35 | run: uv run --frozen python test_expecttest.py 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Type Check 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Install uv 19 | uses: astral-sh/setup-uv@v5 20 | with: 21 | python-version: "3.12" 22 | enable-cache: true 23 | cache-suffix: "lint" 24 | - name: Run flake8 25 | run: uv run --frozen flake8 26 | - name: Run mypy 27 | run: uv run --frozen mypy --exclude=smoketests . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | /.hypothesis/ 3 | /.vscode/ 4 | /.idea/ 5 | /dist/ 6 | *.bak 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to expecttest 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `main`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Facebook's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. 24 | 25 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 26 | disclosure of security bugs. In those cases, please go through the process 27 | outlined on that page and do not file a public issue. 28 | 29 | ## License 30 | By contributing to expecttest, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Edward Z. Yang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # expecttest [![PyPI version](https://badge.fury.io/py/expecttest.svg)](https://badge.fury.io/py/expecttest) 2 | 3 | This library implements expect tests (also known as "golden" tests). Expect 4 | tests are a method of writing tests where instead of hard-coding the expected 5 | output of a test, you run the test to get the output, and the test framework 6 | automatically populates the expected output. If the output of the test changes, 7 | you can rerun the test with the environment variable `EXPECTTEST_ACCEPT=1` to 8 | automatically update the expected output. 9 | 10 | Somewhat unusually, this library implements *inline* expect tests: that is to 11 | say, the expected output isn't saved to an external file, it is saved directly 12 | in the Python file (and we modify your Python file when updating the expect 13 | test.) 14 | 15 | The general recipe for how to use this is as follows: 16 | 17 | 1. Write your test and use `assertExpectedInline()` instead of a normal 18 | `assertEqual`. Leave the expected argument blank with an empty string: 19 | ```py 20 | self.assertExpectedInline(some_func(), """""") 21 | ``` 22 | 23 | 2. Run your test. It should fail, and you get an error message about 24 | accepting the output with `EXPECTTEST_ACCEPT=1` 25 | 26 | 3. Rerun the test with `EXPECTTEST_ACCEPT=1`. Now the previously blank string 27 | literal will contain the expected value of the test. 28 | ```py 29 | self.assertExpectedInline(some_func(), """my_value""") 30 | ``` 31 | 32 | ## A minimal working example 33 | 34 | ```python 35 | # test.py 36 | import unittest 37 | from expecttest import TestCase 38 | 39 | class TestStringMethods(TestCase): 40 | def test_split(self): 41 | s = 'hello world' 42 | self.assertExpectedInline(str(s.split()), """""") 43 | 44 | if __name__ == '__main__': 45 | unittest.main() 46 | ``` 47 | 48 | Run `EXPECTTEST_ACCEPT=1 python test.py` , and the content in triple-quoted string 49 | will be automatically updated. 50 | 51 | For people who use `pytest`: 52 | 53 | ```python 54 | from expecttest import assert_expected_inline 55 | 56 | def test_split(): 57 | s = 'hello world' 58 | assert_expected_inline(str(s.split()), """""") 59 | ``` 60 | 61 | Run `EXPECTTEST_ACCEPT=1 pytest test.py` , and the content in triple-quoted string 62 | will be automatically updated. 63 | 64 | For parameterized tests, advanced fixturing and other cases where the 65 | expectation is in a different place than the assertion, use `Expect`: 66 | 67 | ```python 68 | from expecttest import Expect 69 | 70 | def test_removing_spaces(s: str, expected: Expect) -> None: 71 | expected.assert_expected(s.replace(" ", "")) 72 | 73 | test_removing_spaces("foo bar", Expect("""foobar""")) 74 | test_removing_spaces("foo bar !!", Expect("""""")) 75 | ``` 76 | 77 | Run `EXPECTTEST_ACCEPT=1 pytest test.py` , and the content in triple-quoted string 78 | will be automatically updated. 79 | 80 | ## Some tips and tricks 81 | 82 | - Often, you will want to expect test on a multiline string. This framework 83 | understands triple-quoted strings, so you can just write `"""my_value"""` 84 | and it will turn into triple-quoted strings. 85 | 86 | - Take some time thinking about how exactly you want to design the output 87 | format of the expect test. It is often profitable to design an output 88 | representation specifically for expect tests. 89 | 90 | ## Similar libraries 91 | 92 | - [expect-test](https://docs.rs/expect-test) for Rust 93 | -------------------------------------------------------------------------------- /expecttest/__init__.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | import re 4 | import sys 5 | import traceback 6 | import unittest 7 | import difflib 8 | import dataclasses 9 | from typing import Any, Callable, Dict, List, Match, Tuple, Optional 10 | 11 | 12 | def _accept_enabled() -> bool: 13 | """ 14 | Returns True if we are in accept mode, i.e., the environment variable 15 | EXPECTTEST_ACCEPT is set to a truthy value. 16 | """ 17 | return os.getenv("EXPECTTEST_ACCEPT") not in (None, "", "0", "false", "False") 18 | 19 | 20 | # NB: We do not internally use this property for anything, but it 21 | # is preserved for BC reasons 22 | ACCEPT = _accept_enabled() 23 | 24 | LINENO_AT_START = sys.version_info >= (3, 8) 25 | 26 | 27 | def nth_line(src: str, lineno: int) -> int: 28 | """ 29 | Compute the starting index of the n-th line (where n is 1-indexed) 30 | 31 | >>> nth_line("aaa\\nbb\\nc", 2) 32 | 4 33 | """ 34 | assert lineno >= 1 35 | pos = 0 36 | for _ in range(lineno - 1): 37 | pos = src.find("\n", pos) + 1 38 | return pos 39 | 40 | 41 | def nth_eol(src: str, lineno: int) -> int: 42 | """ 43 | Compute the ending index of the n-th line (before the newline, 44 | where n is 1-indexed) 45 | 46 | >>> nth_eol("aaa\\nbb\\nc", 2) 47 | 6 48 | """ 49 | assert lineno >= 1 50 | pos = -1 51 | for _ in range(lineno): 52 | pos = src.find("\n", pos + 1) 53 | if pos == -1: 54 | return len(src) 55 | return pos 56 | 57 | 58 | def normalize_nl(t: str) -> str: 59 | return t.replace("\r\n", "\n").replace("\r", "\n") 60 | 61 | 62 | def escape_trailing_quote(s: str, quote: str) -> str: 63 | if s and s[-1] == quote: 64 | return s[:-1] + "\\" + quote 65 | else: 66 | return s 67 | 68 | 69 | class EditHistory: 70 | state: Dict[str, List[Tuple[int, int]]] 71 | seen: Dict[str, Dict[int, str]] 72 | 73 | def __init__(self) -> None: 74 | self.state = {} 75 | self.seen = {} 76 | 77 | def reload_file(self, fn: str) -> None: 78 | """ 79 | The idea is that if you reload a file, the line numbers 80 | from traceback are now up to date, but we do NOT want to clear 81 | out seen list as it will tell us if we are expecting the 82 | same line multiple times. Instead, we need to adjust 83 | the seen list for the new world order. 84 | """ 85 | new_seen = {} 86 | for seen_loc, seen_str in self.seen.get(fn, {}).items(): 87 | new_seen[self.adjust_lineno(fn, seen_loc)] = seen_str 88 | 89 | self.seen[fn] = new_seen 90 | self.state.pop(fn, None) 91 | 92 | def adjust_lineno(self, fn: str, lineno: int) -> int: 93 | if fn not in self.state: 94 | return lineno 95 | for edit_loc, edit_diff in self.state[fn]: 96 | if lineno > edit_loc: 97 | lineno += edit_diff 98 | return lineno 99 | 100 | def seen_file(self, fn: str) -> bool: 101 | return fn in self.state 102 | 103 | def seen_edit(self, fn: str, lineno: int) -> Optional[str]: 104 | return self.seen.get(fn, {}).get(lineno, None) 105 | 106 | def record_edit(self, fn: str, lineno: int, delta: int, expect: str) -> None: 107 | self.state.setdefault(fn, []).append((lineno, delta)) 108 | self.seen.setdefault(fn, {})[lineno] = expect 109 | 110 | 111 | EDIT_HISTORY = EditHistory() 112 | 113 | 114 | def ok_for_raw_triple_quoted_string(s: str, quote: str) -> bool: 115 | """ 116 | Is this string representable inside a raw triple-quoted string? 117 | Due to the fact that backslashes are always treated literally, 118 | some strings are not representable. 119 | 120 | >>> ok_for_raw_triple_quoted_string("blah", quote="'") 121 | True 122 | >>> ok_for_raw_triple_quoted_string("'", quote="'") 123 | False 124 | >>> ok_for_raw_triple_quoted_string("a ''' b", quote="'") 125 | False 126 | """ 127 | return quote * 3 not in s and (not s or s[-1] not in [quote, "\\"]) 128 | 129 | 130 | RE_EXPECT = re.compile( 131 | (r"(?Pr?)" r"(?P'''|" r'""")' r"(?P.*?)" r"(?P=quote)"), re.DOTALL 132 | ) 133 | 134 | 135 | def replace_string_literal( 136 | src: str, start_lineno: int, end_lineno: int, new_string: str 137 | ) -> Tuple[str, int]: 138 | r""" 139 | Replace a triple quoted string literal with new contents. 140 | Only handles printable string literals correctly at the moment. This 141 | will preserve the quote style of the original string, and 142 | makes a best effort to preserve raw-ness (unless it is impossible 143 | to do so.) 144 | 145 | Returns a tuple of the replaced string, as well as a delta of 146 | number of lines added/removed. 147 | 148 | >>> replace_string_literal("'''arf'''", 1, 1, "barf") 149 | ("'''barf'''", 0) 150 | >>> r = replace_string_literal(" moo = '''arf'''", 1, 1, "'a'\n\\b\n") 151 | >>> print(r[0]) 152 | moo = '''\ 153 | 'a' 154 | \\b 155 | ''' 156 | >>> r[1] 157 | 3 158 | >>> replace_string_literal(" moo = '''\\\narf'''", 1, 2, "'a'\n\\b\n")[1] 159 | 2 160 | >>> print(replace_string_literal(" f('''\"\"\"''')", 1, 1, "a ''' b")[0]) 161 | f('''a \'\'\' b''') 162 | """ 163 | assert ast.literal_eval(repr(new_string)) == new_string, f"content {new_string!r} cannot be printed as a string literal" 164 | 165 | new_string = normalize_nl(new_string) 166 | 167 | delta = new_string.count("\n") 168 | if delta > 0: 169 | delta += 1 # handle the extra \\\n 170 | 171 | assert start_lineno <= end_lineno 172 | start = nth_line(src, start_lineno) 173 | end = nth_eol(src, end_lineno) 174 | assert start <= end 175 | 176 | def replace(m: Match[str]) -> str: 177 | nonlocal delta 178 | 179 | s = new_string 180 | raw = m.group("raw") == "r" 181 | if not raw or not ok_for_raw_triple_quoted_string(s, quote=m.group("quote")[0]): 182 | raw = False 183 | s = s.replace("\\", "\\\\") 184 | if m.group("quote") == "'''": 185 | s = escape_trailing_quote(s, "'").replace("'''", r"\'\'\'") 186 | else: 187 | s = escape_trailing_quote(s, '"').replace('"""', r"\"\"\"") 188 | 189 | new_body = "\\\n" + s if "\n" in s and not raw else s 190 | delta -= m.group("body").count("\n") 191 | return "".join( 192 | [ 193 | "r" if raw else "", 194 | m.group("quote"), 195 | new_body, 196 | m.group("quote"), 197 | ] 198 | ) 199 | 200 | return ( 201 | src[:start] + RE_EXPECT.sub(replace, src[start:end], count=1) + src[end:], 202 | delta, 203 | ) 204 | 205 | 206 | def replace_many(rep: Dict[str, str], text: str) -> str: 207 | rep = {re.escape(k): v for k, v in rep.items()} 208 | pattern = re.compile("|".join(rep.keys())) 209 | return pattern.sub(lambda m: rep[re.escape(m.group(0))], text) 210 | 211 | 212 | def assert_eq(expect: str, actual: str, *, msg: str) -> None: 213 | # TODO: improve this 214 | if actual != expect: 215 | diff = "".join( 216 | difflib.unified_diff( 217 | expect.splitlines(True), 218 | actual.splitlines(True), 219 | fromfile="expect.txt", 220 | tofile="actual.txt", 221 | ) 222 | ) 223 | raise AssertionError( 224 | f"Mismatch between actual and expect strings:\n\n{diff}\n\n{msg}" 225 | ) 226 | 227 | 228 | class Expect: 229 | """ 230 | An expected string literal, analogous to expect_test::expect! in Rust. 231 | 232 | This saves its position so that you can pass it around for e.g. 233 | parameterized tests and similar. 234 | 235 | Example: 236 | >>> e = Expect("value") # expected value 237 | >>> e.assert_expected("value") # actual value 238 | """ 239 | def __init__(self, expected: str, *, skip: int = 0): 240 | """ 241 | Creates an expectation of the given value. 242 | """ 243 | # n.b. this is not a dataclass because it seems like it would expose 244 | # us to being broken by dataclasses internals changes. 245 | self.expected = expected 246 | 247 | # current frame and parent frame, plus any requested skip 248 | tb = traceback.extract_stack(limit=2 + skip) 249 | fn, lineno, _, _ = tb[0] 250 | self.pos = PositionInfo(fn, lineno) 251 | 252 | def assert_expected(self, actual: str) -> None: 253 | assert_expected_inline(actual, self.expected, pos=self.pos) 254 | 255 | def __repr__(self) -> str: 256 | return f"Expect({self.expected!r})" 257 | 258 | def __str__(self) -> str: 259 | return self.expected 260 | 261 | 262 | @dataclasses.dataclass 263 | class PositionInfo: 264 | filename: str 265 | lineno: int 266 | 267 | def __str__(self) -> str: 268 | return f"{self.filename}:{self.lineno}" 269 | 270 | 271 | def _accept_output( 272 | pos: PositionInfo, 273 | actual: str, 274 | debug_suffix: str, 275 | ) -> None: 276 | """ 277 | Replaces the string literal at "pos" (according to the interpreter) with 278 | the new string "actual". 279 | """ 280 | print("Accepting new output{} at {}".format(debug_suffix, pos)) 281 | with open(pos.filename, "r+") as f: 282 | old = f.read() 283 | old_ast = ast.parse(old) 284 | 285 | # NB: it's only the traceback line numbers that are wrong; 286 | # we reread the file every time we write to it, so AST's 287 | # line numbers are correct 288 | lineno = EDIT_HISTORY.adjust_lineno(pos.filename, pos.lineno) 289 | 290 | # Conservative assumption to start 291 | start_lineno = lineno 292 | end_lineno = lineno 293 | # Try to give a more accurate bounds based on AST 294 | # NB: this walk is in no specified order (in practice it's 295 | # breadth first) 296 | for n in ast.walk(old_ast): 297 | if isinstance(n, ast.Expr): 298 | if hasattr(n, "end_lineno") and n.end_lineno: 299 | assert LINENO_AT_START 300 | if n.lineno == start_lineno: 301 | end_lineno = n.end_lineno # type: ignore[attr-defined] 302 | else: 303 | if n.lineno == end_lineno: 304 | start_lineno = n.lineno 305 | 306 | new, delta = replace_string_literal( 307 | old, start_lineno, end_lineno, actual 308 | ) 309 | 310 | assert old != new, ( 311 | f"Failed to substitute string at {pos}; did you use triple quotes? " 312 | "If this is unexpected, please file a bug report at " 313 | "https://github.com/ezyang/expecttest/issues/new " 314 | f"with the contents of the source file near {pos}" 315 | ) 316 | 317 | # Only write the backup file the first time we hit the 318 | # file 319 | if not EDIT_HISTORY.seen_file(pos.filename): 320 | with open(pos.filename + ".bak", "w") as f_bak: 321 | f_bak.write(old) 322 | f.seek(0) 323 | f.truncate(0) 324 | 325 | f.write(new) 326 | 327 | EDIT_HISTORY.record_edit(pos.filename, lineno, delta, actual) 328 | 329 | 330 | def assert_expected_inline( 331 | actual: str, 332 | expect: str, 333 | skip: int = 0, 334 | *, 335 | pos: Optional[PositionInfo] = None, 336 | expect_filters: Optional[Dict[str, str]] = None, 337 | assert_eq: Any = assert_eq, 338 | debug_id: str = "", 339 | ) -> None: 340 | """ 341 | Assert that actual is equal to expect. The expect argument 342 | MUST be a string literal (triple-quoted strings OK), and will 343 | get updated directly in source when you run the test suite 344 | with EXPECTTEST_ACCEPT=1. 345 | 346 | If you want to write a helper function that makes use of 347 | assertExpectedInline (e.g., expect is not a string literal), 348 | set the skip argument to how many function calls we should 349 | skip to find the string literal to update. 350 | """ 351 | if expect_filters is not None: 352 | actual = replace_many(expect_filters, actual) 353 | 354 | # NB: Intentionally do not use ACCEPT global variable; 355 | # reaccessing environment here allows for modification 356 | # of os.environ to be picked up 357 | if _accept_enabled(): 358 | if actual != expect: 359 | if not pos: 360 | # current frame and parent frame, plus any requested skip 361 | tb = traceback.extract_stack(limit=2 + skip) 362 | fn, lineno, _, _ = tb[0] 363 | pos = PositionInfo(fn, lineno) 364 | 365 | debug_suffix = "" if not debug_id else f" for {debug_id}" 366 | if (prev_actual := EDIT_HISTORY.seen_edit(pos.filename, pos.lineno)) is not None: 367 | assert_eq( 368 | actual, 369 | prev_actual, 370 | msg=f"Uh oh, accepting different values{debug_suffix} at {pos}. Are you running a parametrized test? If so, you need a separate assertExpectedInline invocation per distinct output.", 371 | ) 372 | print( 373 | "Skipping already accepted output{} at {}".format( 374 | debug_suffix, pos 375 | ) 376 | ) 377 | return 378 | _accept_output(pos=pos, actual=actual, debug_suffix=debug_suffix) 379 | else: 380 | help_text = ( 381 | "To accept the new output, re-run test with " 382 | "envvar EXPECTTEST_ACCEPT=1 (we recommend " 383 | "staging/committing your changes before doing this)" 384 | ) 385 | assert_eq(expect, actual, msg=help_text) 386 | 387 | 388 | class TestCase(unittest.TestCase): 389 | longMessage = True 390 | _expect_filters: Dict[str, str] 391 | 392 | def substituteExpected(self, pattern: str, replacement: str) -> None: 393 | if not hasattr(self, "_expect_filters"): 394 | self._expect_filters = {} 395 | 396 | def expect_filters_cleanup() -> None: 397 | del self._expect_filters 398 | 399 | self.addCleanup(expect_filters_cleanup) 400 | if pattern in self._expect_filters: 401 | raise RuntimeError( 402 | "Cannot remap {} to {} (existing mapping is {})".format( 403 | pattern, replacement, self._expect_filters[pattern] 404 | ) 405 | ) 406 | self._expect_filters[pattern] = replacement 407 | 408 | def assertExpectedInline(self, actual: str, expect: str, skip: int = 0) -> None: 409 | """ 410 | Assert that actual is equal to expect. The expect argument 411 | MUST be a string literal (triple-quoted strings OK), and will 412 | get updated directly in source when you run the test suite 413 | with EXPECTTEST_ACCEPT=1. 414 | 415 | If you want to write a helper function that makes use of 416 | assertExpectedInline (e.g., expect is not a string literal), 417 | set the skip argument to how many function calls we should 418 | skip to find the string literal to update. 419 | """ 420 | assert_expected_inline( 421 | actual, 422 | expect, 423 | skip=skip + 1, 424 | expect_filters=getattr(self, "_expect_filters", None), 425 | debug_id=self.id(), 426 | assert_eq=self.assertMultiLineEqualMaybeCppStack, 427 | ) 428 | 429 | def assertExpectedRaisesInline( 430 | self, 431 | exc_type: Any, 432 | callable: Callable[..., Any], 433 | expect: str, 434 | *args: Any, 435 | **kwargs: Any, 436 | ) -> None: 437 | """ 438 | Like assertExpectedInline, but tests the str() representation of 439 | the raised exception from callable. The raised exeption must 440 | be exc_type. 441 | """ 442 | try: 443 | callable(*args, **kwargs) 444 | except exc_type as e: 445 | self.assertExpectedInline(str(e), expect, skip=1) 446 | return 447 | # Don't put this in the try block; the AssertionError will catch it 448 | self.fail(msg="Did not raise when expected to") 449 | 450 | def assertMultiLineEqualMaybeCppStack( 451 | self, expect: str, actual: str, *args: Any, **kwargs: Any 452 | ) -> None: 453 | cpp_stack_header = "\nException raised from" 454 | if cpp_stack_header in actual: 455 | actual = actual.rsplit(cpp_stack_header, maxsplit=1)[0] 456 | if hasattr(self, "assertMultiLineEqual"): 457 | self.assertMultiLineEqual(expect, actual, *args, **kwargs) 458 | else: 459 | self.assertEqual(expect, actual, *args, **kwargs) 460 | 461 | 462 | if __name__ == "__main__": 463 | import doctest 464 | 465 | doctest.testmod() 466 | -------------------------------------------------------------------------------- /expecttest/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytorch/expecttest/65d09767d56b38e355634124ee55ab00b54fd1c4/expecttest/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [ 3 | {name = "Edward Z. Yang", email = "ezyang@mit.edu"}, 4 | ] 5 | license = {text = "MIT"} 6 | requires-python = "<4.0.0,>=3.8.1" 7 | dependencies = [] 8 | name = "expecttest" 9 | version = "0.3.0" 10 | description = "" 11 | readme = "README.md" 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | ] 17 | 18 | [project.urls] 19 | repository = "https://github.com/ezyang/expecttest" 20 | 21 | [tool.uv] 22 | dev-dependencies = [ 23 | "flake8<8,>=7", 24 | "hypothesis>=6,<7", 25 | "mypy<2,>=0.910", 26 | "pytest>=8.3.5", 27 | ] 28 | 29 | [build-system] 30 | requires = ["uv_build>=0.6,<0.7"] 31 | build-backend = "uv_build" 32 | 33 | [tool.uv.build-backend] 34 | module-root = "" 35 | 36 | [tool.mypy] 37 | python_version = "3.8" 38 | strict_optional = true 39 | show_column_numbers = true 40 | show_error_codes = true 41 | warn_no_return = true 42 | disallow_any_unimported = true 43 | exclude = [ 44 | "^.venv/", 45 | ] 46 | 47 | # --strict 48 | warn_unused_configs = true 49 | disallow_any_generics = true 50 | disallow_subclassing_any = true 51 | disallow_untyped_calls = true 52 | disallow_untyped_defs = true 53 | disallow_incomplete_defs = true 54 | check_untyped_defs = true 55 | disallow_untyped_decorators = true 56 | no_implicit_optional = true 57 | warn_redundant_casts = true 58 | warn_unused_ignores = false # purposely disabled 59 | warn_return_any = true 60 | no_implicit_reexport = true 61 | strict_equality = true 62 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | flake8 4 | mypy --exclude=smoketests . 5 | python test_expecttest.py 6 | -------------------------------------------------------------------------------- /smoketests/accept_expected.py: -------------------------------------------------------------------------------- 1 | from expecttest import Expect 2 | 3 | exps = [ 4 | Expect("""ok"""), 5 | Expect("""changeme"""), 6 | ] 7 | 8 | for exp in exps: 9 | exp.assert_expected("ok") 10 | -------------------------------------------------------------------------------- /smoketests/accept_non_ascii.py: -------------------------------------------------------------------------------- 1 | import expecttest 2 | import unittest 3 | 4 | S1 = "你好,世界!" 5 | S2 = "(the parentheses are in chinese)" 6 | 7 | 8 | class Test(expecttest.TestCase): 9 | def bar(self): 10 | self.assertExpectedInline( 11 | S1, 12 | """\ 13 | w""", 14 | ) 15 | 16 | def test_a(self): 17 | self.bar() 18 | self.bar() 19 | 20 | def test_b(self): 21 | self.assertExpectedInline( 22 | S2, 23 | """\ 24 | x 25 | y 26 | z""", 27 | ) 28 | 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /smoketests/accept_twice.py: -------------------------------------------------------------------------------- 1 | import expecttest 2 | import unittest 3 | 4 | S1 = "a\nb" 5 | S2 = "c\nd" 6 | 7 | 8 | class Test(expecttest.TestCase): 9 | def bar(self): 10 | self.assertExpectedInline( 11 | S1, 12 | """\ 13 | w""", 14 | ) 15 | 16 | def test_a(self): 17 | self.bar() 18 | self.bar() 19 | 20 | def test_b(self): 21 | self.assertExpectedInline( 22 | S2, 23 | """\ 24 | x 25 | y 26 | z""", 27 | ) 28 | 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /smoketests/accept_twice_clobber.py: -------------------------------------------------------------------------------- 1 | import expecttest 2 | from expecttest import assert_expected_inline 3 | 4 | S1 = "a\nb" 5 | S2 = "a\nb\nc" 6 | 7 | assert_expected_inline(S2 if hasattr(expecttest, "_TEST2") else S1, """""") 8 | -------------------------------------------------------------------------------- /smoketests/accept_twice_reload.py: -------------------------------------------------------------------------------- 1 | import expecttest 2 | from expecttest import assert_expected_inline 3 | 4 | S1 = "a\nb" 5 | S2 = "a\nb\nc" 6 | 7 | if hasattr(expecttest, "_TEST1"): 8 | assert_expected_inline(S1, """""") 9 | else: 10 | assert_expected_inline(S2, """""") 11 | -------------------------------------------------------------------------------- /smoketests/no_unittest.py: -------------------------------------------------------------------------------- 1 | from expecttest import assert_expected_inline 2 | 3 | S1 = "a\nb" 4 | 5 | assert_expected_inline(S1, """""") 6 | -------------------------------------------------------------------------------- /test_expecttest.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import sys 3 | import string 4 | import shutil 5 | import subprocess 6 | import os 7 | import textwrap 8 | import traceback 9 | import unittest 10 | import tempfile 11 | import runpy 12 | from typing import Any, Dict, Tuple, Generator 13 | from contextlib import contextmanager 14 | from pathlib import Path 15 | 16 | import hypothesis 17 | from hypothesis.strategies import booleans, composite, integers, sampled_from, text 18 | 19 | import expecttest 20 | 21 | 22 | def sh(file: str, accept: bool = False) -> subprocess.CompletedProcess: # type: ignore[type-arg] 23 | env = "" 24 | if accept: 25 | env = "EXPECTTEST_ACCEPT=1 " 26 | print(f" $ {env}python {file}") 27 | r = subprocess.run( 28 | [sys.executable, file], 29 | capture_output=True, 30 | # NB: Always OVERRIDE EXPECTTEST_ACCEPT variable, so outer usage doesn't 31 | # get confused! 32 | env={**os.environ, "EXPECTTEST_ACCEPT": "1" if accept else ""}, 33 | text=True, 34 | ) 35 | if r.stderr: 36 | print(textwrap.indent(r.stderr, " ")) 37 | return r 38 | 39 | 40 | @composite 41 | def text_lineno(draw: Any) -> Tuple[str, int]: 42 | t = draw(text("a\n")) 43 | lineno = draw(integers(min_value=1, max_value=t.count("\n") + 1)) 44 | return (t, lineno) 45 | 46 | 47 | @contextmanager 48 | def smoketest(name: str) -> Generator[str, None, None]: 49 | with tempfile.TemporaryDirectory() as d: 50 | dst = Path(d) / "test.py" 51 | code_dir = Path(__file__).parent 52 | shutil.copy( 53 | code_dir / "smoketests" / name, 54 | dst, 55 | ) 56 | yield str(dst) 57 | 58 | 59 | class TestExpectTest(expecttest.TestCase): 60 | @hypothesis.given(text_lineno()) 61 | def test_nth_line_ref(self, t_lineno: Tuple[str, int]) -> None: 62 | t, lineno = t_lineno 63 | hypothesis.event("lineno = {}".format(lineno)) 64 | 65 | def nth_line_ref(src: str, lineno: int) -> int: 66 | xs = src.split("\n")[:lineno] 67 | xs[-1] = "" 68 | return len("\n".join(xs)) 69 | 70 | self.assertEqual(expecttest.nth_line(t, lineno), nth_line_ref(t, lineno)) 71 | 72 | @hypothesis.given(text(string.printable), booleans(), sampled_from(['"', "'"])) 73 | def test_replace_string_literal_roundtrip( 74 | self, t: str, raw: bool, quote: str 75 | ) -> None: 76 | if raw: 77 | hypothesis.assume( 78 | expecttest.ok_for_raw_triple_quoted_string(t, quote=quote) 79 | ) 80 | prog = """\ 81 | r = {r}{quote}placeholder{quote} 82 | r2 = {r}{quote}placeholder2{quote} 83 | r3 = {r}{quote}placeholder3{quote} 84 | """.format( 85 | r="r" if raw else "", quote=quote * 3 86 | ) 87 | new_prog = expecttest.replace_string_literal(textwrap.dedent(prog), 2, 2, t)[0] 88 | ns: Dict[str, Any] = {} 89 | exec(new_prog, ns) 90 | msg = "program was:\n{}".format(new_prog) 91 | self.assertEqual(ns["r"], "placeholder", msg=msg) # noqa: F821 92 | self.assertEqual(ns["r2"], expecttest.normalize_nl(t), msg=msg) # noqa: F821 93 | self.assertEqual(ns["r3"], "placeholder3", msg=msg) # noqa: F821 94 | 95 | def test_sample_lineno(self) -> None: 96 | prog = r""" 97 | single_single('''0''') 98 | single_multi('''1''') 99 | multi_single('''\ 100 | 2 101 | ''') 102 | multi_multi_less('''\ 103 | 3 104 | 4 105 | ''') 106 | multi_multi_same('''\ 107 | 5 108 | ''') 109 | multi_multi_more('''\ 110 | 6 111 | ''') 112 | different_indent( 113 | RuntimeError, 114 | '''7''' 115 | ) 116 | """ 117 | edits = [ 118 | (2, 2, "a"), 119 | (3, 3, "b\n"), 120 | (4, 6, "c"), 121 | (7, 10, "d\n"), 122 | (11, 13, "e\n"), 123 | (14, 16, "f\ng\n"), 124 | (17, 20, "h"), 125 | ] 126 | history = expecttest.EditHistory() 127 | fn = "not_a_real_file.py" 128 | for start_lineno, end_lineno, actual in edits: 129 | start_lineno = history.adjust_lineno(fn, start_lineno) 130 | end_lineno = history.adjust_lineno(fn, end_lineno) 131 | prog, delta = expecttest.replace_string_literal( 132 | prog, start_lineno, end_lineno, actual 133 | ) 134 | # NB: it doesn't really matter start/end you record edit at 135 | history.record_edit(fn, start_lineno, delta, actual) 136 | self.assertExpectedInline( 137 | prog, 138 | r""" 139 | single_single('''a''') 140 | single_multi('''\ 141 | b 142 | ''') 143 | multi_single('''c''') 144 | multi_multi_less('''\ 145 | d 146 | ''') 147 | multi_multi_same('''\ 148 | e 149 | ''') 150 | multi_multi_more('''\ 151 | f 152 | g 153 | ''') 154 | different_indent( 155 | RuntimeError, 156 | '''h''' 157 | ) 158 | """, 159 | ) 160 | 161 | def test_lineno_assumptions(self) -> None: 162 | def get_tb(s: str) -> traceback.StackSummary: 163 | return traceback.extract_stack(limit=2) 164 | 165 | tb1 = get_tb("") 166 | tb2 = get_tb( 167 | """a 168 | b 169 | c""" 170 | ) 171 | 172 | assert isinstance(tb1[0].lineno, int) 173 | if expecttest.LINENO_AT_START: 174 | # tb2's stack starts on the next line 175 | self.assertEqual(tb1[0].lineno + 1, tb2[0].lineno) 176 | else: 177 | # starts at the end here 178 | self.assertEqual(tb1[0].lineno + 1 + 2, tb2[0].lineno) 179 | 180 | def test_smoketest_accept_twice(self) -> None: 181 | with smoketest("accept_twice.py") as test_py: 182 | r = sh(test_py) 183 | self.assertNotEqual(r.returncode, 0) 184 | r = sh(test_py, accept=True) 185 | self.assertExpectedInline( 186 | r.stdout.replace(test_py, "test.py"), 187 | """\ 188 | Accepting new output for __main__.Test.test_a at test.py:10 189 | Skipping already accepted output for __main__.Test.test_a at test.py:10 190 | Accepting new output for __main__.Test.test_b at test.py:21 191 | """, 192 | ) 193 | r = sh(test_py) 194 | self.assertEqual(r.returncode, 0) 195 | 196 | def test_smoketest_non_ascii(self) -> None: 197 | with smoketest("accept_non_ascii.py") as test_py: 198 | r = sh(test_py) 199 | self.assertNotEqual(r.returncode, 0) 200 | r = sh(test_py, accept=True) 201 | self.assertExpectedInline( 202 | r.stdout.replace(str(test_py), "test.py"), 203 | """\ 204 | Accepting new output for __main__.Test.test_a at test.py:10 205 | Skipping already accepted output for __main__.Test.test_a at test.py:10 206 | Accepting new output for __main__.Test.test_b at test.py:21 207 | """, 208 | ) 209 | r = sh(test_py) 210 | self.assertEqual(r.returncode, 0) 211 | 212 | def test_smoketest_no_unittest(self) -> None: 213 | with smoketest("no_unittest.py") as test_py: 214 | r = sh(test_py) 215 | self.assertNotEqual(r.returncode, 0) 216 | r = sh(test_py, accept=True) 217 | self.assertExpectedInline( 218 | r.stdout.replace(str(test_py), "test.py"), 219 | """\ 220 | Accepting new output at test.py:5 221 | """, 222 | ) 223 | r = sh(test_py) 224 | self.assertEqual(r.returncode, 0) 225 | 226 | def test_smoketest_accept_twice_reload(self) -> None: 227 | with smoketest("accept_twice_reload.py") as test_py: 228 | env = os.environ.copy() 229 | try: 230 | os.environ["EXPECTTEST_ACCEPT"] = "1" 231 | runpy.run_path(test_py) 232 | expecttest.EDIT_HISTORY.reload_file(test_py) 233 | try: 234 | expecttest._TEST1 = True # type: ignore[attr-defined] 235 | runpy.run_path(test_py) 236 | finally: 237 | delattr(expecttest, "_TEST1") 238 | finally: 239 | os.environ.clear() 240 | os.environ.update(env) 241 | 242 | # Should pass 243 | runpy.run_path(test_py) 244 | try: 245 | expecttest._TEST1 = True # type: ignore[attr-defined] 246 | runpy.run_path(test_py) 247 | finally: 248 | delattr(expecttest, "_TEST1") 249 | 250 | def test_smoketest_accept_twice_clobber(self) -> None: 251 | with smoketest("accept_twice_clobber.py") as test_py: 252 | env = os.environ.copy() 253 | try: 254 | os.environ["EXPECTTEST_ACCEPT"] = "1" 255 | runpy.run_path(test_py) 256 | expecttest.EDIT_HISTORY.reload_file(test_py) 257 | try: 258 | expecttest._TEST2 = True # type: ignore[attr-defined] 259 | self.assertRaises(AssertionError, lambda: runpy.run_path(test_py)) 260 | finally: 261 | delattr(expecttest, "_TEST2") 262 | finally: 263 | os.environ.clear() 264 | os.environ.update(env) 265 | 266 | def test_smoketest_expected_objects(self) -> None: 267 | with smoketest("accept_expected.py") as test_py: 268 | r = sh(test_py) 269 | self.assertNotEqual(r.returncode, 0) 270 | r = sh(test_py, accept=True) 271 | self.assertExpectedInline( 272 | r.stdout.replace(test_py, "test.py"), 273 | """\ 274 | Accepting new output at test.py:5 275 | """, 276 | ) 277 | r = sh(test_py) 278 | self.assertEqual(r.returncode, 0) 279 | 280 | 281 | def load_tests( 282 | loader: unittest.TestLoader, tests: unittest.TestSuite, ignore: Any 283 | ) -> unittest.TestSuite: 284 | tests.addTests(doctest.DocTestSuite(expecttest)) 285 | return tests 286 | 287 | 288 | if __name__ == "__main__": 289 | unittest.main() 290 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.8.1, <4.0.0" 4 | resolution-markers = [ 5 | "python_full_version >= '3.9'", 6 | "python_full_version < '3.9'", 7 | ] 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "25.3.0" 12 | source = { registry = "https://pypi.org/simple" } 13 | sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } 14 | wheels = [ 15 | { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, 16 | ] 17 | 18 | [[package]] 19 | name = "colorama" 20 | version = "0.4.6" 21 | source = { registry = "https://pypi.org/simple" } 22 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 25 | ] 26 | 27 | [[package]] 28 | name = "exceptiongroup" 29 | version = "1.3.0" 30 | source = { registry = "https://pypi.org/simple" } 31 | dependencies = [ 32 | { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 33 | { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, 34 | ] 35 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 36 | wheels = [ 37 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 38 | ] 39 | 40 | [[package]] 41 | name = "expecttest" 42 | version = "0.3.0" 43 | source = { editable = "." } 44 | 45 | [package.dev-dependencies] 46 | dev = [ 47 | { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 48 | { name = "flake8", version = "7.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 49 | { name = "hypothesis", version = "6.113.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 50 | { name = "hypothesis", version = "6.135.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 51 | { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 52 | { name = "mypy", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 53 | { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 54 | { name = "pytest", version = "8.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 55 | ] 56 | 57 | [package.metadata] 58 | 59 | [package.metadata.requires-dev] 60 | dev = [ 61 | { name = "flake8", specifier = ">=7,<8" }, 62 | { name = "hypothesis", specifier = ">=6,<7" }, 63 | { name = "mypy", specifier = ">=0.910,<2" }, 64 | { name = "pytest", specifier = ">=8.3.5" }, 65 | ] 66 | 67 | [[package]] 68 | name = "flake8" 69 | version = "7.1.2" 70 | source = { registry = "https://pypi.org/simple" } 71 | resolution-markers = [ 72 | "python_full_version < '3.9'", 73 | ] 74 | dependencies = [ 75 | { name = "mccabe", marker = "python_full_version < '3.9'" }, 76 | { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 77 | { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 78 | ] 79 | sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } 80 | wheels = [ 81 | { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, 82 | ] 83 | 84 | [[package]] 85 | name = "flake8" 86 | version = "7.2.0" 87 | source = { registry = "https://pypi.org/simple" } 88 | resolution-markers = [ 89 | "python_full_version >= '3.9'", 90 | ] 91 | dependencies = [ 92 | { name = "mccabe", marker = "python_full_version >= '3.9'" }, 93 | { name = "pycodestyle", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 94 | { name = "pyflakes", version = "3.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 95 | ] 96 | sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177, upload-time = "2025-03-29T20:08:39.329Z" } 97 | wheels = [ 98 | { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786, upload-time = "2025-03-29T20:08:37.902Z" }, 99 | ] 100 | 101 | [[package]] 102 | name = "hypothesis" 103 | version = "6.113.0" 104 | source = { registry = "https://pypi.org/simple" } 105 | resolution-markers = [ 106 | "python_full_version < '3.9'", 107 | ] 108 | dependencies = [ 109 | { name = "attrs", marker = "python_full_version < '3.9'" }, 110 | { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, 111 | { name = "sortedcontainers", marker = "python_full_version < '3.9'" }, 112 | ] 113 | sdist = { url = "https://files.pythonhosted.org/packages/28/32/6513cd7256f38c19a6c8a1d5ce9792bcd35c7f11651989994731f0e97672/hypothesis-6.113.0.tar.gz", hash = "sha256:5556ac66fdf72a4ccd5d237810f7cf6bdcd00534a4485015ef881af26e20f7c7", size = 408897, upload-time = "2024-10-09T03:51:05.707Z" } 114 | wheels = [ 115 | { url = "https://files.pythonhosted.org/packages/14/fa/4acb477b86a94571958bd337eae5baf334d21b8c98a04b594d0dad381ba8/hypothesis-6.113.0-py3-none-any.whl", hash = "sha256:d539180eb2bb71ed28a23dfe94e67c851f9b09f3ccc4125afad43f17e32e2bad", size = 469790, upload-time = "2024-10-09T03:51:02.629Z" }, 116 | ] 117 | 118 | [[package]] 119 | name = "hypothesis" 120 | version = "6.135.1" 121 | source = { registry = "https://pypi.org/simple" } 122 | resolution-markers = [ 123 | "python_full_version >= '3.9'", 124 | ] 125 | dependencies = [ 126 | { name = "attrs", marker = "python_full_version >= '3.9'" }, 127 | { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, 128 | { name = "sortedcontainers", marker = "python_full_version >= '3.9'" }, 129 | ] 130 | sdist = { url = "https://files.pythonhosted.org/packages/77/d5/f569c4f93c84b7f8ea147da6100050a36940a0ece799887bcd75b554f2ad/hypothesis-6.135.1.tar.gz", hash = "sha256:36eea411ef5dde5612301fcd9a293b6f2a3a5ab96488be2e23e7c5799cbd7b33", size = 449455, upload-time = "2025-06-05T22:08:12.353Z" } 131 | wheels = [ 132 | { url = "https://files.pythonhosted.org/packages/30/15/4bcd915f4bd747bb258482d8e705ef27a1584b7744e26bec72a9289a6e4f/hypothesis-6.135.1-py3-none-any.whl", hash = "sha256:14fab728bfe2409a3934e6e1ea6ae0a706d0bc78187137218a253aec7528b4c8", size = 515130, upload-time = "2025-06-05T22:08:08.467Z" }, 133 | ] 134 | 135 | [[package]] 136 | name = "iniconfig" 137 | version = "2.1.0" 138 | source = { registry = "https://pypi.org/simple" } 139 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 140 | wheels = [ 141 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 142 | ] 143 | 144 | [[package]] 145 | name = "mccabe" 146 | version = "0.7.0" 147 | source = { registry = "https://pypi.org/simple" } 148 | sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } 149 | wheels = [ 150 | { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, 151 | ] 152 | 153 | [[package]] 154 | name = "mypy" 155 | version = "1.14.1" 156 | source = { registry = "https://pypi.org/simple" } 157 | resolution-markers = [ 158 | "python_full_version < '3.9'", 159 | ] 160 | dependencies = [ 161 | { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, 162 | { name = "tomli", marker = "python_full_version < '3.9'" }, 163 | { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 164 | ] 165 | sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } 166 | wheels = [ 167 | { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, 168 | { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, 169 | { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, 170 | { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, 171 | { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, 172 | { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, 173 | { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, 174 | { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, 175 | { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, 176 | { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, 177 | { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, 178 | { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, 179 | { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, 180 | { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, 181 | { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, 182 | { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, 183 | { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, 184 | { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, 185 | { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, 186 | { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, 187 | { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, 188 | { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, 189 | { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, 190 | { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, 191 | { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, 192 | { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, 193 | { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, 194 | { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, 195 | { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, 196 | { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, 197 | { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, 198 | { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, 199 | { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, 200 | { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, 201 | { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, 202 | { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, 203 | { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, 204 | ] 205 | 206 | [[package]] 207 | name = "mypy" 208 | version = "1.16.0" 209 | source = { registry = "https://pypi.org/simple" } 210 | resolution-markers = [ 211 | "python_full_version >= '3.9'", 212 | ] 213 | dependencies = [ 214 | { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, 215 | { name = "pathspec", marker = "python_full_version >= '3.9'" }, 216 | { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, 217 | { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 218 | ] 219 | sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } 220 | wheels = [ 221 | { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, 222 | { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, 223 | { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, 224 | { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, 225 | { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, 226 | { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, 227 | { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, 228 | { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, 229 | { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, 230 | { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, 231 | { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, 232 | { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, 233 | { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, 234 | { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, 235 | { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, 236 | { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, 237 | { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, 238 | { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, 239 | { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, 240 | { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, 241 | { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, 242 | { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, 243 | { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, 244 | { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, 245 | { url = "https://files.pythonhosted.org/packages/bd/eb/c0759617fe2159aee7a653f13cceafbf7f0b6323b4197403f2e587ca947d/mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3", size = 10956081, upload-time = "2025-05-29T13:19:32.264Z" }, 246 | { url = "https://files.pythonhosted.org/packages/70/35/df3c74a2967bdf86edea58b265feeec181d693432faed1c3b688b7c231e3/mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92", size = 10084422, upload-time = "2025-05-29T13:18:01.437Z" }, 247 | { url = "https://files.pythonhosted.org/packages/b3/07/145ffe29f4b577219943b7b1dc0a71df7ead3c5bed4898686bd87c5b5cc2/mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436", size = 11879670, upload-time = "2025-05-29T13:17:45.971Z" }, 248 | { url = "https://files.pythonhosted.org/packages/c6/94/0421562d6b046e22986758c9ae31865d10ea0ba607ae99b32c9d18b16f66/mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2", size = 12610528, upload-time = "2025-05-29T13:34:36.983Z" }, 249 | { url = "https://files.pythonhosted.org/packages/1a/f1/39a22985b78c766a594ae1e0bbb6f8bdf5f31ea8d0c52291a3c211fd3cd5/mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20", size = 12871923, upload-time = "2025-05-29T13:32:21.823Z" }, 250 | { url = "https://files.pythonhosted.org/packages/f3/8e/84db4fb0d01f43d2c82fa9072ca72a42c49e52d58f44307bbd747c977bc2/mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21", size = 9482931, upload-time = "2025-05-29T13:21:32.326Z" }, 251 | { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, 252 | ] 253 | 254 | [[package]] 255 | name = "mypy-extensions" 256 | version = "1.1.0" 257 | source = { registry = "https://pypi.org/simple" } 258 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 259 | wheels = [ 260 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 261 | ] 262 | 263 | [[package]] 264 | name = "packaging" 265 | version = "25.0" 266 | source = { registry = "https://pypi.org/simple" } 267 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 268 | wheels = [ 269 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 270 | ] 271 | 272 | [[package]] 273 | name = "pathspec" 274 | version = "0.12.1" 275 | source = { registry = "https://pypi.org/simple" } 276 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 277 | wheels = [ 278 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 279 | ] 280 | 281 | [[package]] 282 | name = "pluggy" 283 | version = "1.5.0" 284 | source = { registry = "https://pypi.org/simple" } 285 | resolution-markers = [ 286 | "python_full_version < '3.9'", 287 | ] 288 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 289 | wheels = [ 290 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 291 | ] 292 | 293 | [[package]] 294 | name = "pluggy" 295 | version = "1.6.0" 296 | source = { registry = "https://pypi.org/simple" } 297 | resolution-markers = [ 298 | "python_full_version >= '3.9'", 299 | ] 300 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 301 | wheels = [ 302 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 303 | ] 304 | 305 | [[package]] 306 | name = "pycodestyle" 307 | version = "2.12.1" 308 | source = { registry = "https://pypi.org/simple" } 309 | resolution-markers = [ 310 | "python_full_version < '3.9'", 311 | ] 312 | sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } 313 | wheels = [ 314 | { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, 315 | ] 316 | 317 | [[package]] 318 | name = "pycodestyle" 319 | version = "2.13.0" 320 | source = { registry = "https://pypi.org/simple" } 321 | resolution-markers = [ 322 | "python_full_version >= '3.9'", 323 | ] 324 | sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } 325 | wheels = [ 326 | { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, 327 | ] 328 | 329 | [[package]] 330 | name = "pyflakes" 331 | version = "3.2.0" 332 | source = { registry = "https://pypi.org/simple" } 333 | resolution-markers = [ 334 | "python_full_version < '3.9'", 335 | ] 336 | sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } 337 | wheels = [ 338 | { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, 339 | ] 340 | 341 | [[package]] 342 | name = "pyflakes" 343 | version = "3.3.2" 344 | source = { registry = "https://pypi.org/simple" } 345 | resolution-markers = [ 346 | "python_full_version >= '3.9'", 347 | ] 348 | sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175, upload-time = "2025-03-31T13:21:20.34Z" } 349 | wheels = [ 350 | { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164, upload-time = "2025-03-31T13:21:18.503Z" }, 351 | ] 352 | 353 | [[package]] 354 | name = "pygments" 355 | version = "2.19.1" 356 | source = { registry = "https://pypi.org/simple" } 357 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } 358 | wheels = [ 359 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, 360 | ] 361 | 362 | [[package]] 363 | name = "pytest" 364 | version = "8.3.5" 365 | source = { registry = "https://pypi.org/simple" } 366 | resolution-markers = [ 367 | "python_full_version < '3.9'", 368 | ] 369 | dependencies = [ 370 | { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, 371 | { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, 372 | { name = "iniconfig", marker = "python_full_version < '3.9'" }, 373 | { name = "packaging", marker = "python_full_version < '3.9'" }, 374 | { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 375 | { name = "tomli", marker = "python_full_version < '3.9'" }, 376 | ] 377 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 378 | wheels = [ 379 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 380 | ] 381 | 382 | [[package]] 383 | name = "pytest" 384 | version = "8.4.0" 385 | source = { registry = "https://pypi.org/simple" } 386 | resolution-markers = [ 387 | "python_full_version >= '3.9'", 388 | ] 389 | dependencies = [ 390 | { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, 391 | { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, 392 | { name = "iniconfig", marker = "python_full_version >= '3.9'" }, 393 | { name = "packaging", marker = "python_full_version >= '3.9'" }, 394 | { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 395 | { name = "pygments", marker = "python_full_version >= '3.9'" }, 396 | { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, 397 | ] 398 | sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } 399 | wheels = [ 400 | { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, 401 | ] 402 | 403 | [[package]] 404 | name = "sortedcontainers" 405 | version = "2.4.0" 406 | source = { registry = "https://pypi.org/simple" } 407 | sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } 408 | wheels = [ 409 | { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, 410 | ] 411 | 412 | [[package]] 413 | name = "tomli" 414 | version = "2.2.1" 415 | source = { registry = "https://pypi.org/simple" } 416 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 417 | wheels = [ 418 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 419 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 420 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 421 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 422 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 423 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 424 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 425 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 426 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 427 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 428 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 429 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 430 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 431 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 432 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 433 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 434 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 435 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 436 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 437 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 438 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 439 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 440 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 441 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 442 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 443 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 444 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 445 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 446 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 447 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 448 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 449 | ] 450 | 451 | [[package]] 452 | name = "typing-extensions" 453 | version = "4.13.2" 454 | source = { registry = "https://pypi.org/simple" } 455 | resolution-markers = [ 456 | "python_full_version < '3.9'", 457 | ] 458 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 459 | wheels = [ 460 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 461 | ] 462 | 463 | [[package]] 464 | name = "typing-extensions" 465 | version = "4.14.0" 466 | source = { registry = "https://pypi.org/simple" } 467 | resolution-markers = [ 468 | "python_full_version >= '3.9'", 469 | ] 470 | sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } 471 | wheels = [ 472 | { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, 473 | ] 474 | --------------------------------------------------------------------------------