gitbolt 0.0.0.dev17__tar.gz → 0.0.0.dev19__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/PKG-INFO +40 -4
  2. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/README.md +39 -3
  3. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/pyproject.toml +1 -1
  4. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/__init__.py +1 -0
  5. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/base.py +119 -31
  6. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/impl/simple.py +7 -2
  7. gitbolt-0.0.0.dev19/src/gitbolt/subprocess/utils/__init__.py +6 -0
  8. gitbolt-0.0.0.dev19/src/gitbolt/subprocess/utils/models.py +95 -0
  9. gitbolt-0.0.0.dev19/src/gitbolt/subprocess/utils/session.py +562 -0
  10. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt.egg-info/PKG-INFO +40 -4
  11. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt.egg-info/SOURCES.txt +5 -3
  12. gitbolt-0.0.0.dev17/src/gitbolt/subprocess/session.py +0 -33
  13. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/LICENSE +0 -0
  14. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/setup.cfg +0 -0
  15. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/_internal_init.py +0 -0
  16. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/add.py +0 -0
  17. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/base.py +0 -0
  18. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/constants.py +0 -0
  19. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/exceptions.py +0 -0
  20. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/ls_tree.py +0 -0
  21. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/models.py +0 -0
  22. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/py.typed +0 -0
  23. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/pytest_plugin.py +0 -0
  24. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/__init__.py +0 -0
  25. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/_internal_init.py +0 -0
  26. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/add.py +0 -0
  27. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/constants.py +0 -0
  28. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/exceptions.py +0 -0
  29. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/impl/__init__.py +0 -0
  30. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/ls_tree.py +0 -0
  31. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/runner/__init__.py +0 -0
  32. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/runner/base.py +0 -0
  33. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/subprocess/runner/simple.py +0 -0
  34. /gitbolt-0.0.0.dev17/src/gitbolt/subprocess/utils.py → /gitbolt-0.0.0.dev19/src/gitbolt/subprocess/utils/cmd.py +0 -0
  35. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt/utils.py +0 -0
  36. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt.egg-info/dependency_links.txt +0 -0
  37. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt.egg-info/entry_points.txt +0 -0
  38. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt.egg-info/requires.txt +0 -0
  39. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev19}/src/gitbolt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitbolt
3
- Version: 0.0.0.dev17
3
+ Version: 0.0.0.dev19
4
4
  Summary: Fast, flexible and type-safe Git commands in Python.
5
5
  Author-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
6
6
  Maintainer-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
@@ -146,7 +146,7 @@ print(version_stdout)
146
146
 
147
147
  #### 🧑‍💻 Modular at the programmatic level
148
148
 
149
- Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
149
+ Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
150
150
  particular commands.
151
151
 
152
152
  ```python
@@ -327,7 +327,7 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
327
327
 
328
328
  At last, run unchecked commands in git.
329
329
 
330
- Introduced in `0.0.0.dev4` to
330
+ Introduced in `0.0.0.dev4` to
331
331
  - experiment.
332
332
  - have consistent interfaced commands run until all subcommands are provided by the library.
333
333
 
@@ -366,11 +366,47 @@ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
366
366
 
367
367
  Error handling and I/O management is left to the client/caller.
368
368
 
369
+ #### 🏃 Communicate with long-running git sessions
370
+
371
+ Introduced in `0.0.0.dev17` to:
372
+ - Make long-running git command sessions.
373
+ - Have reentrant sessions.
374
+ - Communicate with long-running git command session using their `stdin` and `stdout`.
375
+
376
+ Get long-running process sessions and communicate with them for batching and faster operations.
377
+
378
+ ```python
379
+ import gitbolt
380
+ from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_data, cat_file_tree_content, \
381
+ mktree_tree_make
382
+
383
+ git = gitbolt.get_git_command()
384
+ with git.session(
385
+ cat_file1=["cat-file", "--batch"],
386
+ cat_file2=["cat-file", "--batch"],
387
+ cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
388
+ mktree=["mktree", "--batch", "-z"]
389
+ ) as ses:
390
+ tree_data: list[bytes] = []
391
+ for mode_, sha_, name_ in cat_file_tree_data(ses.commands.cat_file2, b"HEAD^{tree}"):
392
+ print(mode_, sha_, name_)
393
+ for tree_entry in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
394
+ tree_data.append(tree_entry)
395
+ print("+" * 40)
396
+ print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
397
+ print("~" * 40)
398
+ with ses: # is reentrant, if you want
399
+ tree_hash = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_data)) # make tree
400
+ print("Obtained tree: ", tree_hash)
401
+
402
+ assert not ses.active # session no long active
403
+ ```
404
+
369
405
  #### 💻 Run commands received from CLI
370
406
 
371
407
  Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
372
408
 
373
- While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
409
+ While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
374
410
  would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
375
411
  actually running them. An example:
376
412
 
@@ -110,7 +110,7 @@ print(version_stdout)
110
110
 
111
111
  #### 🧑‍💻 Modular at the programmatic level
112
112
 
113
- Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
113
+ Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
114
114
  particular commands.
115
115
 
116
116
  ```python
