From bc63b027e2fc737e2ec0222bfa764e1aa76a427f Mon Sep 17 00:00:00 2001 From: Byron Date: Fri, 28 Aug 2026 08:37:50 +0200 Subject: [PATCH] fix: restore Actor.name_email_regex (#2220) GitPython 3.1.60 removed the Actor.name_email_regex class attribute while replacing internal actor parsing, which broke downstream consumers such as python-semantic-release. Bring back the historical compiled pattern for downstream compatibility, but emit DeprecationWarning whenever the class member is accessed. The warning explains its quadratic behavior on long malformed strings and points callers to the public Actor(name, email) constructor when fields are separate, or direct string parsing when they are not. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 # 252a84ac Add public Actor.from_string constructor (#2220) Add public Actor.from_string constructor (#2220) Promote the existing actor identity parser to Actor.from_string while retaining _from_string as a compatibility alias. Point the name_email_regex deprecation warning to the new public constructor or Actor(name, email). Validation: - actor-related test/test_actor.py and test/test_util.py cases: 12 passed - ruff check and format checks passed - basedpyright git/util.py: no errors Co-authored-by: Codex GPT-5 # dd2181d8 Address review feedback about Actor subclasses Address review feedback about Actor subclasses Review feedback: Actor.from_string() should preserve the class on which the public classmethod is called instead of always returning a base Actor. Construct through cls on both parsing paths and cover subclass construction directly. Validation: - actor-related test/test_actor.py and test/test_util.py cases: 13 passed - ruff check and format checks passed - basedpyright git/util.py: no errors # fcf1ecb1 Address review feedback about constructor typing Address review feedback about constructor typing Review feedback: Actor.from_string() constructs subclasses at runtime but its Actor return annotation prevents type checkers from preserving subclass-specific members. Use the project existing Python 3.7-compatible bound TypeVar pattern so the declared return type follows cls. Validation: - actor-related test/test_actor.py and test/test_util.py cases: 13 passed - ruff check and format checks passed - basedpyright git/util.py: no errors Co-authored-by: Codex GPT-5 --- git/util.py | 26 +++++++++++++++++++++++--- test/test_actor.py | 35 ++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/git/util.py b/git/util.py index b0593feea..a80e667c7 100644 --- a/git/util.py +++ b/git/util.py @@ -80,6 +80,7 @@ Sequence, Tuple, TYPE_CHECKING, + Type, TypeVar, Union, cast, @@ -108,6 +109,7 @@ T_IterableObj = TypeVar("T_IterableObj", bound=Union["IterableObj", "Has_id_attribute"], covariant=True) # So IterableList[Head] is subtype of IterableList[IterableObj]. +T_Actor = TypeVar("T_Actor", bound="Actor") _logger = logging.getLogger(__name__) @@ -853,11 +855,27 @@ def update(self, *args: Any, **kwargs: Any) -> None: self._callable(*args, **kwargs) +class _DeprecatedActorNameEmailRegex: + _pattern = re.compile(r"(.*) <(.*?)>") + + def __get__(self, _instance: Any, _owner: Any) -> Pattern[str]: + warnings.warn( + "Actor.name_email_regex is deprecated and will be removed in GitPython 4.0.0 because searching long " + "malformed strings with it can take quadratic time. Use Actor.from_string() to parse actor identities, " + "or Actor(name, email) when the fields are already separate.", + DeprecationWarning, + stacklevel=2, + ) + return self._pattern + + class Actor: """Actors hold information about a person acting on the repository. They can be committers and authors or anything with a name and an email as mentioned in the git log entries.""" + name_email_regex = _DeprecatedActorNameEmailRegex() + # ENVIRONMENT VARIABLES # These are read when creating new commits. env_author_name = "GIT_AUTHOR_NAME" @@ -891,7 +909,7 @@ def __repr__(self) -> str: return '">' % (self.name, self.email) @classmethod - def _from_string(cls, string: str) -> "Actor": + def from_string(cls: Type[T_Actor], string: str) -> T_Actor: """Create an :class:`Actor` from a string. :param string: @@ -906,10 +924,12 @@ def _from_string(cls, string: str) -> "Actor": left_bracket = line.find("<") right_bracket = line.find(">", left_bracket + 1) if left_bracket >= 0 and right_bracket >= 0: - return Actor(line[:left_bracket].rstrip(), line[left_bracket + 1 : right_bracket]) + return cls(line[:left_bracket].rstrip(), line[left_bracket + 1 : right_bracket]) # Assume the best and use the whole string as name. - return Actor(string, None) + return cls(string, None) + + _from_string = from_string @classmethod def _main_actor( diff --git a/test/test_actor.py b/test/test_actor.py index baf6545f1..68afb80d3 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -3,14 +3,15 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -from git import Actor +from unittest import mock +from git import Actor from test.lib import TestBase class TestActor(TestBase): def test_from_string_should_separate_name_and_email(self): - a = Actor._from_string("Michael Trier ") + a = Actor.from_string("Michael Trier ") self.assertEqual("Michael Trier", a.name) self.assertEqual("mtrier@example.com", a.email) @@ -23,18 +24,34 @@ def test_from_string_should_separate_name_and_email(self): assert len(m) == 1 def test_from_string_should_handle_just_name(self): - a = Actor._from_string("Michael Trier") + a = Actor.from_string("Michael Trier") self.assertEqual("Michael Trier", a.name) self.assertEqual(None, a.email) + def test_from_string_constructs_subclass(self): + class DerivedActor(Actor): + pass + + self.assertIsInstance(DerivedActor.from_string("name "), DerivedActor) + def test_from_string_handles_unterminated_email_without_regex_backtracking(self): value = "A" * 20_000 + " ") + + message = str(context.warning) + self.assertIn("Actor.from_string()", message) + self.assertIn("Actor(name, email)", message) + assert match is not None + self.assertEqual(match.groups(), ("Michael Trier", "mtrier@example.com")) + def test_from_string_does_not_parse_across_lines(self): - self.assertEqual(Actor._from_string("x \n y "), Actor("x", "a")) + self.assertEqual(Actor.from_string("x \n y "), Actor("x", "a")) def test_from_string_uses_git_delimiters(self): for value, expected in ( @@ -45,12 +62,12 @@ def test_from_string_uses_git_delimiters(self): ("Name ", Actor("Name email>", None)), ): - self.assertEqual(Actor._from_string(value), expected) + self.assertEqual(Actor.from_string(value), expected) def test_should_display_representation(self): - a = Actor._from_string("Michael Trier ") + a = Actor.from_string("Michael Trier ") self.assertEqual('">', repr(a)) def test_str_should_alias_name(self): - a = Actor._from_string("Michael Trier ") + a = Actor.from_string("Michael Trier ") self.assertEqual(a.name, str(a))