From 24374bee15a7144558e7d84756b674ea61a0e73d Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:39:06 -0500 Subject: [PATCH 01/67] Allow empty pattern list --- pathspec/_backends/hyperscan/gitignore.py | 15 ++++++-- pathspec/_backends/hyperscan/pathspec.py | 46 ++++++++++++++++------- pathspec/_backends/re2/pathspec.py | 4 +- pathspec/_version.py | 2 +- tests/test_04_pathspec.py | 12 ++++++ tests/test_05_gitignore.py | 12 ++++++ 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/pathspec/_backends/hyperscan/gitignore.py b/pathspec/_backends/hyperscan/gitignore.py index a21b9ac..465b542 100644 --- a/pathspec/_backends/hyperscan/gitignore.py +++ b/pathspec/_backends/hyperscan/gitignore.py @@ -99,12 +99,15 @@ def _init_db( Returns a :class:`list` indexed by expression id (:class:`int`) to its data (:class:`HyperscanExprDat`). """ + # WARNING: Hyperscan raises a `hyperscan.error` exception when compiled with + # zero elements. + assert patterns, patterns + # Prepare patterns. expr_data: list[HyperscanExprDat] = [] exprs: list[bytes] = [] for pattern_index, pattern in patterns: - if pattern.include is None: - continue + assert pattern.include is not None, (pattern_index, pattern) # Encode regex. assert isinstance(pattern, RegexPattern), pattern @@ -189,8 +192,14 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: """ # NOTICE: According to benchmarking, a method callback is 13% faster than # using a closure here. + db = self._db + if self._db is None: + # Database was not initialized because there were no patterns. Return no + # match. + return (None, None) + self._out = (None, -1, 0) - self._db.scan(file.encode('utf8'), match_event_handler=self.__on_match) + db.scan(file.encode('utf8'), match_event_handler=self.__on_match) out_include, out_index = self._out[:2] if out_index == -1: diff --git a/pathspec/_backends/hyperscan/pathspec.py b/pathspec/_backends/hyperscan/pathspec.py index 212a6e5..d93637d 100644 --- a/pathspec/_backends/hyperscan/pathspec.py +++ b/pathspec/_backends/hyperscan/pathspec.py @@ -59,32 +59,40 @@ def __init__( if hyperscan is None: raise hyperscan_error - if not patterns: - raise ValueError(f"{patterns=!r} cannot be empty.") - elif not isinstance(patterns[0], RegexPattern): + if patterns and not isinstance(patterns[0], RegexPattern): raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.") use_patterns = enumerate_patterns( patterns, filter=True, reverse=False, ) - self._db = self._make_db() + debug_exprs = bool(_debug_exprs) + if use_patterns: + db = self._make_db() + expr_data = self._init_db( + db=db, + debug=debug_exprs, + patterns=use_patterns, + sort_ids=_test_sort, + ) + else: + # WARNING: The hyperscan database cannot be initialized with zero + # patterns. + db = None + expr_data = [] + + self._db: Optional[hyperscan.Database] = db """ *_db* (:class:`hyperscan.Database`) is the Hyperscan database. """ - self._debug_exprs = bool(_debug_exprs) + self._debug_exprs = debug_exprs """ *_debug_exprs* (:class:`bool`) is whether to include additional debugging information for the expressions. """ - self._expr_data: list[HyperscanExprDat] = self._init_db( - db=self._db, - debug=self._debug_exprs, - patterns=use_patterns, - sort_ids=_test_sort, - ) + self._expr_data: list[HyperscanExprDat] = expr_data """ *_expr_data* (:class:`list`) maps expression index (:class:`int`) to expression data (:class:`:class:`HyperscanExprDat`). @@ -130,12 +138,15 @@ def _init_db( Returns a :class:`list` indexed by expression id (:class:`int`) to its data (:class:`HyperscanExprDat`). """ + # WARNING: Hyperscan raises a `hyperscan.error` exception when compiled with + # zero elements. + assert patterns, patterns + # Prepare patterns. expr_data: list[HyperscanExprDat] = [] exprs: list[bytes] = [] for pattern_index, pattern in patterns: - if pattern.include is None: - continue + assert pattern.include is not None, (pattern_index, pattern) # Encode regex. assert isinstance(pattern, RegexPattern), pattern @@ -176,6 +187,7 @@ def _init_db( elements=len(exprs), flags=HS_FLAGS, ) + return expr_data @override @@ -191,8 +203,14 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: """ # NOTICE: According to benchmarking, a method callback is 20% faster than # using a closure here. + db = self._db + if self._db is None: + # Database was not initialized because there were no patterns. Return no + # match. + return (None, None) + self._out = (None, -1) - self._db.scan(file.encode('utf8'), match_event_handler=self.__on_match) + db.scan(file.encode('utf8'), match_event_handler=self.__on_match) out_include, out_index = self._out if out_index == -1: diff --git a/pathspec/_backends/re2/pathspec.py b/pathspec/_backends/re2/pathspec.py index 63918ed..d9b6b0e 100644 --- a/pathspec/_backends/re2/pathspec.py +++ b/pathspec/_backends/re2/pathspec.py @@ -57,9 +57,7 @@ def __init__( if re2_error is not None: raise re2_error - if not patterns: - raise ValueError(f"{patterns=!r} cannot be empty.") - elif not isinstance(patterns[0], RegexPattern): + if patterns and not isinstance(patterns[0], RegexPattern): raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.") use_patterns = dict(enumerate_patterns( diff --git a/pathspec/_version.py b/pathspec/_version.py index 48f3e2b..a98bf9d 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.0" +__version__ = "1.0.1.dev1" diff --git a/tests/test_04_pathspec.py b/tests/test_04_pathspec.py index f5d6c73..e6edf79 100644 --- a/tests/test_04_pathspec.py +++ b/tests/test_04_pathspec.py @@ -1029,3 +1029,15 @@ def test_09_issue_80_b(self): self.assertEqual(files - ignores, keeps) self.assertEqual(files - keeps, ignores) + + def test_10_issue_100(self): + """ + Test an empty list of patterns. + """ + for sub_test in self.parameterize_from_lines('gitignore', []): + with sub_test() as spec: + files = {'foo'} + results = list(spec.check_files(files)) + includes = get_includes(results) + debug = debug_results(spec, results) + self.assertEqual(includes, set(), debug) diff --git a/tests/test_05_gitignore.py b/tests/test_05_gitignore.py index 974fd8d..0cb0caf 100644 --- a/tests/test_05_gitignore.py +++ b/tests/test_05_gitignore.py @@ -677,3 +677,15 @@ def test_08_issue_81_c(self): "libfoo/__init__.py", }, debug) self.assertEqual(files - ignores, set()) + + def test_09_issue_100(self): + """ + Test an empty list of patterns. + """ + for sub_test in self.parameterize_from_lines([]): + with sub_test() as spec: + files = {'foo'} + results = list(spec.check_files(files)) + includes = get_includes(results) + debug = debug_results(spec, results) + self.assertEqual(includes, set(), debug) From 6bcf1511216892e2290bc2f205115c42552cbabe Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:51:26 -0500 Subject: [PATCH 02/67] Allow empty pattern list --- CHANGES.rst | 11 +++++++++++ CHANGES_1.in.rst | 11 +++++++++++ README-dist.rst | 11 +++++++++++ pyproject.toml | 2 +- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 809e703..04dbc1c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,17 @@ Change History ============== +1.0.1 (2026-01-06) +------------------ + +Bug fixes: + +- `Issue #100`_: ValueError(f"{patterns=!r} cannot be empty.") when using black. + + +.. _`Issue #100`: https://github.com/cpburnz/python-pathspec/issues/100 + + 1.0.0 (2026-01-05) ------------------ diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index e474e58..5f6dd39 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,4 +1,15 @@ +1.0.1 (2026-01-06) +------------------ + +Bug fixes: + +- `Issue #100`_: ValueError(f"{patterns=!r} cannot be empty.") when using black. + + +.. _`Issue #100`: https://github.com/cpburnz/python-pathspec/issues/100 + + 1.0.0 (2026-01-05) ------------------ diff --git a/README-dist.rst b/README-dist.rst index ae24ef2..0cf36af 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,6 +227,17 @@ Change History ============== +1.0.1 (2026-01-06) +------------------ + +Bug fixes: + +- `Issue #100`_: ValueError(f"{patterns=!r} cannot be empty.") when using black. + + +.. _`Issue #100`: https://github.com/cpburnz/python-pathspec/issues/100 + + 1.0.0 (2026-01-05) ------------------ diff --git a/pyproject.toml b/pyproject.toml index 2efffb0..fb8e3a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.0" +version = "1.0.1.dev1" [project.optional-dependencies] hyperscan = [ From 3d942b5e2d00fd757593d89bf8c8b2ff2d6028b4 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:56:57 -0500 Subject: [PATCH 03/67] Fix publish --- .github/workflows/publish-to-pypi.yml | 5 ++++- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 4710dbc..ba26b13 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,5 +1,8 @@ name: Publish to PyPI -on: release +on: + release: + types: + - released jobs: build: diff --git a/pyproject.toml b/pyproject.toml index fb8e3a4..5e93ee1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.1.dev1" +version = "1.0.1.dev2" [project.optional-dependencies] hyperscan = [ From 75f4d0672305dfe3d0a6f182b870c3edec29f316 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 6 Jan 2026 08:00:41 -0500 Subject: [PATCH 04/67] Release v1.0.1 --- pathspec/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pathspec/_version.py b/pathspec/_version.py index a98bf9d..5cd9f61 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.1.dev1" +__version__ = "1.0.1" diff --git a/pyproject.toml b/pyproject.toml index 5e93ee1..d19968b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.1.dev2" +version = "1.0.1" [project.optional-dependencies] hyperscan = [ From 441261fcfdeb791a33586eec34932c56a1dafb0c Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:59:33 -0500 Subject: [PATCH 05/67] Misc --- pathspec/_meta.py | 4 ++++ pathspec/_version.py | 2 +- pathspec/gitignore.py | 15 +++++++-------- pathspec/patterns/gitignore/basic.py | 2 ++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pathspec/_meta.py b/pathspec/_meta.py index 4c419fa..53ec0fe 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -48,12 +48,16 @@ "Bartłomiej Żak ", "Matthias ", "Avasam ", + "Anıl Karagenç ", "Yannic Schröder ", "axesider ", "TomRuk ", "Oleh Prypin ", + "Lumina ", "Kurt McKee ", "Dobatymo ", "Tomoki Nakamaru ", + "Sebastien Eskenazi ", + "Bar Vered ", ] __license__ = "MPL 2.0" diff --git a/pathspec/_version.py b/pathspec/_version.py index 5cd9f61..5fd4ee1 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.1" +__version__ = "1.0.2.dev1" diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index ca19fb5..e99535c 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -1,9 +1,9 @@ """ This module provides :class:`.GitIgnoreSpec` which replicates *.gitignore* behavior, and handles edge-cases where Git's behavior differs from what's -documented. Git allows including files from excluded directories which appears -to contradict the documentation. This uses :class:`.GitIgnoreSpecPattern` -to fully replicate Git's handling. +documented. Git allows including files from excluded directories which directly +contradicts the documentation. This uses :class:`.GitIgnoreSpecPattern` to fully +replicate Git's handling. """ from __future__ import annotations @@ -106,11 +106,10 @@ def from_lines( :class:`io.TextIOBase` (e.g., from :func:`open` or :class:`io.StringIO`) or the result from :meth:`str.splitlines`. - *pattern_factory* does not need to be set for :class:`GitIgnoreSpec`. It can - be either the name of a registered pattern factory (:class:`str`), or a - :class:`~collections.abc.Callable` used to compile patterns. It must accept - an uncompiled pattern (:class:`str`) and return the compiled pattern - (:class:`.Pattern`). Default is :class:`None` for :class:`.GitIgnoreSpecPattern`. + *pattern_factory* does not need to be set for :class:`GitIgnoreSpec`. If + set, it should be either :data:`"gitignore"` or :class:`.GitIgnoreSpecPattern`. + There is no guarantee it will work with any other pattern class. Default is + :data:`None` for :class:`.GitIgnoreSpecPattern`. *backend* (:class:`str` or :data:`None`) is the pattern (regular expression) matching backend to use. Default is :data:`None` for "best" to use the best diff --git a/pathspec/patterns/gitignore/basic.py b/pathspec/patterns/gitignore/basic.py index 22157e2..95d7915 100644 --- a/pathspec/patterns/gitignore/basic.py +++ b/pathspec/patterns/gitignore/basic.py @@ -262,6 +262,8 @@ def __translate_segments(cls, pattern_segs: list[str]) -> list[str]: if i == 0: # A normalized pattern beginning with double-asterisks ('**') will # match any leading path segments. + # - NOTICE: '(?:^|/)' benchmarks slower using p15 (sm=0.9382, + # hs=0.9966, re2=0.9337). out_parts.append('^(?:.+/)?') elif i < end: From 88f7960b0d0a9e11a6ba08f28d0d8aca1109c089 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:13:17 -0500 Subject: [PATCH 06/67] Fix Python 3.9.0-1 bug with Callable --- CHANGES.rst | 8 ++++++++ CHANGES_1.in.rst | 8 ++++++++ README-dist.rst | 8 ++++++++ benchmarks/hyperscan_gitignore_r2.py | 2 +- benchmarks/hyperscan_pathspec_r1.py | 2 +- justfile | 8 -------- pathspec/_backends/hyperscan/gitignore.py | 2 +- pathspec/_backends/hyperscan/pathspec.py | 2 +- pathspec/_backends/re2/gitignore.py | 3 +-- pathspec/_backends/re2/pathspec.py | 2 +- pathspec/_meta.py | 1 + pathspec/_typing.py | 3 +-- pathspec/gitignore.py | 2 +- pathspec/pathspec.py | 2 +- pathspec/util.py | 2 +- pyproject.toml | 2 +- tests/test_04_pathspec.py | 2 +- tests/test_05_gitignore.py | 2 +- 18 files changed, 38 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 04dbc1c..edd3d0a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ Change History ============== +1.0.2 (2026-01-07) +------------------ + +Bug fixes: + +- Type hint `collections.abc.Callable` is does not properly replace `typing.Callable` until Python 3.9.2. + + 1.0.1 (2026-01-06) ------------------ diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 5f6dd39..a3260c0 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,4 +1,12 @@ +1.0.2 (2026-01-07) +------------------ + +Bug fixes: + +- Type hint `collections.abc.Callable` is does not properly replace `typing.Callable` until Python 3.9.2. + + 1.0.1 (2026-01-06) ------------------ diff --git a/README-dist.rst b/README-dist.rst index 0cf36af..6034260 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,6 +227,14 @@ Change History ============== +1.0.2 (2026-01-07) +------------------ + +Bug fixes: + +- Type hint `collections.abc.Callable` is does not properly replace `typing.Callable` until Python 3.9.2. + + 1.0.1 (2026-01-06) ------------------ diff --git a/benchmarks/hyperscan_gitignore_r2.py b/benchmarks/hyperscan_gitignore_r2.py index 013b9a6..ea7d3e6 100644 --- a/benchmarks/hyperscan_gitignore_r2.py +++ b/benchmarks/hyperscan_gitignore_r2.py @@ -5,10 +5,10 @@ from __future__ import annotations from collections.abc import ( - Callable, Sequence) from typing import ( Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. Union) # Replaced by `X | Y` in 3.10. from typing_extensions import ( diff --git a/benchmarks/hyperscan_pathspec_r1.py b/benchmarks/hyperscan_pathspec_r1.py index 23c334b..0966c0d 100644 --- a/benchmarks/hyperscan_pathspec_r1.py +++ b/benchmarks/hyperscan_pathspec_r1.py @@ -5,10 +5,10 @@ from __future__ import annotations from collections.abc import ( - Callable, Sequence) from typing import ( Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional) # Replaced by `X | None` in 3.10. from typing_extensions import ( override) # Added in 3.12. diff --git a/justfile b/justfile index 27067d1..4adb307 100644 --- a/justfile +++ b/justfile @@ -68,10 +68,6 @@ dist-build: _dist_build [group('Distribution')] dist-prebuild: _dist_prebuild -# Publish the package to PyPI. -[group('Distribution')] -dist-publish: _dist_publish - ################################################################################ # Development @@ -134,7 +130,3 @@ _dist_build: _dist_prebuild _dist_prebuild: {{cpy_run}} python prebuild.py - -_dist_publish: - {{cpy_run}} twine check ./dist/* - {{cpy_run}} twine upload -r pathspec --skip-existing ./dist/* diff --git a/pathspec/_backends/hyperscan/gitignore.py b/pathspec/_backends/hyperscan/gitignore.py index 465b542..2428b59 100644 --- a/pathspec/_backends/hyperscan/gitignore.py +++ b/pathspec/_backends/hyperscan/gitignore.py @@ -7,10 +7,10 @@ from __future__ import annotations from collections.abc import ( - Callable, Sequence) from typing import ( Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. Union) # Replaced by `X | Y` in 3.10. diff --git a/pathspec/_backends/hyperscan/pathspec.py b/pathspec/_backends/hyperscan/pathspec.py index d93637d..d55c314 100644 --- a/pathspec/_backends/hyperscan/pathspec.py +++ b/pathspec/_backends/hyperscan/pathspec.py @@ -7,10 +7,10 @@ from __future__ import annotations from collections.abc import ( - Callable, Sequence) from typing import ( Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional) # Replaced by `X | None` in 3.10. try: diff --git a/pathspec/_backends/re2/gitignore.py b/pathspec/_backends/re2/gitignore.py index e946188..cb2525f 100644 --- a/pathspec/_backends/re2/gitignore.py +++ b/pathspec/_backends/re2/gitignore.py @@ -6,9 +6,8 @@ """ from __future__ import annotations -from collections.abc import ( - Callable) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. Union) # Replaced by `X | Y` in 3.10. diff --git a/pathspec/_backends/re2/pathspec.py b/pathspec/_backends/re2/pathspec.py index d9b6b0e..2c58b45 100644 --- a/pathspec/_backends/re2/pathspec.py +++ b/pathspec/_backends/re2/pathspec.py @@ -7,9 +7,9 @@ from __future__ import annotations from collections.abc import ( - Callable, Sequence) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional) # Replaced by `X | None` in 3.10. try: diff --git a/pathspec/_meta.py b/pathspec/_meta.py index 53ec0fe..f18c27a 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -59,5 +59,6 @@ "Tomoki Nakamaru ", "Sebastien Eskenazi ", "Bar Vered ", + "Tzach Shabtay ", ] __license__ = "MPL 2.0" diff --git a/pathspec/_typing.py b/pathspec/_typing.py index 1041177..049966c 100644 --- a/pathspec/_typing.py +++ b/pathspec/_typing.py @@ -9,10 +9,9 @@ import functools import warnings -from collections.abc import ( - Callable) from typing import ( Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. TypeVar) try: diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index e99535c..2640c6c 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -8,10 +8,10 @@ from __future__ import annotations from collections.abc import ( - Callable, Iterable, Sequence) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. TypeVar, Union, # Replaced by `X | Y` in 3.10. diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index f2004e7..cc89404 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import ( - Callable, Collection, Iterable, Iterator, @@ -13,6 +12,7 @@ from itertools import ( zip_longest) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. TypeVar, Union, # Replaced by `X | Y` in 3.10. diff --git a/pathspec/util.py b/pathspec/util.py index fee5065..ea2dbee 100644 --- a/pathspec/util.py +++ b/pathspec/util.py @@ -8,7 +8,6 @@ import posixpath import stat from collections.abc import ( - Callable, Collection, Iterable, Iterator, @@ -17,6 +16,7 @@ dataclass) from typing import ( Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Generic, Optional, # Replaced by `X | None` in 3.10. TypeVar, diff --git a/pyproject.toml b/pyproject.toml index d19968b..e01e6f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.1" +version = "1.0.2.dev1" [project.optional-dependencies] hyperscan = [ diff --git a/tests/test_04_pathspec.py b/tests/test_04_pathspec.py index e6edf79..d71ecd2 100644 --- a/tests/test_04_pathspec.py +++ b/tests/test_04_pathspec.py @@ -7,7 +7,6 @@ import tempfile import unittest from collections.abc import ( - Callable, Iterable, Iterator, Sequence) @@ -19,6 +18,7 @@ from pathlib import ( Path) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional) # Replaced by `X | None` in 3.10. from unittest import ( SkipTest) diff --git a/tests/test_05_gitignore.py b/tests/test_05_gitignore.py index 0cb0caf..9d00c90 100644 --- a/tests/test_05_gitignore.py +++ b/tests/test_05_gitignore.py @@ -4,7 +4,6 @@ import unittest from collections.abc import ( - Callable, Iterable, Iterator, Sequence) @@ -14,6 +13,7 @@ from functools import ( partial) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional) # Replaced by `X | None` in 3.10. from unittest import ( SkipTest) From f3a6b712ec6ab39bfcae732fc33345c711019e73 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:15:50 -0500 Subject: [PATCH 07/67] Fix rst warning --- pathspec/gitignore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index 2640c6c..ef37654 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -107,7 +107,7 @@ def from_lines( the result from :meth:`str.splitlines`. *pattern_factory* does not need to be set for :class:`GitIgnoreSpec`. If - set, it should be either :data:`"gitignore"` or :class:`.GitIgnoreSpecPattern`. + set, it should be either ``"gitignore"`` or :class:`.GitIgnoreSpecPattern`. There is no guarantee it will work with any other pattern class. Default is :data:`None` for :class:`.GitIgnoreSpecPattern`. From 69d9955dfc99d89f2a181d0146bb87730e6e0f7b Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:58:11 -0500 Subject: [PATCH 08/67] Fix testpypi build --- .github/workflows/publish-to-testpypi.yml | 6 ++++++ prebuild.py | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index 9ff0cf9..97b5722 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -22,6 +22,12 @@ jobs: - name: Install pypa/build run: python3 -m pip install --user build + - name: Add commit to version + run: sed -i -e 's/__version__\s*=\s*"(.+)"/__version__ = "\1+$(git rev-parse --short HEAD)"/' pathspec/_version.py + + - name: Prebuild + run: python3 prebuild.py + - name: Build a binary wheel and a source tarball run: python3 -m build diff --git a/prebuild.py b/prebuild.py index 7e6ea6d..ea03752 100644 --- a/prebuild.py +++ b/prebuild.py @@ -9,8 +9,10 @@ import sys from pathlib import ( Path) - -import tomli +try: + import tomllib # Added in 3.11. +except ModuleNotFoundError: + import tomli as tomllib CHANGES_0_IN_RST = Path("CHANGES_0.in.rst") CHANGES_1_IN_RST = Path("CHANGES_1.in.rst") @@ -95,7 +97,7 @@ def generate_setup_cfg() -> None: """ print(f"Read: {PYPROJECT_TOML}") with PYPROJECT_TOML.open('rb') as fh: - config = tomli.load(fh) + config = tomllib.load(fh) print(f"Write: {SETUP_CFG}") output = configparser.ConfigParser() From 3d8ab2b941976e3db709d018f7e8a7bcf1f4c895 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:08:01 -0500 Subject: [PATCH 09/67] Fix testpypi build --- .github/workflows/publish-to-testpypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index 97b5722..213ae8b 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -23,7 +23,7 @@ jobs: run: python3 -m pip install --user build - name: Add commit to version - run: sed -i -e 's/__version__\s*=\s*"(.+)"/__version__ = "\1+$(git rev-parse --short HEAD)"/' pathspec/_version.py + run: sed -i -E "s/__version__\s*=\s*"'"'\(.+\)'"'"/__version__ = '\1+$(git rev-parse --short HEAD)'/" pathspec/_version.py - name: Prebuild run: python3 prebuild.py From 4e782be9d3c88d07f311d751e21a08b092f3eba0 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:14:42 -0500 Subject: [PATCH 10/67] Fix testpypi build --- prebuild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prebuild.py b/prebuild.py index ea03752..5a9fc2b 100644 --- a/prebuild.py +++ b/prebuild.py @@ -60,7 +60,7 @@ def generate_pyproject_toml() -> None: print(f"Read: {VERSION_PY}") version_input = VERSION_PY.read_text() version = re.search( - '^__version__\\s*=\\s*"([^"]+)"', version_input, re.M, + '^__version__\\s*=\\s*["\'](.+)["\']', version_input, re.M, ).group(1) # Replace version. From 979b01934200185a633553ad29d0b3416ed1e0a7 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:31:18 -0500 Subject: [PATCH 11/67] Trusted publishing is a pain --- .github/workflows/publish-to-testpypi.yml | 7 +-- justfile | 12 ++--- pathspec/_version.py | 2 +- pyproject.toml | 2 +- testpypi_prepublish.py | 65 +++++++++++++++++++++++ 5 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 testpypi_prepublish.py diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index 213ae8b..d2fd69b 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -22,11 +22,8 @@ jobs: - name: Install pypa/build run: python3 -m pip install --user build - - name: Add commit to version - run: sed -i -E "s/__version__\s*=\s*"'"'\(.+\)'"'"/__version__ = '\1+$(git rev-parse --short HEAD)'/" pathspec/_version.py - - - name: Prebuild - run: python3 prebuild.py + - name: Prepublish BS + run: python3 testpypi_prepublish.py - name: Build a binary wheel and a source tarball run: python3 -m build diff --git a/justfile b/justfile index 4adb307..a870c81 100644 --- a/justfile +++ b/justfile @@ -28,10 +28,6 @@ bench-gitignore: _bench_gitignore [group('Development')] bench-pathspec: _bench_pathspec -# Build Sphinx documentation. -[group('Development')] -build-docs: _build_docs - # Run tests using the CPython virtual environment. [group('Development')] test: _test_primary @@ -62,11 +58,15 @@ venv-pypy-update: _venv_pypy_update # Build the package. [group('Distribution')] -dist-build: _dist_build +build-dist: _dist_build + +# Build Sphinx documentation. +[group('Distribution')] +build-docs: _build_docs # Generate files used by distribution. [group('Distribution')] -dist-prebuild: _dist_prebuild +prebuild: _dist_prebuild ################################################################################ diff --git a/pathspec/_version.py b/pathspec/_version.py index 5fd4ee1..0523ba1 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.2.dev1" +__version__ = "1.0.2.a1" diff --git a/pyproject.toml b/pyproject.toml index e01e6f4..ad2721c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.2.dev1" +version = "1.0.2.a1" [project.optional-dependencies] hyperscan = [ diff --git a/testpypi_prepublish.py b/testpypi_prepublish.py new file mode 100644 index 0000000..3ca69d2 --- /dev/null +++ b/testpypi_prepublish.py @@ -0,0 +1,65 @@ +""" +This script mangles the version in "pyproject.toml" to work around deficiencies +with TestPyPI. +""" + +import argparse +import re +import subprocess +import sys +import tomllib +from pathlib import ( + Path) + +PYPROJECT_TOML = Path("pyproject.toml") +VERSION_PY = Path("pathspec/_version.py") + + +def update_pyproject_toml() -> None: + """ + Update "pyproject.toml" by mangling its version. + """ + print("Get last tag.") + tag = subprocess.check_output([ + 'git', 'rev-list', '--tags', '--max-count=1', + ], text=True).strip() + + print("Get commit count.") + count = subprocess.check_output([ + 'git', 'rev-list', f'{tag}..HEAD', '--count', + ], text=True).strip() + + print(f"Read: {VERSION_PY}") + version_input = VERSION_PY.read_text() + version = re.search( + '^__version__\\s*=\\s*["\'](.+)["\']', version_input, re.M, + ).group(1) + version += f".dev{count}" + + print(f"Read: {PYPROJECT_TOML}") + output = PYPROJECT_TOML.read_text() + + # Mangle version. + output = re.sub( + '^version\\s*=\\s*".+"', f'version = "{version}"', output, count=1, flags=re.M, + ) + + print(f"Write: {PYPROJECT_TOML}") + PYPROJECT_TOML.write_text(output) + + +def main() -> int: + """ + Run the script. + """ + # Parse command-line arguments. + parser = argparse.ArgumentParser(description=__doc__) + parser.parse_args() + + update_pyproject_toml() + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From 5f5283fa2e7f0e68b8cbda54f5bb2066628159cf Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:38:27 -0500 Subject: [PATCH 12/67] Trusted publishing is a pain --- .github/workflows/publish-to-testpypi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index d2fd69b..904ce8b 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -12,6 +12,7 @@ jobs: steps: - uses: actions/checkout@v6 with: + fetch-tags: true persist-credentials: false - name: Set up Python From 35df8d98470f595de6f53a574e2fb4b8e9d60ba2 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:42:36 -0500 Subject: [PATCH 13/67] Trusted publishing is a pain --- .github/workflows/publish-to-testpypi.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index 904ce8b..eac23ff 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -15,6 +15,9 @@ jobs: fetch-tags: true persist-credentials: false + - name: Fetch tags + run: git fetch --tags origin + - name: Set up Python uses: actions/setup-python@v6 with: From 4cf4e9788622b7ed451d0206cf1bc00b994a1701 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:18:53 -0500 Subject: [PATCH 14/67] Trusted publishing is a pain --- .github/workflows/publish-to-testpypi.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index eac23ff..b5fc392 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -12,12 +12,20 @@ jobs: steps: - uses: actions/checkout@v6 with: - fetch-tags: true persist-credentials: false - name: Fetch tags run: git fetch --tags origin + - name: DEBUG Check tag + run: git rev-list --tags --max-count=1 + + - name: DEBUG Check tag date + run: git show -s --format='%as' $(git rev-list --tags --max-count=1) + + - name: Fetch old commits + run: git fetch --shallow-since=$(git show -s --format='%as' $(git rev-list --tags --max-count=1)) origin + - name: Set up Python uses: actions/setup-python@v6 with: From 73a02203b9c9e90c58156e36cdda5853fbefc4fa Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:28:43 -0500 Subject: [PATCH 15/67] Trusted publishing is a pain --- .github/workflows/publish-to-testpypi.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index b5fc392..899edbf 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -17,12 +17,6 @@ jobs: - name: Fetch tags run: git fetch --tags origin - - name: DEBUG Check tag - run: git rev-list --tags --max-count=1 - - - name: DEBUG Check tag date - run: git show -s --format='%as' $(git rev-list --tags --max-count=1) - - name: Fetch old commits run: git fetch --shallow-since=$(git show -s --format='%as' $(git rev-list --tags --max-count=1)) origin From cbb66012c088c9f51f33b86a22c1c44e9fdf228b Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:31:00 -0500 Subject: [PATCH 16/67] Release v1.0.2 --- pathspec/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pathspec/_version.py b/pathspec/_version.py index 0523ba1..cc25d4c 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.2.a1" +__version__ = "1.0.2" diff --git a/pyproject.toml b/pyproject.toml index ad2721c..f912999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.2.a1" +version = "1.0.2" [project.optional-dependencies] hyperscan = [ From 0ff66291a073efa3daacb4ccace3ce60420923ba Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:32:29 -0500 Subject: [PATCH 17/67] Release v1.0.2 --- CHANGES.rst | 2 +- CHANGES_1.in.rst | 2 +- README-dist.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index edd3d0a..5cf9d5f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,7 +7,7 @@ Change History Bug fixes: -- Type hint `collections.abc.Callable` is does not properly replace `typing.Callable` until Python 3.9.2. +- Type hint `collections.abc.Callable` does not properly replace `typing.Callable` until Python 3.9.2. 1.0.1 (2026-01-06) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index a3260c0..7af08e7 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -4,7 +4,7 @@ Bug fixes: -- Type hint `collections.abc.Callable` is does not properly replace `typing.Callable` until Python 3.9.2. +- Type hint `collections.abc.Callable` does not properly replace `typing.Callable` until Python 3.9.2. 1.0.1 (2026-01-06) diff --git a/README-dist.rst b/README-dist.rst index 6034260..7b5dd02 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -232,7 +232,7 @@ Change History Bug fixes: -- Type hint `collections.abc.Callable` is does not properly replace `typing.Callable` until Python 3.9.2. +- Type hint `collections.abc.Callable` does not properly replace `typing.Callable` until Python 3.9.2. 1.0.1 (2026-01-06) From 66281233ae20aa1de22345c1eb004dad9592b55d Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:53:27 -0500 Subject: [PATCH 18/67] Fix 101 regression --- CHANGES.rst | 11 +++++++++++ CHANGES_1.in.rst | 11 +++++++++++ README-dist.rst | 11 +++++++++++ pathspec/_meta.py | 1 + pathspec/_version.py | 2 +- pathspec/gitignore.py | 10 ++++++---- pathspec/pathspec.py | 10 ++++++---- pathspec/pattern.py | 9 +++++---- pyproject.toml | 2 +- 9 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5cf9d5f..642dd65 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,17 @@ Change History ============== +1.0.3 (2026-01-09) +------------------ + +Bug fixes: + +- `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. + + +.. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 + + 1.0.2 (2026-01-07) ------------------ diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 7af08e7..0cb64c3 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,4 +1,15 @@ +1.0.3 (2026-01-09) +------------------ + +Bug fixes: + +- `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. + + +.. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 + + 1.0.2 (2026-01-07) ------------------ diff --git a/README-dist.rst b/README-dist.rst index 7b5dd02..b56f385 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,6 +227,17 @@ Change History ============== +1.0.3 (2026-01-09) +------------------ + +Bug fixes: + +- `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. + + +.. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 + + 1.0.2 (2026-01-07) ------------------ diff --git a/pathspec/_meta.py b/pathspec/_meta.py index f18c27a..e8d5247 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -60,5 +60,6 @@ "Sebastien Eskenazi ", "Bar Vered ", "Tzach Shabtay ", + "Adam Dangoor " ] __license__ = "MPL 2.0" diff --git a/pathspec/_version.py b/pathspec/_version.py index cc25d4c..df03eff 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.2" +__version__ = "1.0.3.a1" diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index ef37654..89903e8 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -17,10 +17,6 @@ Union, # Replaced by `X | Y` in 3.10. cast, overload) -try: - from typing import Self # Added in 3.11. -except ImportError: - Self = TypeVar("Self", bound='GitIgnoreSpec') from pathspec.backend import ( BackendNamesHint, @@ -42,6 +38,12 @@ _is_iterable, lookup_pattern) +Self = TypeVar("Self", bound='GitIgnoreSpec') +""" +:class:`GitIgnoreSpec` self type hint to support Python v<3.11 using PEP 673 +recommendation. +""" + class GitIgnoreSpec(PathSpec): """ diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index cc89404..74351fb 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -17,10 +17,12 @@ TypeVar, Union, # Replaced by `X | Y` in 3.10. cast) -try: - from typing import Self # Added in 3.11. -except ImportError: - Self = TypeVar("Self", bound='PathSpec') + +Self = TypeVar("Self", bound='PathSpec') +""" +:class:`PathSpec` self type hint to support Python v<3.11 using PEP 673 +recommendation. +""" from pathspec import util from pathspec.backend import ( diff --git a/pathspec/pattern.py b/pathspec/pattern.py index c4493ce..2106b35 100644 --- a/pathspec/pattern.py +++ b/pathspec/pattern.py @@ -14,16 +14,17 @@ Optional, # Replaced by `X | None` in 3.10. TypeVar, Union) # Replaced by `X | Y` in 3.10. -try: - from typing import Self as RegexPatternSelf # Added in 3.11. -except ImportError: - RegexPatternSelf = TypeVar("RegexPatternSelf", bound='RegexPattern') from ._typing import ( AnyStr, # Removed in 3.18. deprecated, # Added in 3.13. override) # Added in 3.12. +RegexPatternSelf = TypeVar("RegexPatternSelf", bound='RegexPattern') +""" +:class:`RegexPattern` self type hint to support Python v<3.11 using PEP 673 +recommendation. +""" class Pattern(object): """ diff --git a/pyproject.toml b/pyproject.toml index f912999..59ae8f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.2" +version = "1.0.3.a1" [project.optional-dependencies] hyperscan = [ From 85cb3cc18bf925bef3b00b74a8b11049826c0129 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:40:54 -0500 Subject: [PATCH 19/67] Fix docs --- doc/source/api.rst | 3 +++ doc/source/conf.py | 6 +++++- pathspec/_meta.py | 3 ++- pathspec/gitignore.py | 2 +- pathspec/pathspec.py | 2 +- pathspec/pattern.py | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index e2f3ebd..be3d765 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -19,6 +19,8 @@ pathspec.pathspec :show-inheritance: :special-members: __init__, __eq__, __len__ + .. autoclass:: Self + pathspec.gitignore ------------------ @@ -31,6 +33,7 @@ pathspec.gitignore :show-inheritance: :special-members: __init__, __eq__, __len__ + .. autoclass:: Self pathspec.backend ---------------- diff --git a/doc/source/conf.py b/doc/source/conf.py index ea1b90b..09f83e9 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -28,7 +28,11 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] # The autodoc extension doesn't understand the `Self` typehint. # To avoid documentation build errors, autodoc typehints must be disabled. diff --git a/pathspec/_meta.py b/pathspec/_meta.py index e8d5247..d1dec2b 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -60,6 +60,7 @@ "Sebastien Eskenazi ", "Bar Vered ", "Tzach Shabtay ", - "Adam Dangoor " + "Adam Dangoor ", + "Marcel Telka ", ] __license__ = "MPL 2.0" diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index 89903e8..93c3d76 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -40,7 +40,7 @@ Self = TypeVar("Self", bound='GitIgnoreSpec') """ -:class:`GitIgnoreSpec` self type hint to support Python v<3.11 using PEP 673 +:class:`.GitIgnoreSpec` self type hint to support Python v<3.11 using PEP 673 recommendation. """ diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index 74351fb..bb88cbf 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -20,7 +20,7 @@ Self = TypeVar("Self", bound='PathSpec') """ -:class:`PathSpec` self type hint to support Python v<3.11 using PEP 673 +:class:`.PathSpec` self type hint to support Python v<3.11 using PEP 673 recommendation. """ diff --git a/pathspec/pattern.py b/pathspec/pattern.py index 2106b35..a4b8a2c 100644 --- a/pathspec/pattern.py +++ b/pathspec/pattern.py @@ -22,7 +22,7 @@ RegexPatternSelf = TypeVar("RegexPatternSelf", bound='RegexPattern') """ -:class:`RegexPattern` self type hint to support Python v<3.11 using PEP 673 +:class:`.RegexPattern` self type hint to support Python v<3.11 using PEP 673 recommendation. """ From 9867f1a954c68e8a4dc9cdcf8bfc5ad018a7951c Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:41:42 -0500 Subject: [PATCH 20/67] Fix tests --- CHANGES.rst | 4 +++- CHANGES_1.in.rst | 4 +++- README-dist.rst | 4 +++- tox.ini | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 642dd65..8e1b208 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,15 +2,17 @@ Change History ============== -1.0.3 (2026-01-09) +1.0.3 (probably 2026-01-09) ------------------ Bug fixes: - `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. +- `Issue #102`_: No module named 'tomllib'. .. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/101 1.0.2 (2026-01-07) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 0cb64c3..8d778cc 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,13 +1,15 @@ -1.0.3 (2026-01-09) +1.0.3 (probably 2026-01-09) ------------------ Bug fixes: - `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. +- `Issue #102`_: No module named 'tomllib'. .. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/101 1.0.2 (2026-01-07) diff --git a/README-dist.rst b/README-dist.rst index b56f385..aa32ca2 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,15 +227,17 @@ Change History ============== -1.0.3 (2026-01-09) +1.0.3 (probably 2026-01-09) ------------------ Bug fixes: - `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. +- `Issue #102`_: No module named 'tomllib'. .. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/101 1.0.2 (2026-01-07) diff --git a/tox.ini b/tox.ini index 75f1ed1..fa9b237 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ envlist = isolated_build = True [testenv] -commands = python -m unittest {posargs} +commands = python -m unittest discover -t . -s tests/ {posargs} package = wheel [testenv:docs] From f9b556abd5eebe94ec70404f5c386bf4451f5437 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:43:58 -0500 Subject: [PATCH 21/67] Fix docs --- CHANGES.rst | 2 +- CHANGES_1.in.rst | 2 +- README-dist.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8e1b208..e793729 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ Change History 1.0.3 (probably 2026-01-09) ------------------- +--------------------------- Bug fixes: diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 8d778cc..a936418 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,6 +1,6 @@ 1.0.3 (probably 2026-01-09) ------------------- +--------------------------- Bug fixes: diff --git a/README-dist.rst b/README-dist.rst index aa32ca2..c56d687 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -228,7 +228,7 @@ Change History 1.0.3 (probably 2026-01-09) ------------------- +--------------------------- Bug fixes: From 1b6bdda35a44cf48edc67a71d8020c26e84a40ec Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:40:46 -0500 Subject: [PATCH 22/67] Releasse v1.0.3 --- CHANGES.rst | 4 ++-- CHANGES_1.in.rst | 4 ++-- README-dist.rst | 4 ++-- pathspec/_version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e793729..6b28365 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,8 +2,8 @@ Change History ============== -1.0.3 (probably 2026-01-09) ---------------------------- +1.0.3 (2026-01-09) +------------------ Bug fixes: diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index a936418..36f71e6 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,6 +1,6 @@ -1.0.3 (probably 2026-01-09) ---------------------------- +1.0.3 (2026-01-09) +------------------ Bug fixes: diff --git a/README-dist.rst b/README-dist.rst index c56d687..31c736f 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,8 +227,8 @@ Change History ============== -1.0.3 (probably 2026-01-09) ---------------------------- +1.0.3 (2026-01-09) +------------------ Bug fixes: diff --git a/pathspec/_version.py b/pathspec/_version.py index df03eff..6b80549 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.3.a1" +__version__ = "1.0.3" diff --git a/pyproject.toml b/pyproject.toml index 59ae8f4..72235ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.3.a1" +version = "1.0.3" [project.optional-dependencies] hyperscan = [ From db3f54e78f68824f641b186bf4a749d944e2153f Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:43:53 -0500 Subject: [PATCH 23/67] Releasse v1.0.3 --- CHANGES.rst | 2 +- CHANGES_1.in.rst | 2 +- README-dist.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6b28365..747d608 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,7 +12,7 @@ Bug fixes: .. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 -.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/102 1.0.2 (2026-01-07) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 36f71e6..523012e 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -9,7 +9,7 @@ Bug fixes: .. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 -.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/102 1.0.2 (2026-01-07) diff --git a/README-dist.rst b/README-dist.rst index 31c736f..79a7287 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -237,7 +237,7 @@ Bug fixes: .. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 -.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/102 1.0.2 (2026-01-07) From 593a85942f54a64269ff1d55969ff9bf1dd049c9 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:21:29 -0500 Subject: [PATCH 24/67] Improve testpypi --- .github/workflows/publish-to-testpypi.yml | 2 +- testpypi_prepublish.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index 899edbf..bc4d882 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -26,7 +26,7 @@ jobs: python-version: "3.x" - name: Install pypa/build - run: python3 -m pip install --user build + run: python3 -m pip install --user build packaging - name: Prepublish BS run: python3 testpypi_prepublish.py diff --git a/testpypi_prepublish.py b/testpypi_prepublish.py index 3ca69d2..9d5004e 100644 --- a/testpypi_prepublish.py +++ b/testpypi_prepublish.py @@ -4,13 +4,16 @@ """ import argparse +import copy import re import subprocess import sys -import tomllib from pathlib import ( Path) +from packaging.version import ( + Version) + PYPROJECT_TOML = Path("pyproject.toml") VERSION_PY = Path("pathspec/_version.py") @@ -31,10 +34,14 @@ def update_pyproject_toml() -> None: print(f"Read: {VERSION_PY}") version_input = VERSION_PY.read_text() - version = re.search( + raw_version = re.search( '^__version__\\s*=\\s*["\'](.+)["\']', version_input, re.M, ).group(1) - version += f".dev{count}" + version = Version(raw_version) + if not version.is_postrelease: + version = copy.replace(version, post=1) + + version = copy.replace(version, dev=count) print(f"Read: {PYPROJECT_TOML}") output = PYPROJECT_TOML.read_text() From 01057ced620946879a84c2d78043c01fdf4fba38 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:32:38 -0500 Subject: [PATCH 25/67] Fix 103 --- CHANGES_1.in.rst | 8 ++++++++ justfile | 2 +- pathspec/_backends/re2/_base.py | 22 ++++++++++++++++++---- pathspec/_backends/re2/base.py | 15 +++++---------- pathspec/_meta.py | 1 + 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 523012e..af8eef0 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,4 +1,12 @@ +1.0.4 (TBD) +----------- + +- `Issue #103`_: Using re2 fails if pyre2 is also installed. + +.. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 + + 1.0.3 (2026-01-09) ------------------ diff --git a/justfile b/justfile index a870c81..9b3f4f7 100644 --- a/justfile +++ b/justfile @@ -109,7 +109,7 @@ _venv_cpy_create: {{cpy_bin}} -m venv --clear dev/venv-cpy _venv_cpy_update: - {{cpy_run}} pip install -r doc/requirements.txt --upgrade build google-re2 google-re2-stubs hyperscan pip pytest pytest-benchmark setuptools tomli tox twine typing-extensions wheel + {{cpy_run}} pip install -r doc/requirements.txt --upgrade build google-re2 google-re2-stubs hyperscan packaging pip pytest pytest-benchmark setuptools tomli tox twine typing-extensions wheel {{cpy_run}} pip install -e . _venv_pypy_create: diff --git a/pathspec/_backends/re2/_base.py b/pathspec/_backends/re2/_base.py index 72b611a..4e6ae9f 100644 --- a/pathspec/_backends/re2/_base.py +++ b/pathspec/_backends/re2/_base.py @@ -10,17 +10,26 @@ from dataclasses import ( dataclass) from typing import ( + Optional, # Replaced by `X | None` in 3.10. Union) # Replaced by `X | Y` in 3.10. try: import re2 -except ModuleNotFoundError: + re2_error = None +except ModuleNotFoundError as e: re2 = None + re2_error = e RE2_OPTIONS = None else: - RE2_OPTIONS = re2.Options() - RE2_OPTIONS.log_errors = False - RE2_OPTIONS.never_capture = True + # Both the `google-re2` and `pyre2` libraries use the `re2` namespace. + # `google-re2` is the only one currently supported. + try: + RE2_OPTIONS = re2.Options() + RE2_OPTIONS.log_errors = False + RE2_OPTIONS.never_capture = True + except Exception as e: + re2_error = e + RE2_OPTIONS = None RE2_OPTIONS: re2.Options """ @@ -32,6 +41,11 @@ be utilized with :class:`re2.Set`. """ +re2_error: Optional[Exception] +""" +*re2_error* (:class:`Exception` or :data:`None`) is the re2 import error. +""" + @dataclass(frozen=True) class Re2RegexDat(object): diff --git a/pathspec/_backends/re2/base.py b/pathspec/_backends/re2/base.py index e2e3d00..fa24f4d 100644 --- a/pathspec/_backends/re2/base.py +++ b/pathspec/_backends/re2/base.py @@ -7,17 +7,12 @@ from __future__ import annotations from typing import ( - Optional) + Optional) # Replaced by `X | None` in 3.10. -try: - import re2 - re2_error = None -except ModuleNotFoundError as e: - re2 = None - re2_error = e +from ._base import ( + re2_error) -re2_error: Optional[ModuleNotFoundError] +re2_error: Optional[Exception] """ -*re2_error* (:class:`ModuleNotFoundError` or :data:`None`) is the re2 import -error. +*re2_error* (:class:`Exception` or :data:`None`) is the re2 import error. """ diff --git a/pathspec/_meta.py b/pathspec/_meta.py index d1dec2b..4e4c782 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -62,5 +62,6 @@ "Tzach Shabtay ", "Adam Dangoor ", "Marcel Telka ", + "Dmytro Kostochko ", ] __license__ = "MPL 2.0" From 529c0f81b7ba4ed9ad88468f23181a6f74693c56 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:36:33 -0500 Subject: [PATCH 26/67] Improve testpypi --- testpypi_prepublish.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/testpypi_prepublish.py b/testpypi_prepublish.py index 9d5004e..d6eabe1 100644 --- a/testpypi_prepublish.py +++ b/testpypi_prepublish.py @@ -28,16 +28,15 @@ def update_pyproject_toml() -> None: ], text=True).strip() print("Get commit count.") - count = subprocess.check_output([ + count = int(subprocess.check_output([ 'git', 'rev-list', f'{tag}..HEAD', '--count', - ], text=True).strip() + ], text=True).strip()) print(f"Read: {VERSION_PY}") version_input = VERSION_PY.read_text() - raw_version = re.search( + version = Version(re.search( '^__version__\\s*=\\s*["\'](.+)["\']', version_input, re.M, - ).group(1) - version = Version(raw_version) + ).group(1)) if not version.is_postrelease: version = copy.replace(version, post=1) From 39f02a9bd9de3b9b99bba5f794d63d2087a50fec Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:53:11 -0500 Subject: [PATCH 27/67] Release v1.0.4 --- CHANGES.rst | 8 ++++++++ CHANGES_1.in.rst | 4 ++-- README-dist.rst | 8 ++++++++ pathspec/_version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 747d608..6c44900 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ Change History ============== +1.0.4 (2026-01-26) +------------------ + +- `Issue #103`_: Using re2 fails if pyre2 is also installed. + +.. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 + + 1.0.3 (2026-01-09) ------------------ diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index af8eef0..aceba3b 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,6 +1,6 @@ -1.0.4 (TBD) ------------ +1.0.4 (2026-01-26) +------------------ - `Issue #103`_: Using re2 fails if pyre2 is also installed. diff --git a/README-dist.rst b/README-dist.rst index 79a7287..56cf793 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,6 +227,14 @@ Change History ============== +1.0.4 (2026-01-26) +------------------ + +- `Issue #103`_: Using re2 fails if pyre2 is also installed. + +.. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 + + 1.0.3 (2026-01-09) ------------------ diff --git a/pathspec/_version.py b/pathspec/_version.py index 6b80549..421d8fa 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.3" +__version__ = "1.0.4" diff --git a/pyproject.toml b/pyproject.toml index 72235ab..081b8da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.3" +version = "1.0.4" [project.optional-dependencies] hyperscan = [ From 7b2b70841fc874525d1e3f7f20e7dcb0095c440b Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:31:38 +0300 Subject: [PATCH 28/67] Fix escape() not escaping backslash characters The escape() method escapes special gitignore characters so that a literal filename can be used as a pattern. However, it only escapes []-f pr_body_1.md && git checkout master && git checkout -b fix/escape-backslash master#? and misses backslash, which is the escape character in gitignore patterns. Without this fix, escape('foo\\bar') returns 'foo\\bar' unchanged. When this is then parsed as a gitignore pattern, the backslash is consumed as an escape sequence (escaping 'b'), so the resulting pattern matches 'foobar' instead of the intended 'foo\\bar'. Add backslash to the set of characters that get escaped. --- pathspec/patterns/gitignore/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pathspec/patterns/gitignore/base.py b/pathspec/patterns/gitignore/base.py index 0e1dd3c..6bb0f85 100644 --- a/pathspec/patterns/gitignore/base.py +++ b/pathspec/patterns/gitignore/base.py @@ -47,7 +47,7 @@ def escape(s: AnyStr) -> AnyStr: raise TypeError(f"s:{s!r} is not a unicode or byte string.") # Reference: https://git-scm.com/docs/gitignore#_pattern_format - out_string = ''.join((f"\\{x}" if x in '[]!*#?' else x) for x in string) + out_string = ''.join((f"\\{x}" if x in '\\[]!*#?' else x) for x in string) if return_type is bytes: return out_string.encode(_BYTES_ENCODING) From 4d46129a9f36f688fb3089c7e98b003e69d4eafd Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:58:57 -0500 Subject: [PATCH 29/67] Switch to tox.toml --- justfile | 2 - tox.ini | 94 ---------------------------------------------- tox.toml | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 96 deletions(-) delete mode 100644 tox.ini create mode 100644 tox.toml diff --git a/justfile b/justfile index 9b3f4f7..d49b99d 100644 --- a/justfile +++ b/justfile @@ -2,8 +2,6 @@ # This justfile is used to manage development and distribution. # -_default: help - # Display available recipes. help: @just --list diff --git a/tox.ini b/tox.ini deleted file mode 100644 index fa9b237..0000000 --- a/tox.ini +++ /dev/null @@ -1,94 +0,0 @@ -[tox] -; Hyperscan does not have a wheel for Python 3.15 yet. -; Hyperscan does not have wheels for PyPy 3.9 and 3.11. -envlist = - py{39, 310, 311, 312, 313, 314, 315} - py{39, 310, 311, 312, 313, 314}-hyperscan - py{39, 310, 311, 312, 313, 314}-re2 - pypy{39, 310, 311} - pypy{310, 311}-hyperscan - docs -isolated_build = True - -[testenv] -commands = python -m unittest discover -t . -s tests/ {posargs} -package = wheel - -[testenv:docs] -base_path = py313 -commands = sphinx-build -aWEnqb html doc/source doc/build -deps = -rdoc/requirements.txt -extras = - -[testenv:ci-base] -; Placeholder env for base (only simple backend) for CI. - -[testenv:ci-hyperscan] -; Hyperscan env for CI. -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:ci-re2] -; Re2 env for CI. -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -[testenv:py39-hyperscan] -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:py39-re2] -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -[testenv:py310-hyperscan] -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:py310-re2] -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -[testenv:py311-hyperscan] -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:py311-re2] -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -[testenv:py312-hyperscan] -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:py312-re2] -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -[testenv:py313-hyperscan] -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:py313-re2] -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -[testenv:py314-hyperscan] -extras = hyperscan -install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} - -[testenv:py314-re2] -extras = google-re2 -install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} - -;; Compiling hyperscan fails on PyPy 3.9. -;[testenv:pypy39-hyperscan] -;extras = hyperscan - -[testenv:pypy310-hyperscan] -; Compile hyperscan. -extras = hyperscan - -[testenv:pypy311-hyperscan] -; Compile hyperscan. -extras = hyperscan diff --git a/tox.toml b/tox.toml new file mode 100644 index 0000000..2743586 --- /dev/null +++ b/tox.toml @@ -0,0 +1,111 @@ +# Hyperscan does not have a wheel for Python 3.15 yet. +# Hyperscan does not have wheels for PyPy 3.9 and 3.11. +env_list = [ + "py39", + "py310", + "py311", + "py312", + "py313", + "py314", + "py315", + "py39-hyperscan", + "py310-hyperscan", + "py311-hyperscan", + "py312-hyperscan", + "py313-hyperscan", + "py314-hyperscan", + "py39-re2", + "py310-re2", + "py311-re2", + "py312-re2", + "py313-re2", + "py314-re2", + "pypy39", + "pypy310", + "pypy311", + "pypy310-hyperscan", + "pypy311-hyperscan", + "docs" +] + +[env_run_base] +package = "wheel" +commands = [["python", "-m", "unittest", "discover", "-t", ".", "-s", "tests/"]] + +[env.docs] +base_python = ["py313"] +commands = [["sphinx-build", "-aWEnqb", "html", "doc/source", "doc/build"]] +deps = ["-rdoc/requirements.txt"] + +[env.ci-base] +# Placeholder env for base (only simple backend) for CI. + +[env.ci-hyperscan] +# Hyperscan env for CI. +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.ci-re2] +# Re2 env for CI. +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +[env.py39-hyperscan] +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.py39-re2] +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +[env.py310-hyperscan] +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.py310-re2] +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +[env.py311-hyperscan] +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.py311-re2] +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +[env.py312-hyperscan] +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.py312-re2] +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +[env.py313-hyperscan] +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.py313-re2] +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +[env.py314-hyperscan] +extras = ["hyperscan"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=hyperscan", "{opts}", "{packages}"] + +[env.py314-re2] +extras = ["re2"] +install_command = ["python", "-I", "-m", "pip", "install", "--only-binary=google-re2", "{opts}", "{packages}"] + +## Compiling hyperscan fails on PyPy 3.9. +#[env.pypy39-hyperscan] +#extras = ["hyperscan"] + +[env.pypy310-hyperscan] +# Compile hyperscan. +extras = ["hyperscan"] + +[env.pypy311-hyperscan] +# Compile hyperscan. +extras = ["hyperscan"] From 58be948b25695104b9e73f9dae1479fe0c74c9bc Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sat, 21 Feb 2026 13:07:49 -0500 Subject: [PATCH 30/67] Switch to tox.toml --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5f1730e..b9a1ae6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -72,7 +72,7 @@ jobs: run: python -m pip install tox - name: Run tests - run: python -m tox -e ci-${{ matrix.backend }} -- --verbose + run: python -m tox -c tox.toml -e ci-${{ matrix.backend }} -- --verbose docs: # Test documentation builds. From 84d984cae0bc1bdd63bada8d0bcf4e7e218eb482 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sat, 21 Feb 2026 13:10:52 -0500 Subject: [PATCH 31/67] Switch to tox.toml --- .github/workflows/ci.yaml | 7 ++++++- tox-ci-py39.ini | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tox-ci-py39.ini diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b9a1ae6..a78c8a1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -72,7 +72,12 @@ jobs: run: python -m pip install tox - name: Run tests - run: python -m tox -c tox.toml -e ci-${{ matrix.backend }} -- --verbose + if: matrix.python != '3.9' && matrix.python != 'pypy-3.9' + run: python -m tox -e ci-${{ matrix.backend }} -- --verbose + + - name: Run tests (3.9) + if: matrix.python == '3.9' || matrix.python == 'pypy-3.9' + run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose docs: # Test documentation builds. diff --git a/tox-ci-py39.ini b/tox-ci-py39.ini new file mode 100644 index 0000000..3801576 --- /dev/null +++ b/tox-ci-py39.ini @@ -0,0 +1,39 @@ +; This tox config only exists to support running from Python 3.9. + +[tox] +; Hyperscan does not have wheels for PyPy 3.9. +envlist = + py39 + py39-hyperscan + py39-re2 + pypy39 +isolated_build = True + +[testenv] +commands = python -m unittest discover -t . -s tests/ {posargs} +package = wheel + +[testenv:ci-base] +; Placeholder env for base (only simple backend) for CI. + +[testenv:ci-hyperscan] +; Hyperscan env for CI. +extras = hyperscan +install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} + +[testenv:ci-re2] +; Re2 env for CI. +extras = google-re2 +install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} + +[testenv:py39-hyperscan] +extras = hyperscan +install_command = python -I -m pip install --only-binary=hyperscan {opts} {packages} + +[testenv:py39-re2] +extras = google-re2 +install_command = python -I -m pip install --only-binary=google-re2 {opts} {packages} + +;; Compiling hyperscan fails on PyPy 3.9. +;[testenv:pypy39-hyperscan] +;extras = hyperscan From e34226e1d67214a76d2521891d12d7a98a3f3ab4 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 6 Mar 2026 09:49:54 -0500 Subject: [PATCH 32/67] chore: use dep groups Signed-off-by: Henry Schreiner --- pyproject.in.toml | 5 +++++ pyproject.toml | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyproject.in.toml b/pyproject.in.toml index 6b72375..e68743f 100644 --- a/pyproject.in.toml +++ b/pyproject.in.toml @@ -41,10 +41,15 @@ optional = [ re2 = [ "google-re2 >=1.1", ] + +[dependency-groups] tests = [ "pytest >=9", "typing-extensions >=4.15", ] +dev = [ + "pytest", +] [project.urls] "Source Code" = "https://github.com/cpburnz/python-pathspec" diff --git a/pyproject.toml b/pyproject.toml index 081b8da..b0f35b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,15 @@ optional = [ re2 = [ "google-re2 >=1.1", ] -tests = [ - "pytest >=9", + +[dependency-groups] +benchmarks = [ + "pytest >=9; python_version>='3.10'", "typing-extensions >=4.15", ] +dev = [ + "pytest", +] [project.urls] "Source Code" = "https://github.com/cpburnz/python-pathspec" From b70e3fb420ec48a9164c3e50f9aa729535738409 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 6 Mar 2026 10:21:10 -0500 Subject: [PATCH 33/67] fix: nicer debug print outs (and str for regex pattern) Signed-off-by: Henry Schreiner --- pathspec/pathspec.py | 6 ++++++ pathspec/pattern.py | 15 +++++++++++++++ tests/test_02_gitignore_basic.py | 8 ++++++++ tests/test_04_pathspec.py | 10 ++++++++++ 4 files changed, 39 insertions(+) diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index bb88cbf..97422ca 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -98,6 +98,12 @@ def __init__( contains the compiled patterns. """ + def __repr__(self) -> str: + """ + Returns a debug representation of this path-spec. + """ + return f"{self.__class__.__name__}(patterns={self.patterns!r}, backend={self._backend_name!r})" + def __add__(self: Self, other: PathSpec) -> Self: """ Combines the :attr:`self.patterns <.PathSpec.patterns>` patterns from two diff --git a/pathspec/pattern.py b/pathspec/pattern.py index a4b8a2c..a1803a8 100644 --- a/pathspec/pattern.py +++ b/pathspec/pattern.py @@ -158,6 +158,21 @@ def __init__( expression for the pattern. """ + def __repr__(self) -> str: + """ + Returns a debug representation of this regex pattern. + """ + return f"{self.__class__.__name__}(pattern={self.pattern!r}, include={self.include!r})" + + def __str__(self) -> str: + """ + Returns a string representation of this regex pattern. Equivalent to uncompiled pattern. + + The string representation is the uncompiled pattern if it is not + :data:`None`; otherwise, an empty string. + """ + return str(self.pattern or "") + def __copy__(self: RegexPatternSelf) -> RegexPatternSelf: """ Performa a shallow copy of the pattern. diff --git a/tests/test_02_gitignore_basic.py b/tests/test_02_gitignore_basic.py index 537212e..3dbcd78 100644 --- a/tests/test_02_gitignore_basic.py +++ b/tests/test_02_gitignore_basic.py @@ -926,3 +926,11 @@ def test_15_issue_93_c_2(self): pattern = GitIgnoreBasicPattern('[!]') self.assertIs(pattern.include, True) self.assertEqual(pattern.regex.pattern, f'^(?:.+/)?\\[!\\]{_DIR_OPT}') + + def test_16_repr_str(self): + """ + Test debug and string representations. + """ + pattern = GitIgnoreBasicPattern('*.py') + self.assertEqual(repr(pattern), "GitIgnoreBasicPattern(pattern='*.py', include=True)") + self.assertEqual(str(pattern), '*.py') diff --git a/tests/test_04_pathspec.py b/tests/test_04_pathspec.py index d71ecd2..df5c463 100644 --- a/tests/test_04_pathspec.py +++ b/tests/test_04_pathspec.py @@ -1041,3 +1041,13 @@ def test_10_issue_100(self): includes = get_includes(results) debug = debug_results(spec, results) self.assertEqual(includes, set(), debug) + + def test_11_repr(self): + """ + Test the path-spec debug representation. + """ + spec = PathSpec.from_lines('gitignore', ['*.py'], backend='simple') + self.assertEqual( + repr(spec), + "PathSpec(patterns=[GitIgnoreBasicPattern(pattern='*.py', include=True)], backend='simple')", + ) From b1bc2860f52aa24249f7a268039eeb43f7ea2c32 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sat, 7 Mar 2026 08:54:57 -0500 Subject: [PATCH 34/67] Misc --- CHANGES_1.in.rst | 20 ++++++++++++++++++++ pathspec/_meta.py | 2 ++ prebuild.py | 10 +++++++--- pyproject.in.toml | 9 ++++----- pyproject.toml | 10 +++++----- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index aceba3b..8c8c03a 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,4 +1,24 @@ +1.0.5 (TBD) +----------- + +TODO: Review #104. +TODO: Review #105. +TODO: Tests for #106. + +Bug fixes: + +- `Pull #106`_: Fix escape() not escaping backslash characters. + +Improvements: + +- `Pull #110`_: Nicer debug print outs (and str for regex pattern). + + +.. _`Pull #106`: https://github.com/cpburnz/python-pathspec/pull/106 +.. _`Pull #110`: https://github.com/cpburnz/python-pathspec/pull/110 + + 1.0.4 (2026-01-26) ------------------ diff --git a/pathspec/_meta.py b/pathspec/_meta.py index 4e4c782..dcaa5b9 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -63,5 +63,7 @@ "Adam Dangoor ", "Marcel Telka ", "Dmytro Kostochko ", + "Kadir Can Ozden ", + "Henry Schreiner ", ] __license__ = "MPL 2.0" diff --git a/prebuild.py b/prebuild.py index 5a9fc2b..66d63d4 100644 --- a/prebuild.py +++ b/prebuild.py @@ -54,8 +54,11 @@ def generate_pyproject_toml() -> None: # files for an editable install for some odd reason. # - See . + output: list[str] = [] + output.append("# GENERATED FILE: Edit 'pyproject.in.toml' instead.\n") + print(f"Read: {PYPROJECT_IN_TOML}") - output = PYPROJECT_IN_TOML.read_text() + text = PYPROJECT_IN_TOML.read_text() print(f"Read: {VERSION_PY}") version_input = VERSION_PY.read_text() @@ -64,10 +67,11 @@ def generate_pyproject_toml() -> None: ).group(1) # Replace version. - output = output.replace("__VERSION__", version) + text = text.replace("__VERSION__", version) + output.append(text) print(f"Write: {PYPROJECT_TOML}") - PYPROJECT_TOML.write_text(output) + PYPROJECT_TOML.write_text("".join(output)) def generate_readme_dist() -> None: diff --git a/pyproject.in.toml b/pyproject.in.toml index e68743f..4c29bf7 100644 --- a/pyproject.in.toml +++ b/pyproject.in.toml @@ -43,12 +43,11 @@ re2 = [ ] [dependency-groups] -tests = [ - "pytest >=9", - "typing-extensions >=4.15", -] dev = [ - "pytest", + "pytest >=8", + "pytest-benchmark >=5", + "tomli; python_version<'3.11'", + "typing-extensions >=4.4", ] [project.urls] diff --git a/pyproject.toml b/pyproject.toml index b0f35b5..b5637a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ +# GENERATED FILE: Edit 'pyproject.in.toml' instead. [build-system] build-backend = "flit_core.buildapi" requires = ["flit_core >=3.2,<5"] @@ -43,12 +44,11 @@ re2 = [ ] [dependency-groups] -benchmarks = [ - "pytest >=9; python_version>='3.10'", - "typing-extensions >=4.15", -] dev = [ - "pytest", + "pytest >=8", + "pytest-benchmark >=5", + "tomli; python_version<'3.11'", + "typing-extensions >=4.4", ] [project.urls] From 361b5355c2c3b8fa96b5d70d9035f532243f862a Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:30:09 -0500 Subject: [PATCH 35/67] Issue 93 tests --- tests/test_02_gitignore_basic.py | 57 ++++++++++++++++++++++++-------- tests/test_03_gitignore_spec.py | 57 ++++++++++++++++++++++++-------- 2 files changed, 86 insertions(+), 28 deletions(-) diff --git a/tests/test_02_gitignore_basic.py b/tests/test_02_gitignore_basic.py index 3dbcd78..3c50277 100644 --- a/tests/test_02_gitignore_basic.py +++ b/tests/test_02_gitignore_basic.py @@ -905,27 +905,56 @@ def test_15_issue_93_b_2_double(self): self.assertFalse(pattern.match_file(' foo')) self.assertTrue(pattern.match_file(' foo')) - def test_15_issue_93_c_1(self): + def test_15_issue_93_c_1_valid(self): """ - Test patterns with invalid range notation. + Test patterns with valid range notations. """ - # TODO BUG: This test is a placeholder for the current behavior. Git behaves - # differently for this scenario. + for raw_pattern, regex in [ + ('[a-z]', f'^(?:.+/)?[a-z]{_DIR_OPT}'), + ('a[a-z]', f'^(?:.+/)?a[a-z]{_DIR_OPT}'), + ]: + with self.subTest(f"p={raw_pattern!r}"): + pattern = GitIgnoreBasicPattern(raw_pattern) + self.assertIs(pattern.include, True) + self.assertEqual(pattern.regex.pattern, regex) + + def test_15_issue_93_c_2_invalid(self): + """ + Test patterns with invalid range notations. + """ + # TODO BUG: These tests need to pass. # - See . - pattern = GitIgnoreBasicPattern('[') - self.assertIs(pattern.include, True) - self.assertEqual(pattern.regex.pattern, f'^(?:.+/)?\\[{_DIR_OPT}') + for raw_pattern in [ + '[!]', + '[z-a]', + 'a[z-a]', + ]: + with self.subTest(f"p={raw_pattern!r}"): + pattern = GitIgnoreBasicPattern(raw_pattern) + self.assertIs(pattern.include, None) + self.assertIs(pattern.regex.pattern, None) - def test_15_issue_93_c_2(self): + def test_15_issue_93_c_3_unclosed(self): """ - Test patterns with invalid range notation. + Test patterns with unclosed range notations. """ - # TODO BUG: This test is a placeholder for the current behavior. Git behaves - # differently for this scenario. + # TODO BUG: These tests need to pass. # - See . - pattern = GitIgnoreBasicPattern('[!]') - self.assertIs(pattern.include, True) - self.assertEqual(pattern.regex.pattern, f'^(?:.+/)?\\[!\\]{_DIR_OPT}') + for raw_pattern in [ + '[!', + '[-', + '[a', + '[a-', + '[a-z', + 'a[', + 'a[-', + 'a[a-', + 'a[a-z', + ]: + with self.subTest(f"p={raw_pattern!r}"): + pattern = GitIgnoreBasicPattern(raw_pattern) + self.assertIs(pattern.include, None) + self.assertIs(pattern.regex.pattern, None) def test_16_repr_str(self): """ diff --git a/tests/test_03_gitignore_spec.py b/tests/test_03_gitignore_spec.py index 9a691d6..bd87675 100644 --- a/tests/test_03_gitignore_spec.py +++ b/tests/test_03_gitignore_spec.py @@ -907,24 +907,53 @@ def test_15_issue_93_b_2_double(self): self.assertFalse(pattern.match_file(' foo')) self.assertTrue(pattern.match_file(' foo')) - def test_15_issue_93_c_1(self): + def test_15_issue_93_c_1_valid(self): """ - Test patterns with invalid range notation. + Test patterns with valid range notations. """ - # TODO BUG: This test is a placeholder for the current behavior. Git behaves - # differently for this scenario. + for raw_pattern, regex in [ + ('[a-z]', f'^(?:.+/)?[a-z]{_DIR_MARK_OPT}'), + ('a[a-z]', f'^(?:.+/)?a[a-z]{_DIR_MARK_OPT}'), + ]: + with self.subTest(f"p={raw_pattern!r}"): + pattern = GitIgnoreSpecPattern(raw_pattern) + self.assertIs(pattern.include, True) + self.assertEqual(pattern.regex.pattern, regex) + + def test_15_issue_93_c_2_invalid(self): + """ + Test patterns with invalid range notations. + """ + # TODO BUG: These tests need to pass. # - See . - pattern = GitIgnoreSpecPattern('[') - self.assertIs(pattern.include, True) - self.assertEqual(pattern.regex.pattern, f'^(?:.+/)?\\[{_DIR_MARK_OPT}') + for raw_pattern in [ + '[!]', + '[z-a]', + 'a[z-a]', + ]: + with self.subTest(f"p={raw_pattern!r}"): + pattern = GitIgnoreSpecPattern(raw_pattern) + self.assertIs(pattern.include, None) + self.assertIs(pattern.regex.pattern, None) - def test_15_issue_93_c_2(self): + def test_15_issue_93_c_3_unclosed(self): """ - Test patterns with invalid range notation. + Test patterns with unclosed range notations. """ - # TODO BUG: This test is a placeholder for the current behavior. Git behaves - # differently for this scenario. + # TODO BUG: These tests need to pass. # - See . - pattern = GitIgnoreSpecPattern('[!]') - self.assertIs(pattern.include, True) - self.assertEqual(pattern.regex.pattern, f'^(?:.+/)?\\[!\\]{_DIR_MARK_OPT}') + for raw_pattern in [ + '[!', + '[-', + '[a', + '[a-', + '[a-z', + 'a[', + 'a[-', + 'a[a-', + 'a[a-z', + ]: + with self.subTest(f"p={raw_pattern!r}"): + pattern = GitIgnoreSpecPattern(raw_pattern) + self.assertIs(pattern.include, None) + self.assertIs(pattern.regex.pattern, None) From 0f25585c043dc547d929596dcb9ea1e18fdce94f Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 6 Mar 2026 10:53:01 -0500 Subject: [PATCH 36/67] fix(types): a few updates to typing Signed-off-by: Henry Schreiner --- pathspec/pattern.py | 2 +- pathspec/util.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pathspec/pattern.py b/pathspec/pattern.py index a1803a8..394958c 100644 --- a/pathspec/pattern.py +++ b/pathspec/pattern.py @@ -183,7 +183,7 @@ def __copy__(self: RegexPatternSelf) -> RegexPatternSelf: other.pattern = self.pattern return other - def __eq__(self, other: RegexPattern) -> bool: + def __eq__(self, other: object) -> bool: """ Tests the equality of this regex pattern with *other* (:class:`RegexPattern`) by comparing their :attr:`~Pattern.include` and :attr:`~RegexPattern.regex` diff --git a/pathspec/util.py b/pathspec/util.py index ea2dbee..9a3f2c1 100644 --- a/pathspec/util.py +++ b/pathspec/util.py @@ -146,10 +146,11 @@ def detailed_match_files( # Add files and record pattern. for result_file in result_files: if result_file in return_files: + # We know here that .patterns is a list, becasue we made it here if all_matches: - return_files[result_file].patterns.append(pattern) + return_files[result_file].patterns.append(pattern) # type: ignore[attr-defined] else: - return_files[result_file].patterns[0] = pattern + return_files[result_file].patterns[0] = pattern # type: ignore[index] else: return_files[result_file] = MatchDetail([pattern]) From 1794ebaee4222738d0d6571cd599895841862ab8 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:41:41 -0400 Subject: [PATCH 37/67] Invalid range notation --- pathspec/patterns/gitignore/base.py | 49 +++++++++++++++++++++++----- pathspec/patterns/gitignore/basic.py | 2 +- pathspec/patterns/gitignore/spec.py | 20 ++++++++---- tests/test_02_gitignore_basic.py | 18 +++++++--- tests/test_03_gitignore_spec.py | 18 +++++++--- 5 files changed, 83 insertions(+), 24 deletions(-) diff --git a/pathspec/patterns/gitignore/base.py b/pathspec/patterns/gitignore/base.py index 6bb0f85..0decd65 100644 --- a/pathspec/patterns/gitignore/base.py +++ b/pathspec/patterns/gitignore/base.py @@ -4,10 +4,14 @@ import re +from typing import ( + Literal) + from pathspec.pattern import ( RegexPattern) from pathspec._typing import ( - AnyStr) # Removed in 3.18. + AnyStr, # Removed in 3.18. + assert_unreachable) _BYTES_ENCODING = 'latin1' """ @@ -55,7 +59,10 @@ def escape(s: AnyStr) -> AnyStr: return out_string @staticmethod - def _translate_segment_glob(pattern: str) -> str: + def _translate_segment_glob( + pattern: str, + range_error: Literal['literal', 'raise'], + ) -> str: """ Translates the glob pattern to a regular expression. This is used in the constructor to translate a path segment glob pattern to its corresponding @@ -63,6 +70,14 @@ def _translate_segment_glob(pattern: str) -> str: *pattern* (:class:`str`) is the glob pattern. + *range_error* (:class:`int`) is how to handle invalid range notation in the + pattern: + + - :data:`"literal"`: Invalid notation will be treated as a literal string. + + - :data:`"raise"`: Invalid notation will cause a :class:`_RangeError` to be + raised. + Returns the regular expression (:class:`str`). """ # NOTE: This is derived from `fnmatch.translate()` and is similar to the @@ -96,9 +111,9 @@ def _translate_segment_glob(pattern: str) -> str: regex += '[^/]' elif char == '[': - # Bracket expression wildcard. Except for the beginning exclamation - # mark, the whole bracket expression can be used directly as regex, but - # we have to find where the expression ends. + # Bracket expression (range notation) wildcard. Except for the beginning + # exclamation mark, the whole bracket expression can be used directly as + # regex, but we have to find where the expression ends. # - "[][!]" matches ']', '[' and '!'. # - "[]-]" matches ']' and '-'. # - "[!]a-]" matches any character except ']', 'a' and '-'. @@ -152,9 +167,19 @@ def _translate_segment_glob(pattern: str) -> str: i = j else: - # Failed to find closing bracket, treat opening bracket as a bracket - # literal instead of as an expression. - regex += '\\[' + # Failed to find closing bracket. + if range_error == 'literal': + # Treat opening bracket as a bracket literal instead of as an + # expression. + regex += '\\[' + elif range_error == 'raise': + # Treat invalid range notation as an error. + raise _RangeError(( + f"Invalid range notation={pattern[i:j]!r} found in pattern=" + f"{pattern!r}." + )) + else: + assert_unreachable(f"{range_error=!r} is invalid.") else: # Regular character, escape it for regex. @@ -174,3 +199,11 @@ class GitIgnorePatternError(ValueError): pattern. """ pass + + +class _RangeError(GitIgnorePatternError): + """ + The :class:`_RangeError` class indicates an invalid range notation was found + in a gitignore pattern. + """ + pass diff --git a/pathspec/patterns/gitignore/basic.py b/pathspec/patterns/gitignore/basic.py index 95d7915..94fbcf1 100644 --- a/pathspec/patterns/gitignore/basic.py +++ b/pathspec/patterns/gitignore/basic.py @@ -293,7 +293,7 @@ def __translate_segments(cls, pattern_segs: list[str]) -> list[str]: else: # Match segment glob pattern. - out_parts.append(cls._translate_segment_glob(seg)) + out_parts.append(cls._translate_segment_glob(seg, 'literal')) if i == end: if seg == '*': diff --git a/pathspec/patterns/gitignore/spec.py b/pathspec/patterns/gitignore/spec.py index ee77457..0486ee9 100644 --- a/pathspec/patterns/gitignore/spec.py +++ b/pathspec/patterns/gitignore/spec.py @@ -19,7 +19,8 @@ from .base import ( GitIgnorePatternError, _BYTES_ENCODING, - _GitIgnoreBasePattern) + _GitIgnoreBasePattern, + _RangeError) _DIR_MARK = 'ps_d' """ @@ -80,7 +81,7 @@ def __normalize_segments( elif len(pattern_segs) == 1 or (len(pattern_segs) == 2 and not pattern_segs[1]): # A single segment pattern with or without a trailing slash ('/') will # match any descendant path. This is equivalent to "**/{pattern}". Prepend - # double-asterisk segment to make pattern relative to root. + # a double-asterisk segment to make the pattern relative to root. if pattern_segs[0] != '**': pattern_segs.insert(0, '**') @@ -98,8 +99,8 @@ def __normalize_segments( if not pattern_segs[-1]: # A pattern ending with a slash ('/') will match all descendant paths if # it is a directory but not if it is a regular file. This is equivalent to - # "{pattern}/**". Set empty last segment to a double-asterisk to include - # all descendants. + # "{pattern}/**". Set the empty last segment to a double-asterisk to + # include all descendants. pattern_segs[-1] = '**' # EDGE CASE: Collapse duplicate double-asterisk sequences (i.e., '**/**'). @@ -210,8 +211,8 @@ def pattern_to_regex( if pattern_str.startswith('!'): # A pattern starting with an exclamation mark ('!') negates the pattern - # (exclude instead of include). Escape the exclamation mark with a back - # slash to match a literal exclamation mark (i.e., '\!'). + # (exclude instead of include). Escape the exclamation mark with a + # backslash to match a literal exclamation mark (i.e., '\!'). include = False # Remove leading exclamation mark. pattern_str = pattern_str[1:] @@ -243,6 +244,9 @@ def pattern_to_regex( # Build regular expression from pattern. try: regex_parts = cls.__translate_segments(is_dir_pattern, pattern_segs) + except _RangeError: + # EDGE CASE: Git discards patterns with range notation errors. + return (None, None) except ValueError as e: raise GitIgnorePatternError(( f"Invalid git pattern: {original_pattern!r}" @@ -279,6 +283,8 @@ def __translate_segments( *pattern_segs* (:class:`list` of :class:`str`) contains the pattern segments. + Raises :class:`_RangeError` if invalid range notation is found. + Returns the regular expression parts (:class:`list` of :class:`str`). """ # Build regular expression from pattern. @@ -322,7 +328,7 @@ def __translate_segments( else: # Match segment glob pattern. - out_parts.append(cls._translate_segment_glob(seg)) + out_parts.append(cls._translate_segment_glob(seg, 'raise')) if i == end: # A pattern ending without a slash ('/') will match a file or a diff --git a/tests/test_02_gitignore_basic.py b/tests/test_02_gitignore_basic.py index 3c50277..26a447a 100644 --- a/tests/test_02_gitignore_basic.py +++ b/tests/test_02_gitignore_basic.py @@ -926,13 +926,23 @@ def test_15_issue_93_c_2_invalid(self): # - See . for raw_pattern in [ '[!]', - '[z-a]', - 'a[z-a]', + 'a[!]', ]: with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreBasicPattern(raw_pattern) self.assertIs(pattern.include, None) - self.assertIs(pattern.regex.pattern, None) + self.assertIs(pattern.regex, None) + + # The `re` module fails to compile these. + # - NOTE: Technically, these should result in null patterns rather than + # exceptions to fully replicate Git's behavior. + for raw_pattern in [ + '[z-a]', + 'a[z-a]', + ]: + with self.subTest(f"p={raw_pattern!r}"): + with self.assertRaises(re.PatternError): + GitIgnoreBasicPattern(raw_pattern) def test_15_issue_93_c_3_unclosed(self): """ @@ -954,7 +964,7 @@ def test_15_issue_93_c_3_unclosed(self): with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreBasicPattern(raw_pattern) self.assertIs(pattern.include, None) - self.assertIs(pattern.regex.pattern, None) + self.assertIs(pattern.regex, None) def test_16_repr_str(self): """ diff --git a/tests/test_03_gitignore_spec.py b/tests/test_03_gitignore_spec.py index bd87675..e46ab7b 100644 --- a/tests/test_03_gitignore_spec.py +++ b/tests/test_03_gitignore_spec.py @@ -928,13 +928,23 @@ def test_15_issue_93_c_2_invalid(self): # - See . for raw_pattern in [ '[!]', - '[z-a]', - 'a[z-a]', + 'a[!]', ]: with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreSpecPattern(raw_pattern) self.assertIs(pattern.include, None) - self.assertIs(pattern.regex.pattern, None) + self.assertIs(pattern.regex, None) + + # The `re` module fails to compile these. + # - NOTE: Technically, these should result in null patterns rather than + # exceptions to fully replicate Git's behavior. + for raw_pattern in [ + '[z-a]', + 'a[z-a]', + ]: + with self.subTest(f"p={raw_pattern!r}"): + with self.assertRaises(re.PatternError): + GitIgnoreSpecPattern(raw_pattern) def test_15_issue_93_c_3_unclosed(self): """ @@ -956,4 +966,4 @@ def test_15_issue_93_c_3_unclosed(self): with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreSpecPattern(raw_pattern) self.assertIs(pattern.include, None) - self.assertIs(pattern.regex.pattern, None) + self.assertIs(pattern.regex, None) From dedb622991eca74d589c9b4f724250de19a08696 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:01:52 -0400 Subject: [PATCH 38/67] Finish invalid range notation support --- CHANGES_1.in.rst | 5 ++- pathspec/patterns/gitignore/basic.py | 2 + pathspec/patterns/gitignore/spec.py | 8 ++-- tests/test_02_gitignore_basic.py | 55 ++++++++++++++++------------ tests/test_03_gitignore_spec.py | 21 +++++++---- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 8c8c03a..19b70f3 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -2,12 +2,11 @@ 1.0.5 (TBD) ----------- -TODO: Review #104. -TODO: Review #105. TODO: Tests for #106. Bug fixes: +- `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. - `Pull #106`_: Fix escape() not escaping backslash characters. Improvements: @@ -22,6 +21,8 @@ Improvements: 1.0.4 (2026-01-26) ------------------ +Bug fixes: + - `Issue #103`_: Using re2 fails if pyre2 is also installed. .. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 diff --git a/pathspec/patterns/gitignore/basic.py b/pathspec/patterns/gitignore/basic.py index 94fbcf1..88e735d 100644 --- a/pathspec/patterns/gitignore/basic.py +++ b/pathspec/patterns/gitignore/basic.py @@ -293,6 +293,8 @@ def __translate_segments(cls, pattern_segs: list[str]) -> list[str]: else: # Match segment glob pattern. + # - EDGE CASE: The gitignore docs defer to *fnmatch(3)* which treats + # invalid range notation as a literal. out_parts.append(cls._translate_segment_glob(seg, 'literal')) if i == end: diff --git a/pathspec/patterns/gitignore/spec.py b/pathspec/patterns/gitignore/spec.py index 0486ee9..3bbe16f 100644 --- a/pathspec/patterns/gitignore/spec.py +++ b/pathspec/patterns/gitignore/spec.py @@ -2,8 +2,9 @@ This module provides :class:`GitIgnoreSpecPattern` which implements Git's `gitignore`_ patterns, and handles edge-cases where Git's behavior differs from what's documented. Git allows including files from excluded directories which -appears to contradict the documentation. This is used by -:class:`~pathspec.gitignore.GitIgnoreSpec` to fully replicate Git's handling. +appears to contradict the documentation. Git discards patterns with invalid +range notation. This is used by :class:`~pathspec.gitignore.GitIgnoreSpec` to +fully replicate Git's handling. .. _`gitignore`: https://git-scm.com/docs/gitignore """ @@ -245,7 +246,7 @@ def pattern_to_regex( try: regex_parts = cls.__translate_segments(is_dir_pattern, pattern_segs) except _RangeError: - # EDGE CASE: Git discards patterns with range notation errors. + # EDGE CASE: Git discards patterns with invalid range notation. return (None, None) except ValueError as e: raise GitIgnorePatternError(( @@ -328,6 +329,7 @@ def __translate_segments( else: # Match segment glob pattern. + # - EDGE CASE: Git discards patterns with invalid range notation. out_parts.append(cls._translate_segment_glob(seg, 'raise')) if i == end: diff --git a/tests/test_02_gitignore_basic.py b/tests/test_02_gitignore_basic.py index 26a447a..b83d11d 100644 --- a/tests/test_02_gitignore_basic.py +++ b/tests/test_02_gitignore_basic.py @@ -907,10 +907,14 @@ def test_15_issue_93_b_2_double(self): def test_15_issue_93_c_1_valid(self): """ - Test patterns with valid range notations. + Test patterns with valid range notation. """ for raw_pattern, regex in [ + ('[!a-z]', f'^(?:.+/)?[^a-z]{_DIR_OPT}'), + ('[^a-z]', f'^(?:.+/)?[^a-z]{_DIR_OPT}'), ('[a-z]', f'^(?:.+/)?[a-z]{_DIR_OPT}'), + ('a[!a-z]', f'^(?:.+/)?a[^a-z]{_DIR_OPT}'), + ('a[^a-z]', f'^(?:.+/)?a[^a-z]{_DIR_OPT}'), ('a[a-z]', f'^(?:.+/)?a[a-z]{_DIR_OPT}'), ]: with self.subTest(f"p={raw_pattern!r}"): @@ -920,22 +924,21 @@ def test_15_issue_93_c_1_valid(self): def test_15_issue_93_c_2_invalid(self): """ - Test patterns with invalid range notations. + Test patterns with invalid range notation. """ - # TODO BUG: These tests need to pass. - # - See . - for raw_pattern in [ - '[!]', - 'a[!]', + # The basic pattern treats invalid range notation as a literal. + for raw_pattern, regex in [ + ('[!]', f'^(?:.+/)?\\[!\\]{_DIR_OPT}'), + ('[^]', f'^(?:.+/)?\\[\\^\\]{_DIR_OPT}'), + ('a[!]', f'^(?:.+/)?a\\[!\\]{_DIR_OPT}'), + ('a[^]', f'^(?:.+/)?a\\[\\^\\]{_DIR_OPT}'), ]: with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreBasicPattern(raw_pattern) - self.assertIs(pattern.include, None) - self.assertIs(pattern.regex, None) + self.assertIs(pattern.include, True) + self.assertEqual(pattern.regex.pattern, regex) # The `re` module fails to compile these. - # - NOTE: Technically, these should result in null patterns rather than - # exceptions to fully replicate Git's behavior. for raw_pattern in [ '[z-a]', 'a[z-a]', @@ -946,25 +949,29 @@ def test_15_issue_93_c_2_invalid(self): def test_15_issue_93_c_3_unclosed(self): """ - Test patterns with unclosed range notations. + Test patterns with unclosed range notation. """ # TODO BUG: These tests need to pass. # - See . - for raw_pattern in [ - '[!', - '[-', - '[a', - '[a-', - '[a-z', - 'a[', - 'a[-', - 'a[a-', - 'a[a-z', + for raw_pattern, regex in [ + ('[!', f'^(?:.+/)?\\[!{_DIR_OPT}'), + ('[', f'^(?:.+/)?\\[{_DIR_OPT}'), + ('[-', f'^(?:.+/)?\\[\\-{_DIR_OPT}'), + ('[^', f'^(?:.+/)?\\[\\^{_DIR_OPT}'), + ('[a', f'^(?:.+/)?\\[a{_DIR_OPT}'), + ('[a-', f'^(?:.+/)?\\[a\\-{_DIR_OPT}'), + ('[a-z', f'^(?:.+/)?\\[a\\-z{_DIR_OPT}'), + ('a[!', f'^(?:.+/)?a\\[!{_DIR_OPT}'), + ('a[', f'^(?:.+/)?a\\[{_DIR_OPT}'), + ('a[-', f'^(?:.+/)?a\\[\\-{_DIR_OPT}'), + ('a[^', f'^(?:.+/)?a\\[\\^{_DIR_OPT}'), + ('a[a-', f'^(?:.+/)?a\\[a\\-{_DIR_OPT}'), + ('a[a-z', f'^(?:.+/)?a\\[a\\-z{_DIR_OPT}'), ]: with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreBasicPattern(raw_pattern) - self.assertIs(pattern.include, None) - self.assertIs(pattern.regex, None) + self.assertIs(pattern.include, True) + self.assertEqual(pattern.regex.pattern, regex) def test_16_repr_str(self): """ diff --git a/tests/test_03_gitignore_spec.py b/tests/test_03_gitignore_spec.py index e46ab7b..b456e3e 100644 --- a/tests/test_03_gitignore_spec.py +++ b/tests/test_03_gitignore_spec.py @@ -909,10 +909,14 @@ def test_15_issue_93_b_2_double(self): def test_15_issue_93_c_1_valid(self): """ - Test patterns with valid range notations. + Test patterns with valid range notation. """ for raw_pattern, regex in [ + ('[!a-z]', f'^(?:.+/)?[^a-z]{_DIR_MARK_OPT}'), + ('[^a-z]', f'^(?:.+/)?[^a-z]{_DIR_MARK_OPT}'), ('[a-z]', f'^(?:.+/)?[a-z]{_DIR_MARK_OPT}'), + ('a[!a-z]', f'^(?:.+/)?a[^a-z]{_DIR_MARK_OPT}'), + ('a[^a-z]', f'^(?:.+/)?a[^a-z]{_DIR_MARK_OPT}'), ('a[a-z]', f'^(?:.+/)?a[a-z]{_DIR_MARK_OPT}'), ]: with self.subTest(f"p={raw_pattern!r}"): @@ -922,13 +926,14 @@ def test_15_issue_93_c_1_valid(self): def test_15_issue_93_c_2_invalid(self): """ - Test patterns with invalid range notations. + Test patterns with invalid range notation. """ - # TODO BUG: These tests need to pass. - # - See . + # The spec pattern discards patterns with invalid range notation. for raw_pattern in [ '[!]', + '[^]', 'a[!]', + 'a[^]', ]: with self.subTest(f"p={raw_pattern!r}"): pattern = GitIgnoreSpecPattern(raw_pattern) @@ -948,18 +953,20 @@ def test_15_issue_93_c_2_invalid(self): def test_15_issue_93_c_3_unclosed(self): """ - Test patterns with unclosed range notations. + Test patterns with unclosed range notation. """ - # TODO BUG: These tests need to pass. - # - See . for raw_pattern in [ '[!', + '[', '[-', + '[^', '[a', '[a-', '[a-z', + 'a[!', 'a[', 'a[-', + 'a[^', 'a[a-', 'a[a-z', ]: From 2262a958c71afd4542885839d1248e0aac0a468d Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:06:50 -0400 Subject: [PATCH 39/67] Finish invalid range notation support --- tests/test_02_gitignore_basic.py | 6 +++++- tests/test_03_gitignore_spec.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_02_gitignore_basic.py b/tests/test_02_gitignore_basic.py index b83d11d..6346d9f 100644 --- a/tests/test_02_gitignore_basic.py +++ b/tests/test_02_gitignore_basic.py @@ -4,6 +4,10 @@ import re import unittest +try: + from re import PatternError as re_PatternError # Added in 3.13. +except ImportError: + from re import error as re_PatternError from pathspec.patterns.gitignore.base import ( GitIgnorePatternError, @@ -944,7 +948,7 @@ def test_15_issue_93_c_2_invalid(self): 'a[z-a]', ]: with self.subTest(f"p={raw_pattern!r}"): - with self.assertRaises(re.PatternError): + with self.assertRaises(re_PatternError): GitIgnoreBasicPattern(raw_pattern) def test_15_issue_93_c_3_unclosed(self): diff --git a/tests/test_03_gitignore_spec.py b/tests/test_03_gitignore_spec.py index b456e3e..2d30333 100644 --- a/tests/test_03_gitignore_spec.py +++ b/tests/test_03_gitignore_spec.py @@ -4,6 +4,10 @@ import re import unittest +try: + from re import PatternError as re_PatternError # Added in 3.13. +except ImportError: + from re import error as re_PatternError from pathspec.patterns.gitignore.base import ( GitIgnorePatternError, @@ -948,7 +952,7 @@ def test_15_issue_93_c_2_invalid(self): 'a[z-a]', ]: with self.subTest(f"p={raw_pattern!r}"): - with self.assertRaises(re.PatternError): + with self.assertRaises(re_PatternError): GitIgnoreSpecPattern(raw_pattern) def test_15_issue_93_c_3_unclosed(self): From 0f4789abb21e07e28f5dc968baf143aea77334ec Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:17:30 -0400 Subject: [PATCH 40/67] Update CI --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a78c8a1..8edab1c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,10 +60,10 @@ jobs: backend: re2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -85,10 +85,10 @@ jobs: name: Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.13" From 230f8cfc120a3f62a25aae6309894c5b825c626f Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:31:14 -0400 Subject: [PATCH 41/67] Update CI --- .github/workflows/{ci.yaml => test-all.yaml} | 7 ++- .github/workflows/test-quick.yaml | 66 ++++++++++++++++++++ pyproject.in.toml | 3 +- 3 files changed, 73 insertions(+), 3 deletions(-) rename .github/workflows/{ci.yaml => test-all.yaml} (97%) create mode 100644 .github/workflows/test-quick.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/test-all.yaml similarity index 97% rename from .github/workflows/ci.yaml rename to .github/workflows/test-all.yaml index 8edab1c..b1d684c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/test-all.yaml @@ -1,12 +1,15 @@ -name: CI +name: Test All permissions: contents: read on: push: - branches: [master] + branches: + - master pull_request: + branches: + - master jobs: test: diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml new file mode 100644 index 0000000..05feb2e --- /dev/null +++ b/.github/workflows/test-quick.yaml @@ -0,0 +1,66 @@ +name: Test Quick + +permissions: + contents: read + +on: + push: + branches-ignore: + - master + pull_request: + branches-ignore: + - master + +jobs: + test: + name: Test / ${{ matrix.python }} (${{ matrix.backend }}) / ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: + - ubuntu + python: + - "3.9" + - "3.14" + backend: + - base + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + + - name: Install tox + run: python -m pip install tox + + - name: Run tests + if: matrix.python != '3.9' + run: python -m tox -e ci-${{ matrix.backend }} -- --verbose + + - name: Run tests (3.9) + if: matrix.python == '3.9' + run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose + + docs: + # Test documentation builds. + # This environment mirrors the ReadTheDocs build environment. + name: Docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install tox + run: python -m pip install tox + + - name: Run tests + run: python -m tox -e docs diff --git a/pyproject.in.toml b/pyproject.in.toml index 4c29bf7..9748b40 100644 --- a/pyproject.in.toml +++ b/pyproject.in.toml @@ -51,9 +51,10 @@ dev = [ ] [project.urls] -"Source Code" = "https://github.com/cpburnz/python-pathspec" +"Change Log" = "https://python-path-specification.readthedocs.io/en/latest/changes.html" "Documentation" = "https://python-path-specification.readthedocs.io/en/latest/index.html" "Issue Tracker" = "https://github.com/cpburnz/python-pathspec/issues" +"Source Code" = "https://github.com/cpburnz/python-pathspec" [tool.flit.sdist] include = [ From 2f08550f0ebd9b5e0078dd3b4fe0d29da321702f Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 17 Mar 2026 21:28:19 +0000 Subject: [PATCH 42/67] Drop the traceback from the module-level backend error objects. --- pathspec/_backends/hyperscan/base.py | 2 +- pathspec/_backends/re2/_base.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pathspec/_backends/hyperscan/base.py b/pathspec/_backends/hyperscan/base.py index ac219b4..e05151f 100644 --- a/pathspec/_backends/hyperscan/base.py +++ b/pathspec/_backends/hyperscan/base.py @@ -15,7 +15,7 @@ hyperscan_error = None except ModuleNotFoundError as e: hyperscan = None - hyperscan_error = e + hyperscan_error = e.with_traceback(None) hyperscan_error: Optional[ModuleNotFoundError] """ diff --git a/pathspec/_backends/re2/_base.py b/pathspec/_backends/re2/_base.py index 4e6ae9f..be3327a 100644 --- a/pathspec/_backends/re2/_base.py +++ b/pathspec/_backends/re2/_base.py @@ -18,7 +18,7 @@ re2_error = None except ModuleNotFoundError as e: re2 = None - re2_error = e + re2_error = e.with_traceback(None) RE2_OPTIONS = None else: # Both the `google-re2` and `pyre2` libraries use the `re2` namespace. @@ -28,7 +28,7 @@ RE2_OPTIONS.log_errors = False RE2_OPTIONS.never_capture = True except Exception as e: - re2_error = e + re2_error = e.with_traceback(None) RE2_OPTIONS = None RE2_OPTIONS: re2.Options From fad67b15ec2648e30232e7fd993a4fb0b85b64ce Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:11:18 -0400 Subject: [PATCH 43/67] Misc --- .github/workflows/test-quick.yaml | 19 ------------------- pathspec/_meta.py | 1 + pathspec/_typing.py | 2 +- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index 05feb2e..cdeb01a 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -45,22 +45,3 @@ jobs: - name: Run tests (3.9) if: matrix.python == '3.9' run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose - - docs: - # Test documentation builds. - # This environment mirrors the ReadTheDocs build environment. - name: Docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v6 - with: - python-version: "3.13" - - - name: Install tox - run: python -m pip install tox - - - name: Run tests - run: python -m tox -e docs diff --git a/pathspec/_meta.py b/pathspec/_meta.py index dcaa5b9..4fe8653 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -65,5 +65,6 @@ "Dmytro Kostochko ", "Kadir Can Ozden ", "Henry Schreiner ", + "Yilei ", ] __license__ = "MPL 2.0" diff --git a/pathspec/_typing.py b/pathspec/_typing.py index 049966c..b304fcc 100644 --- a/pathspec/_typing.py +++ b/pathspec/_typing.py @@ -2,7 +2,7 @@ This module provides stubs for type hints not supported by all relevant Python versions. -NOTICE: This project should have zero required dependencies which means it +NOTICE: This project should have zero required dependencies, which means it cannot simply require :module:`typing_extensions`, and I do not want to maintain a vendored copy of :module:`typing_extensions`. """ From 3d044f4390ffe32aa63331705ab6505a4678d645 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:27:47 -0400 Subject: [PATCH 44/67] Specialize patterns --- pathspec/gitignore.py | 2 +- pathspec/pathspec.py | 52 +++++++++++++++++++++++++++++++++++---- pathspec/util.py | 6 +++++ tests/test_04_pathspec.py | 22 ++++++++++++++++- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index 93c3d76..8ca276d 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -45,7 +45,7 @@ """ -class GitIgnoreSpec(PathSpec): +class GitIgnoreSpec(PathSpec[GitIgnoreSpecPattern]): """ The :class:`GitIgnoreSpec` class extends :class:`.PathSpec` to replicate *gitignore* behavior. This is uses :class:`.GitIgnoreSpecPattern` to fully diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index 97422ca..da2a91c 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -13,10 +13,13 @@ zip_longest) from typing import ( Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Generic, + Literal, Optional, # Replaced by `X | None` in 3.10. TypeVar, Union, # Replaced by `X | Y` in 3.10. - cast) + cast, + overload) Self = TypeVar("Self", bound='PathSpec') """ @@ -32,19 +35,22 @@ make_pathspec_backend) from pathspec.pattern import ( Pattern) +from pathspec.patterns.gitignore.basic import ( + GitIgnoreBasicPattern) from pathspec._typing import ( AnyStr, # Removed in 3.18. deprecated) # Added in 3.13. from pathspec.util import ( CheckResult, StrPath, + TPattern, TStrPath, TreeEntry, _is_iterable, normalize_file) -class PathSpec(object): +class PathSpec(Generic[TPattern]): """ The :class:`PathSpec` class is a wrapper around a list of compiled :class:`.Pattern` instances. @@ -52,10 +58,10 @@ class PathSpec(object): def __init__( self, - patterns: Union[Sequence[Pattern], Iterable[Pattern]], + patterns: Union[Sequence[TPattern], Iterable[TPattern]], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + _test_backend_factory: Optional[Callable[[Sequence[TPattern]], _Backend]] = None, ) -> None: """ Initializes the :class:`.PathSpec` instance. @@ -92,7 +98,7 @@ def __init__( *_backend_name* (:class:`str`) is the name of backend to use. """ - self.patterns: Sequence[Pattern] = patterns + self.patterns: Sequence[TPattern] = patterns """ *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) contains the compiled patterns. @@ -225,6 +231,42 @@ def check_tree_files( files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links) yield from self.check_files(files) + @overload + @classmethod + def from_lines( + cls: type[PathSpec], + pattern_factory: Literal['gitignore'], + lines: Iterable[AnyStr], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> PathSpec[GitIgnoreBasicPattern]: + ... + + @overload + @classmethod + def from_lines( + cls: type[PathSpec], + pattern_factory: Callable[[AnyStr], TPattern], + lines: Iterable[AnyStr], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[TPattern]], _Backend]] = None, + ) -> PathSpec[TPattern]: + ... + + @overload + @classmethod + def from_lines( + cls: type[PathSpec], + pattern_factory: Union[str, Callable[[AnyStr], Pattern]], + lines: Iterable[AnyStr], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> PathSpec[Pattern]: + ... + @classmethod def from_lines( cls: type[Self], diff --git a/pathspec/util.py b/pathspec/util.py index 9a3f2c1..a56f7d2 100644 --- a/pathspec/util.py +++ b/pathspec/util.py @@ -30,6 +30,12 @@ StrPath = Union[str, os.PathLike[str]] +TPattern = TypeVar('TPattern', bound=Pattern) +""" +Type variable for :class:`.Pattern`. This is used by :class:`pathspec.pathspec.PathSpec` +to specialize the type of patterns. +""" + TStrPath = TypeVar("TStrPath", bound=StrPath) """ Type variable for :class:`str` or :class:`os.PathLike`. diff --git a/tests/test_04_pathspec.py b/tests/test_04_pathspec.py index df5c463..7685088 100644 --- a/tests/test_04_pathspec.py +++ b/tests/test_04_pathspec.py @@ -19,7 +19,9 @@ Path) from typing import ( Callable, # Replaced by `collections.abc.Callable` in 3.9.2. - Optional) # Replaced by `X | None` in 3.10. + Literal, + Optional, # Replaced by `X | None` in 3.10. + overload) from unittest import ( SkipTest) @@ -38,6 +40,8 @@ Pattern) from pathspec.patterns.gitignore.base import ( GitIgnorePatternError) +from pathspec.patterns.gitignore.basic import ( + GitIgnoreBasicPattern) from pathspec._typing import ( AnyStr) # Removed in 3.18. from pathspec.util import ( @@ -89,6 +93,22 @@ def make_files(self, files: Iterable[str]) -> None: """ return make_files(self.temp_dir, files) + @overload + def parameterize_from_lines( + self, + pattern_factory: Literal['gitignore'], + lines: Iterable[AnyStr], + ) -> Iterator[Callable[[], AbstractContextManager[PathSpec[GitIgnoreBasicPattern]]]]: + ... + + @overload + def parameterize_from_lines( + self, + pattern_factory: str, + lines: Iterable[AnyStr], + ) -> Iterator[Callable[[], AbstractContextManager[PathSpec[Pattern]]]]: + ... + def parameterize_from_lines( self, pattern_factory: str, From 15bffaef535597b3c6004beeec7bf779b852279e Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:04:21 -0400 Subject: [PATCH 45/67] Tests for 106 --- tests/test_02_gitignore_base.py | 40 +++++++++++++++++++ ...re_basic.py => test_03_gitignore_basic.py} | 0 ...nore_spec.py => test_04_gitignore_spec.py} | 0 ...est_04_pathspec.py => test_05_pathspec.py} | 0 ...t_05_gitignore.py => test_06_gitignore.py} | 0 5 files changed, 40 insertions(+) create mode 100644 tests/test_02_gitignore_base.py rename tests/{test_02_gitignore_basic.py => test_03_gitignore_basic.py} (100%) rename tests/{test_03_gitignore_spec.py => test_04_gitignore_spec.py} (100%) rename tests/{test_04_pathspec.py => test_05_pathspec.py} (100%) rename tests/{test_05_gitignore.py => test_06_gitignore.py} (100%) diff --git a/tests/test_02_gitignore_base.py b/tests/test_02_gitignore_base.py new file mode 100644 index 0000000..f315194 --- /dev/null +++ b/tests/test_02_gitignore_base.py @@ -0,0 +1,40 @@ +""" +This script tests :class:`._GitIgnoreBasePattern`. +""" + +import unittest + +from pathspec.patterns.gitignore.base import ( + _BYTES_ENCODING, + _GitIgnoreBasePattern) + + +class GitIgnoreBasePatternTest(unittest.TestCase): + """ + The :class:`GitIgnoreBasePatternTest` class tests the :class:`_GitIgnoreBasePattern` + implementation. + """ + + def test_01_escape_bytes(self): + """ + Test escaping binary strings. + """ + byte_to_escaped = {__b: b'\\' + __b for __b in ( + __c.encode(_BYTES_ENCODING) for __c in '\\[]!*#?' + )} + for char_ord in range(256): + char_byte = chr(char_ord).encode(_BYTES_ENCODING) + escape_val = _GitIgnoreBasePattern.escape(char_byte) + expect_val = byte_to_escaped.get(char_byte, char_byte) + self.assertEqual(escape_val, expect_val) + + def test_01_escape_str(self): + """ + Test escaping unicode strings. + """ + char_to_escaped = {__c: f"\\{__c}" for __c in '\\[]!*#?'} + for char_ord in range(128): + char = chr(char_ord) + escape_val = _GitIgnoreBasePattern.escape(char) + expect_val = char_to_escaped.get(char, char) + self.assertEqual(escape_val, expect_val) diff --git a/tests/test_02_gitignore_basic.py b/tests/test_03_gitignore_basic.py similarity index 100% rename from tests/test_02_gitignore_basic.py rename to tests/test_03_gitignore_basic.py diff --git a/tests/test_03_gitignore_spec.py b/tests/test_04_gitignore_spec.py similarity index 100% rename from tests/test_03_gitignore_spec.py rename to tests/test_04_gitignore_spec.py diff --git a/tests/test_04_pathspec.py b/tests/test_05_pathspec.py similarity index 100% rename from tests/test_04_pathspec.py rename to tests/test_05_pathspec.py diff --git a/tests/test_05_gitignore.py b/tests/test_06_gitignore.py similarity index 100% rename from tests/test_05_gitignore.py rename to tests/test_06_gitignore.py From 5e6de594ec0c530afc654a929bbd194793d13b2b Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:04:45 -0400 Subject: [PATCH 46/67] Specialize patterns --- CHANGES_1.in.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 19b70f3..a00f9ac 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -2,8 +2,6 @@ 1.0.5 (TBD) ----------- -TODO: Tests for #106. - Bug fixes: - `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. @@ -11,10 +9,12 @@ Bug fixes: Improvements: +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[PatternType]` for better debugging of `PathSpec().patterns`. - `Pull #110`_: Nicer debug print outs (and str for regex pattern). .. _`Pull #106`: https://github.com/cpburnz/python-pathspec/pull/106 +.. _`Issue #108`: https://github.com/cpburnz/python-pathspec/issue/108 .. _`Pull #110`: https://github.com/cpburnz/python-pathspec/pull/110 From 36faddae807a997d04ccfc8cf00931819464260c Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:06:48 -0400 Subject: [PATCH 47/67] Specialize patterns --- CHANGES_1.in.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index a00f9ac..faf145f 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -9,7 +9,7 @@ Bug fixes: Improvements: -- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[PatternType]` for better debugging of `PathSpec().patterns`. +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. - `Pull #110`_: Nicer debug print outs (and str for regex pattern). From 0d7c7deb138050c8586000682134d820a176bc10 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:21:06 -0400 Subject: [PATCH 48/67] Pin all Github actions --- .github/workflows/publish-to-pypi.yml | 10 +++++----- .github/workflows/publish-to-testpypi.yml | 10 +++++----- .github/workflows/test-all.yaml | 8 ++++---- .github/workflows/test-quick.yaml | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index ba26b13..766984f 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: python-version: "3.x" @@ -26,7 +26,7 @@ jobs: run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 from 2026-02-26. with: name: python-package-distributions path: dist/ @@ -44,12 +44,12 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v6 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: print-hash: true diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index bc4d882..938a295 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. with: persist-credentials: false @@ -21,7 +21,7 @@ jobs: run: git fetch --shallow-since=$(git show -s --format='%as' $(git rev-list --tags --max-count=1)) origin - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: python-version: "3.x" @@ -35,7 +35,7 @@ jobs: run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 from 2026-02-26. with: name: python-package-distributions path: dist/ @@ -53,13 +53,13 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v6 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. with: name: python-package-distributions path: dist/ - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: repository-url: https://test.pypi.org/legacy/ print-hash: true diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index b1d684c..b70f89f 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -63,10 +63,10 @@ jobs: backend: re2 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -88,10 +88,10 @@ jobs: name: Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: python-version: "3.13" diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index cdeb01a..673006f 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -27,10 +27,10 @@ jobs: - base steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: python-version: ${{ matrix.python }} allow-prereleases: true From 45907bf50a5cabe525306b99e85779639d9ca55e Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:34:25 -0400 Subject: [PATCH 49/67] Test Iron Proxy for CI --- .github/egress-rules.yaml | 4 ++++ .github/workflows/publish-to-testpypi.yml | 16 ++++++++++++++++ .github/workflows/test-all.yaml | 16 ++++++++++++++++ .github/workflows/test-quick.yaml | 8 ++++++++ 4 files changed, 44 insertions(+) create mode 100644 .github/egress-rules.yaml diff --git a/.github/egress-rules.yaml b/.github/egress-rules.yaml new file mode 100644 index 0000000..a0af730 --- /dev/null +++ b/.github/egress-rules.yaml @@ -0,0 +1,4 @@ +domains: + - files.pythonhosted.org + - pypi.org + - test.pypi.org diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index 938a295..aa0153a 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -14,6 +14,11 @@ jobs: with: persist-credentials: false + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-rules.yaml + warn: 'true' + - name: Fetch tags run: git fetch --tags origin @@ -40,6 +45,9 @@ jobs: name: python-package-distributions path: dist/ + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() + publish-to-testpypi: name: Publish Python distribution to TestPyPI needs: @@ -52,6 +60,11 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-rules.yaml + warn: 'true' + - name: Download all the dists uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. with: @@ -64,3 +77,6 @@ jobs: repository-url: https://test.pypi.org/legacy/ print-hash: true verbose: true + + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index b70f89f..a6326b1 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -65,6 +65,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-rules.yaml + warn: 'true' + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: @@ -82,6 +87,9 @@ jobs: if: matrix.python == '3.9' || matrix.python == 'pypy-3.9' run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() + docs: # Test documentation builds. # This environment mirrors the ReadTheDocs build environment. @@ -90,6 +98,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-rules.yaml + warn: 'true' + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: @@ -100,3 +113,6 @@ jobs: - name: Run tests run: python -m tox -e docs + + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index 673006f..f9391fe 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -29,6 +29,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-rules.yaml + warn: 'true' + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: @@ -45,3 +50,6 @@ jobs: - name: Run tests (3.9) if: matrix.python == '3.9' run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose + + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() From 06391d861d68ba4763e8c377c8bb1b9392bcc76a Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:48:27 -0400 Subject: [PATCH 50/67] Test Iron Proxy for CI --- .../{egress-rules.yaml => egress-build-rules.yaml} | 1 - .github/egress-pypi-rules.yaml | 2 ++ .github/egress-testpypi-rules.yaml | 2 ++ .github/workflows/publish-to-pypi.yml | 14 ++++++++++++++ .github/workflows/publish-to-testpypi.yml | 6 ++---- .github/workflows/test-all.yaml | 6 ++---- .github/workflows/test-quick.yaml | 3 +-- 7 files changed, 23 insertions(+), 11 deletions(-) rename .github/{egress-rules.yaml => egress-build-rules.yaml} (73%) create mode 100644 .github/egress-pypi-rules.yaml create mode 100644 .github/egress-testpypi-rules.yaml diff --git a/.github/egress-rules.yaml b/.github/egress-build-rules.yaml similarity index 73% rename from .github/egress-rules.yaml rename to .github/egress-build-rules.yaml index a0af730..3aecd78 100644 --- a/.github/egress-rules.yaml +++ b/.github/egress-build-rules.yaml @@ -1,4 +1,3 @@ domains: - files.pythonhosted.org - pypi.org - - test.pypi.org diff --git a/.github/egress-pypi-rules.yaml b/.github/egress-pypi-rules.yaml new file mode 100644 index 0000000..600dc62 --- /dev/null +++ b/.github/egress-pypi-rules.yaml @@ -0,0 +1,2 @@ +domains: + - pypi.org diff --git a/.github/egress-testpypi-rules.yaml b/.github/egress-testpypi-rules.yaml new file mode 100644 index 0000000..c40e9ff --- /dev/null +++ b/.github/egress-testpypi-rules.yaml @@ -0,0 +1,2 @@ +domains: + - test.pypi.org diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 766984f..c295cdf 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -14,6 +14,10 @@ jobs: with: persist-credentials: false + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-build-rules.yaml + - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: @@ -31,6 +35,9 @@ jobs: name: python-package-distributions path: dist/ + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() + publish-to-pypi: name: Publish Python distribution to PyPI needs: @@ -43,6 +50,10 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-testpypi-rules.yaml + - name: Download all the dists uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. with: @@ -53,3 +64,6 @@ jobs: uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: print-hash: true + + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index aa0153a..b8f697b 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -16,8 +16,7 @@ jobs: - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. with: - egress-rules: .github/egress-rules.yaml - warn: 'true' + egress-rules: .github/egress-build-rules.yaml - name: Fetch tags run: git fetch --tags origin @@ -62,8 +61,7 @@ jobs: steps: - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. with: - egress-rules: .github/egress-rules.yaml - warn: 'true' + egress-rules: .github/egress-testpypi-rules.yaml - name: Download all the dists uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index a6326b1..2a38290 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -67,8 +67,7 @@ jobs: - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. with: - egress-rules: .github/egress-rules.yaml - warn: 'true' + egress-rules: .github/egress-build-rules.yaml - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. @@ -100,8 +99,7 @@ jobs: - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. with: - egress-rules: .github/egress-rules.yaml - warn: 'true' + egress-rules: .github/egress-build-rules.yaml - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index f9391fe..29021b3 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -31,8 +31,7 @@ jobs: - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. with: - egress-rules: .github/egress-rules.yaml - warn: 'true' + egress-rules: .github/egress-build-rules.yaml - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. From ccaedca31c5cd904c5bb55df0f0045c675f77b7f Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:07:14 -0400 Subject: [PATCH 51/67] Test Iron Proxy for CI --- .github/egress-build-rules.yaml | 1 + .github/workflows/test-all.yaml | 3 ++- .github/workflows/test-quick.yaml | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/egress-build-rules.yaml b/.github/egress-build-rules.yaml index 3aecd78..79cf49b 100644 --- a/.github/egress-build-rules.yaml +++ b/.github/egress-build-rules.yaml @@ -1,3 +1,4 @@ domains: + - docs.python.org - files.pythonhosted.org - pypi.org diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index 2a38290..df2c5d8 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -66,6 +66,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: matrix.os == 'ubuntu' with: egress-rules: .github/egress-build-rules.yaml @@ -87,7 +88,7 @@ jobs: run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. - if: always() + if: always() && matrix.os == 'ubuntu' docs: # Test documentation builds. diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index 29021b3..307809c 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -30,6 +30,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: matrix.os == 'ubuntu' with: egress-rules: .github/egress-build-rules.yaml @@ -51,4 +52,4 @@ jobs: run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. - if: always() + if: always() && matrix.os == 'ubuntu' From 0b04daeafaea8c82a6fa3e86090061dc47c61ea6 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:09:13 -0400 Subject: [PATCH 52/67] Test Iron Proxy for CI --- .github/workflows/test-quick.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index 307809c..c0d541f 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -53,3 +53,29 @@ jobs: - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. if: always() && matrix.os == 'ubuntu' + + docs: + # Test documentation builds. + # This environment mirrors the ReadTheDocs build environment. + name: Docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. + + - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + with: + egress-rules: .github/egress-build-rules.yaml + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. + with: + python-version: "3.13" + + - name: Install tox + run: python -m pip install tox + + - name: Run tests + run: python -m tox -e docs + + - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + if: always() From a1abeba97f1fdbc3bc0e64e6c4d7ee9b63c4cf77 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:20:16 -0400 Subject: [PATCH 53/67] Test Iron Proxy for CI --- .github/workflows/publish-to-pypi.yml | 7 ------- .github/workflows/publish-to-testpypi.yml | 7 ------- 2 files changed, 14 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index c295cdf..7d544c7 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -50,10 +50,6 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. - with: - egress-rules: .github/egress-testpypi-rules.yaml - - name: Download all the dists uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. with: @@ -64,6 +60,3 @@ jobs: uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: print-hash: true - - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. - if: always() diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yml index b8f697b..a27d128 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yml @@ -59,10 +59,6 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. - with: - egress-rules: .github/egress-testpypi-rules.yaml - - name: Download all the dists uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 from 2026-03-11. with: @@ -75,6 +71,3 @@ jobs: repository-url: https://test.pypi.org/legacy/ print-hash: true verbose: true - - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. - if: always() From c9249c8b4ca165ca8c5eea191cea4c0e6f3aa827 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:39:19 -0400 Subject: [PATCH 54/67] Release v1.1.0 --- CHANGES.rst | 21 +++++++++++++++++++++ CHANGES_1.in.rst | 6 +++--- DEV.md | 5 ----- README-dist.rst | 21 +++++++++++++++++++++ pathspec/_version.py | 2 +- pyproject.toml | 5 +++-- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6c44900..fa18abb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,30 @@ Change History ============== +1.1.0 (2026-04-22) +------------------ + +Bug fixes: + +- `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. +- `Pull #106`_: Fix escape() not escaping backslash characters. + +Improvements: + +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. +- `Pull #110`_: Nicer debug print outs (and str for regex pattern). + + +.. _`Pull #106`: https://github.com/cpburnz/python-pathspec/pull/106 +.. _`Issue #108`: https://github.com/cpburnz/python-pathspec/issues/108 +.. _`Pull #110`: https://github.com/cpburnz/python-pathspec/pull/110 + + 1.0.4 (2026-01-26) ------------------ +Bug fixes: + - `Issue #103`_: Using re2 fails if pyre2 is also installed. .. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index faf145f..f06e3ed 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,6 +1,6 @@ -1.0.5 (TBD) ------------ +1.1.0 (2026-04-22) +------------------ Bug fixes: @@ -14,7 +14,7 @@ Improvements: .. _`Pull #106`: https://github.com/cpburnz/python-pathspec/pull/106 -.. _`Issue #108`: https://github.com/cpburnz/python-pathspec/issue/108 +.. _`Issue #108`: https://github.com/cpburnz/python-pathspec/issues/108 .. _`Pull #110`: https://github.com/cpburnz/python-pathspec/pull/110 diff --git a/DEV.md b/DEV.md index b6a9d75..3247a40 100644 --- a/DEV.md +++ b/DEV.md @@ -2,11 +2,6 @@ Development Notes ================= -TODO ----- - -- Release v1.0.0. - Python Versions --------------- diff --git a/README-dist.rst b/README-dist.rst index 56cf793..7b05e6a 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,9 +227,30 @@ Change History ============== +1.1.0 (2026-04-22) +------------------ + +Bug fixes: + +- `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. +- `Pull #106`_: Fix escape() not escaping backslash characters. + +Improvements: + +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. +- `Pull #110`_: Nicer debug print outs (and str for regex pattern). + + +.. _`Pull #106`: https://github.com/cpburnz/python-pathspec/pull/106 +.. _`Issue #108`: https://github.com/cpburnz/python-pathspec/issues/108 +.. _`Pull #110`: https://github.com/cpburnz/python-pathspec/pull/110 + + 1.0.4 (2026-01-26) ------------------ +Bug fixes: + - `Issue #103`_: Using re2 fails if pyre2 is also installed. .. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 diff --git a/pathspec/_version.py b/pathspec/_version.py index 421d8fa..fd2f0b5 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.0.4" +__version__ = "1.1.0" diff --git a/pyproject.toml b/pyproject.toml index b5637a4..321c133 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.0.4" +version = "1.1.0" [project.optional-dependencies] hyperscan = [ @@ -52,9 +52,10 @@ dev = [ ] [project.urls] -"Source Code" = "https://github.com/cpburnz/python-pathspec" +"Change Log" = "https://python-path-specification.readthedocs.io/en/latest/changes.html" "Documentation" = "https://python-path-specification.readthedocs.io/en/latest/index.html" "Issue Tracker" = "https://github.com/cpburnz/python-pathspec/issues" +"Source Code" = "https://github.com/cpburnz/python-pathspec" [tool.flit.sdist] include = [ From 6727491ff877e570e450b078c345d9346db7e531 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:52:05 -0400 Subject: [PATCH 55/67] Improve type checking with mypy and pyright --- CHANGES_1.in.rst | 19 +++++- justfile | 16 ++++- pathspec/_backends/hyperscan/_base.py | 4 +- pathspec/_backends/hyperscan/base.py | 6 +- pathspec/_backends/hyperscan/gitignore.py | 17 +++-- pathspec/_backends/hyperscan/pathspec.py | 16 +++-- pathspec/_backends/re2/_base.py | 8 +-- pathspec/_backends/re2/base.py | 2 +- pathspec/_backends/re2/gitignore.py | 9 +-- pathspec/_backends/re2/pathspec.py | 12 ++-- pathspec/_backends/simple/gitignore.py | 2 +- pathspec/_typing.py | 12 ++-- pathspec/backend.py | 13 +++- pathspec/gitignore.py | 49 ++++++++------- pathspec/pathspec.py | 77 ++++++++++++++--------- pathspec/pattern.py | 35 ++++++----- pathspec/patterns/__init__.py | 4 +- pathspec/patterns/gitignore/base.py | 5 +- pathspec/patterns/gitignore/basic.py | 11 ++-- pathspec/patterns/gitignore/spec.py | 11 ++-- pathspec/util.py | 48 ++++++++------ tests/check_usage.py | 49 +++++++++++++++ tests/test_05_pathspec.py | 65 ++++++++++++++++++- 23 files changed, 349 insertions(+), 141 deletions(-) create mode 100644 tests/check_usage.py diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index f06e3ed..0572dd0 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,7 +1,25 @@ +1.1.1 (TDB) +----------- + +Improvements: + +- Improved type checking with mypy and pyright. + +Bug fixes: + +- Fixed typing on `PathSpec[TPattern]` to `PathSpec[TPattern_co]`. +- Added missing variant type-hint `type[Pattern]` to `PathSpec.from_lines()` parameter `pattern_factory`. +- Fixed possible type error when using `+` and `+=` operators on `PathSpec`. + + 1.1.0 (2026-04-22) ------------------ +New features: + +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. + Bug fixes: - `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. @@ -9,7 +27,6 @@ Bug fixes: Improvements: -- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. - `Pull #110`_: Nicer debug print outs (and str for regex pattern). diff --git a/justfile b/justfile index d49b99d..990d1fa 100644 --- a/justfile +++ b/justfile @@ -26,6 +26,14 @@ bench-gitignore: _bench_gitignore [group('Development')] bench-pathspec: _bench_pathspec +# Run type checking with mypy. +[group('Development')] +check-mypy: _check_mypy + +# Run type checking with pyright. +[group('Development')] +check-pyright: _check_pyright + # Run tests using the CPython virtual environment. [group('Development')] test: _test_primary @@ -94,6 +102,12 @@ _bench_pathspec: _build_docs: {{cpy_run}} sphinx-build -aWEnqb html doc/source doc/build +_check_mypy: + {{cpy_run}} mypy pathspec tests/check_usage.py + +_check_pyright: + {{cpy_run}} pyright pathspec tests/check_usage.py + _test_all: {{cpy_run}} tox @@ -107,7 +121,7 @@ _venv_cpy_create: {{cpy_bin}} -m venv --clear dev/venv-cpy _venv_cpy_update: - {{cpy_run}} pip install -r doc/requirements.txt --upgrade build google-re2 google-re2-stubs hyperscan packaging pip pytest pytest-benchmark setuptools tomli tox twine typing-extensions wheel + {{cpy_run}} pip install -r doc/requirements.txt --uploaded-prior-to "$(date -d '7 days ago' '+%Y-%m-%d')" --upgrade build google-re2 google-re2-stubs hyperscan mypy packaging pip pyright pytest pytest-benchmark setuptools tomli tox twine typing-extensions wheel {{cpy_run}} pip install -e . _venv_pypy_create: diff --git a/pathspec/_backends/hyperscan/_base.py b/pathspec/_backends/hyperscan/_base.py index cb58f48..ae94032 100644 --- a/pathspec/_backends/hyperscan/_base.py +++ b/pathspec/_backends/hyperscan/_base.py @@ -15,12 +15,12 @@ try: import hyperscan except ModuleNotFoundError: - hyperscan = None + hyperscan = None # type: ignore[assignment] HS_FLAGS = 0 else: HS_FLAGS = hyperscan.HS_FLAG_SINGLEMATCH | hyperscan.HS_FLAG_UTF8 -HS_FLAGS: int +HS_FLAGS: int # type: ignore[no-redef] """ The hyperscan flags to use: diff --git a/pathspec/_backends/hyperscan/base.py b/pathspec/_backends/hyperscan/base.py index e05151f..783804e 100644 --- a/pathspec/_backends/hyperscan/base.py +++ b/pathspec/_backends/hyperscan/base.py @@ -8,16 +8,16 @@ from __future__ import annotations from typing import ( - Optional) + Optional) # Replaced by `X | None` in 3.10. try: import hyperscan hyperscan_error = None except ModuleNotFoundError as e: - hyperscan = None + hyperscan = None # type: ignore[assignment] hyperscan_error = e.with_traceback(None) -hyperscan_error: Optional[ModuleNotFoundError] +hyperscan_error: Optional[ModuleNotFoundError] # type: ignore[no-redef] """ *hyperscan_error* (:class:`ModuleNotFoundError` or :data:`None`) is the hyperscan import error. diff --git a/pathspec/_backends/hyperscan/gitignore.py b/pathspec/_backends/hyperscan/gitignore.py index 2428b59..fd98b6c 100644 --- a/pathspec/_backends/hyperscan/gitignore.py +++ b/pathspec/_backends/hyperscan/gitignore.py @@ -17,7 +17,7 @@ try: import hyperscan except ModuleNotFoundError: - hyperscan = None + hyperscan = None # type: ignore[assignment] from pathspec.pattern import ( RegexPattern) @@ -45,7 +45,7 @@ class HyperscanGiBackend(HyperscanPsBackend): """ # Change type hint. - _out: tuple[Optional[bool], int, int] + _out: tuple[Optional[bool], int, int] # type: ignore[assignment] def __init__( self, @@ -76,7 +76,7 @@ def __init__( @override @staticmethod def _init_db( - db: hyperscan.Database, + db: hyperscan.Database, # type: ignore debug: bool, patterns: list[tuple[int, RegexPattern]], sort_ids: Optional[Callable[[list[int]], None]], @@ -108,6 +108,7 @@ def _init_db( exprs: list[bytes] = [] for pattern_index, pattern in patterns: assert pattern.include is not None, (pattern_index, pattern) + assert pattern.regex is not None, (pattern_index, pattern) # Encode regex. assert isinstance(pattern, RegexPattern), pattern @@ -119,7 +120,7 @@ def _init_db( # Hyperscan does not support capture groups. Handle this scenario. regex_str: str if isinstance(regex, str): - regex_str: str = regex + regex_str = regex else: assert isinstance(regex, bytes), regex regex_str = regex.decode(_BYTES_ENCODING) @@ -193,7 +194,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: # NOTICE: According to benchmarking, a method callback is 13% faster than # using a closure here. db = self._db - if self._db is None: + if db is None: # Database was not initialized because there were no patterns. Return no # match. return (None, None) @@ -201,6 +202,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: self._out = (None, -1, 0) db.scan(file.encode('utf8'), match_event_handler=self.__on_match) + out_index: Optional[int] out_include, out_index = self._out[:2] if out_index == -1: out_index = None @@ -242,4 +244,7 @@ def __on_match( or (priority == prev_priority and index > prev_index) or priority > prev_priority ): - self._out = (include, expr_dat.index, priority) + out_tup = (include, expr_dat.index, priority) + self._out = out_tup # type: ignore + + return None diff --git a/pathspec/_backends/hyperscan/pathspec.py b/pathspec/_backends/hyperscan/pathspec.py index d55c314..f7a8ddc 100644 --- a/pathspec/_backends/hyperscan/pathspec.py +++ b/pathspec/_backends/hyperscan/pathspec.py @@ -16,7 +16,7 @@ try: import hyperscan except ModuleNotFoundError: - hyperscan = None + hyperscan = None # type: ignore[assignment] from pathspec.backend import ( _Backend) @@ -57,6 +57,7 @@ def __init__( compiled patterns. """ if hyperscan is None: + assert hyperscan_error is not None, (hyperscan, hyperscan_error) raise hyperscan_error if patterns and not isinstance(patterns[0], RegexPattern): @@ -81,7 +82,7 @@ def __init__( db = None expr_data = [] - self._db: Optional[hyperscan.Database] = db + self._db: Optional[hyperscan.Database] = db # type: ignore """ *_db* (:class:`hyperscan.Database`) is the Hyperscan database. """ @@ -115,7 +116,7 @@ def __init__( @staticmethod def _init_db( - db: hyperscan.Database, + db: hyperscan.Database, # type: ignore debug: bool, patterns: list[tuple[int, RegexPattern]], sort_ids: Optional[Callable[[list[int]], None]], @@ -147,6 +148,7 @@ def _init_db( exprs: list[bytes] = [] for pattern_index, pattern in patterns: assert pattern.include is not None, (pattern_index, pattern) + assert pattern.regex is not None, (pattern_index, pattern) # Encode regex. assert isinstance(pattern, RegexPattern), pattern @@ -204,7 +206,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: # NOTICE: According to benchmarking, a method callback is 20% faster than # using a closure here. db = self._db - if self._db is None: + if db is None: # Database was not initialized because there were no patterns. Return no # match. return (None, None) @@ -212,6 +214,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: self._out = (None, -1) db.scan(file.encode('utf8'), match_event_handler=self.__on_match) + out_index: Optional[int] out_include, out_index = self._out if out_index == -1: out_index = None @@ -219,12 +222,13 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: return (out_include, out_index) @staticmethod - def _make_db() -> hyperscan.Database: + def _make_db() -> hyperscan.Database: # type: ignore """ Create the Hyperscan database. Returns the database (:class:`hyperscan.Database`). """ + assert hyperscan is not None, (hyperscan, hyperscan_error) return hyperscan.Database(mode=hyperscan.HS_MODE_BLOCK) def __on_match( @@ -249,3 +253,5 @@ def __on_match( prev_index = self._out[1] if index > prev_index: self._out = (expr_dat.include, index) + + return None diff --git a/pathspec/_backends/re2/_base.py b/pathspec/_backends/re2/_base.py index be3327a..32d6349 100644 --- a/pathspec/_backends/re2/_base.py +++ b/pathspec/_backends/re2/_base.py @@ -17,7 +17,7 @@ import re2 re2_error = None except ModuleNotFoundError as e: - re2 = None + re2 = None # type: ignore[assignment] re2_error = e.with_traceback(None) RE2_OPTIONS = None else: @@ -28,10 +28,10 @@ RE2_OPTIONS.log_errors = False RE2_OPTIONS.never_capture = True except Exception as e: - re2_error = e.with_traceback(None) + re2_error = e.with_traceback(None) # type: ignore[assignment] RE2_OPTIONS = None -RE2_OPTIONS: re2.Options +RE2_OPTIONS: re2.Options # type: ignore[no-redef] """ The re2 options to use: @@ -41,7 +41,7 @@ be utilized with :class:`re2.Set`. """ -re2_error: Optional[Exception] +re2_error: Optional[Exception] # type: ignore[no-redef] """ *re2_error* (:class:`Exception` or :data:`None`) is the re2 import error. """ diff --git a/pathspec/_backends/re2/base.py b/pathspec/_backends/re2/base.py index fa24f4d..5af026a 100644 --- a/pathspec/_backends/re2/base.py +++ b/pathspec/_backends/re2/base.py @@ -12,7 +12,7 @@ from ._base import ( re2_error) -re2_error: Optional[Exception] +re2_error: Optional[Exception] # type: ignore[no-redef] """ *re2_error* (:class:`Exception` or :data:`None`) is the re2 import error. """ diff --git a/pathspec/_backends/re2/gitignore.py b/pathspec/_backends/re2/gitignore.py index cb2525f..41bbdef 100644 --- a/pathspec/_backends/re2/gitignore.py +++ b/pathspec/_backends/re2/gitignore.py @@ -14,7 +14,7 @@ try: import re2 except ModuleNotFoundError: - re2 = None + re2 = None # type: ignore[assignment] from pathspec.pattern import ( RegexPattern) @@ -44,7 +44,7 @@ class Re2GiBackend(Re2PsBackend): def _init_set( debug: bool, patterns: dict[int, RegexPattern], - regex_set: re2.Set, + regex_set: re2.Set, # type: ignore sort_indices: Optional[Callable[[list[int]], None]], ) -> list[Re2RegexDat]: """ @@ -77,6 +77,7 @@ def _init_set( if pattern.include is None: continue + assert pattern.regex is not None, pattern assert isinstance(pattern, RegexPattern), pattern regex = pattern.regex.pattern @@ -124,7 +125,7 @@ def _init_set( is_dir_pattern=is_dir_pattern, )) - regex_set.Add(regex) + regex_set.Add(regex) # type: ignore[type-var] # Compile patterns. regex_set.Compile() @@ -142,7 +143,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: :data:`None`). """ # Find best match. - match_ids: Optional[list[int]] = self._set.Match(file) + match_ids: Optional[list[int]] = self._set.Match(file) # type: ignore[assignment] if not match_ids: return (None, None) diff --git a/pathspec/_backends/re2/pathspec.py b/pathspec/_backends/re2/pathspec.py index 2c58b45..ca994b5 100644 --- a/pathspec/_backends/re2/pathspec.py +++ b/pathspec/_backends/re2/pathspec.py @@ -15,7 +15,7 @@ try: import re2 except ModuleNotFoundError: - re2 = None + re2 = None # type: ignore[assignment] from pathspec.backend import ( _Backend) @@ -88,7 +88,7 @@ def __init__( (:class:`Re2RegexDat`). """ - self._set: re2.Set = regex_set + self._set: re2.Set = regex_set # type: ignore """ *_set* (:class:`re2.Set`) is the re2 regex set. """ @@ -97,7 +97,7 @@ def __init__( def _init_set( debug: bool, patterns: dict[int, RegexPattern], - regex_set: re2.Set, + regex_set: re2.Set, # type: ignore sort_indices: Optional[Callable[[list[int]], None]], ) -> list[Re2RegexDat]: """ @@ -130,6 +130,7 @@ def _init_set( if pattern.include is None: continue + assert pattern.regex is not None, pattern assert isinstance(pattern, RegexPattern), pattern regex = pattern.regex.pattern @@ -154,12 +155,13 @@ def _init_set( return regex_data @staticmethod - def _make_set() -> re2.Set: + def _make_set() -> re2.Set: # type: ignore """ Create the re2 regex set. Returns the set (:class:`re2.Set`). """ + assert re2 is not None, (re2, re2_error) return re2.Set.SearchSet(RE2_OPTIONS) @override @@ -177,7 +179,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: # - WARNING: According to the documentation on `RE2::Set::Match()`, there is # no guarantee matches will be produced in order! Later expressions have # higher priority. - match_ids: Optional[list[int]] = self._set.Match(file) + match_ids: Optional[list[int]] = self._set.Match(file) # type: ignore[assignment] if not match_ids: return (None, None) diff --git a/pathspec/_backends/simple/gitignore.py b/pathspec/_backends/simple/gitignore.py index bdacc7e..29701f5 100644 --- a/pathspec/_backends/simple/gitignore.py +++ b/pathspec/_backends/simple/gitignore.py @@ -28,7 +28,7 @@ class SimpleGiBackend(SimplePsBackend): """ # Change type hint. - _patterns: list[tuple[int, RegexPattern]] + _patterns: list[tuple[int, RegexPattern]] # type: ignore[assignment] def __init__( self, diff --git a/pathspec/_typing.py b/pathspec/_typing.py index b304fcc..ffb4867 100644 --- a/pathspec/_typing.py +++ b/pathspec/_typing.py @@ -17,7 +17,7 @@ try: from typing import AnyStr # Removed in 3.18. except ImportError: - AnyStr = TypeVar('AnyStr', str, bytes) + AnyStr = TypeVar('AnyStr', str, bytes) # type: ignore[misc] try: from typing import Never # Added in 3.11. except ImportError: @@ -26,12 +26,12 @@ F = TypeVar('F', bound=Callable[..., Any]) try: - from warnings import deprecated # Added in 3.13. + from warnings import deprecated # Added in 3.13. # type: ignore except ImportError: try: - from typing_extensions import deprecated + from typing_extensions import deprecated # type: ignore except ImportError: - def deprecated( + def deprecated( # type: ignore[no-redef] message: str, /, *, category: Optional[type[Warning]] = DeprecationWarning, @@ -42,7 +42,7 @@ def decorator(f: F) -> F: def wrapper(*a, **k): warnings.warn(message, category=category, stacklevel=stacklevel+1) return f(*a, **k) - return wrapper + return wrapper # type: ignore[return-value] return decorator try: @@ -51,7 +51,7 @@ def wrapper(*a, **k): try: from typing_extensions import override except ImportError: - def override(f: F) -> F: + def override(f: F, /) -> F: return f diff --git a/pathspec/backend.py b/pathspec/backend.py index f1def28..3cfbe03 100644 --- a/pathspec/backend.py +++ b/pathspec/backend.py @@ -5,15 +5,26 @@ subject to change. """ +from collections.abc import ( + Sequence) from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Literal, - Optional) + Optional) # Replaced by `X | None` in 3.10. + +from .pattern import ( + Pattern) BackendNamesHint = Literal['best', 'hyperscan', 're2', 'simple'] """ The supported backend values. """ +_TestBackendFactoryHint = Optional[Callable[[Sequence[Pattern]], '_Backend']] +""" +Type hint for the test backend factory argument. +""" + class _Backend(object): """ diff --git a/pathspec/gitignore.py b/pathspec/gitignore.py index 8ca276d..9e8e6f8 100644 --- a/pathspec/gitignore.py +++ b/pathspec/gitignore.py @@ -20,7 +20,8 @@ from pathspec.backend import ( BackendNamesHint, - _Backend) + _Backend, + _TestBackendFactoryHint) from pathspec._backends.agg import ( make_gitignore_backend) from pathspec.pathspec import ( @@ -66,15 +67,15 @@ def __eq__(self, other: object) -> bool: return NotImplemented # Support reversed order of arguments from PathSpec. - @overload + @overload # type: ignore[override] @classmethod def from_lines( cls: type[Self], - pattern_factory: Union[str, Callable[[AnyStr], Pattern], None], + pattern_factory: Union[str, type[Pattern], Callable[[AnyStr], Pattern], None], lines: Iterable[AnyStr], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> Self: ... @@ -83,22 +84,22 @@ def from_lines( def from_lines( cls: type[Self], lines: Iterable[AnyStr], - pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None, + pattern_factory: Union[str, type[Pattern], Callable[[AnyStr], Pattern], None] = None, *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> Self: ... - @override + @override # type: ignore[misc] @classmethod - def from_lines( + def from_lines( # type: ignore cls: type[Self], lines: Iterable[AnyStr], - pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None, + pattern_factory: Union[str, type[Pattern], Callable[[AnyStr], Pattern], None] = None, *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> Self: """ Compiles the pattern lines. @@ -122,26 +123,32 @@ def from_lines( """ if (isinstance(lines, (str, bytes)) or callable(lines)) and _is_iterable(pattern_factory): # Support reversed order of arguments from PathSpec. - pattern_factory, lines = lines, pattern_factory + pattern_factory, lines = lines, pattern_factory # type: ignore + use_factory: Callable[[AnyStr], GitIgnoreSpecPattern] if pattern_factory is None: - pattern_factory = GitIgnoreSpecPattern + use_factory = GitIgnoreSpecPattern # type: ignore[assignment] elif pattern_factory == 'gitignore': # Force use of GitIgnoreSpecPattern for "gitignore" to handle edge-cases. # This makes usage easier. - pattern_factory = GitIgnoreSpecPattern - - if isinstance(pattern_factory, str): - pattern_factory = lookup_pattern(pattern_factory) + use_factory = GitIgnoreSpecPattern # type: ignore[assignment] + elif isinstance(pattern_factory, str): + use_factory = lookup_pattern(pattern_factory) # type: ignore[assignment] + else: + use_factory = pattern_factory # type: ignore[assignment] - if issubclass(pattern_factory, GitIgnoreBasicPattern): + if ( + isinstance(use_factory, type) + and issubclass(use_factory, GitIgnoreBasicPattern) + ): raise TypeError(( - f"{pattern_factory=!r} cannot be {GitIgnoreBasicPattern} because it " - f"will give unexpected results." + f"pattern_factory={pattern_factory!r} (resolved to {use_factory}) " + f"cannot be {GitIgnoreBasicPattern} because it will give unexpected " + f"results." )) # TypeError - self = super().from_lines(pattern_factory, lines, backend=backend, _test_backend_factory=_test_backend_factory) - return cast(Self, self) + self = super().from_lines(use_factory, lines, backend=backend, _test_backend_factory=_test_backend_factory) # type: ignore[arg-type,type-var] + return self # type: ignore[return-value] @override @staticmethod diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index da2a91c..3e3872a 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -29,8 +29,9 @@ from pathspec import util from pathspec.backend import ( + BackendNamesHint, _Backend, - BackendNamesHint) + _TestBackendFactoryHint) from pathspec._backends.agg import ( make_pathspec_backend) from pathspec.pattern import ( @@ -44,13 +45,14 @@ CheckResult, StrPath, TPattern, + TPattern_co, TStrPath, TreeEntry, _is_iterable, normalize_file) -class PathSpec(Generic[TPattern]): +class PathSpec(Generic[TPattern_co]): """ The :class:`PathSpec` class is a wrapper around a list of compiled :class:`.Pattern` instances. @@ -58,10 +60,10 @@ class PathSpec(Generic[TPattern]): def __init__( self, - patterns: Union[Sequence[TPattern], Iterable[TPattern]], + patterns: Union[Sequence[TPattern_co], Iterable[TPattern_co]], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[TPattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> None: """ Initializes the :class:`.PathSpec` instance. @@ -75,17 +77,19 @@ def __init__( available backend. Priority of backends is: "re2", "hyperscan", "simple". The "simple" backend is always available. """ - if not isinstance(patterns, Sequence): - patterns = list(patterns) + if isinstance(patterns, Sequence): + use_patterns = patterns + else: + use_patterns = list(patterns) if backend is None: backend = 'best' - backend = cast(BackendNamesHint, backend) + backend_name = cast(BackendNamesHint, backend) if _test_backend_factory is not None: - use_backend = _test_backend_factory(patterns) + use_backend = _test_backend_factory(use_patterns) else: - use_backend = self._make_backend(backend, patterns) + use_backend = self._make_backend(backend_name, use_patterns) self._backend: _Backend = use_backend """ @@ -93,12 +97,12 @@ def __init__( backend. """ - self._backend_name: BackendNamesHint = backend + self._backend_name: BackendNamesHint = backend_name """ *_backend_name* (:class:`str`) is the name of backend to use. """ - self.patterns: Sequence[TPattern] = patterns + self.patterns: Sequence[TPattern_co] = use_patterns """ *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) contains the compiled patterns. @@ -116,7 +120,7 @@ def __add__(self: Self, other: PathSpec) -> Self: :class:`PathSpec` instances. """ if isinstance(other, PathSpec): - return self.__class__(self.patterns + other.patterns, backend=self._backend_name) + return self.__class__([*self.patterns, *other.patterns], backend=self._backend_name) else: return NotImplemented @@ -131,13 +135,13 @@ def __eq__(self, other: object) -> bool: else: return NotImplemented - def __iadd__(self: Self, other: PathSpec) -> Self: + def __iadd__(self: Self, other: PathSpec) -> Self: # type: ignore[misc] """ Adds the :attr:`self.patterns <.PathSpec.patterns>` from *other* (:class:`PathSpec`) to this instance. """ if isinstance(other, PathSpec): - self.patterns += other.patterns + self.patterns = [*self.patterns, *other.patterns] self._backend = self._make_backend(self._backend_name, self.patterns) return self else: @@ -239,7 +243,7 @@ def from_lines( lines: Iterable[AnyStr], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> PathSpec[GitIgnoreBasicPattern]: ... @@ -247,11 +251,23 @@ def from_lines( @classmethod def from_lines( cls: type[PathSpec], - pattern_factory: Callable[[AnyStr], TPattern], + pattern_factory: str, + lines: Iterable[AnyStr], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: _TestBackendFactoryHint = None, + ) -> PathSpec[Pattern]: + ... + + @overload + @classmethod + def from_lines( + cls: type[PathSpec], + pattern_factory: type[TPattern], lines: Iterable[AnyStr], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[TPattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> PathSpec[TPattern]: ... @@ -259,22 +275,22 @@ def from_lines( @classmethod def from_lines( cls: type[PathSpec], - pattern_factory: Union[str, Callable[[AnyStr], Pattern]], + pattern_factory: Callable[[AnyStr], TPattern], lines: Iterable[AnyStr], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, - ) -> PathSpec[Pattern]: + _test_backend_factory: _TestBackendFactoryHint = None, + ) -> PathSpec[TPattern]: ... @classmethod def from_lines( cls: type[Self], - pattern_factory: Union[str, Callable[[AnyStr], Pattern]], + pattern_factory: Union[str, type[Pattern], Callable[[AnyStr], Pattern]], lines: Iterable[AnyStr], *, backend: Union[BackendNamesHint, str, None] = None, - _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + _test_backend_factory: _TestBackendFactoryHint = None, ) -> Self: """ Compiles the pattern lines. @@ -296,17 +312,20 @@ def from_lines( Returns the :class:`PathSpec` instance. """ + use_factory: Callable[[AnyStr], Pattern] if isinstance(pattern_factory, str): - pattern_factory = util.lookup_pattern(pattern_factory) - - if not callable(pattern_factory): + use_factory = util.lookup_pattern(pattern_factory) # type: ignore[assignment] + elif callable(pattern_factory): + use_factory = pattern_factory # type: ignore[assignment] + else: raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") if not _is_iterable(lines): raise TypeError(f"lines:{lines!r} is not an iterable.") - patterns = [pattern_factory(line) for line in lines if line] - return cls(patterns, backend=backend, _test_backend_factory=_test_backend_factory) + patterns = [use_factory(__line) for __line in lines if __line] # type: ignore[arg-type] + self = cls(patterns, backend=backend, _test_backend_factory=_test_backend_factory) + return self @staticmethod def _make_backend( @@ -389,11 +408,11 @@ def match_file( def match_files( self, - files: Iterable[StrPath], + files: Iterable[TStrPath], separators: Optional[Collection[str]] = None, *, negate: Optional[bool] = None, - ) -> Iterator[StrPath]: + ) -> Iterator[TStrPath]: """ Matches the files to this path-spec. diff --git a/pathspec/pattern.py b/pathspec/pattern.py index 394958c..83dd067 100644 --- a/pathspec/pattern.py +++ b/pathspec/pattern.py @@ -117,36 +117,38 @@ def __init__( .. note:: Subclasses do not need to support the *include* parameter. """ - + regex: Optional[re.Pattern] = None if isinstance(pattern, (str, bytes)): assert include is None, ( - f"include:{include!r} must be null when pattern:{pattern!r} is a string." + f"{include=!r} must be null when {pattern=!r} is a string." ) - regex, include = self.pattern_to_regex(pattern) + raw_regex, include = self.pattern_to_regex(pattern) # NOTE: Make sure to allow a null regular expression to be # returned for a null-operation. if include is not None: - regex = re.compile(regex) + assert raw_regex is not None, ( + f"{raw_regex=!r} must be non-null when {include=!r} is not None." + ) + regex = re.compile(raw_regex) elif pattern is not None and hasattr(pattern, 'match'): # Assume pattern is a precompiled regular expression. - # - NOTE: Used specified *include*. + # - NOTE: Use specified *include*. regex = pattern elif pattern is None: - # NOTE: Make sure to allow a null pattern to be passed for a - # null-operation. + # NOTE: Make sure to allow a null pattern to be passed for a null + # operation. assert include is None, ( - f"include:{include!r} must be null when pattern:{pattern!r} is null." + f"{include=!r} must be null when {pattern=!r} is null." ) - regex = None else: - raise TypeError(f"pattern:{pattern!r} is not a string, re.Pattern, or None.") + raise TypeError(f"{pattern=!r} is not a string, re.Pattern, or None.") super(RegexPattern, self).__init__(include) - self.pattern: Union[AnyStr, re.Pattern, None] = pattern + self.pattern: Union[str, bytes, re.Pattern, None] = pattern """ *pattern* (:class:`str`, :class:`bytes`, :class:`re.Pattern`, or :data:`None`) is the uncompiled, input pattern. This is for reference. @@ -205,10 +207,11 @@ def match_file(self, file: AnyStr) -> Optional[RegexMatchResult]: Returns the match result (:class:`.RegexMatchResult`) if *file* matched; otherwise, :data:`None`. """ - if self.include is not None: - match = self.regex.search(file) - if match is not None: - return RegexMatchResult(match) + if ( + (regex := self.regex) is not None + and (match := regex.search(file)) is not None + ): + return RegexMatchResult(match) return None @@ -235,7 +238,7 @@ def pattern_to_regex( .. note:: The default implementation simply returns *pattern* and :data:`True`. """ - return pattern, True + return (pattern, True) @dataclass() diff --git a/pathspec/patterns/__init__.py b/pathspec/patterns/__init__.py index f1738a5..8ece497 100644 --- a/pathspec/patterns/__init__.py +++ b/pathspec/patterns/__init__.py @@ -3,8 +3,8 @@ """ # Load pattern implementations. -from .gitignore import basic as _ -from .gitignore import spec as _ +from .gitignore import basic as _0 +from .gitignore import spec as _1 # DEPRECATED: Deprecated since 0.11.0 (from 2023-01-24). Expose the # GitWildMatchPattern class in this module for backward compatibility with diff --git a/pathspec/patterns/gitignore/base.py b/pathspec/patterns/gitignore/base.py index 0decd65..6cb86b4 100644 --- a/pathspec/patterns/gitignore/base.py +++ b/pathspec/patterns/gitignore/base.py @@ -54,9 +54,10 @@ def escape(s: AnyStr) -> AnyStr: out_string = ''.join((f"\\{x}" if x in '\\[]!*#?' else x) for x in string) if return_type is bytes: - return out_string.encode(_BYTES_ENCODING) + out_bytes = out_string.encode(_BYTES_ENCODING) + return out_bytes # type: ignore[return-value] else: - return out_string + return out_string # type: ignore[return-value] @staticmethod def _translate_segment_glob( diff --git a/pathspec/patterns/gitignore/basic.py b/pathspec/patterns/gitignore/basic.py index 88e735d..770eab1 100644 --- a/pathspec/patterns/gitignore/basic.py +++ b/pathspec/patterns/gitignore/basic.py @@ -190,11 +190,11 @@ def pattern_to_regex( include = True # Split pattern into segments. - pattern_segs = pattern_str.split('/') + orig_segs = pattern_str.split('/') # Check whether the pattern is specifically a directory pattern before # normalization. - is_dir_pattern = not pattern_segs[-1] + is_dir_pattern = not orig_segs[-1] if pattern_str == '/': # EDGE CASE: A single slash ('/') is not addressed by the gitignore @@ -207,7 +207,7 @@ def pattern_to_regex( # Normalize pattern to make processing easier. try: pattern_segs, override_regex = cls.__normalize_segments( - is_dir_pattern, pattern_segs, + is_dir_pattern, orig_segs, ) except ValueError as e: raise GitIgnorePatternError(( @@ -237,9 +237,10 @@ def pattern_to_regex( # Encode regex if needed. out_regex: AnyStr if regex is not None and return_type is bytes: - out_regex = regex.encode(_BYTES_ENCODING) + regex_bytes = regex.encode(_BYTES_ENCODING) + out_regex = regex_bytes # type: ignore[assignment] else: - out_regex = regex + out_regex = regex # type: ignore[assignment] return (out_regex, include) diff --git a/pathspec/patterns/gitignore/spec.py b/pathspec/patterns/gitignore/spec.py index 3bbe16f..5f5644b 100644 --- a/pathspec/patterns/gitignore/spec.py +++ b/pathspec/patterns/gitignore/spec.py @@ -221,16 +221,16 @@ def pattern_to_regex( include = True # Split pattern into segments. - pattern_segs = pattern_str.split('/') + orig_segs = pattern_str.split('/') # Check whether the pattern is specifically a directory pattern before # normalization. - is_dir_pattern = not pattern_segs[-1] + is_dir_pattern = not orig_segs[-1] # Normalize pattern to make processing easier. try: pattern_segs, override_regex = cls.__normalize_segments( - is_dir_pattern, pattern_segs, + is_dir_pattern, orig_segs, ) except ValueError as e: raise GitIgnorePatternError(( @@ -263,9 +263,10 @@ def pattern_to_regex( # Encode regex if needed. out_regex: AnyStr if regex is not None and return_type is bytes: - out_regex = regex.encode(_BYTES_ENCODING) + regex_bytes = regex.encode(_BYTES_ENCODING) + out_regex = regex_bytes # type: ignore[assignment] else: - out_regex = regex + out_regex = regex # type: ignore[assignment] return (out_regex, include) diff --git a/pathspec/util.py b/pathspec/util.py index a56f7d2..e847ff8 100644 --- a/pathspec/util.py +++ b/pathspec/util.py @@ -1,6 +1,7 @@ """ This module provides utility methods for dealing with path-specs. """ +from __future__ import annotations import os import os.path @@ -20,7 +21,8 @@ Generic, Optional, # Replaced by `X | None` in 3.10. TypeVar, - Union) # Replaced by `X | Y` in 3.10. + Union, # Replaced by `X | Y` in 3.10. + cast) from .pattern import ( Pattern) @@ -36,13 +38,19 @@ to specialize the type of patterns. """ -TStrPath = TypeVar("TStrPath", bound=StrPath) +TPattern_co = TypeVar('TPattern_co', bound=Pattern, covariant=True) +""" +Type variable for :class:`.Pattern` that is covariant. This is used by +:class:`pathspec.pathspec.PathSpec` to specialize the type of patterns. +""" + +TStrPath = TypeVar('TStrPath', bound=StrPath) """ Type variable for :class:`str` or :class:`os.PathLike`. """ NORMALIZE_PATH_SEPS = [ - __sep + cast(str, __sep) for __sep in [os.sep, os.altsep] if __sep and __sep != posixpath.sep ] @@ -53,7 +61,7 @@ :data:`os.altsep`. """ -_registered_patterns = {} +_registered_patterns: dict[str, Callable[[Union[str, bytes]], Pattern]] = {} """ *_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the registered pattern factory (:class:`~collections.abc.Callable`). @@ -125,7 +133,7 @@ def detailed_match_files( patterns: Iterable[Pattern], files: Iterable[str], all_matches: Optional[bool] = None, -) -> dict[str, 'MatchDetail']: +) -> dict[str, MatchDetail]: """ Matches the files to the patterns, and returns which patterns matched the files. @@ -144,7 +152,7 @@ def detailed_match_files( (:class:`str`) to the patterns that matched in order (:class:`.MatchDetail`). """ all_files = files if isinstance(files, Collection) else list(files) - return_files = {} + return_files: dict[str, MatchDetail] = {} for pattern in patterns: if pattern.include is not None: result_files = pattern.match(all_files) # TODO: Replace with `.match_file()`. @@ -152,9 +160,9 @@ def detailed_match_files( # Add files and record pattern. for result_file in result_files: if result_file in return_files: - # We know here that .patterns is a list, becasue we made it here + # We know here that .patterns is a list, because we made it here if all_matches: - return_files[result_file].patterns.append(pattern) # type: ignore[attr-defined] + return_files[result_file].patterns.append(pattern) # type: ignore[attr-defined] else: return_files[result_file].patterns[0] = pattern # type: ignore[index] else: @@ -247,7 +255,7 @@ def _iter_tree_entries_next( root_full: str, dir_rel: str, memo: dict[str, str], - on_error: Callable[[OSError], None], + on_error: Optional[Callable[[OSError], None]], follow_links: bool, ) -> Iterator['TreeEntry']: """ @@ -360,7 +368,7 @@ def _iter_tree_files_next( root_full: str, dir_rel: str, memo: dict[str, str], - on_error: Callable[[OSError], None], + on_error: Optional[Callable[[OSError], None]], follow_links: bool, ) -> Iterator[str]: """ @@ -420,14 +428,14 @@ def _iter_tree_files_next( def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]: """ - Lookups a registered pattern factory by name. + Looks up a registered pattern factory by name. *name* (:class:`str`) is the name of the pattern factory. Returns the registered pattern factory (:class:`~collections.abc.Callable`). If no pattern factory is registered, raises :exc:`KeyError`. """ - return _registered_patterns[name] + return _registered_patterns[name] # type: ignore[return-value] def match_file(patterns: Iterable[Pattern], file: str) -> bool: @@ -506,6 +514,8 @@ def normalize_file( if separators is None: separators = NORMALIZE_PATH_SEPS + assert separators is not None, separators + # Convert path object to string. norm_file: str = os.fspath(file) @@ -549,7 +559,7 @@ def normalize_files( the original file paths (:class:`list` of :class:`str` or :class:`os.PathLike`). """ - norm_files = {} + norm_files: dict[str, list[StrPath]] = {} for path in files: norm_file = normalize_file(path, separators=separators) if norm_file in norm_files: @@ -562,7 +572,7 @@ def normalize_files( def register_pattern( name: str, - pattern_factory: Callable[[AnyStr], Pattern], + pattern_factory: Union[Callable[[Union[str, bytes]], Pattern], type[Pattern]], override: Optional[bool] = None, ) -> None: """ @@ -580,15 +590,15 @@ def register_pattern( is :data:`None` for :data:`False`. """ if not isinstance(name, str): - raise TypeError(f"name:{name!r} is not a string.") + raise TypeError(f"{name=!r} is not a string.") if not callable(pattern_factory): - raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") + raise TypeError(f"{pattern_factory=!r} is not callable.") if name in _registered_patterns and not override: raise AlreadyRegisteredError(name, _registered_patterns[name]) - _registered_patterns[name] = pattern_factory + _registered_patterns[name] = pattern_factory # type: ignore class AlreadyRegisteredError(Exception): @@ -600,7 +610,7 @@ class AlreadyRegisteredError(Exception): def __init__( self, name: str, - pattern_factory: Callable[[AnyStr], Pattern], + pattern_factory: Callable[[Union[str, bytes]], Pattern], ) -> None: """ Initializes the :exc:`AlreadyRegisteredError` instance. @@ -630,7 +640,7 @@ def name(self) -> str: return self.args[0] @property - def pattern_factory(self) -> Callable[[AnyStr], Pattern]: + def pattern_factory(self) -> Callable[[Union[str, bytes]], Pattern]: """ *pattern_factory* (:class:`~collections.abc.Callable`) is the registered pattern factory. diff --git a/tests/check_usage.py b/tests/check_usage.py new file mode 100644 index 0000000..a035c94 --- /dev/null +++ b/tests/check_usage.py @@ -0,0 +1,49 @@ +# pyright: strict + +from pathspec import ( + PathSpec, + GitIgnoreSpec) +from pathspec.patterns.gitignore.basic import ( + GitIgnoreBasicPattern) + + +def check_gi_1(): + spec = GitIgnoreSpec.from_lines(['**']) + return spec + + +def check_gi_2(): + pattern = 'gitignore' + spec = GitIgnoreSpec.from_lines(pattern, ['**']) + return spec + + +def check_gi_3(): + pattern = 'gitignore' + spec = GitIgnoreSpec.from_lines(['**'], pattern) + return spec + + +def check_ps_1(): + spec = PathSpec.from_lines('gitignore', ['**']) + return spec + + +def check_ps_2(): + pat_type = 'gitignore' + spec = PathSpec.from_lines(pat_type, ['**']) + return spec + + +def check_ps_3(): + spec = PathSpec.from_lines(GitIgnoreBasicPattern, ['**']) + return spec + + +def check_ps_4(): + from typing import AnyStr + def pattern_factory(pattern: AnyStr) -> GitIgnoreBasicPattern: + return GitIgnoreBasicPattern(pattern) + + spec = PathSpec.from_lines(pattern_factory, ['**']) + return spec diff --git a/tests/test_05_pathspec.py b/tests/test_05_pathspec.py index 7685088..08a1310 100644 --- a/tests/test_05_pathspec.py +++ b/tests/test_05_pathspec.py @@ -593,7 +593,7 @@ def test_02_ne(self): ], backend='simple') self.assertNotEqual(first_spec, second_spec) - def test_03_add(self): + def test_03_add_1_simple(self): """ Test spec addition using :data:`+` operator. """ @@ -624,7 +624,38 @@ def test_03_add(self): 'test.txt', }, debug) - def test_03_iadd(self): + def test_03_add_2_mixed(self): + """ + Test spec addition using :data:`+` operator with mixed sequence types. + """ + first_spec = PathSpec(tuple(map(GitIgnoreBasicPattern, [ + 'test.png', + 'test.txt', + ])), backend='simple') + second_spec = PathSpec(list(map(GitIgnoreBasicPattern, [ + 'test.html', + 'test.jpg', + ])), backend='simple') + combined_spec = first_spec + second_spec + files = { + 'test.html', + 'test.jpg', + 'test.png', + 'test.txt', + } + + results = list(combined_spec.check_files(files)) + includes = get_includes(results) + debug = debug_results(combined_spec, results) + + self.assertEqual(includes, { + 'test.html', + 'test.jpg', + 'test.png', + 'test.txt', + }, debug) + + def test_03_iadd_1_simple(self): """ Test spec addition using :data:`+=` operator. """ @@ -654,6 +685,36 @@ def test_03_iadd(self): 'test.txt', }, debug) + def test_03_iadd_2_mixed(self): + """ + Test spec addition using :data:`+=` operator with mixed sequence types. + """ + spec = PathSpec(tuple(map(GitIgnoreBasicPattern, [ + 'test.png', + 'test.txt', + ])), backend='simple') + spec += PathSpec(list(map(GitIgnoreBasicPattern, [ + 'test.html', + 'test.jpg', + ])), backend='simple') + files = { + 'test.html', + 'test.jpg', + 'test.png', + 'test.txt', + } + + results = list(spec.check_files(files)) + includes = get_includes(results) + debug = debug_results(spec, results) + + self.assertEqual(includes, { + 'test.html', + 'test.jpg', + 'test.png', + 'test.txt', + }, debug) + def test_04_len(self): """ Test spec length. From ecf71a99ca739479d450b9830f43416ea0c519c7 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:35:49 -0400 Subject: [PATCH 56/67] Release v1.1.1 --- CHANGES.rst | 19 ++++++++++++++++++- CHANGES_1.in.rst | 4 ++-- README-dist.rst | 19 ++++++++++++++++++- pathspec/_version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fa18abb..8e69760 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,27 @@ Change History ============== +1.1.1 (2026-04-26) +------------------ + +Improvements: + +- Improved type checking with mypy and pyright. + +Bug fixes: + +- Fixed typing on `PathSpec[TPattern]` to `PathSpec[TPattern_co]`. +- Added missing variant type-hint `type[Pattern]` to `PathSpec.from_lines()` parameter `pattern_factory`. +- Fixed possible type error when using `+` and `+=` operators on `PathSpec`. + + 1.1.0 (2026-04-22) ------------------ +New features: + +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. + Bug fixes: - `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. @@ -12,7 +30,6 @@ Bug fixes: Improvements: -- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. - `Pull #110`_: Nicer debug print outs (and str for regex pattern). diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 0572dd0..38ba72d 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,6 +1,6 @@ -1.1.1 (TDB) ------------ +1.1.1 (2026-04-26) +------------------ Improvements: diff --git a/README-dist.rst b/README-dist.rst index 7b05e6a..5826746 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -227,9 +227,27 @@ Change History ============== +1.1.1 (2026-04-26) +------------------ + +Improvements: + +- Improved type checking with mypy and pyright. + +Bug fixes: + +- Fixed typing on `PathSpec[TPattern]` to `PathSpec[TPattern_co]`. +- Added missing variant type-hint `type[Pattern]` to `PathSpec.from_lines()` parameter `pattern_factory`. +- Fixed possible type error when using `+` and `+=` operators on `PathSpec`. + + 1.1.0 (2026-04-22) ------------------ +New features: + +- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. + Bug fixes: - `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git. @@ -237,7 +255,6 @@ Bug fixes: Improvements: -- `Issue #108`_: Specialize pattern type for `PathSpec` as `PathSpec[TPattern]` for better debugging of `PathSpec().patterns`. - `Pull #110`_: Nicer debug print outs (and str for regex pattern). diff --git a/pathspec/_version.py b/pathspec/_version.py index fd2f0b5..2e5c07a 100644 --- a/pathspec/_version.py +++ b/pathspec/_version.py @@ -2,4 +2,4 @@ This module defines the version. """ -__version__ = "1.1.0" +__version__ = "1.1.1" diff --git a/pyproject.toml b/pyproject.toml index 321c133..ce256ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" requires-python = ">=3.9" -version = "1.1.0" +version = "1.1.1" [project.optional-dependencies] hyperscan = [ From b5a84cf71faf3705bb84ff66070640d8f2ab059e Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Tue, 28 Apr 2026 22:23:40 -0400 Subject: [PATCH 57/67] Check Python version for backports --- pathspec/_typing.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pathspec/_typing.py b/pathspec/_typing.py index ffb4867..bc28128 100644 --- a/pathspec/_typing.py +++ b/pathspec/_typing.py @@ -8,30 +8,36 @@ """ import functools +import sys import warnings from typing import ( Any, Callable, # Replaced by `collections.abc.Callable` in 3.9.2. Optional, # Replaced by `X | None` in 3.10. TypeVar) -try: - from typing import AnyStr # Removed in 3.18. -except ImportError: - AnyStr = TypeVar('AnyStr', str, bytes) # type: ignore[misc] -try: - from typing import Never # Added in 3.11. -except ImportError: - from typing import NoReturn as Never F = TypeVar('F', bound=Callable[..., Any]) -try: - from warnings import deprecated # Added in 3.13. # type: ignore -except ImportError: +# AnyStr is deprecated since 3.13, and will be removed in 3.18. +if sys.version_info >= (3, 18): + AnyStr = TypeVar('AnyStr', str, bytes) +else: + from typing import AnyStr + +# Never was added in 3.11. +if sys.version_info >= (3, 11): + from typing import Never +else: + from typing import NoReturn as Never + +# deprecated was added in 3.13. +if sys.version_info >= (3, 13): + from warnings import deprecated +else: try: - from typing_extensions import deprecated # type: ignore + from typing_extensions import deprecated except ImportError: - def deprecated( # type: ignore[no-redef] + def deprecated( message: str, /, *, category: Optional[type[Warning]] = DeprecationWarning, @@ -42,12 +48,13 @@ def decorator(f: F) -> F: def wrapper(*a, **k): warnings.warn(message, category=category, stacklevel=stacklevel+1) return f(*a, **k) - return wrapper # type: ignore[return-value] + return wrapper return decorator -try: - from typing import override # Added in 3.12. -except ImportError: +# override was added in 3.12. +if sys.version_info >= (3, 12): + from typing import override +else: try: from typing_extensions import override except ImportError: From ceaa41af724a98ea50f194381e664fd6e81ca956 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:17:00 -0400 Subject: [PATCH 58/67] CI deps --- ...blish-to-pypi.yml => publish-to-pypi.yaml} | 11 +++-- ...-testpypi.yml => publish-to-testpypi.yaml} | 11 +++-- .github/workflows/test-all.yaml | 48 ++++++++++--------- .github/workflows/test-quick.yaml | 18 +++---- 4 files changed, 47 insertions(+), 41 deletions(-) rename .github/workflows/{publish-to-pypi.yml => publish-to-pypi.yaml} (74%) rename .github/workflows/{publish-to-testpypi.yml => publish-to-testpypi.yaml} (78%) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yaml similarity index 74% rename from .github/workflows/publish-to-pypi.yml rename to .github/workflows/publish-to-pypi.yaml index 7d544c7..d9a1fb2 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yaml @@ -14,14 +14,15 @@ jobs: with: persist-credentials: false - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. with: egress-rules: .github/egress-build-rules.yaml - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: - python-version: "3.x" + cache: pip + python-version: '3.x' - name: Install pypa/build run: python3 -m pip install --user build @@ -30,12 +31,12 @@ jobs: run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 from 2026-02-26. + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 from 2026-04-10. with: name: python-package-distributions path: dist/ - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action/summary@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: always() publish-to-pypi: @@ -57,6 +58,6 @@ jobs: path: dist/ - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. with: print-hash: true diff --git a/.github/workflows/publish-to-testpypi.yml b/.github/workflows/publish-to-testpypi.yaml similarity index 78% rename from .github/workflows/publish-to-testpypi.yml rename to .github/workflows/publish-to-testpypi.yaml index a27d128..3780d6b 100644 --- a/.github/workflows/publish-to-testpypi.yml +++ b/.github/workflows/publish-to-testpypi.yaml @@ -14,7 +14,7 @@ jobs: with: persist-credentials: false - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. with: egress-rules: .github/egress-build-rules.yaml @@ -27,7 +27,8 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: - python-version: "3.x" + cache: pip + python-version: '3.x' - name: Install pypa/build run: python3 -m pip install --user build packaging @@ -39,12 +40,12 @@ jobs: run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 from 2026-02-26. + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 from 2026-04-10. with: name: python-package-distributions path: dist/ - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action/summary@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: always() publish-to-testpypi: @@ -66,7 +67,7 @@ jobs: path: dist/ - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. with: repository-url: https://test.pypi.org/legacy/ print-hash: true diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index df2c5d8..593a2a9 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -23,15 +23,15 @@ jobs: - macos - windows python: - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "3.14" - - "pypy-3.9" - - "pypy-3.10" - - "pypy-3.11" + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' + - 'pypy-3.9' + - 'pypy-3.10' + - 'pypy-3.11' backend: - base - hyperscan @@ -39,33 +39,33 @@ jobs: exclude: # Hyperscan does hot have wheels for Python 3.9 on Windows. - os: windows - python: "3.9" + python: '3.9' backend: hyperscan # Hyperscan does not have wheels for PyPy 3.9 and 3.11. - - python: "pypy-3.9" + - python: 'pypy-3.9' backend: hyperscan - - python: "pypy-3.11" + - python: 'pypy-3.11' backend: hyperscan # Hyperscan does hot have wheels for PyPy 3.10 on Mac. - os: macos - python: "pypy-3.10" + python: 'pypy-3.10' backend: hyperscan # Hyperscan does hot have wheels for PyPy 3.10 on Windows. - os: windows - python: "pypy-3.10" + python: 'pypy-3.10' backend: hyperscan # Re2 does hot have wheels for PyPy. - - python: "pypy-3.9" + - python: 'pypy-3.9' backend: re2 - - python: "pypy-3.10" + - python: 'pypy-3.10' backend: re2 - - python: "pypy-3.11" + - python: 'pypy-3.11' backend: re2 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: matrix.os == 'ubuntu' with: egress-rules: .github/egress-build-rules.yaml @@ -73,8 +73,9 @@ jobs: - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: - python-version: ${{ matrix.python }} allow-prereleases: true + cache: pip + python-version: ${{ matrix.python }} - name: Install tox run: python -m pip install tox @@ -87,7 +88,7 @@ jobs: if: matrix.python == '3.9' || matrix.python == 'pypy-3.9' run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action/summary@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: always() && matrix.os == 'ubuntu' docs: @@ -98,14 +99,15 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. with: egress-rules: .github/egress-build-rules.yaml - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: - python-version: "3.13" + cache: pip + python-version: '3.13' - name: Install tox run: python -m pip install tox @@ -113,5 +115,5 @@ jobs: - name: Run tests run: python -m tox -e docs - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action/summary@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: always() diff --git a/.github/workflows/test-quick.yaml b/.github/workflows/test-quick.yaml index c0d541f..b9ca76b 100644 --- a/.github/workflows/test-quick.yaml +++ b/.github/workflows/test-quick.yaml @@ -21,15 +21,15 @@ jobs: os: - ubuntu python: - - "3.9" - - "3.14" + - '3.9' + - '3.14' backend: - base steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: matrix.os == 'ubuntu' with: egress-rules: .github/egress-build-rules.yaml @@ -37,8 +37,9 @@ jobs: - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: - python-version: ${{ matrix.python }} allow-prereleases: true + cache: pip + python-version: ${{ matrix.python }} - name: Install tox run: python -m pip install tox @@ -51,7 +52,7 @@ jobs: if: matrix.python == '3.9' run: python -m tox -c tox-ci-py39.ini -e ci-${{ matrix.backend }} -- --verbose - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action/summary@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: always() && matrix.os == 'ubuntu' docs: @@ -62,14 +63,15 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 from 2026-01-09. - - uses: ironsh/iron-proxy-action@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. with: egress-rules: .github/egress-build-rules.yaml - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 from 2026-01-21. with: - python-version: "3.13" + cache: pip + python-version: '3.13' - name: Install tox run: python -m pip install tox @@ -77,5 +79,5 @@ jobs: - name: Run tests run: python -m tox -e docs - - uses: ironsh/iron-proxy-action/summary@67ae2cdb5cc549c5cb94e76235953f4a9fcb183c # v0.1.3 from 2026-04-07. + - uses: ironsh/iron-proxy-action/summary@fa1fd82ec32396d044deba7040a6db576bec927a # v0.1.4 from 2026-04-28. if: always() From 909ce8ae5f5892adced3eb2517aa25b0a105cc93 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:20:41 -0400 Subject: [PATCH 59/67] Revert pypa/gh-action-pypi-publish due to failure --- .github/workflows/publish-to-pypi.yaml | 2 +- .github/workflows/publish-to-testpypi.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yaml b/.github/workflows/publish-to-pypi.yaml index d9a1fb2..e44abe6 100644 --- a/.github/workflows/publish-to-pypi.yaml +++ b/.github/workflows/publish-to-pypi.yaml @@ -58,6 +58,6 @@ jobs: path: dist/ - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: print-hash: true diff --git a/.github/workflows/publish-to-testpypi.yaml b/.github/workflows/publish-to-testpypi.yaml index 3780d6b..99c61d1 100644 --- a/.github/workflows/publish-to-testpypi.yaml +++ b/.github/workflows/publish-to-testpypi.yaml @@ -67,7 +67,7 @@ jobs: path: dist/ - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: repository-url: https://test.pypi.org/legacy/ print-hash: true From b04961dddea7a833dd827f262210146d920d1f5a Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:31:18 -0400 Subject: [PATCH 60/67] CI deps --- .github/workflows/publish-to-pypi.yaml | 2 +- .github/workflows/publish-to-testpypi.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yaml b/.github/workflows/publish-to-pypi.yaml index e44abe6..d9a1fb2 100644 --- a/.github/workflows/publish-to-pypi.yaml +++ b/.github/workflows/publish-to-pypi.yaml @@ -58,6 +58,6 @@ jobs: path: dist/ - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. with: print-hash: true diff --git a/.github/workflows/publish-to-testpypi.yaml b/.github/workflows/publish-to-testpypi.yaml index 99c61d1..3780d6b 100644 --- a/.github/workflows/publish-to-testpypi.yaml +++ b/.github/workflows/publish-to-testpypi.yaml @@ -67,7 +67,7 @@ jobs: path: dist/ - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. with: repository-url: https://test.pypi.org/legacy/ print-hash: true From 2d6020ec42fc5495b3c32fcbde0985378243e469 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 24 May 2026 17:12:08 -0400 Subject: [PATCH 61/67] Licensing --- LICENSE-MIT | 7 +++++++ LICENSE => LICENSE-MPL-2.0 | 0 LICENSING.md | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 LICENSE-MIT rename LICENSE => LICENSE-MPL-2.0 (100%) create mode 100644 LICENSING.md diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..e6ecec2 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright 2013-2026 Caleb P. Burns + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE b/LICENSE-MPL-2.0 similarity index 100% rename from LICENSE rename to LICENSE-MPL-2.0 diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..71fbd6a --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,17 @@ +# Licensing + +*pathspec* was originally licensed under the Mozilla Public License 2.0 only (see LICENSE-MPL-2.0). It is currently being relicensed. New code is available +under both licenses (see LICENSE-MIT and LICENSE-MPL-2.0). + +## New Code (on or after May 24, 2025) + +All contributions made on or after May 24, 2025 are dual-licensed under: +- MIT License (see LICENSE-MIT). +- Mozilla Public License 2.0 (see LICENSE-MPL-2.0). + +You may use this code under the terms of either license. + +## Original Code (before May 24, 2025) + +All code contributed before May 24, 2025 is licensed under the +Mozilla Public License 2.0 only (see LICENSE-MPL-2.0). From 921e661f704db88960c8d913f7ccb92e878ec208 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sun, 24 May 2026 19:27:10 -0400 Subject: [PATCH 62/67] Licensing --- CHANGES.rst | 11 +++++++++++ CHANGES_1.in.rst | 11 +++++++++++ LICENSING.md | 13 ++++++------- MANIFEST.in | 2 +- README-dist.rst | 17 ++++++++++++++--- README.rst | 6 +++--- justfile | 2 +- pyproject.in.toml | 4 +++- pyproject.toml | 4 +++- 9 files changed, 53 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8e69760..f599b09 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,17 @@ Change History ============== +1.2.0 (TBD) +----------- + +Major changes: + +- TODO `Issue #116`_: Change license from MPL-2.0 to dual MIT and MPL-2.0. + + +.. _`Issue #116`: https://github.com/cpburnz/python-pathspec/issues/116 + + 1.1.1 (2026-04-26) ------------------ diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 38ba72d..cf41641 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -1,4 +1,15 @@ +1.2.0 (TBD) +----------- + +Major changes: + +- TODO `Issue #116`_: Change license from MPL-2.0 to dual MIT and MPL-2.0. + + +.. _`Issue #116`: https://github.com/cpburnz/python-pathspec/issues/116 + + 1.1.1 (2026-04-26) ------------------ diff --git a/LICENSING.md b/LICENSING.md index 71fbd6a..8bd8e05 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -1,17 +1,16 @@ # Licensing -*pathspec* was originally licensed under the Mozilla Public License 2.0 only (see LICENSE-MPL-2.0). It is currently being relicensed. New code is available +*pathspec* was originally licensed under the Mozilla Public License 2.0 only (see LICENSE-MPL-2.0). It is being relicensed to MIT. New code is available under both licenses (see LICENSE-MIT and LICENSE-MPL-2.0). +## Original Code (before May 24, 2025) + +All code committed before May 24, 2025 is licensed under the Mozilla Public License 2.0 only (see LICENSE-MPL-2.0). + ## New Code (on or after May 24, 2025) -All contributions made on or after May 24, 2025 are dual-licensed under: +All code committed on or after May 24, 2025 is dual-licensed under: - MIT License (see LICENSE-MIT). - Mozilla Public License 2.0 (see LICENSE-MPL-2.0). You may use this code under the terms of either license. - -## Original Code (before May 24, 2025) - -All code contributed before May 24, 2025 is licensed under the -Mozilla Public License 2.0 only (see LICENSE-MPL-2.0). diff --git a/MANIFEST.in b/MANIFEST.in index b25d4ff..935ba8f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,7 +6,7 @@ include *.py include *.rst include *.toml include pathspec/py.typed -include LICENSE +include LICENSE-* recursive-include benchmarks * recursive-include doc * recursive-include tests * diff --git a/README-dist.rst b/README-dist.rst index 5826746..39c3972 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -145,8 +145,7 @@ the files to keep, and exclude files like *.gitignore*, you need to set License ------- -*pathspec* is licensed under the `Mozilla Public License Version 2.0`_. See -`LICENSE`_ or the `FAQ`_ for more information. +*pathspec* is currently licensed under the `Mozilla Public License Version 2.0`_. See `LICENSING.md`_, `LICENSE-MPL-2.0`_ or the `FAQ`_ for more information. In summary, you may use *pathspec* with any closed or open source project without affecting the license of the larger work so long as you: @@ -156,7 +155,8 @@ without affecting the license of the larger work so long as you: - and release any custom changes made to *pathspec*. .. _`Mozilla Public License Version 2.0`: http://www.mozilla.org/MPL/2.0 -.. _`LICENSE`: LICENSE +.. _`LICENSE-MPL-2.0`: LICENSE-MPL-2.0 +.. _`LICENSING.md`: LICENSING.md .. _`FAQ`: http://www.mozilla.org/MPL/2.0/FAQ.html @@ -227,6 +227,17 @@ Change History ============== +1.2.0 (TBD) +----------- + +Major changes: + +- TODO `Issue #116`_: Change license from MPL-2.0 to dual MIT and MPL-2.0. + + +.. _`Issue #116`: https://github.com/cpburnz/python-pathspec/issues/116 + + 1.1.1 (2026-04-26) ------------------ diff --git a/README.rst b/README.rst index 4c12d96..9824fb9 100644 --- a/README.rst +++ b/README.rst @@ -145,8 +145,7 @@ the files to keep, and exclude files like *.gitignore*, you need to set License ------- -*pathspec* is licensed under the `Mozilla Public License Version 2.0`_. See -`LICENSE`_ or the `FAQ`_ for more information. +*pathspec* is currently licensed under the `Mozilla Public License Version 2.0`_. See `LICENSING.md`_, `LICENSE-MPL-2.0`_ or the `FAQ`_ for more information. In summary, you may use *pathspec* with any closed or open source project without affecting the license of the larger work so long as you: @@ -156,7 +155,8 @@ without affecting the license of the larger work so long as you: - and release any custom changes made to *pathspec*. .. _`Mozilla Public License Version 2.0`: http://www.mozilla.org/MPL/2.0 -.. _`LICENSE`: LICENSE +.. _`LICENSE-MPL-2.0`: LICENSE-MPL-2.0 +.. _`LICENSING.md`: LICENSING.md .. _`FAQ`: http://www.mozilla.org/MPL/2.0/FAQ.html diff --git a/justfile b/justfile index 990d1fa..28174bf 100644 --- a/justfile +++ b/justfile @@ -137,7 +137,7 @@ _venv_pypy_update: ################################################################################ _dist_build: _dist_prebuild - find ./dist -type f -delete + if [ -d ./dist ]; then find ./dist -type f -delete; fi {{cpy_run}} python -m build _dist_prebuild: diff --git a/pyproject.in.toml b/pyproject.in.toml index 9748b40..16ca0e6 100644 --- a/pyproject.in.toml +++ b/pyproject.in.toml @@ -25,6 +25,8 @@ classifiers = [ "Topic :: Utilities", ] description = "Utility library for gitignore style pattern matching of file paths." +# TODO: Once fully relicensed, use: +#license = {text = "MIT OR MPL-2.0"} license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" @@ -65,7 +67,7 @@ include = [ "*.py", "*.rst", "*.toml", - "LICENSE", + "LICENSE-*", "benchmarks/", "doc/", "tests/", diff --git a/pyproject.toml b/pyproject.toml index ce256ea..160d73d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ classifiers = [ "Topic :: Utilities", ] description = "Utility library for gitignore style pattern matching of file paths." +# TODO: Once fully relicensed, use: +#license = {text = "MIT OR MPL-2.0"} license = {text = "MPL 2.0"} name = "pathspec" readme = "README-dist.rst" @@ -66,7 +68,7 @@ include = [ "*.py", "*.rst", "*.toml", - "LICENSE", + "LICENSE-*", "benchmarks/", "doc/", "tests/", From 71320dafb7e1c772cff407b0e0692f4439df61dc Mon Sep 17 00:00:00 2001 From: Guillermo Date: Mon, 25 May 2026 13:15:29 +0200 Subject: [PATCH 63/67] fix: ignore invalid gitignore bracket ranges --- pathspec/patterns/gitignore/base.py | 9 +++++++++ tests/test_04_gitignore_spec.py | 8 +++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pathspec/patterns/gitignore/base.py b/pathspec/patterns/gitignore/base.py index 6cb86b4..5c0a4d9 100644 --- a/pathspec/patterns/gitignore/base.py +++ b/pathspec/patterns/gitignore/base.py @@ -161,6 +161,15 @@ def _translate_segment_glob( # as literal slashes by regex as defined by POSIX. expr += pattern[i:j].replace('\\', '\\\\') + if range_error == 'raise': + try: + re.compile(expr) + except re.error as e: + raise _RangeError(( + f"Invalid range notation={pattern[i:j]!r} found in " + f"pattern={pattern!r}." + )) from e + # Add regex bracket expression to regex result. regex += expr diff --git a/tests/test_04_gitignore_spec.py b/tests/test_04_gitignore_spec.py index 2d30333..9b4ae36 100644 --- a/tests/test_04_gitignore_spec.py +++ b/tests/test_04_gitignore_spec.py @@ -944,16 +944,14 @@ def test_15_issue_93_c_2_invalid(self): self.assertIs(pattern.include, None) self.assertIs(pattern.regex, None) - # The `re` module fails to compile these. - # - NOTE: Technically, these should result in null patterns rather than - # exceptions to fully replicate Git's behavior. for raw_pattern in [ '[z-a]', 'a[z-a]', ]: with self.subTest(f"p={raw_pattern!r}"): - with self.assertRaises(re_PatternError): - GitIgnoreSpecPattern(raw_pattern) + pattern = GitIgnoreSpecPattern(raw_pattern) + self.assertIs(pattern.include, None) + self.assertIs(pattern.regex, None) def test_15_issue_93_c_3_unclosed(self): """ From 4b623ea3be20a1223c2ba5ce19190cde2e2f7b35 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Sat, 30 May 2026 22:46:42 -0400 Subject: [PATCH 64/67] Misc --- CHANGES.rst | 5 +++++ CHANGES_1.in.rst | 5 +++++ README-dist.rst | 5 +++++ pathspec/_meta.py | 1 + tests/test_04_gitignore_spec.py | 9 +-------- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f599b09..f47bc6e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,8 +9,13 @@ Major changes: - TODO `Issue #116`_: Change license from MPL-2.0 to dual MIT and MPL-2.0. +Bug fixes: + +- `Pull #123`_: Ignore invalid gitignore bracket ranges for `GitIgnoreSpec`. + .. _`Issue #116`: https://github.com/cpburnz/python-pathspec/issues/116 +.. _`Pull #123`: https://github.com/cpburnz/python-pathspec/pull/123 1.1.1 (2026-04-26) diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index cf41641..540225a 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -6,8 +6,13 @@ Major changes: - TODO `Issue #116`_: Change license from MPL-2.0 to dual MIT and MPL-2.0. +Bug fixes: + +- `Pull #123`_: Ignore invalid gitignore bracket ranges for `GitIgnoreSpec`. + .. _`Issue #116`: https://github.com/cpburnz/python-pathspec/issues/116 +.. _`Pull #123`: https://github.com/cpburnz/python-pathspec/pull/123 1.1.1 (2026-04-26) diff --git a/README-dist.rst b/README-dist.rst index 39c3972..7dcca83 100644 --- a/README-dist.rst +++ b/README-dist.rst @@ -234,8 +234,13 @@ Major changes: - TODO `Issue #116`_: Change license from MPL-2.0 to dual MIT and MPL-2.0. +Bug fixes: + +- `Pull #123`_: Ignore invalid gitignore bracket ranges for `GitIgnoreSpec`. + .. _`Issue #116`: https://github.com/cpburnz/python-pathspec/issues/116 +.. _`Pull #123`: https://github.com/cpburnz/python-pathspec/pull/123 1.1.1 (2026-04-26) diff --git a/pathspec/_meta.py b/pathspec/_meta.py index 4fe8653..2be42f0 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -66,5 +66,6 @@ "Kadir Can Ozden ", "Henry Schreiner ", "Yilei ", + "Guillermo Garcia ", ] __license__ = "MPL 2.0" diff --git a/tests/test_04_gitignore_spec.py b/tests/test_04_gitignore_spec.py index 9b4ae36..4b525f0 100644 --- a/tests/test_04_gitignore_spec.py +++ b/tests/test_04_gitignore_spec.py @@ -936,16 +936,9 @@ def test_15_issue_93_c_2_invalid(self): for raw_pattern in [ '[!]', '[^]', + '[z-a]', 'a[!]', 'a[^]', - ]: - with self.subTest(f"p={raw_pattern!r}"): - pattern = GitIgnoreSpecPattern(raw_pattern) - self.assertIs(pattern.include, None) - self.assertIs(pattern.regex, None) - - for raw_pattern in [ - '[z-a]', 'a[z-a]', ]: with self.subTest(f"p={raw_pattern!r}"): From 13efefc4d36ff56e6d08c8921f73666d884c18b9 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:07:35 -0400 Subject: [PATCH 65/67] TestPyPI --- .github/workflows/publish-to-testpypi.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-testpypi.yaml b/.github/workflows/publish-to-testpypi.yaml index 3780d6b..445dc8b 100644 --- a/.github/workflows/publish-to-testpypi.yaml +++ b/.github/workflows/publish-to-testpypi.yaml @@ -67,7 +67,8 @@ jobs: path: dist/ - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. + #uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. with: repository-url: https://test.pypi.org/legacy/ print-hash: true From f142e0abdb584afa108dd66fab217c37c75c22da Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:18:59 -0400 Subject: [PATCH 66/67] TestPyPI --- .github/workflows/publish-to-testpypi.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-testpypi.yaml b/.github/workflows/publish-to-testpypi.yaml index 445dc8b..3780d6b 100644 --- a/.github/workflows/publish-to-testpypi.yaml +++ b/.github/workflows/publish-to-testpypi.yaml @@ -67,8 +67,7 @@ jobs: path: dist/ - name: Publish distribution to TestPyPI - #uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 from 2025-09-03. + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 from 2026-04-07. with: repository-url: https://test.pypi.org/legacy/ print-hash: true From 6568072c2703c72796cd02467feb924540157c92 Mon Sep 17 00:00:00 2001 From: Caleb Burns <2126043+cpburnz@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:27:27 -0400 Subject: [PATCH 67/67] SECURITY.md --- SECURITY.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1408cdf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Reporting a Vulnerability + +Contact information is available on PyPI. + +## Scope + +The following are not considered vulnerabilities in pathspec: + +- *ReDoS / regular expression complexity*: pathspec is a pattern-matching library, not a sandboxed input parser. + The regex complexity is proportional to that of the pattern. + Applications that expose pattern input to untrusted users are responsible for sanitizing that input. + +- Any actual vulnerabilities discovered in Python's re module, hyperscan, or google-re2 should be reported to the maintainers of those libraries.