Как создать в памяти файловый объект с файловым дескриптором в Python?

Я планирую использовать subprocess.Popen (в Python 3.11.2) для реализации git mktree < foo.txt. Теперь передо мной стоит вопрос.

Чтобы воспроизвести мою ситуацию, вот скрипт, создающий среду.

#!/bin/bash

export GIT_AUTHOR_NAME=foo
export [email protected]
export GIT_AUTHOR_DATE = "Tue Jun 4 21:40:15 2024 +0800"
export GIT_COMMITTER_NAME=foo
export [email protected]
export GIT_COMMITTER_DATE = "Tue Jun 4 21:40:15 2024 +0800"

rm -rf foo
git init foo
mkdir -p foo/hello
echo hello > foo/hello/hello.txt
echo hello > foo/hello.txt
echo world > foo/world.txt
git -C foo add .
git -C foo commit -m 'hello world'
git -C foo log --no-decorate
git -C foo ls-tree HEAD hello.txt
git -C foo ls-tree HEAD world.txt

Ожидается, что он напечатает коммит и две записи blob.

commit d2b25fd15c1435f515dd6379eca8d691dde6abeb
Author: foo <[email protected]>
Date:   Tue Jun 4 21:40:15 2024 +0800

    hello world
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    hello.txt
100644 blob cc628ccd10742baea8241c5924df992b5c019f71    world.txt

Я хочу создать коммит из дерева, в котором есть только hello.txt и world.txt. Итак, сначала мне нужно создать новый объект дерева. (Обновление: получив правильное решение, я обнаружил, что в коде есть фатальная ошибка. Он создает объект дерева с неправильным содержимым из-за str и байтов.)

import subprocess

# get the blob entry of hello.txt
o, e = subprocess.Popen(
    ['git', 'ls-tree', 'HEAD', 'hello.txt'],
    stdout=subprocess.PIPE,
    env = {'GIT_DIR': 'foo/.git'},
).communicate()
line1 = o.decode()

# get the blob entry of world.txt
o, e = subprocess.Popen(
    ['git', 'ls-tree', 'HEAD', 'world.txt'],
    stdout=subprocess.PIPE,
    env = {'GIT_DIR': 'foo/.git'},
).communicate()
line2 = o.decode()

# write the 2 lines to foo.txt
with open('foo.txt', 'w') as f:
    f.write(line1)
    f.write(line2)

# create a new tree object from foo.txt
with open('foo.txt') as f:
    o, e = subprocess.Popen(
        ['git', 'mktree'],
        stdin=f,
        stdout=subprocess.PIPE,
        env = {'GIT_DIR': 'foo/.git'},
    ).communicate()
    tree = o.decode()
    print(f'created tree object {tree}')

Интересно, могу ли я использовать файловый объект в памяти, чтобы мне не приходилось создавать и удалять foo.txt. Поскольку io.StringIO рекомендуется во многих ответах, я пробую код.

import subprocess
import io

line1 = '100644 blob ce013625030ba8dba906f756967f9e9ca394464a\thello.txt\n'
line2 = '100644 blob cc628ccd10742baea8241c5924df992b5c019f71\tworld.txt\n'

with io.StringIO(line1 + line2) as f:
    o, e = subprocess.Popen(
        ['git', 'mktree'],
        stdin=f,
        stdout=subprocess.PIPE,
        env = {'GIT_DIR': 'foo/.git'},
    ).communicate()
    tree = o.decode()
    print(f'created tree object {tree}')

Это вызывает исключение.

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "C:\Python311\Lib\subprocess.py", line 892, in __init__
    errread, errwrite) = self._get_handles(stdin, stdout, stderr)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\subprocess.py", line 1339, in _get_handles
    p2cread = msvcrt.get_osfhandle(stdin.fileno())
                                   ^^^^^^^^^^^^^^
io.UnsupportedOperation: fileno

Согласно документу io, похоже, io.StringIO не имеет дескриптора файла.

файлно()

Возвращает базовый дескриптор файла (целое число) поток, если он существует. OSError возникает, если объект ввода-вывода не используйте файловый дескриптор.

Есть ли в памяти файловый объект, имеющий файловый дескриптор? Или есть какой-нибудь способ обойти исключение с помощью io.StringIO в subprocess.Popen?

Обновлять:

С помощью ответа @Ture Pålsson я получаю ожидаемое решение.

import subprocess

# get the blob entry of hello.txt
line1, _ = subprocess.Popen(
    ['git', 'ls-tree', 'HEAD', 'hello.txt'],
    stdout=subprocess.PIPE,
    env = {'GIT_DIR': 'foo/.git'},
).communicate()

# get the blob entry of world.txt
line2, _ = subprocess.Popen(
    ['git', 'ls-tree', 'HEAD', 'world.txt'],
    stdout=subprocess.PIPE,
    env = {'GIT_DIR': 'foo/.git'},
).communicate()

# create a tree object from the lines
# although Popen's text=True allows communicate's input to be str,
# here it should be bytes.
# str input creates a tree object with wrong content.
o, e = subprocess.Popen(
    ['git', 'mktree'],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    env = {'GIT_DIR': 'foo/.git'},
).communicate(input=line1+line2)


tree = o.decode().strip()
print(f'created tree object {tree}')

Попробуйте использовать круглые скобки вокруг вызова StringIO, например: with (io.StringIO(line1 + line2)) as f:

OldBoy 05.06.2024 17:12

@OldBoy Я попробовал добавить круглые скобки, и это вызвало то же исключение. Не могли бы вы объяснить разницу между скобками и без них? Спасибо.

ElpieKay 06.06.2024 09:23

Если вы планируете делать что-то достаточно тяжелое, я бы рекомендовал libgit2 (например, через pygit2). Это будет проще писать в сценарии и, вероятно, значительно быстрее.

nneonneo 06.06.2024 11:19

Наверное, я вчера что-то неправильно понял, вроде работает и без. Это наводит меня на мысль, что ошибка не имеет ничего общего с StreamIO.

OldBoy 06.06.2024 11:46
subprocess.run поставляется из коробки практически со всем, что вы здесь делаете вручную. В настоящее время нет необходимости использовать subprocess.Popen, если только вы не хотите, чтобы процесс выполнялся одновременно.
MisterMiyagi 06.06.2024 11:48

@nneonneo Спасибо за совет. Однажды я узнал о pygit2 и обнаружил, что это похоже на молоток по комару. Мои задачи просты, и я знаком с командами git, поэтому обычно использую GitPython. Его способность напрямую переводить и вызывать команды git мне подходит. Однако эта возможность не применима в случае использования стандартного ввода.

ElpieKay 06.06.2024 11:50

@MisterMiyagi Спасибо. Я узнаю о subprocess.run.

ElpieKay 06.06.2024 11:51
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
7
78
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если я чего-то не упустил, вы можете установить stdin=PIPE при создании объекта Popen и передать входные данные в качестве аргумента communicate.

Вот что я использовал в качестве «теста на дым»:

import subprocess

with subprocess.Popen(['cat'],
                      encoding='utf-8',
                      stdin=subprocess.PIPE,
                      stdout=subprocess.PIPE) as p:
    o, e = p.communicate('foo')
print(o, e)

Спасибо за ответ. Не могли бы вы дать демо?

ElpieKay 06.06.2024 09:25

Я искал и нашел этот ответ. Теперь я понимаю, что означает ваш ответ. Еще раз спасибо!

ElpieKay 06.06.2024 10:24

Другие вопросы по теме