From 115f48068d0b164f3a3fa96bd6fb9bc30f84712a Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 08:44:45 +0200 Subject: [PATCH 01/20] docs: update README to match CLI and tag implementation --- README.md | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3781d10..12f4a84 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,39 @@ CodeStripper can be used as a Python Module and as a command line tool. The comm | Flag | Long form | Description | Default value | Required | |----------|------|-------------|---------------|----------| | `` | None | files to include for code stripping (glob) | None | True | -| -e | --exclude | files to exclude for code stripping (glob) | None | False | -| -c | --comment | comment symbol(s) for the given language | // | False | +| -x | --exclude | files to exclude for code stripping (glob), can be repeated | None | False | +| -c | --comment | comment symbol(s) for a file extension, format `:` or `::` (e.g. `.java://`), can be repeated | built-in mapping (see below) | False | | -o | --output | the output directory to store the stripped files | out | False | | -r | --recursive | do NOT use recursive globs for include/exclude | True | False | -| -v | --verbosity | increase output verbosity | None | False | -| -d | --dry | execute a dry run | False | False | +| -v | --verbosity | increase output verbosity (can be repeated) | 0 | False | +| -d | --dry-run | execute a dry run, no output is written | False | False | | -w | --working-directory | set the working directory for include/exclude | pwd | False | +| -e | --fail-on-error | do NOT fail if an error occurs during code stripping | fail on error | False | +| -b | --binary | what to do if a binary file is matched: `fail`, `ignore` or `include` | fail | False | +| -u | --unknown | what to do if a file with an unknown extension is matched: `fail`, `ignore` or `include` | fail | False | + +### Supported comment styles + +The comment symbol is chosen based on the file extension. The following extensions are supported by default: + +| Comment | Extensions | +|---------|------------| +| `//` | `.java`, `.cs`, `.js`, `.php`, `.swift` | +| `#` | `.py`, `.r`, `.ps1`, `.rb`, `.yml`, `.yaml`, files without extension | +| `%` | `.tex`, `.m` | +| `--` | `.sql`, `.lua` | +| `` | `.xml` | +| `(* *)` | `.ml` | + +Use `-c` to add or override an extension, for example `-c .kt://` or `-c .html:''`. + +### Python module + +```python +from codestripper.code_stripper import strip_files + +stripped = strip_files(["src/Test.java"], working_directory=".", output="out", dry_run=False) +``` ## Examples @@ -150,7 +176,6 @@ classDiagram Tag <|-- RangeTag class Tag{ - <> +offset: int +start: int +end: int @@ -169,10 +194,10 @@ classDiagram +SingleTag(data: TagData) } class RangeOpenTag{ - +RangeOpen(parent: Type, data: TagData) + +RangeOpenTag(parent: Type, data: TagData) } class RangeCloseTag{ - +RangeOpen(parent: Type, data: TagData) + +RangeCloseTag(parent: Type, data: TagData) } class RangeTag{ +inset: int @@ -180,6 +205,7 @@ classDiagram +end: int +open_tag: RangeOpenTag +close_tag: RangeCloseTag + +tags: List[Tag] +RangeTag(open_tag: RangeOpenTag, close_tag: RangeCloseTag) +add_tags(tags: Iterable[Tag]) } @@ -212,7 +238,7 @@ class TestTag(SingleTag): # None means the line is removed def is_valid(self) -> bool: - # Return wether the tag is valid + # Return whether the tag is valid ``` - RangeTag: Range needs a `RangeOpenTag`, `RangeCloseTag` and a `RangeTag` @@ -228,7 +254,7 @@ class TestOpenTag(RangeOpenTag): # None means the line is removed def is_valid(self) -> bool: - # Return wether the tag is valid + # Return whether the tag is valid class TestCloseTag(RangeCloseTag): # Same as RangeOpenTag @@ -242,7 +268,7 @@ class TestRangeTag(RangeTag): def execute(self, content: str) -> Union[str, None]: # Manipulate lines between the tags ``` -3. Add the new tag(s) to the `default_tags` in the `tokenizer`, +3. Export the new tag(s) in `codestripper/tags/__init__.py`, import them in the `tokenizer` and add them to its `default_tags`, ```python default_tags: Set[Type[SingleTag]] = { @@ -250,7 +276,8 @@ default_tags: Set[Type[SingleTag]] = { RemoveOpenTag, ..., TestTag, - TestOpenTag + TestOpenTag, + TestCloseTag } ``` -> :warning: **Only the `SingleTag`(s) need to be added, not the `RangeTag`** \ No newline at end of file +> :warning: **Only the `SingleTag`(s) (including `RangeOpenTag` and `RangeCloseTag`) need to be added, not the `RangeTag`** \ No newline at end of file From 70d2b86af73f3908e4369efd99e59b7681a08327 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:03:58 +0200 Subject: [PATCH 02/20] fix: make -b/--binary and -u/--unknown work on the command line --- codestripper/cli.py | 8 ++++--- codestripper/utils/enums.py | 4 ++-- tests/test_main.py | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/codestripper/cli.py b/codestripper/cli.py index a9873c5..18155b9 100644 --- a/codestripper/cli.py +++ b/codestripper/cli.py @@ -27,9 +27,10 @@ def add_commandline_arguments(parser: argparse.ArgumentParser) -> None: help="set the working directory for include/exclude", default=os.getcwd()) parser.add_argument("-e", "--fail-on-error", action="store_false", help="Fail if an error occurs during code stripping") - parser.add_argument("-b", "--binary", choices=list(UnexpectedInputOptions), default=UnexpectedInputOptions.FAIL, + unexpected_choices = [option.value for option in UnexpectedInputOptions] + parser.add_argument("-b", "--binary", choices=unexpected_choices, default=UnexpectedInputOptions.FAIL.value, action="store", help="What to do if binary file is matched") - parser.add_argument("-u", "--unknown", choices=list(UnexpectedInputOptions), default=UnexpectedInputOptions.FAIL, + parser.add_argument("-u", "--unknown", choices=unexpected_choices, default=UnexpectedInputOptions.FAIL.value, action="store", help="What to do if a file with unknown extension is matched") @@ -51,4 +52,5 @@ def main() -> None: # Strip all the files strip_files(files, cwd, comments=args.comment, output=args.output, dry_run=args.dry_run, - fail_on_error=args.fail_on_error) + fail_on_error=args.fail_on_error, binary=UnexpectedInputOptions(args.binary), + unknown_extension=UnexpectedInputOptions(args.unknown)) diff --git a/codestripper/utils/enums.py b/codestripper/utils/enums.py index f0a9a69..9f91cf4 100644 --- a/codestripper/utils/enums.py +++ b/codestripper/utils/enums.py @@ -2,6 +2,6 @@ class UnexpectedInputOptions(enum.Enum): - FAIL = "fail", - IGNORE = "ignore", + FAIL = "fail" + IGNORE = "ignore" INCLUDE = "include" \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 3ffb974..accb0ae 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -25,3 +25,46 @@ def test_main(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture): files = glob.glob("out/**/*.java", recursive=True) assert len(info) == 1 and len(files) == 4 + + +def test_main_unknown_include(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "file.unknownext").write_text("content") + monkeypatch.chdir(tmp_path) + args = ["codestripper", "-u", "include", "-o", "out", "file.unknownext"] + with patch.object(sys, 'argv', args): + main() + assert (tmp_path / "out" / "file.unknownext").is_file() + + +def test_main_unknown_ignore(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "file.unknownext").write_text("content") + monkeypatch.chdir(tmp_path) + args = ["codestripper", "-u", "ignore", "-o", "out", "file.unknownext"] + with patch.object(sys, 'argv', args): + main() + assert not (tmp_path / "out" / "file.unknownext").exists() + + +def test_main_binary_include(monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(test_project_dir) + shutil.rmtree("out", ignore_errors=True) + args = ["codestripper", "-b", "include", "-o", "out", "-w", "testproject", "test.jpg"] + with patch.object(sys, 'argv', args): + main() + assert os.path.isfile("out/test.jpg") + + +def test_main_binary_ignore(monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(test_project_dir) + shutil.rmtree("out", ignore_errors=True) + args = ["codestripper", "-b", "ignore", "-o", "out", "-w", "testproject", "test.jpg"] + with patch.object(sys, 'argv', args): + main() + assert not os.path.exists("out/test.jpg") + + +def test_main_invalid_choice(monkeypatch: pytest.MonkeyPatch): + args = ["codestripper", "-b", "nonsense", "test.jpg"] + with patch.object(sys, 'argv', args): + with pytest.raises(SystemExit): + main() From 0f723ef6e409eee6861aacf54c923879221459af Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:15:29 +0200 Subject: [PATCH 03/20] fix: continue with remaining files when FAIL is hit for unknown extension or binary file --- codestripper/code_stripper.py | 12 +++++++----- tests/test_codestripper.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index afc40a1..ac73ff1 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -36,9 +36,11 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None try: with open(os.path.join(cwd, file), 'r') as handle: content = handle.read() - except UnicodeDecodeError as e: + except UnicodeDecodeError: if binary == UnexpectedInputOptions.FAIL: - raise e + logger.error(f"{file}: binary file matched, use the binary option to ignore or include such files") + has_errors = True + continue elif binary == UnexpectedInputOptions.IGNORE: logger.info(f"Ignoring binary file: '{file}'") continue @@ -55,10 +57,10 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None file_extension = file_extension.lower() if not file_extension in comments_mapping: if unknown_extension == UnexpectedInputOptions.FAIL: - logger.error(f"Unknown extension: '{file_extension}', " - f"please specify which comment to use for this file extension.") + logger.error(f"{file}: unknown extension: '{file_extension}', " + f"please specify which comment to use for this file extension.") has_errors = True - break + continue elif unknown_extension == UnexpectedInputOptions.IGNORE: logger.info(f"Unknown extension: '{file_extension}' ignored") continue diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index eda6d46..b3a5e72 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -174,3 +174,39 @@ def test_project_with_binary_include(monkeypatch: pytest.MonkeyPatch): stripped = strip_files(files, "testproject", output="out",binary=UnexpectedInputOptions.INCLUDE) assert "test.jpg" in stripped + + +def _write_mixed_files(tmp_path: Path, bad_name: str, bad_content: bytes): + (tmp_path / "a.java").write_text("class A {}\n") + (tmp_path / bad_name).write_bytes(bad_content) + (tmp_path / "c.java").write_text("class C {}\n") + return ["a.java", bad_name, "c.java"] + + +@pytest.mark.parametrize("bad_name,bad_content,option", [ + ("b.unknownext", b"content", "unknown_extension"), + ("b.bin", b"\xff\xfe\xfd\x80", "binary"), +]) +def test_fail_continues_with_remaining_files(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture, + tmp_path: Path, bad_name: str, bad_content: bytes, option: str): + monkeypatch.chdir(tmp_path) + files = _write_mixed_files(tmp_path, bad_name, bad_content) + with caplog.at_level(logging.ERROR, logger='codestripper'): + stripped = strip_files(files, ".", output="out", fail_on_error=False, **{option: UnexpectedInputOptions.FAIL}) + assert sorted(stripped) == ["a.java", "c.java"] + assert (tmp_path / "out" / "c.java").is_file() + assert len([rec for rec in caplog.records if rec.levelno == logging.ERROR]) == 1 + + +@pytest.mark.parametrize("bad_name,bad_content,option", [ + ("b.unknownext", b"content", "unknown_extension"), + ("b.bin", b"\xff\xfe\xfd\x80", "binary"), +]) +def test_fail_raises_after_processing_when_fail_on_error(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, + bad_name: str, bad_content: bytes, option: str): + monkeypatch.chdir(tmp_path) + files = _write_mixed_files(tmp_path, bad_name, bad_content) + with pytest.raises(Exception, match="errors stripping"): + strip_files(files, ".", output="out", fail_on_error=True, **{option: UnexpectedInputOptions.FAIL}) + assert (tmp_path / "out" / "a.java").is_file() + assert (tmp_path / "out" / "c.java").is_file() From 80bc770f103126278ff83f0d50299855f2f996a9 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:26:09 +0200 Subject: [PATCH 04/20] fix: build uncomment regex per comment style and remove close symbol --- codestripper/tags/uncomment.py | 25 +++++++++++++++++-------- tests/tags/test_uncomment.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/codestripper/tags/uncomment.py b/codestripper/tags/uncomment.py index 2c47bc4..2d1e30c 100644 --- a/codestripper/tags/uncomment.py +++ b/codestripper/tags/uncomment.py @@ -1,7 +1,17 @@ import re -from typing import Union +from functools import lru_cache +from typing import Union, Pattern, Optional, Tuple -from codestripper.tags.tag import RangeTag, RangeOpenTag, RangeCloseTag, TagData, Tag +from codestripper.tags.tag import RangeTag, RangeOpenTag, RangeCloseTag, TagData +from codestripper.utils.comments import Comment + + +@lru_cache(maxsize=None) +def _uncomment_patterns(comment: Comment) -> Tuple[Pattern, Optional[Pattern]]: + """Regexes that remove the open (keeping the leading whitespace) and, if present, the close symbol of a comment""" + open_pattern = re.compile(rf"(?P\s*){re.escape(comment.open)}") + close_pattern = re.compile(re.escape(comment.close)) if comment.close is not None else None + return open_pattern, close_pattern class UncommentOpenTag(RangeOpenTag): @@ -19,15 +29,14 @@ def __init__(self, data: TagData) -> None: class UncommentRangeTag(RangeTag): - regex = None def __init__(self, open_tag: RangeOpenTag, close_tag: RangeCloseTag): super().__init__(open_tag, close_tag) def execute(self, content: str) -> Union[str, None]: - if UncommentRangeTag.regex is None: - whitespace = r"(?P\s*)" - UncommentRangeTag.regex = re.compile(f"{whitespace}{self.open_tag.data.comment.open}") - range = content[self.start:self.end] - replacement = UncommentRangeTag.regex.sub(r"\g", range) + open_pattern, close_pattern = _uncomment_patterns(self.open_tag.data.comment) + lines = content[self.start:self.end] + replacement = open_pattern.sub(r"\g", lines) + if close_pattern is not None: + replacement = close_pattern.sub("", replacement) return replacement diff --git a/tests/tags/test_uncomment.py b/tests/tags/test_uncomment.py index 52d7f74..e1e50ee 100644 --- a/tests/tags/test_uncomment.py +++ b/tests/tags/test_uncomment.py @@ -30,3 +30,21 @@ def test_uncomment_without_comments(): """ output = CodeStripper(case, Comment("//")).strip() assert output == expected, "Uncomment shouldn't process non-commented lines" + + +def test_uncomment_multiple_comment_styles(): + """The comment style of an earlier run must not leak into a later one""" + slashes = "//cs:uncomment:start\n//test\n//cs:uncomment:end\n" + assert CodeStripper(slashes, Comment("//")).strip() == "test\n" + hashes = "#cs:uncomment:start\n#test\n#cs:uncomment:end\n" + assert CodeStripper(hashes, Comment("#")).strip() == "test\n" + + +def test_uncomment_special_regex_characters(): + case = "(*cs:uncomment:start*)\n(*test*)\n(*cs:uncomment:end*)\n" + assert CodeStripper(case, Comment("(*", "*)")).strip() == "test\n" + + +def test_uncomment_removes_close_symbol(): + case = "\n \n\n" + assert CodeStripper(case, Comment("")).strip() == " \n" From bfb0078a1b7134d47d9c06617a25fea2bede2abe Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:28:07 +0200 Subject: [PATCH 05/20] fix: print dry run output to stdout instead of logging it --- README.md | 2 +- codestripper/code_stripper.py | 4 +++- tests/test_codestripper.py | 27 +++++++++++++++------------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 12f4a84..9c5236f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ CodeStripper can be used as a Python Module and as a command line tool. The comm | -o | --output | the output directory to store the stripped files | out | False | | -r | --recursive | do NOT use recursive globs for include/exclude | True | False | | -v | --verbosity | increase output verbosity (can be repeated) | 0 | False | -| -d | --dry-run | execute a dry run, no output is written | False | False | +| -d | --dry-run | execute a dry run, prints the stripped files (with a `==> <==` header) to stdout instead of writing them | False | False | | -w | --working-directory | set the working directory for include/exclude | pwd | False | | -e | --fail-on-error | do NOT fail if an error occurs during code stripping | fail on error | False | | -b | --binary | what to do if a binary file is matched: `fail`, `ignore` or `include` | fail | False | diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index ac73ff1..1bbbbad 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -80,7 +80,9 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None continue stripped_files.append(file) if dry_run: - logger.info(stripped) + # A dry run has no other output, so print instead of log (logging depends on the verbosity) + print(f"==> {file} <==") + print(stripped, end="" if stripped.endswith("\n") else "\n") else: path = os.path.join(out, file) os.makedirs(os.path.dirname(path), exist_ok=True) diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index b3a5e72..54061e2 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -36,18 +36,21 @@ def test_project_with_unknown_extension_include(monkeypatch: pytest.MonkeyPatch) assert "test.test" in stripped -def test_project(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture): - monkeypatch.chdir(test_project_dir) - with caplog.at_level(logging.INFO, logger='codestripper'): - files = FileUtils(["testproject/**/*.java", "testproject/pom.xml"]).get_matching_files() - strip_files(files, dry_run=True) - stripped = [rec.message for rec in caplog.records] - tags_in_content = False - for content in stripped: - if content.__contains__("//cs:"): - tags_in_content = True - break - assert len(stripped) == 6 and not tags_in_content +def test_project(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture): + monkeypatch.chdir(test_project_dir) + files = FileUtils(["testproject/**/*.java", "testproject/pom.xml"]).get_matching_files() + stripped_files = strip_files(files, dry_run=True) + output = capsys.readouterr().out + assert output.count("==> ") == 5 and len(stripped_files) == 5 + assert "//cs:" not in output + + +def test_dry_run_writes_nothing(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture, tmp_path: Path): + (tmp_path / "a.java").write_text("class A {\n int x;//cs:remove\n}\n") + monkeypatch.chdir(tmp_path) + strip_files(["a.java"], ".", output="out", dry_run=True) + assert capsys.readouterr().out == "==> a.java <==\nclass A {\n}\n" + assert not (tmp_path / "out").exists() def test_project_out(monkeypatch: pytest.MonkeyPatch): From aca719bae547276fc565c06f53fdb8fc9b329645 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:33:40 +0200 Subject: [PATCH 06/20] fix: validate -c option, lowercase extension and stop mutating global comments mapping --- README.md | 2 +- codestripper/cli.py | 16 +++++++++++++--- codestripper/code_stripper.py | 14 ++++---------- codestripper/utils/comments.py | 30 ++++++++++++++++++++++++++++-- tests/test_codestripper.py | 7 +++++++ tests/test_main.py | 9 +++++++++ tests/utils/test_comments.py | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 tests/utils/test_comments.py diff --git a/README.md b/README.md index 9c5236f..f0fe593 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ CodeStripper can be used as a Python Module and as a command line tool. The comm |----------|------|-------------|---------------|----------| | `` | None | files to include for code stripping (glob) | None | True | | -x | --exclude | files to exclude for code stripping (glob), can be repeated | None | False | -| -c | --comment | comment symbol(s) for a file extension, format `:` or `::` (e.g. `.java://`), can be repeated | built-in mapping (see below) | False | +| -c | --comment | comment symbol(s) for a file extension, format `:` or `::` (e.g. `.java://`), the extension starts with a `.` and is case-insensitive, can be repeated | built-in mapping (see below) | False | | -o | --output | the output directory to store the stripped files | out | False | | -r | --recursive | do NOT use recursive globs for include/exclude | True | False | | -v | --verbosity | increase output verbosity (can be repeated) | 0 | False | diff --git a/codestripper/cli.py b/codestripper/cli.py index 18155b9..56d7424 100644 --- a/codestripper/cli.py +++ b/codestripper/cli.py @@ -4,18 +4,28 @@ from codestripper.code_stripper import strip_files from codestripper.utils import FileUtils, set_logger_level, get_working_directory +from codestripper.utils.comments import parse_comment from codestripper.utils.enums import UnexpectedInputOptions +def comment_argument(value: str) -> str: + """Argparse type that validates a comment specification, the specification itself is parsed later""" + try: + parse_comment(value) + except ValueError as error: + raise argparse.ArgumentTypeError(str(error)) + return value + + def add_commandline_arguments(parser: argparse.ArgumentParser) -> None: """Add command line arguments""" # Add positional arguments parser.add_argument("include", nargs="+", help="files to include for code stripping (multiple files or glob)") # Add optional arguments parser.add_argument("-x", "--exclude", action="append", - help="files to include for code stripping (glob)", default=[]) - parser.add_argument("-c", "--comment", action="append", - help="comment symbol(s) for the given language, usage: : (e.g. .java://") + help="files to exclude for code stripping (glob)", default=[]) + parser.add_argument("-c", "--comment", action="append", type=comment_argument, + help="comment symbol(s) for the given language, usage: :[:] (e.g. .java://)") parser.add_argument("-v", "--verbosity", action="count", help="increase output verbosity", default=0) parser.add_argument("-o", "--output", action="store", help="output directory to store the stripped files", default="out") diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index 1bbbbad..b0d92b7 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -9,7 +9,7 @@ from codestripper.tags.tag import Tag, RangeTag from codestripper.tokenizer import Tokenizer from codestripper.utils import get_working_directory -from codestripper.utils.comments import comments_mapping, Comment +from codestripper.utils.comments import get_comments_mapping, Comment from codestripper.utils.enums import UnexpectedInputOptions logger = logging.getLogger("codestripper") @@ -19,13 +19,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None output: Union[Path, str] = "out", dry_run: bool = False, fail_on_error: bool = False, binary: UnexpectedInputOptions = UnexpectedInputOptions.FAIL, unknown_extension: UnexpectedInputOptions = UnexpectedInputOptions.FAIL) -> List[str]: - if comments is not None: - for comment in comments: - parts = comment.split(":") - if len(parts) == 2: - comments_mapping[parts[0]] = Comment(parts[1]) - else: - comments_mapping[parts[0]] = Comment(parts[1], parts[2]) + mapping = get_comments_mapping(comments) cwd = get_working_directory(working_directory) out = os.path.join(os.getcwd(), output) @@ -55,7 +49,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None try: _, file_extension = os.path.splitext(file) file_extension = file_extension.lower() - if not file_extension in comments_mapping: + if file_extension not in mapping: if unknown_extension == UnexpectedInputOptions.FAIL: logger.error(f"{file}: unknown extension: '{file_extension}', " f"please specify which comment to use for this file extension.") @@ -68,7 +62,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None # Keep the complete content stripped = content else: - com = comments_mapping[file_extension] + com = mapping[file_extension] stripped = CodeStripper(content, com).strip() except IgnoreFileError: logger.info(f"File '{file}' is ignored, because of ignore tag") diff --git a/codestripper/utils/comments.py b/codestripper/utils/comments.py index f976f1d..d518a50 100644 --- a/codestripper/utils/comments.py +++ b/codestripper/utils/comments.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional, Dict +from typing import Optional, Dict, Iterable, Tuple @dataclass(frozen=True) @@ -26,4 +26,30 @@ class Comment: ".yml": Comment("#"), ".yaml": Comment("#"), "": Comment("#"), # Default comment style for unknown file types -} \ No newline at end of file +} + + +def parse_comment(specification: str) -> Tuple[str, Comment]: + """ + Parse a comment specification: : or :: (e.g. .java://) + + :return: the (lowercase) extension and the comment + :raises ValueError: if the specification is invalid + """ + parts = specification.split(":") + if len(parts) not in (2, 3) or any(len(part) == 0 for part in parts): + raise ValueError(f"Invalid comment '{specification}', expected : or " + f":: (e.g. .java://)") + extension = parts[0].lower() + if not extension.startswith("."): + raise ValueError(f"Invalid comment '{specification}', the extension '{parts[0]}' should start with a '.'") + return extension, Comment(*parts[1:]) + + +def get_comments_mapping(specifications: Optional[Iterable[str]] = None) -> Dict[str, Comment]: + """Get a copy of the default comments mapping, extended (or overridden) by the given specifications""" + mapping = dict(comments_mapping) + for specification in specifications or []: + extension, comment = parse_comment(specification) + mapping[extension] = comment + return mapping diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index 54061e2..449c304 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -213,3 +213,10 @@ def test_fail_raises_after_processing_when_fail_on_error(monkeypatch: pytest.Mon strip_files(files, ".", output="out", fail_on_error=True, **{option: UnexpectedInputOptions.FAIL}) assert (tmp_path / "out" / "a.java").is_file() assert (tmp_path / "out" / "c.java").is_file() + + +def test_comments_option_is_not_kept_between_calls(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "a.custom").write_text("class A {\n int x;!!cs:remove\n}\n") + monkeypatch.chdir(tmp_path) + assert strip_files(["a.custom"], ".", output="out", comments=[".custom:!!"]) == ["a.custom"] + assert strip_files(["a.custom"], ".", output="out", unknown_extension=UnexpectedInputOptions.IGNORE) == [] diff --git a/tests/test_main.py b/tests/test_main.py index accb0ae..b6d552c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -68,3 +68,12 @@ def test_main_invalid_choice(monkeypatch: pytest.MonkeyPatch): with patch.object(sys, 'argv', args): with pytest.raises(SystemExit): main() + + +@pytest.mark.parametrize("comment", [".java", "java://", ".java:"]) +def test_main_invalid_comment(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture, comment: str): + args = ["codestripper", "-c", comment, "test.java"] + with patch.object(sys, 'argv', args): + with pytest.raises(SystemExit): + main() + assert "Invalid comment" in capsys.readouterr().err diff --git a/tests/utils/test_comments.py b/tests/utils/test_comments.py new file mode 100644 index 0000000..2cd7681 --- /dev/null +++ b/tests/utils/test_comments.py @@ -0,0 +1,34 @@ +import pytest + +from codestripper.utils.comments import Comment, comments_mapping, get_comments_mapping, parse_comment + + +def test_parse_comment_open_only(): + assert parse_comment(".java://") == (".java", Comment("//")) + + +def test_parse_comment_open_and_close(): + assert parse_comment(".html:") == (".html", Comment("")) + + +def test_parse_comment_lowercases_extension(): + assert parse_comment(".Java://") == (".java", Comment("//")) + + +@pytest.mark.parametrize("specification", [".java", "", ".java:", ":", ":#", ".java:a:b:c", "java://", ".java:://"]) +def test_parse_comment_invalid(specification: str): + with pytest.raises(ValueError): + parse_comment(specification) + + +def test_get_comments_mapping_overrides_and_extends(): + mapping = get_comments_mapping([".java:#", ".kt://"]) + assert mapping[".java"] == Comment("#") + assert mapping[".kt"] == Comment("//") + assert mapping[".py"] == comments_mapping[".py"] + + +def test_get_comments_mapping_does_not_change_defaults(): + get_comments_mapping([".java:#", ".kt://"]) + assert comments_mapping[".java"] == Comment("//") + assert ".kt" not in comments_mapping From 77af27b8e225885dbaecd008f660959093a95992 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:36:32 +0200 Subject: [PATCH 07/20] fix: resolve working directory before checking it is inside the current directory --- codestripper/cli.py | 5 ++++- codestripper/utils/file.py | 24 +++++++++++++++--------- tests/test_main.py | 9 +++++++++ tests/utils/test_file.py | 35 ++++++++++++++++++++++++++++++++--- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/codestripper/cli.py b/codestripper/cli.py index 56d7424..bce974a 100644 --- a/codestripper/cli.py +++ b/codestripper/cli.py @@ -57,7 +57,10 @@ def main() -> None: set_logger_level(logger_name, args.verbosity) # Find the files, based on the command line arguments - cwd = get_working_directory(args.working_directory) + try: + cwd = get_working_directory(args.working_directory) + except ValueError as error: + parser.error(str(error)) files = FileUtils(args.include, args.exclude, cwd, args.recursive, logger_name).get_matching_files() # Strip all the files diff --git a/codestripper/utils/file.py b/codestripper/utils/file.py index 9c6f9ee..5c2d561 100644 --- a/codestripper/utils/file.py +++ b/codestripper/utils/file.py @@ -6,15 +6,21 @@ def get_working_directory(working_directory: Union[str, None]) -> str: - if working_directory is not None: - if os.path.isabs(working_directory): - cwd = working_directory - else: - cwd = str(os.path.join(os.getcwd(), working_directory)) - Path(cwd).relative_to(os.getcwd()) - return cwd - else: - return os.getcwd() + """ + Get the absolute working directory, which should be (a subdirectory of) the current working directory + + :raises ValueError: if the working directory is not inside the current working directory + """ + current = os.path.realpath(os.getcwd()) + if working_directory is None: + return current + # Resolve '..' and symbolic links first, so they cannot be used to escape the current directory + cwd = os.path.realpath(os.path.join(current, working_directory)) + try: + Path(cwd).relative_to(current) + except ValueError: + raise ValueError(f"Working directory '{working_directory}' is not inside the current directory '{current}'") + return cwd class FileUtils: diff --git a/tests/test_main.py b/tests/test_main.py index b6d552c..a946027 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -77,3 +77,12 @@ def test_main_invalid_comment(monkeypatch: pytest.MonkeyPatch, capsys: pytest.Ca with pytest.raises(SystemExit): main() assert "Invalid comment" in capsys.readouterr().err + + +def test_main_working_directory_outside(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture): + monkeypatch.chdir(test_project_dir) + args = ["codestripper", "-w", "..", "*.java"] + with patch.object(sys, 'argv', args): + with pytest.raises(SystemExit): + main() + assert "is not inside the current directory" in capsys.readouterr().err diff --git a/tests/utils/test_file.py b/tests/utils/test_file.py index 8f7ee69..7d6fd82 100644 --- a/tests/utils/test_file.py +++ b/tests/utils/test_file.py @@ -39,7 +39,36 @@ def test_cwd(monkeypatch: pytest.MonkeyPatch): assert len(files) == 1 -def test_non_relative(monkeypatch: pytest.MonkeyPatch): +def test_cwd_default(monkeypatch: pytest.MonkeyPatch): monkeypatch.chdir(test_data_dir) - with pytest.raises(ValueError) as ex: - get_working_directory("/etc/passwd") + assert get_working_directory(None) == os.path.realpath(test_data_dir) + + +def test_cwd_absolute_inside(monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(test_data_dir) + inside = test_data_dir / "data" / "recursive" + assert get_working_directory(str(inside)) == os.path.realpath(inside) + + +def test_cwd_normalizes_inside_path(monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(test_data_dir) + assert get_working_directory("data/recursive/..") == os.path.realpath(test_data_dir / "data") + + +@pytest.mark.parametrize("path", ["/etc/passwd", "..", "../other", "data/../../other"]) +def test_non_relative(path: str, monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(test_data_dir) + with pytest.raises(ValueError, match="is not inside the current directory"): + get_working_directory(path) + + +@pytest.mark.skipif(os.name == "nt", reason="creating symbolic links requires extra privileges on Windows") +def test_symlink_outside_is_rejected(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + outside = tmp_path / "outside" + outside.mkdir() + inside = tmp_path / "inside" + inside.mkdir() + (inside / "link").symlink_to(outside, target_is_directory=True) + monkeypatch.chdir(inside) + with pytest.raises(ValueError, match="is not inside the current directory"): + get_working_directory("link") From 17fb0e9e44e2d5de144494e8554c42f20b113a51 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:41:51 +0200 Subject: [PATCH 08/20] fix: do not change the current directory when matching files --- codestripper/utils/file.py | 12 +++++------- tests/utils/test_file.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/codestripper/utils/file.py b/codestripper/utils/file.py index 5c2d561..ffc622e 100644 --- a/codestripper/utils/file.py +++ b/codestripper/utils/file.py @@ -38,17 +38,17 @@ def __init__(self, else: self.excluded = excluded self.recursive = recursive - self.old_cwd = os.getcwd() self.cwd = get_working_directory(working_directory) def __get_normalized_files(self, file_names: Iterable[str], relative_to: Path, recursive=True) -> \ Generator[str, None, None]: for file_name in file_names: - path = os.path.join(self.cwd, file_name) + # Only the given file name is a glob pattern, so escape the working directory to match it literally + path = os.path.join(glob.escape(self.cwd), file_name) for file in glob.glob(path, recursive=recursive): - tmp = Path(file).relative_to(relative_to) - if tmp.is_file(): - yield str(tmp) + # The found file is absolute, so no need to change the current directory to check it + if Path(file).is_file(): + yield str(Path(file).relative_to(relative_to)) def __convert_to_paths_set(self, file_names: Iterable[str], recursive=True) -> Set[str]: """Convert the file name(s) that are passed as CLI arguments to file paths (can contain GLOB)""" @@ -59,11 +59,9 @@ def __convert_to_paths_set(self, file_names: Iterable[str], recursive=True) -> S def get_matching_files(self) -> Iterable[str]: """Get files that fulfill requirements, match included and do not match excluded""" - os.chdir(self.cwd) included_files = self.__convert_to_paths_set(self.included, self.recursive) self.logger.debug(f"Included files are: {included_files}") excluded_files = self.__convert_to_paths_set(self.excluded, self.recursive) self.logger.debug(f"Excluded files are: {excluded_files}") - os.chdir(self.old_cwd) return included_files - excluded_files diff --git a/tests/utils/test_file.py b/tests/utils/test_file.py index 7d6fd82..7658941 100644 --- a/tests/utils/test_file.py +++ b/tests/utils/test_file.py @@ -72,3 +72,24 @@ def test_symlink_outside_is_rejected(monkeypatch: pytest.MonkeyPatch, tmp_path: monkeypatch.chdir(inside) with pytest.raises(ValueError, match="is not inside the current directory"): get_working_directory("link") + + +def test_does_not_change_current_directory(monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(test_data_dir) + + def fail_on_chdir(path): + raise AssertionError("The current directory should not be changed") + + monkeypatch.setattr(os, "chdir", fail_on_chdir) + files = FileUtils(["*.java"], ["*.txt"], working_directory="data/recursive").get_matching_files() + assert len(files) == 1 + assert os.getcwd() == str(test_data_dir) + + +def test_glob_characters_in_working_directory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + special = tmp_path / "dir[1]" + special.mkdir() + (special / "a.java").write_text("class A {}") + monkeypatch.chdir(tmp_path) + files = FileUtils(["*.java"], working_directory="dir[1]").get_matching_files() + assert set(files) == {"a.java"} From e4ff33842f356ba8a5b085ab0b9669ec137e8f78 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:44:03 +0200 Subject: [PATCH 09/20] fix: avoid duplicate log handlers and clamp verbosity to debug --- codestripper/utils/logger.py | 28 +++++++++++++++------------- tests/utils/test_logger.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/codestripper/utils/logger.py b/codestripper/utils/logger.py index 06394ba..d99acbd 100644 --- a/codestripper/utils/logger.py +++ b/codestripper/utils/logger.py @@ -30,21 +30,23 @@ def format(self, record: logging.LogRecord) -> str: return formatter.format(record) +class CodeStripperHandler(logging.StreamHandler): + """Handler that is added by `set_logger_level`, so that it can be replaced instead of duplicated""" + + +# Level per verbosity, more verbosity than available uses the last level +VERBOSITY_LEVELS = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] + + def set_logger_level(logger_name: str, verbosity: int = 0, add_colours: bool = True) -> None: logger = logging.getLogger(logger_name) - handler = logging.StreamHandler() + level = VERBOSITY_LEVELS[min(max(verbosity, 0), len(VERBOSITY_LEVELS) - 1)] + handler = CodeStripperHandler() if add_colours: handler.setFormatter(ColourFormatter()) - if verbosity == 0: - logger.setLevel(logging.ERROR) - handler.setLevel(logging.ERROR) - elif verbosity == 1: - logger.setLevel(logging.WARNING) - handler.setLevel(logging.WARNING) - elif verbosity == 2: - logger.setLevel(logging.INFO) - handler.setLevel(logging.INFO) - elif verbosity == 3: - logger.setLevel(logging.DEBUG) - handler.setLevel(logging.DEBUG) + logger.setLevel(level) + handler.setLevel(level) + # Replace the handler of an earlier call, otherwise every message is logged multiple times + for existing in [h for h in logger.handlers if isinstance(h, CodeStripperHandler)]: + logger.removeHandler(existing) logger.addHandler(handler) diff --git a/tests/utils/test_logger.py b/tests/utils/test_logger.py index 7fba775..c8b840b 100644 --- a/tests/utils/test_logger.py +++ b/tests/utils/test_logger.py @@ -47,3 +47,31 @@ def test_set_logger(verbosity: int, level: int, add_colours: bool): formatter = logger.handlers[0].formatter correct_formatter = isinstance(formatter, ColourFormatter) if add_colours else formatter is None assert correct_level and correct_formatter, "set_logger should set correct formatter and level" + + +@pytest.mark.parametrize("verbosity, level", [(4, 10), (10, 10), (-1, 40)]) +def test_set_logger_out_of_range_verbosity(verbosity: int, level: int): + logger_name = f"testrange{verbosity}" + set_logger_level(logger_name, verbosity) + logger = logging.getLogger(logger_name) + assert logger.level == level and logger.handlers[0].level == level + + +def test_set_logger_twice_does_not_duplicate_handlers(capsys: pytest.CaptureFixture): + logger_name = "testtwice" + set_logger_level(logger_name, 0) + set_logger_level(logger_name, 2) + logger = logging.getLogger(logger_name) + assert len(logger.handlers) == 1 and logger.level == logging.INFO + logger.info("only once") + assert capsys.readouterr().err.count("only once") == 1 + + +def test_set_logger_keeps_other_handlers(): + logger_name = "testother" + logger = logging.getLogger(logger_name) + other = logging.NullHandler() + logger.addHandler(other) + set_logger_level(logger_name, 1) + set_logger_level(logger_name, 1) + assert other in logger.handlers and len(logger.handlers) == 2 From 0b0498b93488ce6626452fb94324729ce4297b46 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:48:51 +0200 Subject: [PATCH 10/20] fix: report line and reason for invalid range tags and raise StripError --- codestripper/__init__.py | 1 + codestripper/code_stripper.py | 4 ++-- codestripper/errors.py | 27 +++++++++++++++++++-------- codestripper/tags/ignore_file.py | 1 + codestripper/tags/legacy.py | 1 + codestripper/tags/remove.py | 1 + codestripper/tags/tag.py | 3 +++ tests/tags/test_tags.py | 32 ++++++++++++++++++++++++++++++++ tests/test_codestripper.py | 13 +++++++++++-- 9 files changed, 71 insertions(+), 12 deletions(-) diff --git a/codestripper/__init__.py b/codestripper/__init__.py index cdaf458..0beb38a 100644 --- a/codestripper/__init__.py +++ b/codestripper/__init__.py @@ -1 +1,2 @@ from codestripper.code_stripper import CodeStripper, strip_files +from codestripper.errors import StripError diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index b0d92b7..9c04b54 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Union, Iterable, List, Optional -from codestripper.errors import InvalidTagError, TokenizerError +from codestripper.errors import InvalidTagError, StripError, TokenizerError from codestripper.tags import IgnoreFileError from codestripper.tags.tag import Tag, RangeTag from codestripper.tokenizer import Tokenizer @@ -83,7 +83,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None with open(path, 'w+') as handle: handle.write(stripped) if has_errors and fail_on_error: - raise Exception("There were errors stripping some files, see log for details") + raise StripError("There were errors stripping some files, see log for details") return stripped_files diff --git a/codestripper/errors.py b/codestripper/errors.py index 9ae353e..e48f610 100644 --- a/codestripper/errors.py +++ b/codestripper/errors.py @@ -1,6 +1,19 @@ from codestripper.tags.tag import Tag, RangeTag, SingleTag +def _line_number(tag: Tag) -> int: + """The line the tag is on, for a range this is the line of the open tag""" + if isinstance(tag, SingleTag): + return tag.data.line_number + if isinstance(tag, RangeTag): + return tag.open_tag.data.line_number + return -1 + + +class StripError(Exception): + """Raised when stripping one or more files failed, the details are logged""" + + class InvalidTagError(Exception): """Raise if the tag is not valid""" def __init__(self, tag: Tag): @@ -8,9 +21,7 @@ def __init__(self, tag: Tag): @property def line_number(self) -> int: - if isinstance(self.tag, SingleTag): - return self.tag.data.line_number - return -1 + return _line_number(self.tag) @property def message(self) -> str: @@ -20,7 +31,10 @@ def __str__(self): return self.__repr__() def __repr__(self): - return f"Tag {self.tag.__class__.__name__} is invalid" + message = f"Tag {self.tag.__class__.__name__} is invalid" + if self.tag.invalid_reason: + message += f": {self.tag.invalid_reason}" + return message class TokenizerError(Exception): @@ -31,7 +45,4 @@ def __init__(self, tag: Tag, message: str): @property def line_number(self) -> int: - if isinstance(self.tag, SingleTag): - return self.tag.data.line_number - else: - return -1 + return _line_number(self.tag) diff --git a/codestripper/tags/ignore_file.py b/codestripper/tags/ignore_file.py index 612c574..9c3b0c5 100644 --- a/codestripper/tags/ignore_file.py +++ b/codestripper/tags/ignore_file.py @@ -5,6 +5,7 @@ class IgnoreFileTag(SingleTag): + invalid_reason = "the ignore tag is only allowed on the first line" regex = r'cs:ignore' def __init__(self, data: TagData) -> None: diff --git a/codestripper/tags/legacy.py b/codestripper/tags/legacy.py index 7f08697..ed28ef1 100644 --- a/codestripper/tags/legacy.py +++ b/codestripper/tags/legacy.py @@ -36,6 +36,7 @@ def execute(self, content: str) -> Union[str, None]: class LegacyRangeTag(RangeTag): + invalid_reason = "the range does not contain any lines" def __init__(self, open_tag: LegacyOpenTag, close_tag: LegacyCloseTag) -> None: super().__init__(open_tag, close_tag) diff --git a/codestripper/tags/remove.py b/codestripper/tags/remove.py index 04dccba..3d42f88 100644 --- a/codestripper/tags/remove.py +++ b/codestripper/tags/remove.py @@ -24,6 +24,7 @@ def __init__(self, data: TagData) -> None: class RemoveRangeTag(RangeTag): + invalid_reason = "the range does not contain any lines" def __init__(self, open_tag: RemoveOpenTag, close_tag: RemoveCloseTag) -> None: super().__init__(open_tag, close_tag) diff --git a/codestripper/tags/tag.py b/codestripper/tags/tag.py index 88f5d44..3ecebda 100644 --- a/codestripper/tags/tag.py +++ b/codestripper/tags/tag.py @@ -23,6 +23,9 @@ def __repr__(self) -> str: class Tag: + # Why the tag is not valid (if `is_valid` returns False), used in the error message + invalid_reason: str = "" + def __init__(self) -> None: self._offset = 0 diff --git a/tests/tags/test_tags.py b/tests/tags/test_tags.py index 75b7d18..c85fc68 100644 --- a/tests/tags/test_tags.py +++ b/tests/tags/test_tags.py @@ -126,3 +126,35 @@ def test_invalid_tag(): def test_data(): data = TagData("test", 0, 0, 0, 0, 0, 0, 0, "//") assert str(data).__contains__("test") + + +def test_invalid_range_tag_reports_line_and_reason(): + case = "line 1\n//cs:remove:start\n//cs:remove:end\n" + with pytest.raises(InvalidTagError) as ex: + CodeStripper(case, Comment("//")).strip() + assert ex.value.line_number == 2 + assert "RemoveRangeTag" in ex.value.message and "does not contain any lines" in ex.value.message + + +def test_invalid_legacy_range_tag_reports_line(): + case = "line 1\nline 2\n//Start Solution::replacewith::\n//End Solution::replacewith::\n" + with pytest.raises(InvalidTagError) as ex: + CodeStripper(case, Comment("//")).strip() + assert ex.value.line_number == 3 + + +def test_invalid_single_tag_reports_line_and_reason(): + case = "line 1\nline 2\n//cs:ignore\n" + with pytest.raises(InvalidTagError) as ex: + CodeStripper(case, Comment("//")).strip() + assert ex.value.line_number == 3 + assert "only allowed on the first line" in ex.value.message + + +def test_tokenizer_error_line_numbers(): + with pytest.raises(TokenizerError) as ex: + CodeStripper("a\nb\n//cs:remove:start\nc\n", Comment("//")).strip() + assert ex.value.line_number == 3 + with pytest.raises(TokenizerError) as ex: + CodeStripper("a\n//cs:remove:end\n", Comment("//")).strip() + assert ex.value.line_number == 2 diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index 449c304..5fea3f5 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -8,6 +8,7 @@ from _pytest.logging import LogCaptureFixture from codestripper.code_stripper import strip_files +from codestripper.errors import StripError from codestripper.utils import FileUtils from codestripper.utils.enums import UnexpectedInputOptions @@ -136,7 +137,7 @@ def test_fail_on_error(monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptur files = FileUtils(["**/*.java"], working_directory="files").get_matching_files() with caplog.at_level(logging.ERROR, logger='codestripper'): - with pytest.raises(Exception): + with pytest.raises(StripError): strip_files(files, "files", output="out", fail_on_error=True) errors = [rec.message for rec in caplog.records] assert len(errors) == 4 @@ -209,7 +210,7 @@ def test_fail_raises_after_processing_when_fail_on_error(monkeypatch: pytest.Mon bad_name: str, bad_content: bytes, option: str): monkeypatch.chdir(tmp_path) files = _write_mixed_files(tmp_path, bad_name, bad_content) - with pytest.raises(Exception, match="errors stripping"): + with pytest.raises(StripError, match="errors stripping"): strip_files(files, ".", output="out", fail_on_error=True, **{option: UnexpectedInputOptions.FAIL}) assert (tmp_path / "out" / "a.java").is_file() assert (tmp_path / "out" / "c.java").is_file() @@ -220,3 +221,11 @@ def test_comments_option_is_not_kept_between_calls(monkeypatch: pytest.MonkeyPat monkeypatch.chdir(tmp_path) assert strip_files(["a.custom"], ".", output="out", comments=[".custom:!!"]) == ["a.custom"] assert strip_files(["a.custom"], ".", output="out", unknown_extension=UnexpectedInputOptions.IGNORE) == [] + + +def test_error_log_contains_line_of_range_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture, tmp_path: Path): + (tmp_path / "a.java").write_text("class A {\n//cs:remove:start\n//cs:remove:end\n}\n") + monkeypatch.chdir(tmp_path) + with caplog.at_level(logging.ERROR, logger='codestripper'): + strip_files(["a.java"], ".", output="out") + assert [rec.message for rec in caplog.records if rec.levelno == logging.ERROR][0].startswith("a.java:2: ") From 3267d02b90fc7c3b3f4fbd09c84ffb145bb94f6e Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:50:04 +0200 Subject: [PATCH 11/20] fix: only match cs:ignore as a whole tag --- codestripper/tags/ignore_file.py | 3 ++- tests/tags/test_ignore_file.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/codestripper/tags/ignore_file.py b/codestripper/tags/ignore_file.py index 9c3b0c5..398ef89 100644 --- a/codestripper/tags/ignore_file.py +++ b/codestripper/tags/ignore_file.py @@ -6,7 +6,8 @@ class IgnoreFileTag(SingleTag): invalid_reason = "the ignore tag is only allowed on the first line" - regex = r'cs:ignore' + # Not followed by a word character or ':', so 'cs:ignored' or 'cs:ignore:' are not this tag + regex = r'cs:ignore(?![\w:])' def __init__(self, data: TagData) -> None: super().__init__(data) diff --git a/tests/tags/test_ignore_file.py b/tests/tags/test_ignore_file.py index b314c87..66c02c0 100644 --- a/tests/tags/test_ignore_file.py +++ b/tests/tags/test_ignore_file.py @@ -25,4 +25,22 @@ def test_ignored_file(): def test_ignored_file_closing(): case = "" with pytest.raises(IgnoreFileError): - CodeStripper(case, Comment("")).strip() \ No newline at end of file + CodeStripper(case, Comment("")).strip() + + +@pytest.mark.parametrize("case", [ + "//cs:ignore some explanation\nclass A {}\n", + "//cs:ignore \nclass A {}\n", +]) +def test_ignored_file_with_trailing_text(case: str): + with pytest.raises(IgnoreFileError): + CodeStripper(case, Comment("//")).strip() + + +@pytest.mark.parametrize("case", [ + "//cs:ignored\nclass A {}\n", + "//cs:ignore_this\nclass A {}\n", + "//cs:ignore:something\nclass A {}\n", +]) +def test_not_an_ignore_tag(case: str): + assert CodeStripper(case, Comment("//")).strip() == case From bd2214288b29c9e43cf903ed9e0731ed67d6d4f7 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 09:53:42 +0200 Subject: [PATCH 12/20] fix: read and write files as utf-8 --- codestripper/code_stripper.py | 4 ++-- tests/test_codestripper.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index 9c04b54..125071b 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -28,7 +28,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None has_errors: bool = False for file in files: try: - with open(os.path.join(cwd, file), 'r') as handle: + with open(os.path.join(cwd, file), 'r', encoding='utf-8') as handle: content = handle.read() except UnicodeDecodeError: if binary == UnexpectedInputOptions.FAIL: @@ -80,7 +80,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None else: path = os.path.join(out, file) os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, 'w+') as handle: + with open(path, 'w+', encoding='utf-8') as handle: handle.write(stripped) if has_errors and fail_on_error: raise StripError("There were errors stripping some files, see log for details") diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index 5fea3f5..5c2fa9e 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -1,3 +1,4 @@ +import builtins import logging import os.path import re @@ -229,3 +230,31 @@ def test_error_log_contains_line_of_range_tag(monkeypatch: pytest.MonkeyPatch, c with caplog.at_level(logging.ERROR, logger='codestripper'): strip_files(["a.java"], ".", output="out") assert [rec.message for rec in caplog.records if rec.levelno == logging.ERROR][0].startswith("a.java:2: ") + + +def test_non_ascii_content_is_kept(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + content = "class A {\n String s = \"é ✓ 日本\";\n int x;//cs:remove\n}\n" + (tmp_path / "a.java").write_bytes(content.encode("utf-8")) + monkeypatch.chdir(tmp_path) + strip_files(["a.java"], ".", output="out") + expected = "class A {\n String s = \"é ✓ 日本\";\n}\n" + # Read as text: on Windows the newlines are written as \r\n, which is not what is tested here + assert (tmp_path / "out" / "a.java").read_text(encoding="utf-8") == expected + + +def test_files_are_opened_as_utf8(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + """Without an explicit encoding the result depends on the platform (e.g. cp1252 on Windows)""" + (tmp_path / "a.java").write_text("class A {}\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + real_open = builtins.open + encodings = [] + + def spy(file, mode="r", *args, **kwargs): + if "b" not in mode: + encodings.append(kwargs.get("encoding")) + return real_open(file, mode, *args, **kwargs) + + monkeypatch.setattr(builtins, "open", spy) + strip_files(["a.java"], ".", output="out") + monkeypatch.undo() + assert encodings == ["utf-8", "utf-8"] From 77f047d9fe2a307022431aaf3b64c4a6d546f599 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:00:33 +0200 Subject: [PATCH 13/20] build: switch from Poetry to uv and require Python 3.10 --- .github/workflows/release.yml | 28 +- .github/workflows/test.yml | 72 ++--- poetry.lock | 290 ------------------- pyproject.toml | 42 +-- uv.lock | 508 ++++++++++++++++++++++++++++++++++ 5 files changed, 558 insertions(+), 382 deletions(-) delete mode 100644 poetry.lock create mode 100644 uv.lock diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f41b4bb..9a685e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,28 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - name: Install uv + uses: astral-sh/setup-uv@v10.1.0 with: python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Load cached venv - id: cached-poetry-venv - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} - - name: Install dependencies - if: steps.cached-poetry-venv.outputs.cache-hit != 'true' - run: poetry install + enable-cache: true - name: Build - run: poetry build + run: uv build - name: Publish - run: | - poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} - poetry publish \ No newline at end of file + run: uv publish + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df6e4d0..7afe010 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,29 +20,15 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - name: Install uv + uses: astral-sh/setup-uv@v10.1.0 with: python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Load cached venv - id: cached-poetry-venv - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('poetry.lock') }} + enable-cache: true - name: Install dependencies - if: steps.cached-poetry-venv.outputs.cache-hit != 'true' - run: poetry install --no-interaction + run: uv sync --locked - name: Run tests - run: | - source $VENV - poetry run pytest --cov --cov-report=xml --junitxml=tests.xml + run: uv run pytest --cov --cov-report=xml --junitxml=tests.xml - name: Test Report uses: dorny/test-reporter@v1 if: success() || failure() @@ -57,28 +43,16 @@ jobs: python-version: [ "3.12" ] runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v10.1.0 with: python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Load cached venv - id: cached-poetry-venv - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ matrix.python-version }}-{{ runner.os }}-${{ hashFiles('poetry.lock') }} + enable-cache: true - name: Install dependencies - if: steps.cached-poetry-venv.outputs.cache-hit != 'true' - run: poetry install --no-interaction + run: uv sync --locked - name: Run tests - run: poetry run pytest --cov --cov-report=xml --junitxml=tests.xml + run: uv run pytest --cov --cov-report=xml --junitxml=tests.xml - name: Code Coverage Report uses: irongut/CodeCoverageSummary@v1.3.0 with: @@ -99,25 +73,13 @@ jobs: python-version: [ "3.12" ] runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v10.1.0 with: python-version: ${{ matrix.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Load cached venv - id: cached-poetry-venv - uses: actions/cache@v3 - with: - path: .venv - key: venv-${{ matrix.python-version }}-{{ runner.os }}-${{ hashFiles('poetry.lock') }} + enable-cache: true - name: Install dependencies - if: steps.cached-poetry-venv.outputs.cache-hit != 'true' - run: poetry install --no-interaction + run: uv sync --locked - name: Type checker - run: poetry run mypy codestripper + run: uv run mypy codestripper diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 09e7d1b..0000000 --- a/poetry.lock +++ /dev/null @@ -1,290 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["test"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.6.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -groups = ["test"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "mypy" -version = "1.11.2" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, - {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, - {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, - {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, - {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, - {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, - {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, - {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, - {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, - {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, - {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, - {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, - {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, - {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, - {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, - {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, - {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, - {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, - {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["test"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pytest" -version = "7.4.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.7" -groups = ["test"] -files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -groups = ["test"] -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -groups = ["test"] -markers = "python_full_version <= \"3.11.0a6\"" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[metadata] -lock-version = "2.1" -python-versions = "^3.8" -content-hash = "f6b4ee9e49abf530993f51a230f2e875b05a7c1fad31620b83e902c0cc3d369c" diff --git a/pyproject.toml b/pyproject.toml index a1642ac..531f5db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,36 @@ -[tool.poetry] +[project] name = "codestripper" version = "1.2.0" description = "CodeStripper used to strip code from assignments" -authors = ["Bonajo "] -maintainers = ["Bonajo "] -homepage = "https://github.com/FontysVenlo/codestripper" -repository = "https://github.com/FontysVenlo/codestripper" readme = "README.md" -packages = [{include = "codestripper"}] +requires-python = ">=3.10" license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Bonajo", email = "m.bonajo@fontys.nl" }] +maintainers = [{ name = "Bonajo", email = "m.bonajo@fontys.nl" }] +dependencies = [] -[tool.poetry.dependencies] -python = "^3.8" +[project.urls] +Homepage = "https://github.com/FontysVenlo/codestripper" +Repository = "https://github.com/FontysVenlo/codestripper" -[tool.poetry.group.test.dependencies] -pytest = "^7.0.0" -pytest-cov = "^4.0.0" -mypy = "^1.0.0" +[project.scripts] +codestripper = "codestripper.cli:main" + +[dependency-groups] +test = [ + "pytest>=7.0.0,<8", + "pytest-cov>=4.0.0,<5", + "mypy>=1.0.0,<2", +] + +[tool.uv] +default-groups = ["test"] [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["uv_build>=0.12.9,<0.13"] +build-backend = "uv_build" -[tool.poetry.scripts] -codestripper = 'codestripper.cli:main' +[tool.uv.build-backend] +module-name = "codestripper" +module-root = "" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..2d48515 --- /dev/null +++ b/uv.lock @@ -0,0 +1,508 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "codestripper" +version = "1.2.0" +source = { editable = "." } + +[package.dev-dependencies] +test = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +test = [ + { name = "mypy", specifier = ">=1.0.0,<2" }, + { name = "pytest", specifier = ">=7.0.0,<8" }, + { name = "pytest-cov", specifier = ">=4.0.0,<5" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "coverage" +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/62/a8b4dd53308a4b1571e6bdd77ee562c9b1c1742db9886c0aef2f69463067/coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853", size = 223178, upload-time = "2026-09-13T19:08:21.805Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/b9325b8d486a5c05fd4a9eb1cd9c9f697c361ddeda0e73567d5c2d8959f6/coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097", size = 223701, upload-time = "2026-09-13T19:08:23.391Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ee/3ab3ac6e9e7165a623695b73a83d1bfbdf5069681a68a012cb5abb096060/coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a", size = 250434, upload-time = "2026-09-13T19:08:24.948Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/31ffc231e9879876f611cf1ac7622cf092d76762c07ca64dce47b29a200b/coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881", size = 252264, upload-time = "2026-09-13T19:08:26.734Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f4/4fb4f6b16de07dbae3d3a111ae28d32ad657ce6ff4d364c49c10962daad5/coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43", size = 254127, upload-time = "2026-09-13T19:08:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/048e88eb610f44b7542dac10fa45bb2ffad5fbfcdab0f03289a843da934b/coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b", size = 256043, upload-time = "2026-09-13T19:08:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/97/59/d27b113c80b04c7fc64c69457132648e1d287b83a1de41189a504b50b52b/coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec", size = 251092, upload-time = "2026-09-13T19:08:31.57Z" }, + { url = "https://files.pythonhosted.org/packages/45/42/b7eafaceff08dc1dcbfc924fa9233031fd00c8b8287cc4a1a0dcdedcb0f9/coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9", size = 252170, upload-time = "2026-09-13T19:08:33.191Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/57dcc5360aa6d8d24370c12fe855f6ebf5fbf2718c154fb9fdec7161de07/coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38", size = 250173, upload-time = "2026-09-13T19:08:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/379a8c7ac0f3063769dc43fd2475e9fc9b2722cb88eabe71f4d862660c93/coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a", size = 253986, upload-time = "2026-09-13T19:08:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/25/70218d2a6077730843b75ee2525fa64a750283d71cc4c063dc0b75ba6979/coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594", size = 250444, upload-time = "2026-09-13T19:08:38.434Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/6c41c441baa6c8a1fed5615e4e1e7d0ca649b33b472ee28591a92d70a1ff/coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773", size = 251046, upload-time = "2026-09-13T19:08:40.133Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/01166d6f8aa42acd148b4c2c88173d60eefdd0270ea2bcbba79be35bc193/coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc", size = 225242, upload-time = "2026-09-13T19:08:41.672Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/a76060478919becf0fbea2df2053a6e1a7195f11edf8dfd51c58c1636b43/coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90", size = 225868, upload-time = "2026-09-13T19:08:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/d09495dfd5191b5593852bbe2f037afae768157443378d066d9ecea69a49/coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da", size = 223307, upload-time = "2026-09-13T19:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/310196a80258e28ebf6e5aa814a5a6ffbb18c03cf3c0c0c04191ed893e88/coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07", size = 223817, upload-time = "2026-09-13T19:08:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/73/29/ed0fda1fcd440cdfc07e72a07f3c9c3b43f65a6b3d53bff2cba9b9de50e8/coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5", size = 254222, upload-time = "2026-09-13T19:08:48.358Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/5e2507fbd71ec1047620978f39568b0d2ed6c4468d08495a0c24f5677f65/coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475", size = 256134, upload-time = "2026-09-13T19:08:49.926Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b1/a9f97846554bc240473656f9c3874591105f29c08eccf4282daadc6bc281/coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567", size = 258240, upload-time = "2026-09-13T19:08:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/2b1ab208d06719394851cdd7dc322157870a93b178410b1bda6cac7755f9/coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e", size = 260202, upload-time = "2026-09-13T19:08:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/af/14/ff4db31d6e3126d5b72aab1e868d35ec94e1b71dee46f5c3b54cb1b2438e/coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d", size = 254305, upload-time = "2026-09-13T19:08:54.833Z" }, + { url = "https://files.pythonhosted.org/packages/ce/27/c28faa4818f61c49ca3bfb4a77e1c191e79a40447504b89af0bd859cd6fc/coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b", size = 255935, upload-time = "2026-09-13T19:08:56.453Z" }, + { url = "https://files.pythonhosted.org/packages/46/5a/42e64e5b716048cae33e2e15f8c6fe04a927467056c533b8e88900da895e/coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf", size = 253995, upload-time = "2026-09-13T19:08:58.159Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/860287acd5a1e29acd337f302b476b54148203590091b1a1555b2e914b50/coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01", size = 257767, upload-time = "2026-09-13T19:08:59.958Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c5/4b68006da567b4b413352f891776c35776b3557a0479a28187ab57c21659/coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08", size = 253715, upload-time = "2026-09-13T19:09:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0e/b2d0c47cfbf11770e197f1d0f11a92864eb29fd7eaca45bc9915573d9725/coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572", size = 254624, upload-time = "2026-09-13T19:09:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/3bab8821b2b942155578fc34e88e21b0670d922f039ab23651f6035ba7f7/coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2", size = 225404, upload-time = "2026-09-13T19:09:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ce/4f4ce667a97d1c538b46a79b89161395ad2beffc248b6c782afc9f521927/coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7", size = 225879, upload-time = "2026-09-13T19:09:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/26/17/f3dce5e47351ab34522dd037fd64b111742958e24861bdd2820971bc1a26/coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22", size = 225428, upload-time = "2026-09-13T19:09:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" }, + { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" }, + { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" }, + { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" }, + { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" }, + { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/ae/16/e11addf5322d98e86307da9e00c94644b97cb72c534c74cda89705bebeef/coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743", size = 223544, upload-time = "2026-09-13T19:11:09.108Z" }, + { url = "https://files.pythonhosted.org/packages/92/d9/7ccd6484f615961d94b09c75904f87d86375109596069ae3a495a205dbbe/coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e", size = 223882, upload-time = "2026-09-13T19:11:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/59/37/2fd78152a557df4a7e4ad78d6851ede90c211838d526e4916c6016f45144/coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae", size = 254987, upload-time = "2026-09-13T19:11:13.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/58e2f9b8b13cc422aa7f474a498945ec7f446ef473b5b6bae18d2981f2a9/coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370", size = 257902, upload-time = "2026-09-13T19:11:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/63f8f6a67c03b86455c726be273b2aa5b57fd1122f22350d141bbb142e91/coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae", size = 259522, upload-time = "2026-09-13T19:11:18.206Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/1f2261da7483e345fe55de11e41ffc16a543511e37c0912134c0dccc4f6c/coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de", size = 261723, upload-time = "2026-09-13T19:11:20.628Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8a/7eb1a361e044b7293f49c921a974b532627ed37f3bef5e7e9f58bcd0d5a2/coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1", size = 255462, upload-time = "2026-09-13T19:11:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/67/66/6d94f7ea87e99c519def5cfa5d4a1ab20247d6b403eb6e6b5b63d6aa5985/coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c", size = 257617, upload-time = "2026-09-13T19:11:24.933Z" }, + { url = "https://files.pythonhosted.org/packages/3d/52/c3de0a3868589a1463a4f1cb0222eca0e5fa9f91da865ccf20d2e19ec19c/coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6", size = 255495, upload-time = "2026-09-13T19:11:27.158Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a3/ea10e75f20fc1e7be10b166a494826a965d19b1dc9c8a81c511a01153311/coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f", size = 259728, upload-time = "2026-09-13T19:11:29.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/96/9d8020d97de46f0390739beda196322586cdd87e1fc357223e6a28bc4135/coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df", size = 254904, upload-time = "2026-09-13T19:11:31.86Z" }, + { url = "https://files.pythonhosted.org/packages/5b/21/2ff8867d4560f4f639a82d8fcf13c27eea88a96193daba096027c48bb929/coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040", size = 256828, upload-time = "2026-09-13T19:11:34.078Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4b/feacad51cb5163e3baa13281f8a3098f15d7934cc1f28cfe656557eb4e46/coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827", size = 225722, upload-time = "2026-09-13T19:11:36.314Z" }, + { url = "https://files.pythonhosted.org/packages/39/34/66bbc6ffd3c51fa67604c566920e772c5e3baa57f88dcf7b109c7f18b2cb/coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d", size = 226195, upload-time = "2026-09-13T19:11:38.493Z" }, + { url = "https://files.pythonhosted.org/packages/f7/96/4bad920d4caed127c37c136a64b0730fa495517d2eb58809e0d81e6e7dff/coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855", size = 225625, upload-time = "2026-09-13T19:11:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2b/a88c7906eeb7da4394e932060d7008e153b3a2cd8d11782d82faa03bb7a3/coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6", size = 224271, upload-time = "2026-09-13T19:11:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1f/98db59c595680d16f553890deac6d72610e04f192c1964ec9b69cbe790ab/coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d", size = 224564, upload-time = "2026-09-13T19:11:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/699e236d4cb97ec282b0655afc85acf12349e71a02fff0f1a7d0fb643079/coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6", size = 265423, upload-time = "2026-09-13T19:11:47.666Z" }, + { url = "https://files.pythonhosted.org/packages/96/1b/6eaa21912863b3e3bf23cfe605fb62929bb1a41dc33d2bfa8e92000232bd/coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1", size = 268503, upload-time = "2026-09-13T19:11:50.12Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ac/4eb46bffa98c28a80559a58c12f17e19308d52b31174af1bd49decf76606/coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1", size = 271059, upload-time = "2026-09-13T19:11:52.363Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d4/17a51ef1f6a084c9abbead522cfbee0fe9d29a593128781e724b7a0795b3/coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b", size = 272039, upload-time = "2026-09-13T19:11:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/9fdeba8b8aa0848030f45f869ddedc306795a8c6e53e901b8553afbf5415/coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de", size = 265869, upload-time = "2026-09-13T19:11:57.423Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ab3eb506b2e4dd278440fbbbc7ed18424a86d8a981db0b90fdba66f5d34d/coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef", size = 268883, upload-time = "2026-09-13T19:11:59.71Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0c/f4edefdce33f01954a74237df5ee83ccef5dcd1165e04abf0adf47543fca/coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876", size = 265359, upload-time = "2026-09-13T19:12:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/df/86/ce5ed885fc7ce2adc9a5971134ace257d0f84f19c3d117da89c179b9662e/coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8", size = 270056, upload-time = "2026-09-13T19:12:04.46Z" }, + { url = "https://files.pythonhosted.org/packages/48/a1/5dbd5f95070cece25c6df9e6ea4ded2a87dc695ce82a49bcb2116452b342/coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220", size = 265498, upload-time = "2026-09-13T19:12:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ff/254b26f3300f87c7c53ac97da41ae75f0e2ac066aa964bc073c0f3750b38/coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0", size = 267459, upload-time = "2026-09-13T19:12:09.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/20/bf9958f8ddbbf6a2d39da3d4009f33e0fcf19c2d2fd99715c6494cdc8e8c/coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5", size = 226021, upload-time = "2026-09-13T19:12:11.933Z" }, + { url = "https://files.pythonhosted.org/packages/35/8f/12d46948704d9e437c8dca2718e5d54ecc2f85396b64b500c29d8dbe10db/coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db", size = 226853, upload-time = "2026-09-13T19:12:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7f/1ca5fd0601054fce5a3539375b997e67683646e2bf62f60c41e70b529fcc/coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0", size = 226163, upload-time = "2026-09-13T19:12:16.589Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/12/e2e9ca532cf5a0e08c9489826c4a35c6958c92ba0313fda70e8c6c3912be/librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489", size = 148673, upload-time = "2026-08-07T10:46:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7c/02005e23478bd5950618d9712e0fd2b4c511657857f3efd8ba6a5feabcdd/librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52", size = 153547, upload-time = "2026-08-07T10:46:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/d8848a735f5642077fc4b3b4bebcdb08edf10178e3add45597f5201a368f/librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8", size = 494355, upload-time = "2026-08-07T10:46:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0b/8604f41ea02feace490e9e405a338a15f9905369f55b239a9ce31c946f24/librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1", size = 485459, upload-time = "2026-08-07T10:46:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ac/84153bda1ce0da609182527ab92b40d961809e544eefdc5a1c2422971416/librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d", size = 498398, upload-time = "2026-08-07T10:46:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3a/5ca6cd282b2c244bec8ec84102e09773264e9c02891d56ab3a8f0e4d7083/librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702", size = 515474, upload-time = "2026-08-07T10:46:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/73/d3/bd34110234779eb843c6ed66aba7c9b2091d3dd85989f1fb9922f564cb7a/librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9", size = 509484, upload-time = "2026-08-07T10:46:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/43c3f7f071d71631a7daa3b835ef2168ea39f20692d81464d4e47fbaa6d6/librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b", size = 532534, upload-time = "2026-08-07T10:46:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1c/b854adf036ea817c40408873a5b794d65a91d9f0f39826f2ad2a2d5d7f48/librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db", size = 537087, upload-time = "2026-08-07T10:46:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/c9a890e244e7dd725d3bd8b560e41f0aec787eaf343b46956a290ab7b841/librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451", size = 536575, upload-time = "2026-08-07T10:46:33.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c5/c8e70b60b704299555f55db468eb46b1c81bfc60201ffbfe20407d89870c/librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389", size = 517142, upload-time = "2026-08-07T10:46:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/767a90c41f5d381b3195bc88ac0ec4afda35777c9c781e1f9848fedd965e/librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c", size = 558714, upload-time = "2026-08-07T10:46:36.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/3c0624b8dc8301ab808f2b3a910995bcabe28df070fb9a0e5505ae997dae/librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e", size = 104426, upload-time = "2026-08-07T10:46:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/e91c0382304bedb2db9c6801897319a9dcb68daac5e975819b562362f20d/librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053", size = 125057, upload-time = "2026-08-07T10:46:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" }, + { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] From 6b13dee9a318dd0b0e76022c66cf9125b4a92115 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:01:29 +0200 Subject: [PATCH 14/20] fix: fail on error by default in strip_files, like the command line --- README.md | 2 ++ codestripper/code_stripper.py | 2 +- tests/test_codestripper.py | 17 ++++++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f0fe593..0093316 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ from codestripper.code_stripper import strip_files stripped = strip_files(["src/Test.java"], working_directory=".", output="out", dry_run=False) ``` +`strip_files` returns the files that were stripped. Files that fail (e.g. an invalid tag) are logged and the other files are still processed, after which a `StripError` is raised. Pass `fail_on_error=False` to only log the errors, like the `-e` flag of the command line tool. + ## Examples This section contains examples for all supported tags. diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index 125071b..a9d6c94 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -16,7 +16,7 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None, * ,comments: Optional[List[str]] = None, - output: Union[Path, str] = "out", dry_run: bool = False, fail_on_error: bool = False, + output: Union[Path, str] = "out", dry_run: bool = False, fail_on_error: bool = True, binary: UnexpectedInputOptions = UnexpectedInputOptions.FAIL, unknown_extension: UnexpectedInputOptions = UnexpectedInputOptions.FAIL) -> List[str]: mapping = get_comments_mapping(comments) diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index 5c2fa9e..0a14805 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -101,7 +101,7 @@ def test_log_missing_close_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptu monkeypatch.chdir(test_project_dir) files = FileUtils(["MissingClose.java"], working_directory="files").get_matching_files() with caplog.at_level(logging.ERROR, logger='codestripper'): - strip_files(files, "files", output="out") + strip_files(files, "files", output="out", fail_on_error=False) errors = [rec.message for rec in caplog.records] assert len(errors) == 1 and "MissingClose.java" in errors[0] and "1" in errors[0] @@ -110,7 +110,7 @@ def test_log_wrong_close_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCapture monkeypatch.chdir(test_project_dir) files = FileUtils(["WrongClose.java"], working_directory="files").get_matching_files() with caplog.at_level(logging.ERROR, logger='codestripper'): - strip_files(files, "files", output="out") + strip_files(files, "files", output="out", fail_on_error=False) errors = [rec.message for rec in caplog.records] assert len(errors) == 1 and "WrongClose.java" in errors[0] and "3" in errors[0] @@ -119,7 +119,7 @@ def test_log_missing_open_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptur monkeypatch.chdir(test_project_dir) files = FileUtils(["MissingOpen.java"], working_directory="files").get_matching_files() with caplog.at_level(logging.ERROR, logger='codestripper'): - strip_files(files, "files") + strip_files(files, "files", fail_on_error=False) errors = [rec.message for rec in caplog.records] assert len(errors) == 1 and "MissingOpen.java" in errors[0] and "1" in errors[0] @@ -128,7 +128,7 @@ def test_log_invalid_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixt monkeypatch.chdir(test_project_dir) files = FileUtils(["InvalidTag.java"], working_directory="files").get_matching_files() with caplog.at_level(logging.ERROR, logger='codestripper'): - strip_files(files, "files", output="out") + strip_files(files, "files", output="out", fail_on_error=False) errors = [rec.message for rec in caplog.records] assert len(errors) == 1 and "InvalidTag.java" in errors[0] and "2" in errors[0] @@ -228,7 +228,7 @@ def test_error_log_contains_line_of_range_tag(monkeypatch: pytest.MonkeyPatch, c (tmp_path / "a.java").write_text("class A {\n//cs:remove:start\n//cs:remove:end\n}\n") monkeypatch.chdir(tmp_path) with caplog.at_level(logging.ERROR, logger='codestripper'): - strip_files(["a.java"], ".", output="out") + strip_files(["a.java"], ".", output="out", fail_on_error=False) assert [rec.message for rec in caplog.records if rec.levelno == logging.ERROR][0].startswith("a.java:2: ") @@ -258,3 +258,10 @@ def spy(file, mode="r", *args, **kwargs): strip_files(["a.java"], ".", output="out") monkeypatch.undo() assert encodings == ["utf-8", "utf-8"] + + +def test_fail_on_error_is_default(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "a.java").write_text("class A {\n//cs:remove:start\n//cs:remove:end\n}\n") + monkeypatch.chdir(tmp_path) + with pytest.raises(StripError): + strip_files(["a.java"], ".", output="out") From 4589f9a3352f8e4502d7e95935290a55cbae5322 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:02:55 +0200 Subject: [PATCH 15/20] fix: do not strip files from the output directory again --- codestripper/code_stripper.py | 7 +++++++ tests/test_codestripper.py | 27 +++++++++++++++++++++++++++ tests/test_main.py | 9 +++++++++ 3 files changed, 43 insertions(+) diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index a9d6c94..b65cc03 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -23,10 +23,17 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None cwd = get_working_directory(working_directory) out = os.path.join(os.getcwd(), output) + # Files in the output directory are the result of an earlier run, so they should not be stripped again. + # Only relevant if the output directory is a subdirectory of the working directory (not the directory itself) + real_out = Path(os.path.realpath(out)) + skip_output_directory = real_out != Path(cwd) and real_out.is_relative_to(cwd) stripped_files: List[str] = [] has_errors: bool = False for file in files: + if skip_output_directory and Path(cwd, file).is_relative_to(real_out): + logger.debug(f"Skipping '{file}', it is in the output directory") + continue try: with open(os.path.join(cwd, file), 'r', encoding='utf-8') as handle: content = handle.read() diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index 0a14805..fb60221 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -265,3 +265,30 @@ def test_fail_on_error_is_default(monkeypatch: pytest.MonkeyPatch, tmp_path: Pat monkeypatch.chdir(tmp_path) with pytest.raises(StripError): strip_files(["a.java"], ".", output="out") + + +def test_output_directory_is_skipped(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "a.java").write_text("class A {\n int x;//cs:remove\n}\n") + (tmp_path / "out").mkdir() + (tmp_path / "out" / "a.java").write_text("class Old {}\n") + monkeypatch.chdir(tmp_path) + files = FileUtils(["**/*.java"]).get_matching_files() + assert sorted(files) == [os.path.join("a.java"), os.path.join("out", "a.java")] + assert strip_files(files, ".", output="out") == ["a.java"] + assert not (tmp_path / "out" / "out").exists() + assert (tmp_path / "out" / "a.java").read_text() == "class A {\n}\n" + + +def test_output_directory_is_working_directory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "a.java").write_text("class A {\n int x;//cs:remove\n}\n") + monkeypatch.chdir(tmp_path) + assert strip_files(["a.java"], ".", output=".") == ["a.java"] + assert (tmp_path / "a.java").read_text() == "class A {\n}\n" + + +def test_output_directory_outside_working_directory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "project").mkdir() + (tmp_path / "project" / "a.java").write_text("class A {}\n") + monkeypatch.chdir(tmp_path) + assert strip_files(["a.java"], "project", output="project_out") == ["a.java"] + assert (tmp_path / "project_out" / "a.java").is_file() diff --git a/tests/test_main.py b/tests/test_main.py index a946027..4b62bfe 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -86,3 +86,12 @@ def test_main_working_directory_outside(monkeypatch: pytest.MonkeyPatch, capsys: with pytest.raises(SystemExit): main() assert "is not inside the current directory" in capsys.readouterr().err + + +def test_main_twice_does_not_strip_output(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + (tmp_path / "a.java").write_text("class A {}\n") + monkeypatch.chdir(tmp_path) + for _ in range(2): + with patch.object(sys, 'argv', ["codestripper", "**/*.java"]): + main() + assert sorted(path.relative_to(tmp_path).as_posix() for path in tmp_path.rglob("*.java")) == ["a.java", "out/a.java"] From e29102efb0390ace8de3b1e1cade759f9494461f Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:05:33 +0200 Subject: [PATCH 16/20] refactor: keep tokenizer state per instance --- codestripper/tokenizer.py | 13 ++++++------- tests/test_tokenizer.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 tests/test_tokenizer.py diff --git a/codestripper/tokenizer.py b/codestripper/tokenizer.py index eb39f75..49b6e10 100644 --- a/codestripper/tokenizer.py +++ b/codestripper/tokenizer.py @@ -41,21 +41,20 @@ def calculate_mappings(tags: Set[Type[SingleTag]], comment: Comment) -> Tuple[Cr class Tokenizer: + # Only the (expensive) calculation is shared between tokenizers, the state of a tokenizer is per instance mapping_cache: Dict[str, Tuple[CreateTagMapping, Pattern]] = {} - mappings: CreateTagMapping = {} - regex: Pattern = re.compile("") - comment: Comment def __init__(self, content: str, comment: Comment) -> None: self.content = content + self.comment = comment self.ordered_tags: List[Tag] = [] self.open_stack: List[RangeOpenTag] = [] self.range_stack: Dict[int, Optional[List[Tag]]] = {} - Tokenizer.comment = comment if not str(comment) in Tokenizer.mapping_cache: Tokenizer.mapping_cache[str(comment)] = calculate_mappings(default_tags, comment) - Tokenizer.mappings = Tokenizer.mapping_cache[str(comment)][0] - Tokenizer.regex = Tokenizer.mapping_cache[str(comment)][1] + self.mappings: CreateTagMapping + self.regex: Pattern + self.mappings, self.regex = Tokenizer.mapping_cache[str(comment)] self.group_count = self.regex.groups def tokenize(self) -> List[Tag]: @@ -78,7 +77,7 @@ def tokenize(self) -> List[Tag]: continue # All groups should be named else: data = self.__create_tag_data(self.content, line_number, line_start, line_end, match, parameter) - tag = Tokenizer.mappings[kind](data) + tag = self.mappings[kind](data) self.__handle_tag(tag) if len(self.open_stack) != 0: t = self.open_stack[0] diff --git a/tests/test_tokenizer.py b/tests/test_tokenizer.py new file mode 100644 index 0000000..1a679ce --- /dev/null +++ b/tests/test_tokenizer.py @@ -0,0 +1,17 @@ +from codestripper.tags import RemoveTag +from codestripper.tokenizer import Tokenizer +from codestripper.utils.comments import Comment + + +def test_tokenizers_do_not_share_state(): + """A tokenizer that is created later should not change how an earlier tokenizer works""" + slashes = Tokenizer("a\n//cs:remove\n", Comment("//")) + hashes = Tokenizer("a\n#cs:remove\n", Comment("#")) + + hash_tags = hashes.tokenize() + slash_tags = slashes.tokenize() + + assert len(slash_tags) == 1 and isinstance(slash_tags[0], RemoveTag) + assert slash_tags[0].data.comment == Comment("//") + assert len(hash_tags) == 1 and hash_tags[0].data.comment == Comment("#") + From e17c3b9327e8bf62b9761a05e9d3c9afb03357ba Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:08:58 +0200 Subject: [PATCH 17/20] chore: add ruff linter and remove unused imports and dead code --- codestripper/__main__.py | 1 - codestripper/cli.py | 1 - codestripper/code_stripper.py | 86 +++++++++++++++++------------------ codestripper/tags/legacy.py | 3 -- codestripper/tags/tag.py | 2 +- codestripper/tokenizer.py | 12 ++--- codestripper/utils/enums.py | 2 +- codestripper/utils/file.py | 2 +- pyproject.toml | 19 +++++++- tests/tags/test_replace.py | 4 +- tests/test_codestripper.py | 6 ++- tests/test_main.py | 6 ++- tests/utils/test_file.py | 6 ++- tests/utils/test_logger.py | 3 +- uv.lock | 29 ++++++++++++ 15 files changed, 113 insertions(+), 69 deletions(-) diff --git a/codestripper/__main__.py b/codestripper/__main__.py index 7733ee8..e6668ee 100644 --- a/codestripper/__main__.py +++ b/codestripper/__main__.py @@ -1,4 +1,3 @@ -import sys from codestripper.cli import main diff --git a/codestripper/cli.py b/codestripper/cli.py index bce974a..a4aae3e 100644 --- a/codestripper/cli.py +++ b/codestripper/cli.py @@ -1,6 +1,5 @@ import argparse import os -from typing import List from codestripper.code_stripper import strip_files from codestripper.utils import FileUtils, set_logger_level, get_working_directory diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index b65cc03..9589eb4 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -15,9 +15,10 @@ logger = logging.getLogger("codestripper") -def strip_files(files: Iterable[str], working_directory: Union[str, None] = None, * ,comments: Optional[List[str]] = None, - output: Union[Path, str] = "out", dry_run: bool = False, fail_on_error: bool = True, - binary: UnexpectedInputOptions = UnexpectedInputOptions.FAIL, unknown_extension: UnexpectedInputOptions = UnexpectedInputOptions.FAIL) -> List[str]: +def strip_files(files: Iterable[str], working_directory: Union[str, None] = None, *, + comments: Optional[List[str]] = None, output: Union[Path, str] = "out", dry_run: bool = False, + fail_on_error: bool = True, binary: UnexpectedInputOptions = UnexpectedInputOptions.FAIL, + unknown_extension: UnexpectedInputOptions = UnexpectedInputOptions.FAIL) -> List[str]: mapping = get_comments_mapping(comments) @@ -51,44 +52,43 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None shutil.copy2(os.path.join(cwd, file), path) stripped_files.append(file) continue - if content is not None: - stripped = "" - try: - _, file_extension = os.path.splitext(file) - file_extension = file_extension.lower() - if file_extension not in mapping: - if unknown_extension == UnexpectedInputOptions.FAIL: - logger.error(f"{file}: unknown extension: '{file_extension}', " - f"please specify which comment to use for this file extension.") - has_errors = True - continue - elif unknown_extension == UnexpectedInputOptions.IGNORE: - logger.info(f"Unknown extension: '{file_extension}' ignored") - continue - else: - # Keep the complete content - stripped = content + stripped = "" + try: + _, file_extension = os.path.splitext(file) + file_extension = file_extension.lower() + if file_extension not in mapping: + if unknown_extension == UnexpectedInputOptions.FAIL: + logger.error(f"{file}: unknown extension: '{file_extension}', " + f"please specify which comment to use for this file extension.") + has_errors = True + continue + elif unknown_extension == UnexpectedInputOptions.IGNORE: + logger.info(f"Unknown extension: '{file_extension}' ignored") + continue else: - com = mapping[file_extension] - stripped = CodeStripper(content, com).strip() - except IgnoreFileError: - logger.info(f"File '{file}' is ignored, because of ignore tag") - continue - except (TokenizerError, InvalidTagError) as ex: - has_errors = True - message = f"{file}:{ex.line_number}: {ex.message}" - logger.error(message) - continue - stripped_files.append(file) - if dry_run: - # A dry run has no other output, so print instead of log (logging depends on the verbosity) - print(f"==> {file} <==") - print(stripped, end="" if stripped.endswith("\n") else "\n") + # Keep the complete content + stripped = content else: - path = os.path.join(out, file) - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, 'w+', encoding='utf-8') as handle: - handle.write(stripped) + com = mapping[file_extension] + stripped = CodeStripper(content, com).strip() + except IgnoreFileError: + logger.info(f"File '{file}' is ignored, because of ignore tag") + continue + except (TokenizerError, InvalidTagError) as ex: + has_errors = True + message = f"{file}:{ex.line_number}: {ex.message}" + logger.error(message) + continue + stripped_files.append(file) + if dry_run: + # A dry run has no other output, so print instead of log (logging depends on the verbosity) + print(f"==> {file} <==") + print(stripped, end="" if stripped.endswith("\n") else "\n") + else: + path = os.path.join(out, file) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w+', encoding='utf-8') as handle: + handle.write(stripped) if has_errors and fail_on_error: raise StripError("There were errors stripping some files, see log for details") return stripped_files @@ -101,11 +101,9 @@ def __init__(self, content: str, comment: Comment) -> None: self.comment = comment def strip(self) -> str: - - if self.content is not None: - tokenizer = Tokenizer(self.content, self.comment) - tags = tokenizer.tokenize() - self.__traverse(tags) + tokenizer = Tokenizer(self.content, self.comment) + tags = tokenizer.tokenize() + self.__traverse(tags) return self.content def __traverse(self, tags: List[Tag], offset=0) -> int: diff --git a/codestripper/tags/legacy.py b/codestripper/tags/legacy.py index ed28ef1..2fb9793 100644 --- a/codestripper/tags/legacy.py +++ b/codestripper/tags/legacy.py @@ -45,8 +45,5 @@ def is_valid(self) -> bool: return self.end - self.start > 0 def execute(self, content: str) -> Union[str, None]: - # old_open_size = self.open_tag.end - self.open_tag.start - # open = self.open_tag.execute(content, offset) - # offset += (len(open) - old_open_size) return None diff --git a/codestripper/tags/tag.py b/codestripper/tags/tag.py index 3ecebda..4987593 100644 --- a/codestripper/tags/tag.py +++ b/codestripper/tags/tag.py @@ -1,6 +1,6 @@ import re from dataclasses import dataclass -from typing import Type, Union, List, Pattern, Iterable, Optional +from typing import Type, List, Iterable, Optional from codestripper.utils.comments import Comment diff --git a/codestripper/tokenizer.py b/codestripper/tokenizer.py index 49b6e10..4c00ac4 100644 --- a/codestripper/tokenizer.py +++ b/codestripper/tokenizer.py @@ -50,7 +50,7 @@ def __init__(self, content: str, comment: Comment) -> None: self.ordered_tags: List[Tag] = [] self.open_stack: List[RangeOpenTag] = [] self.range_stack: Dict[int, Optional[List[Tag]]] = {} - if not str(comment) in Tokenizer.mapping_cache: + if str(comment) not in Tokenizer.mapping_cache: Tokenizer.mapping_cache[str(comment)] = calculate_mappings(default_tags, comment) self.mappings: CreateTagMapping self.regex: Pattern @@ -82,8 +82,6 @@ def tokenize(self) -> List[Tag]: if len(self.open_stack) != 0: t = self.open_stack[0] raise TokenizerError(t, f"There is still an unclosed {t.__class__.__name__} tag!") - # if len(self.range_stack) != 0: - # raise TokenizerError(self.range_stack[0][0], f"") return self.ordered_tags def __add_range_stack(self, index: int, tag: Tag) -> None: @@ -96,10 +94,13 @@ def __handle_tag(self, tag: Tag) -> None: self.open_stack.append(tag) elif isinstance(tag, RangeCloseTag): if len(self.open_stack) == 0: - raise TokenizerError(tag, f"Cannot close tag {tag.__class__.__name__}, as there is no matching open tag") + raise TokenizerError( + tag, f"Cannot close tag {tag.__class__.__name__}, as there is no matching open tag") range_open = self.open_stack.pop() if range_open.parent != tag.parent: - raise TokenizerError(tag, f"Cannot match closing tag: {tag.__class__.__name__} to open tag: {range_open.__class__.__name__}") + raise TokenizerError( + tag, f"Cannot match closing tag: {tag.__class__.__name__} " + f"to open tag: {range_open.__class__.__name__}") range_tag: RangeTag = tag.parent(range_open, tag) index = len(self.open_stack) embedded = self.range_stack.pop(index + 1, None) @@ -130,7 +131,6 @@ def __create_tag_data(self, content: str, line_number: int, line_start: int, lin else: parameter_start = command_end parameter_end = command_start - parameter = line[parameter_start:parameter_end] return TagData(line=line, line_number=line_number, line_start=line_start, diff --git a/codestripper/utils/enums.py b/codestripper/utils/enums.py index 9f91cf4..ca6c63d 100644 --- a/codestripper/utils/enums.py +++ b/codestripper/utils/enums.py @@ -4,4 +4,4 @@ class UnexpectedInputOptions(enum.Enum): FAIL = "fail" IGNORE = "ignore" - INCLUDE = "include" \ No newline at end of file + INCLUDE = "include" diff --git a/codestripper/utils/file.py b/codestripper/utils/file.py index ffc622e..333f209 100644 --- a/codestripper/utils/file.py +++ b/codestripper/utils/file.py @@ -2,7 +2,7 @@ import os import logging from pathlib import Path -from typing import Dict, Generator, Iterable, Set, Union, List +from typing import Generator, Iterable, Set, Union def get_working_directory(working_directory: Union[str, None]) -> str: diff --git a/pyproject.toml b/pyproject.toml index 531f5db..cb27cbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,9 @@ Repository = "https://github.com/FontysVenlo/codestripper" codestripper = "codestripper.cli:main" [dependency-groups] +lint = [ + "ruff>=0.16.8", +] test = [ "pytest>=7.0.0,<8", "pytest-cov>=4.0.0,<5", @@ -25,7 +28,7 @@ test = [ ] [tool.uv] -default-groups = ["test"] +default-groups = ["test", "lint"] [build-system] requires = ["uv_build>=0.12.9,<0.13"] @@ -34,3 +37,17 @@ build-backend = "uv_build" [tool.uv.build-backend] module-name = "codestripper" module-root = "" + +[tool.ruff] +# Same as the PyCharm default +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +# Comparable to the PyCharm inspections: PEP 8 (E, W), unused code and unresolved names (F), +# naming conventions (N) and shadowing of builtins (A) +select = ["E", "W", "F", "N", "A"] + +[tool.ruff.lint.per-file-ignores] +# Imports in an __init__.py are the public interface of the package +"__init__.py" = ["F401"] diff --git a/tests/tags/test_replace.py b/tests/tags/test_replace.py index b3ee394..7a909b3 100644 --- a/tests/tags/test_replace.py +++ b/tests/tags/test_replace.py @@ -17,9 +17,7 @@ def test_replace_empty(): case = """ asd//cs:replace: """ - expected = """ - - """ + expected = "\n \n " # The replacement is empty, the whitespace in front of the tag is kept output = CodeStripper(case, Comment("//")).strip() assert output == expected, "Replace should replace with empty string keeping whitespace" diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index fb60221..71efd40 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -21,7 +21,8 @@ def test_project_with_unknown_extension_fail(monkeypatch: pytest.MonkeyPatch): files = FileUtils(["**/*.java", "pom.xml", "**/*.test"], working_directory="testproject").get_matching_files() with pytest.raises(Exception): - strip_files(files, "testproject", output="out",unknown_extension=UnexpectedInputOptions.FAIL, fail_on_error=True) + strip_files(files, "testproject", output="out", unknown_extension=UnexpectedInputOptions.FAIL, + fail_on_error=True) def test_project_with_unknown_extension_ignore(monkeypatch: pytest.MonkeyPatch): @@ -224,7 +225,8 @@ def test_comments_option_is_not_kept_between_calls(monkeypatch: pytest.MonkeyPat assert strip_files(["a.custom"], ".", output="out", unknown_extension=UnexpectedInputOptions.IGNORE) == [] -def test_error_log_contains_line_of_range_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture, tmp_path: Path): +def test_error_log_contains_line_of_range_tag(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture, + tmp_path: Path): (tmp_path / "a.java").write_text("class A {\n//cs:remove:start\n//cs:remove:end\n}\n") monkeypatch.chdir(tmp_path) with caplog.at_level(logging.ERROR, logger='codestripper'): diff --git a/tests/test_main.py b/tests/test_main.py index 4b62bfe..ed44c66 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -17,7 +17,8 @@ def test_main(monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture): monkeypatch.chdir(test_project_dir) shutil.rmtree("out", ignore_errors=True) - args = ["filename", "-c", ".test:!!", "-c", ".cs:#", "-x", "*.class", "-vv", "-o", "out", "-w", "testproject", "**/*.java"] + args = ["filename", "-c", ".test:!!", "-c", ".cs:#", "-x", "*.class", "-vv", "-o", "out", "-w", "testproject", + "**/*.java"] with patch.object(sys, 'argv', args): with caplog.at_level(logging.INFO, logger='codestripper'): main() @@ -94,4 +95,5 @@ def test_main_twice_does_not_strip_output(monkeypatch: pytest.MonkeyPatch, tmp_p for _ in range(2): with patch.object(sys, 'argv', ["codestripper", "**/*.java"]): main() - assert sorted(path.relative_to(tmp_path).as_posix() for path in tmp_path.rglob("*.java")) == ["a.java", "out/a.java"] + found = sorted(path.relative_to(tmp_path).as_posix() for path in tmp_path.rglob("*.java")) + assert found == ["a.java", "out/a.java"] diff --git a/tests/utils/test_file.py b/tests/utils/test_file.py index 7658941..7fb6f0d 100644 --- a/tests/utils/test_file.py +++ b/tests/utils/test_file.py @@ -19,12 +19,14 @@ (["**/*.java"], ["data/recursive/*.java"], False, ["data/test1.java"]), (["**/*.java"], ["data/*.java"], True, ["data/recursive/test2.java"]), (["**/*.java"], ["data/*.java"], False, []), - (["data/**/*"], [], True, ["data/test1.java", "data/test1.txt", "data/test2.txt", "data/recursive/test2.java", "data/recursive/test3.txt"]), + (["data/**/*"], [], True, ["data/test1.java", "data/test1.txt", "data/test2.txt", "data/recursive/test2.java", + "data/recursive/test3.txt"]), (["data/test1*"], [], True, ["data/test1.java", "data/test1.txt"]), (["data/test1*"], ["**/*.txt"], True, ["data/test1.java"]) ] ) -def test_glob(included: List[str], excluded: List[str], recursive: bool, expected: List[Path], monkeypatch: pytest.MonkeyPatch): +def test_glob(included: List[str], excluded: List[str], recursive: bool, expected: List[Path], + monkeypatch: pytest.MonkeyPatch): monkeypatch.chdir(test_data_dir) expected_paths = [str(Path(path)) for path in expected] files = FileUtils(included, excluded, working_directory=None, recursive=recursive).get_matching_files() diff --git a/tests/utils/test_logger.py b/tests/utils/test_logger.py index c8b840b..5980554 100644 --- a/tests/utils/test_logger.py +++ b/tests/utils/test_logger.py @@ -18,7 +18,8 @@ ) def test_colour_formatter(level: int, name: str, control: bool): formatter = ColourFormatter() - record = LogRecord(name="test", level=level, pathname="testpath", lineno=0, msg="Test message", args=None, exc_info=None) + record = LogRecord(name="test", level=level, pathname="testpath", lineno=0, msg="Test message", args=None, + exc_info=None) formatted = formatter.format(record) contains_name = name in formatted contains_control = '\x1b[' in formatted diff --git a/uv.lock b/uv.lock index 2d48515..273cd2b 100644 --- a/uv.lock +++ b/uv.lock @@ -12,6 +12,9 @@ version = "1.2.0" source = { editable = "." } [package.dev-dependencies] +lint = [ + { name = "ruff" }, +] test = [ { name = "mypy" }, { name = "pytest" }, @@ -21,6 +24,7 @@ test = [ [package.metadata] [package.metadata.requires-dev] +lint = [{ name = "ruff", specifier = ">=0.16.8" }] test = [ { name = "mypy", specifier = ">=1.0.0,<2" }, { name = "pytest", specifier = ">=7.0.0,<8" }, @@ -444,6 +448,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, ] +[[package]] +name = "ruff" +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, +] + [[package]] name = "tomli" version = "2.4.1" From 0f5fa8fa28a154b58de7012b2cb136f58b294e2f Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:10:19 +0200 Subject: [PATCH 18/20] fix: include the tags in the tokenizer cache key --- codestripper/tokenizer.py | 16 ++++++++---- tests/tags/test_tags.py | 51 ++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/codestripper/tokenizer.py b/codestripper/tokenizer.py index 4c00ac4..49e48d0 100644 --- a/codestripper/tokenizer.py +++ b/codestripper/tokenizer.py @@ -1,5 +1,5 @@ import re -from typing import Optional, Set, Dict, Callable, List, Pattern, Tuple, Type +from typing import Optional, Set, Dict, Callable, FrozenSet, List, Pattern, Tuple, Type from codestripper.errors import TokenizerError from codestripper.tags import ReplaceTag, UncommentCloseTag, IgnoreFileTag, RemoveOpenTag, RemoveCloseTag, \ @@ -40,9 +40,13 @@ def calculate_mappings(tags: Set[Type[SingleTag]], comment: Comment) -> Tuple[Cr return mappings, regex # type: ignore +# The mappings and regex depend on the comment and on the tags that are used +CacheKey = Tuple[Comment, FrozenSet[Type[SingleTag]]] + + class Tokenizer: # Only the (expensive) calculation is shared between tokenizers, the state of a tokenizer is per instance - mapping_cache: Dict[str, Tuple[CreateTagMapping, Pattern]] = {} + mapping_cache: Dict[CacheKey, Tuple[CreateTagMapping, Pattern]] = {} def __init__(self, content: str, comment: Comment) -> None: self.content = content @@ -50,11 +54,13 @@ def __init__(self, content: str, comment: Comment) -> None: self.ordered_tags: List[Tag] = [] self.open_stack: List[RangeOpenTag] = [] self.range_stack: Dict[int, Optional[List[Tag]]] = {} - if str(comment) not in Tokenizer.mapping_cache: - Tokenizer.mapping_cache[str(comment)] = calculate_mappings(default_tags, comment) + # The tags can be changed (e.g. a custom tag is added), so they are part of the key + key: CacheKey = (comment, frozenset(default_tags)) + if key not in Tokenizer.mapping_cache: + Tokenizer.mapping_cache[key] = calculate_mappings(default_tags, comment) self.mappings: CreateTagMapping self.regex: Pattern - self.mappings, self.regex = Tokenizer.mapping_cache[str(comment)] + self.mappings, self.regex = Tokenizer.mapping_cache[key] self.group_count = self.regex.groups def tokenize(self) -> List[Tag]: diff --git a/tests/tags/test_tags.py b/tests/tags/test_tags.py index c85fc68..5adcdc9 100644 --- a/tests/tags/test_tags.py +++ b/tests/tags/test_tags.py @@ -3,7 +3,7 @@ from codestripper import tokenizer from codestripper.code_stripper import CodeStripper from codestripper.errors import InvalidTagError, TokenizerError -from codestripper.tags.tag import RangeOpenTag, TagData, RangeCloseTag, RangeTag +from codestripper.tags.tag import RangeOpenTag, SingleTag, TagData, RangeCloseTag, RangeTag from codestripper.utils.comments import Comment @@ -106,20 +106,15 @@ def test_mismatch_open_close(): CodeStripper(case, Comment("//")).strip() -def test_invalid_tag(): +def test_invalid_tag(monkeypatch: pytest.MonkeyPatch): case = """ test line - ^^cs:invalid:start - ^^cs:invalid:end + //cs:invalid:start + //cs:invalid:end """ - default_tags = tokenizer.default_tags - tokenizer.default_tags = { - InvalidOpenTag, - InvalidCloseTag - } + monkeypatch.setattr(tokenizer, "default_tags", {InvalidOpenTag, InvalidCloseTag}) with pytest.raises(InvalidTagError) as ex: - CodeStripper(case, Comment("^^")).strip() - tokenizer.default_tags = default_tags + CodeStripper(case, Comment("//")).strip() assert "InvalidRangeTag" in str(ex) @@ -158,3 +153,37 @@ def test_tokenizer_error_line_numbers(): with pytest.raises(TokenizerError) as ex: CodeStripper("a\n//cs:remove:end\n", Comment("//")).strip() assert ex.value.line_number == 2 + + +class CustomTag(SingleTag): + regex = r'cs:custom' + + def execute(self, content: str) -> str: + return "custom" + + +def test_tag_added_after_comment_was_used(monkeypatch: pytest.MonkeyPatch): + """The cached regex of a comment should not hide tags that are added later""" + case = "a\n//cs:custom\nb\n" + # Use the comment before the tag is added, so its regex is cached + assert CodeStripper(case, Comment("//")).strip() == case + + monkeypatch.setattr(tokenizer, "default_tags", {*tokenizer.default_tags, CustomTag}) + assert CodeStripper(case, Comment("//")).strip() == "a\ncustom\nb\n" + + +def test_tag_added_to_default_tags_in_place(monkeypatch: pytest.MonkeyPatch): + case = "a\n//cs:custom\nb\n" + assert CodeStripper(case, Comment("//")).strip() == case + + monkeypatch.setattr(tokenizer, "default_tags", set(tokenizer.default_tags)) + tokenizer.default_tags.add(CustomTag) + assert CodeStripper(case, Comment("//")).strip() == "a\ncustom\nb\n" + + +def test_removed_tag_is_no_longer_used(monkeypatch: pytest.MonkeyPatch): + case = "a\n//cs:remove\nb\n" + assert CodeStripper(case, Comment("//")).strip() == "a\nb\n" + + monkeypatch.setattr(tokenizer, "default_tags", set(tokenizer.default_tags) - {tokenizer.RemoveTag}) + assert CodeStripper(case, Comment("//")).strip() == case From cf73bdd9a76f8f0d7b74cca86e048b57d6dc68e2 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:12:45 +0200 Subject: [PATCH 19/20] build: bump pytest to 9.1.1 to fix the /tmp directory vulnerability --- pyproject.toml | 4 ++-- uv.lock | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cb27cbd..7aa5ef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,8 @@ lint = [ "ruff>=0.16.8", ] test = [ - "pytest>=7.0.0,<8", - "pytest-cov>=4.0.0,<5", + "pytest>=9.0.3,<10", + "pytest-cov>=7.0.0,<8", "mypy>=1.0.0,<2", ] diff --git a/uv.lock b/uv.lock index 273cd2b..bd91ef4 100644 --- a/uv.lock +++ b/uv.lock @@ -27,8 +27,8 @@ test = [ lint = [{ name = "ruff", specifier = ">=0.16.8" }] test = [ { name = "mypy", specifier = ">=1.0.0,<2" }, - { name = "pytest", specifier = ">=7.0.0,<8" }, - { name = "pytest-cov", specifier = ">=4.0.0,<5" }, + { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "pytest-cov", specifier = ">=7.0.0,<8" }, ] [[package]] @@ -418,9 +418,18 @@ wheels = [ { 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" }, ] +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + [[package]] name = "pytest" -version = "7.4.4" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -428,24 +437,26 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] From d74beacc7fd4f31639dbc3178cb2b7df3b7f4905 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Mon, 21 Sep 2026 10:55:36 +0200 Subject: [PATCH 20/20] fix: do not copy binary files during a dry run --- codestripper/code_stripper.py | 10 +++++++--- tests/test_codestripper.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/codestripper/code_stripper.py b/codestripper/code_stripper.py index 9589eb4..963a22d 100644 --- a/codestripper/code_stripper.py +++ b/codestripper/code_stripper.py @@ -47,10 +47,14 @@ def strip_files(files: Iterable[str], working_directory: Union[str, None] = None logger.info(f"Ignoring binary file: '{file}'") continue else: - path = os.path.join(out, file) - os.makedirs(os.path.dirname(path), exist_ok=True) - shutil.copy2(os.path.join(cwd, file), path) stripped_files.append(file) + if dry_run: + print(f"==> {file} <==") + print("(binary file, copied unchanged)") + else: + path = os.path.join(out, file) + os.makedirs(os.path.dirname(path), exist_ok=True) + shutil.copy2(os.path.join(cwd, file), path) continue stripped = "" try: diff --git a/tests/test_codestripper.py b/tests/test_codestripper.py index 71efd40..2db4a16 100644 --- a/tests/test_codestripper.py +++ b/tests/test_codestripper.py @@ -294,3 +294,13 @@ def test_output_directory_outside_working_directory(monkeypatch: pytest.MonkeyPa monkeypatch.chdir(tmp_path) assert strip_files(["a.java"], "project", output="project_out") == ["a.java"] assert (tmp_path / "project_out" / "a.java").is_file() + + +def test_dry_run_does_not_copy_binary_file(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture, + tmp_path: Path): + (tmp_path / "image.bin").write_bytes(b"\xff\xfe\xfd\x80") + monkeypatch.chdir(tmp_path) + stripped = strip_files(["image.bin"], ".", output="out", dry_run=True, binary=UnexpectedInputOptions.INCLUDE) + assert stripped == ["image.bin"] + assert capsys.readouterr().out == "==> image.bin <==\n(binary file, copied unchanged)\n" + assert not (tmp_path / "out").exists()