@@ -291,7 +291,7 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
291
291
 
292
292
  At last, run unchecked commands in git.
293
293
 
294
- Introduced in `0.0.0.dev4` to
294
+ Introduced in `0.0.0.dev4` to
295
295
  - experiment.
296
296
  - have consistent interfaced commands run until all subcommands are provided by the library.
297
297
 
@@ -330,11 +330,47 @@ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
330
330
 
331
331
  Error handling and I/O management is left to the client/caller.
332
332
 
333
+ #### 🏃 Communicate with long-running git sessions
334
+
335
+ Introduced in `0.0.0.dev17` to:
336
+ - Make long-running git command sessions.
337
+ - Have reentrant sessions.
338
+ - Communicate with long-running git command session using their `stdin` and `stdout`.
339
+
340
+ Get long-running process sessions and communicate with them for batching and faster operations.
341
+
342
+ ```python
343
+ import gitbolt
344
+ from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_data, cat_file_tree_content, \
345
+ mktree_tree_make
346
+
347
+ git = gitbolt.get_git_command()
348
+ with git.session(
349
+ cat_file1=["cat-file", "--batch"],
350
+ cat_file2=["cat-file", "--batch"],
351
+ cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
352
+ mktree=["mktree", "--batch", "-z"]
353
+ ) as ses:
354
+ tree_data: list[bytes] = []
355
+ for mode_, sha_, name_ in cat_file_tree_data(ses.commands.cat_file2, b"HEAD^{tree}"):
356
+ print(mode_, sha_, name_)
357
+ for tree_entry in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
358
+ tree_data.append(tree_entry)
359
+ print("+" * 40)
360
+ print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
361
+ print("~" * 40)
362
+ with ses: # is reentrant, if you want
363
+ tree_hash = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_data)) # make tree
364
+ print("Obtained tree: ", tree_hash)
365
+
366
+ assert not ses.active # session no long active
367
+ ```
368
+
333
369
  #### 💻 Run commands received from CLI
334
370
 
335
371
  Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
336
372
 
337
- While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
373
+ While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
338
374
  would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
339
375
  actually running them. An example:
340
376
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitbolt"
3
- version = "0.0.0.dev17"
3
+ version = "0.0.0.dev19"
4
4
  description = "Fast, flexible and type-safe Git commands in Python."
5
5
  requires-python = ">=3.12"
6
6
  readme = "README.md"
@@ -17,6 +17,7 @@ from gitbolt.base import LsTree as LsTree
17
17
  from gitbolt.base import Version as Version
18
18
  from gitbolt.base import Add as Add
19
19
  from gitbolt.subprocess.base import GitCommand
20
+ from gitbolt.subprocess.base import GitSession as GitSession
20
21
  # endregion
21
22
 
22
23
 
@@ -7,6 +7,7 @@ Git command interfaces with default implementation using subprocess calls.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import sys
10
11
  from abc import abstractmethod, ABC
11
12
  from collections.abc import Callable
12
13
  from contextlib import AbstractContextManager
@@ -306,8 +307,7 @@ class GitCommand(Git, ABC):
306
307
  @abstractmethod
307
308
  def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
308
309
  """
309
- Run long-running git session with multiple unchecked subcommands using ``subprocess.Popen`` and
310
- communicate with them.
310
+ Run long-running git session with multiple unchecked subcommands using ``subprocess.Popen`` and communicate with them.
311
311
 
312
312
  Client/Caller can communicate with ``GitSession``'s processes with their stdin and stdout.
313
313
 
@@ -326,65 +326,155 @@ class GitCommand(Git, ABC):
326
326
  >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
327
327
  ... pass # any communication can be done by Popen semantics.
328
328
 
329
+ Run special Popen:
330
+
331
+ >>> import gitbolt
332
+ >>> _git = gitbolt.get_git_command()
333
+ >>> ses = _git.session(ls_tree=["ls-tree", "HEAD"],
334
+ ... cat_file=lambda : _git.subcmd_unchecked.popen(["cat-file", "--batch"], env=None))
335
+ >>> with ses: # start the session by ctx mgr
336
+ ... pass # any communication can be done by Popen semantics.
337
+
338
+ Is reentrant:
339
+
340
+ >>> import gitbolt
341
+ >>> _git = gitbolt.get_git_command()
342
+ >>> ses = _git.session(cat_file=["cat-file", "--batch-check"])
343
+ >>> with ses: # start the session by ctx mgr
344
+ ... with ses: # reentrant
345
+ ... pass
346
+ ... pass # any communication can be done by Popen semantics.
347
+
348
+
349
+ Note: All commands/processes are assumed:
350
+
351
+ - in bytes mode.
352
+ - pipe their stdin and stdout.
353
+
329
354
  :param commands: list of string git commands suppliable to ``subprocess.Popen`` or ``subprocess.Popen`` lambdas.
330
355
  :returns: A (not yet started) long-running ``GitSession`` context manager.
331
356
  """
332
357
  ...
333
358
 
334
359
 
360
+ # TODO: extract a base session class from this
335
361
  class GitSession(HasGitUnderneath[GitCommand], AbstractContextManager):
336
-
337
362
  def __init__(self, git: GitCommand, **commands: Callable[[], Popen[bytes]]):
338
363
  """
339
- Context Manager to start a git long-running session. Useful when a command is to be held in open state and be
340
- communicated with its stdin and stdout. This is way faster that spawning multiple processes each time.
364
+ Reusable and reentrant context Manager to start a git long-running session. Useful when a command is to be
365
+ held in open state and be communicated with its stdin and stdout. This is way faster that spawning multiple
366
+ processes each time.
341
367
 
342
368
  Examples:
343
369
 
344
370
  Obtain a session:
345
371
 
346
372
  >>> import gitbolt
373
+ >>> from gitbolt.subprocess.base import GitSession
347
374
  >>> _git = gitbolt.get_git_command()
348
- >>> ses = _git.session(ls_tree=["ls-tree", "HEAD"], cat_file=["cat-file", "--batch"])
375
+ >>> ses = GitSession(_git, ls_tree= lambda : _git.subcmd_unchecked.popen(["ls-tree", "HEAD"], text=False),
376
+ ... cat_file= lambda : _git.subcmd_unchecked.popen(["cat-file", "--batch"], text=False))
349
377
  >>> with ses: # start the session by ctx mgr
350
378
  ... pass # any communication can be done by Popen semantics.
351
379
 
352
380
  Start the session in one go:
353
381
 
354
- >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
382
+ >>> with GitSession(_git, cat_file= lambda : _git.subcmd_unchecked.popen(["cat-file", "--batch"], text=False)) as ses: # obtain, start and ctx manage the session.
355
383
  ... pass # any communication can be done by Popen semantics.
356
384
 
385
+
386
+ Note: All processes are assumed:
387
+
388
+ - in bytes mode.
389
+ - pipe their stdin and stdout.
390
+
357
391
  :param git: ``gitbolt.subprocess.GitCommand`` instance.
358
392
  :param commands: commands in kwargs fashion.
359
393
  """
360
394
  self._git = git
361
395
  self.unstarted_commands: dict[str, Callable[[], Popen[bytes]]] = commands
362
396
  self.started_commands: dict[str, Popen[bytes]] = dict()
363
- self.commands: SimpleNamespace | None = None
397
+ self._commands: SimpleNamespace | None = None
398
+ self.__started = False
399
+ self.__done = False
400
+ self.__depth = 0
364
401
 
402
+ @override
365
403
  def __enter__(self) -> Self:
366
- processes_started_keys: list[str] = []
367
- for unstarted_command_key, unstarted_command in self.unstarted_commands.items():
368
- self.started_commands[unstarted_command_key] = unstarted_command().__enter__()
369
- processes_started_keys.append(unstarted_command_key)
370
- self.commands = SimpleNamespace(**self.started_commands)
371
- for pk in processes_started_keys:
372
- del self.unstarted_commands[pk]
404
+ if self.depth == 0:
405
+ processes_started_keys: list[str] = []
406
+ for unstarted_command_key, unstarted_command in self.unstarted_commands.items():
407
+ try:
408
+ self.started_commands[unstarted_command_key] = unstarted_command().__enter__()
409
+ except Exception:
410
+ for k in reversed(processes_started_keys):
411
+ self.started_commands[k].__exit__(*sys.exc_info())
412
+ raise
413
+ processes_started_keys.append(unstarted_command_key)
414
+ self._commands = SimpleNamespace(**self.started_commands)
415
+ for pk in processes_started_keys:
416
+ del self.unstarted_commands[pk]
417
+ self.__started = True
418
+ self.__depth += 1
419
+ return self
373
420
 
421
+ @override
374
422
  @property
375
423
  def git(self) -> GitCommand:
376
424
  return self._git
377
425
 
378
- def __exit__(self, exc_type, exc_value, traceback, /):
379
- process_done_keys: list[str] = []
380
- for started_popen_key, started_popen in self.started_commands.items():
381
- started_popen.__exit__(exc_type, exc_value, traceback)
382
- process_done_keys.append(started_popen_key)
383
- self.commands = None
384
- for pk in process_done_keys:
385
- del self.started_commands[pk]
426
+ @override
427
+ def __exit__(self, exc_type, exc_value, traceback, /) -> Literal[False]:
428
+ self.__depth -= 1
429
+ if self.__depth == 0:
430
+ process_done_keys: list[str] = []
431
+ for started_popen_key, started_popen in self.started_commands.items():
432
+ started_popen.__exit__(exc_type, exc_value, traceback)
433
+ process_done_keys.append(started_popen_key)
434
+ self._commands = None
435
+ for pk in process_done_keys:
436
+ del self.started_commands[pk]
437
+ self.__done = True
386
438
  return False
387
439
 
440
+ @property
441
+ def started(self) -> bool:
442
+ """
443
+ :returns: Whether the session has started.
444
+ """
445
+ return self.__started
446
+
447
+ @property
448
+ def done(self) -> bool:
449
+ """
450
+ :returns: Whether the session has completed and thus exited/closed.
451
+ """
452
+ return self.__done
453
+
454
+ @property
455
+ def active(self) -> bool:
456
+ """
457
+ :returns: Whether the session is currently actively running.
458
+ """
459
+ return self.started and not self.done
460
+
461
+ @property
462
+ def commands(self) -> SimpleNamespace:
463
+ """
464
+ :returns: All the registered commands and these can be accessed by member notations.
465
+ :raises RuntimeError: if commands are queried when the session is inactive.
466
+ """
467
+ if self._commands is None or not self.active:
468
+ raise RuntimeError("GitSession not active")
469
+ return self._commands
470
+
471
+ @property
472
+ def depth(self) -> int:
473
+ """
474
+ :returns: The current session depth. Multilevel context managers increment the depth each y one.
475
+ """
476
+ return self.__depth
477
+
388
478
 
389
479
  class GitSubcmdCommand(GitSubCommand, HasGitUnderneath["GitCommand"], Protocol):
390
480
  """
@@ -696,30 +786,30 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
696
786
  self,
697
787
  subcommand_args: list[str],
698
788
  *popen_args: Any,
699
- text: Literal[True] = True,
789
+ text: Literal[False] = False,
700
790
  **popen_kwargs: Any,
701
- ) -> Popen[str]: ...
791
+ ) -> Popen[bytes]: ...
702
792
 
703
793
  @overload
704
794
  def popen(
705
795
  self,
706
796
  subcommand_args: list[str],
707
797
  *popen_args: Any,
708
- text: Literal[False] = False,
798
+ text: Literal[True] = True,
709
799
  **popen_kwargs: Any,
710
- ) -> Popen[bytes]: ...
800
+ ) -> Popen[str]: ...
711
801
 
712
802
  def popen(
713
803
  self,
714
804
  subcommand_args: list[str],
715
805
  *popen_args: Any,
716
- text: Literal[True, False] = False,
806
+ text: Literal[False, True] = False,
717
807
  **popen_kwargs: Any,
718
808
  ) -> Popen[str] | Popen[bytes]:
719
809
  """
720
810
  Open unchecked git subcommand communicable process, using ``subprocess.Popen``.
721
811
 
722
- All the arguments are congruent to ``subprocess.Popen`` and mostly passes as-is.
812
+ All the arguments are congruent to ``subprocess.Popen`` and mostly pass as-is.
723
813
 
724
814
  :param subcommand_args: the full subcommand argument list.
725
815
  :param popen_args: additional subprocess positionals.
@@ -734,7 +824,6 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
734
824
  stdin = popen_kwargs.pop("stdin", PIPE)
735
825
  stdout = popen_kwargs.pop("stdout", PIPE)
736
826
  stderr = popen_kwargs.pop("stderr", PIPE)
737
- bufsize = popen_kwargs.pop("bufsize", 0)
738
827
  # Popen the git command
739
828
  result = self.git.runner.popen_git_command(
740
829
  main_cmd_args,
@@ -746,7 +835,6 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
746
835
  stdin=stdin,
747
836
  stdout=stdout,
748
837
  stderr=stderr,
749
- bufsize=bufsize,
750
838
  **popen_kwargs,
751
839
  )
752
840
  return result
@@ -170,7 +170,6 @@ class UncheckedSubcmdImpl(UncheckedSubcmd, GitSubcmdCommandImpl):
170
170
 
171
171
 
172
172
  class SimpleGitCommand(GitCommand, RootDirOp):
173
-
174
173
  def __init__(
175
174
  self,
176
175
  git_root_dir: Path = Path.cwd(),
@@ -250,11 +249,17 @@ class SimpleGitCommand(GitCommand, RootDirOp):
250
249
 
251
250
  def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
252
251
  cmds: dict[str, Callable[[], Popen[bytes]]] = {}
252
+
253
+ # curb the lambda-late-binding-trap
254
+ # resource: https://medium.com/skiller-whale/late-binding-variables-its-a-trap-c17af980164f
255
+ def lambda_for_subcmd_popen(_runnable_cmd: list[str]):
256
+ return lambda: self.subcmd_unchecked.popen(_runnable_cmd)
257
+
253
258
  for cmd_name, runnable_cmd in commands.items():
254
259
  if callable(runnable_cmd):
255
260
  cmds[cmd_name] = runnable_cmd
256
261
  else:
257
- cmds[cmd_name] = lambda : self.subcmd_unchecked.popen(runnable_cmd, text=False)
262
+ cmds[cmd_name] = lambda_for_subcmd_popen(runnable_cmd)
258
263
  return GitSession(self, **cmds)
259
264
 
260
265
 
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Utilities related to processors specific to git commands using subprocess.
6
+ """
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Models useful for session utilities.
6
+ """
7
+ import dataclasses
8
+ import datetime
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class GitRawActor:
13
+ name: bytes
14
+ email: bytes
15
+ time: datetime.datetime
16
+
17
+ @classmethod
18
+ def from_commit_bytes(cls, commit_bytes: bytes, email_start_pattern: bytes = b" <",
19
+ email_end_pattern: bytes = b"> ") -> "GitRawActor":
20
+ """
21
+ Obtain ``Actor`` object from the author/committer bytes.
22
+
23
+ Examples:
24
+
25
+ Simple timestamp without timezone:
26
+
27
+ >>> _ss_actor1 = GitRawActor.from_commit_bytes(b"Suhas <sss@ss.ss> 1780211249")
28
+ >>> assert _ss_actor1.name == b"Suhas"
29
+ >>> assert _ss_actor1.email == b"sss@ss.ss"
30
+ >>> assert _ss_actor1.time.timestamp() == 1780211249.0
31
+
32
+ Simple timestamp with timezone:
33
+
34
+ >>> _ss_actor2 = GitRawActor.from_commit_bytes(b"Suhas Srivastava <sss@vaastav.tech> 1780211249 +0530")
35
+ >>> assert _ss_actor2.name == b"Suhas Srivastava"
36
+ >>> assert _ss_actor2.email == b"sss@vaastav.tech"
37
+ >>> assert _ss_actor2.time.timestamp() == 1780211249.0
38
+
39
+ :param commit_bytes: author/committer information in bytes form.
40
+ :param email_start_pattern: email spearates name and timestamp. Thus is required as a separator.
41
+ Marks email beginning.
42
+ :param email_end_pattern: email spearates name and timestamp. Thus is required as a separator.
43
+ Marks email ending.
44
+ :returns: ``Actor`` object with all the parsed and set values.
45
+ """
46
+ email_pattern_start_index = commit_bytes.find(email_start_pattern)
47
+ email_pattern_end_index = commit_bytes.rfind(email_end_pattern)
48
+ email_start_index = email_pattern_start_index+len(email_start_pattern)
49
+ email_end_index = email_pattern_end_index
50
+ name = commit_bytes[:email_pattern_start_index]
51
+ email = commit_bytes[email_start_index:email_end_index]
52
+ time_bytes = commit_bytes[email_end_index+len(email_end_pattern):]
53
+ time_main, time_zone = time_bytes.split() if b" " in time_bytes else (time_bytes, b"")
54
+ date_time_for_iso_str = datetime.datetime.fromtimestamp(int(time_main))
55
+ date_time = datetime.datetime.fromisoformat(f"{date_time_for_iso_str.date()}T{date_time_for_iso_str.time()}{time_zone.decode()}")
56
+ return GitRawActor(name, email, date_time)
57
+
58
+
59
+ @dataclasses.dataclass
60
+ class GitRawSignature:
61
+ signature: bytes
62
+
63
+
64
+ @dataclasses.dataclass
65
+ class GPGGitRawSignature(GitRawSignature):
66
+ pass
67
+
68
+
69
+ @dataclasses.dataclass
70
+ class SSHGitRawSignature(GitRawSignature):
71
+ pass
72
+
73
+
74
+ @dataclasses.dataclass
75
+ class RawCommitBytesObj:
76
+ """
77
+ Commit info all in bytes as read from ``git cat-file --batch``.
78
+ """
79
+ commit_hash: bytes
80
+ tree_hash: bytes
81
+ parents: list[bytes]
82
+ author: GitRawActor
83
+ committer: GitRawActor
84
+ signature: GitRawSignature | None
85
+ message: bytes
86
+
87
+
88
+ @dataclasses.dataclass
89
+ class RawBytesValsOfCommit:
90
+ tree_val: bytes
91
+ commit_parents_vals: list[bytes]
92
+ author_val: bytes
93
+ committer_val: bytes
94
+ signature_val: bytes | None
95
+ commit_message: bytes