Odwołaj wymagania.txt do pliku install_requires kwarg w pliku setup.py setuptools

279

Mam requirements.txtplik, którego używam z Travis-CI. Wydaje się głupie powielać wymagania zarówno requirements.txta setup.py, więc miałem nadzieję przekazać uchwyt pliku do install_requireskwarg w setuptools.setup.

czy to możliwe? Jeśli tak, to jak mam to zrobić?

Oto mój requirements.txtplik:

guessit>=0.5.2
tvdb_api>=1.8.2
hachoir-metadata>=1.3.3
hachoir-core>=1.3.3
hachoir-parser>=1.3.4
blz
źródło
4
install_requiressłuży do deklarowania zależności od pakietów, które są wymagane do działania pakietu i są używane przez programistę pakietu, natomiast requirements.txtsłuży do automatyzacji instalacji środowisk, co umożliwia instalowanie dodatkowego oprogramowania i przypinanie wersji i jest używany przez sysadmins wdrażających pakiet. Ich rola i grupa docelowa różnią się znacznie, więc próba łączenia ich jak życzenia OP jest prawdziwym błędem projektowym imho.
Zart
7
Moje 2 centy. Nie używaj pliku wymagania.txt w pliku setup.py. Cele są różne, ared caremad.io/2013/07/setup-vs-requirement
Philippe Ombredanne
3
Widzę wiele skomplikowanych odpowiedzi. Co jest złego w zwykłym starym [line.strip() for line in open("requirements.txt").readlines()]?
Felipe SS Schneider
Nie jest to zalecane. Ale jeśli jest to naprawdę potrzebne, jest to proste: sama setuptools ma już wszystko, co niezbędnepkg_resources.parse_requirements()
sinoroc

Odpowiedzi:

246

Można odwrócić go dookoła i lista w zależności setup.pyi mieć pojedynczy charakter - kropka .- w requirements.txtzamian.


Alternatywnie, nawet jeśli nie jest to zalecane, nadal można przeanalizować requirements.txtplik (jeśli nie odwołuje żadnych zewnętrznych wymagań przez adres URL) za pomocą następującego hacka (testowanego z pip 9.0.1):

install_reqs = parse_requirements('requirements.txt', session='hack')

Nie filtruje to jednak znaczników środowiska .


W starych wersjach pipa, a dokładniej starszych niż 6.0 , istnieje publiczny interfejs API, którego można użyć do osiągnięcia tego. Plik wymagań może zawierać komentarze ( #) i może zawierać inne pliki ( --requirementlub -r). Zatem jeśli naprawdę chcesz parsować a requirements.txt, możesz użyć parsera pip:

from pip.req import parse_requirements

# parse_requirements() returns generator of pip.req.InstallRequirement objects
install_reqs = parse_requirements(<requirements_path>)

# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs]

setup(
    ...
    install_requires=reqs
)
Romain Hardouin
źródło
26
Co jeśli użytkownik nie ma zainstalowanego pipa? Ka-boom?
Gringo Suave
82
@GringoSuave Jeśli użytkownik nie ma zainstalowanego pipa, musi go najpierw zainstalować.
guettli
7
Musisz także podać adresy URL w pliku wymagań, na wypadek, gdyby jakieś wiersze -e lub -f („edytowalne” git repo) wskazywały na pakiety inne niż pypi. Użyj tego:setup(..., dependency_links=[str(req_line.url) for req_line in parse_requirements(<requirements_path>)], ...)
płyty grzewcze
91
Naprawdę nie chcesz tego robić. Mówiąc jako opiekun pip pip w ogóle nie obsługuje wywoływania takiego API. W rzeczywistości pip 1.6 (następna wersja w tej chwili) przenosi tę funkcję.
Donald Stufft
26
To nie powinna być już akceptowana odpowiedź, jeśli w ogóle powinna. Jest rażąco zepsute. Nawet gdy zadziałało, jest rażąco niepotrzebne. Ponieważ pipdomyślnie analizowane są zależności setup.pypodczas braku requirements.txt, prosta odpowiedź , na którą Tobu zauważył poniżej, polega na wyszczególnieniu setup.pyi usunięciu wszystkich zależności requirements.txt. W przypadku aplikacji wymagających obu, wystarczy zmniejszyć listę zależności requirements.txtdo .postaci. Gotowe.
Cecil Curry
194

Na pierwszy rzut oka, wydaje się, że requirements.txti setup.pysą głupie duplikaty, ale ważne jest, aby zrozumieć, że podczas gdy forma jest podobna, zamierzona funkcja jest zupełnie inna.

Celem autora pakietu, określając zależności, jest powiedzenie „gdziekolwiek instalujesz ten pakiet, są to inne pakiety, których potrzebujesz, aby ten pakiet działał”.

W przeciwieństwie do tego autor wdrażania (którym może być ta sama osoba w innym czasie) ma inne zadanie, mówiąc: „Oto lista pakietów, które zebraliśmy i przetestowaliśmy i które muszę teraz zainstalować”.

Autor pakietu pisze dla wielu różnych scenariuszy, ponieważ udostępniają swoją pracę w taki sposób, że mogą nie wiedzieć, i nie mają możliwości dowiedzenia się, jakie pakiety zostaną zainstalowane wraz z pakietem. Aby być dobrym sąsiadem i uniknąć konfliktów wersji zależności z innymi pakietami, muszą określić możliwie szeroki zakres wersji zależności. To, co install_requiressię setup.pyrobi.

Autor wdrożenia pisze o bardzo innym, bardzo konkretnym celu: pojedynczej instancji zainstalowanej aplikacji lub usługi zainstalowanej na określonym komputerze. Aby dokładnie kontrolować wdrożenie i upewnić się, że odpowiednie pakiety zostały przetestowane i wdrożone, autor wdrażania musi określić dokładną wersję i lokalizację źródłową każdego pakietu do zainstalowania, w tym zależności i zależności zależności. Dzięki tej specyfikacji wdrożenie może być wielokrotnie stosowane na kilku komputerach lub testowane na maszynie testowej, a autor wdrażania może być pewny, że za każdym razem wdrażane są te same pakiety. To właśnie requirements.txtrobi.

Widać więc, że chociaż oba wyglądają jak duża lista pakietów i wersji, te dwie rzeczy mają bardzo różne zadania. I zdecydowanie łatwo to pomieszać i źle to zrobić! Ale właściwym sposobem myślenia o tym jest requirements.txt„odpowiedź” na „pytanie” wynikające z wymagań we wszystkich setup.pyplikach pakietu. Zamiast pisać ręcznie, często jest generowany przez polecenie pipowi, aby sprawdził wszystkie setup.pypliki w zestawie pożądanych pakietów, znalazł zestaw pakietów, które jego zdaniem spełniają wszystkie wymagania, a następnie, po ich zainstalowaniu, „zamroził” msgstr "ta lista pakietów do pliku tekstowego ( pip freezestąd pochodzi nazwa).

Więc na wynos:

  • setup.pypowinien zadeklarować możliwie najlżejsze wersje zależności, które nadal są wykonalne. Jego zadaniem jest określenie, z czym konkretny pakiet może współpracować.
  • requirements.txtjest manifestem wdrażania, który definiuje całe zadanie instalacyjne i nie powinien być traktowany jako powiązany z żadnym pakietem. Jego zadaniem jest zadeklarowanie wyczerpującej listy wszystkich niezbędnych pakietów do wdrożenia.
  • Ponieważ te dwie rzeczy mają tak różne treści i powody istnienia, nie można po prostu skopiować jednej do drugiej.

Bibliografia:

Jonathan Hanson
źródło
10
To jedno z najlepszych wyjaśnień, które pozwoliło mi uporządkować ten bałagan zwany instalacją pakietu! :)
Kounavi,
6
Nadal nie jest dla mnie jasne, dlaczego deweloper miałby kontrolować wersję requirements.txtwraz ze źródłem pakietu, który zawiera konkretne / zamrożone wymagania dotyczące instalacji lub testowania. Czy na pewno setup.pymożna w tym celu wykorzystać w samym projekcie? Mogę sobie tylko wyobrazić użycie takiego pliku dla narzędzi używanych do wspierania zarządzania projektem (np. Refaktoryzacja, tworzenie wydań itp.).
Sam Brightman,
2
@samBrightman Zgadzam się całkowicie, nie sądzę, że pakiety bibliotek lub pakiety aplikacji powinny zatwierdzać swój plik wymagania.txt do repozytorium z kodem. Myślę, że powinien to być artefakt generowany podczas testowania kompilacji, a następnie wykorzystywany do dokumentowania manifestu kompilacji i ostatecznie generowania artefaktu wdrażania.
Jonathan Hanson
6
Mówisz więc, że requirements.txtjest więcej dokumentacji dotyczącej stanu świata, który wyprodukował daną kompilację, nawet jeśli zwykle nie jest używana w samym procesie kompilacji? To ma sens. Wygląda jednak na to, że kilka systemów polega na duplikacji: Travis instaluje domyślne (stare) pakiety w twoim virtualenv i mówi, aby użyć requirements.txt. Jeśli zapytam, jak zapewnić, by zależności były w użyciu setup.py, ludzie nalegają, żebym ich użył requirements.txt.
Sam Brightman,
2
Najlepszą radą, jaką możesz z tego wyciągnąć, jest znalezienie odpowiedniego dla siebie modelu, udokumentowanie go i upewnienie się, że wszyscy, z którymi pracujesz, rozumieją go. Zastanów się, dlaczego robisz każdy kawałek i czy naprawdę ma to sens w twoim przypadku użycia. I staraj się jak najlepiej czytać o obecnym stanie budowania, pakowania i publikowania w Pythonie, na wypadek, gdyby sytuacja się poprawiła. Ale nie wstrzymuj oddechu.
Jonathan Hanson
89

Nie może obsłużyć pliku. install_requiresArgument może być tylko ciągiem lub listą ciągów .

Możesz oczywiście odczytać plik w skrypcie instalacyjnym i przekazać go jako listę ciągów do install_requires.

import os
from setuptools import setup

with open('requirements.txt') as f:
    required = f.read().splitlines()

setup(...
install_requires=required,
...)
Fredrick Brennan
źródło
5
Chociaż użyteczne, zmienia specyfikację wymagań z deklaratywnej na imperatywną. Uniemożliwia to niektórym narzędziom sprawdzenie, jakie są twoje wymagania. Na przykład PyCharm oferuje automatyczną instalację wszystkich wymagań określonych w install_requires. Jednak to nie działa, jeśli nie używasz deklaratywnej składni.
Piotr Dobrogost
55
@PiotrDobrogost Być może programista PyCharm powinien wtedy naprawić swój program. setup.pyto program, który powinien zostać uruchomiony, a nie plik danych, który należy przeanalizować. To nie pogarsza tej odpowiedzi.
Fredrick Brennan
5
Wskazuję tylko możliwe problemy; ta odpowiedź jest w porządku. Nie tylko PyCharm ma problem z „ukrywaniem” informacji za kodem. Jest to uniwersalny problem, dlatego istnieje ogólna tendencja do deklaratywnej specyfikacji metadanych w pakiecie Pythona.
Piotr Dobrogost
32
Działa dobrze, o ile włożymy include requirements.txtgo MANIFEST.indo swojej biblioteki lub nie będzie można zainstalować biblioteki z dystrybucji źródłowej.
Pankrat
4
Wiem, że to stare pytanie, ale przynajmniej w dzisiejszych czasach można skonfigurować PyCharm do analizy pliku wymagań w Preferencje-> Narzędzia-> Zintegrowane narzędzia Pythona-> Plik wymagań pakietu
lekksi
64

Pliki wymagań używają rozszerzonego formatu pip, który jest użyteczny tylko wtedy, gdy potrzebujesz uzupełnić swoje setup.pysilniejsze ograniczenia, na przykład określając dokładne adresy URL, z których niektóre zależności muszą pochodzić, lub dane wyjściowe, pip freezeaby zamrozić cały zestaw pakietów do znanego działania wersje. Jeśli nie potrzebujesz dodatkowych ograniczeń, użyj tylko setup.py. Jeśli czujesz, że naprawdę potrzebujesz wysyłki requirements.txt, możesz utworzyć jedną linię:

.

Będzie ważny i będzie odnosił się dokładnie do zawartości tego, setup.pyktóry znajduje się w tym samym katalogu.

Tobu
źródło
9
Ale w tym przypadku próbowałbym również zainstalować moją aplikację. Co jeśli nie będę go potrzebować i chcę tylko zainstalować install_requires?
ffeast
2
Aby wyjaśnić, o co pyta @ffeast, czy wymagania istnieją tylko w setup.py, czy istnieje sposób na zainstalowanie wymagań (odpowiednik pip install -r requirements.txt ) bez instalowania samego pakietu?
haridsv
1
@ffeast @haridsv -e .powinno wystarczyć. Sprawdź tę stronę: caremad.io/posts/2013/07/setup-vs-requirement
dexhunter
4
@ DexD.Hunter nadal próbuje zainstalować samą aplikację. To nie jest to, co chcemy
ffeast
38

Chociaż nie jest to dokładna odpowiedź na pytanie, polecam post na blogu Donalda Stuffta pod adresem https://caremad.io/2013/07/setup-vs-requirement/, aby dobrze zająć się tym problemem. Użyłem go do wielkiego sukcesu.

Krótko mówiąc, requirements.txtnie jest setup.pyalternatywą, ale uzupełnieniem wdrożenia. Zachowaj odpowiednią abstrakcję zależności pakietów w setup.py. Ustaw requirements.txtlub więcej, aby pobrać określone wersje zależności pakietów do programowania, testowania lub produkcji.

Np. Z pakietami zawartymi w repozytorium pod deps/:

# fetch specific dependencies
--no-index
--find-links deps/

# install package
# NOTE: -e . for editable mode
.

pip wykonuje pakiety setup.pyi instaluje określone wersje zależności zadeklarowane w install_requires. Nie ma duplikatu, a cel obu artefaktów zostaje zachowany.

Famousgarkin
źródło
7
Nie działa to, gdy chcesz udostępnić pakiet, za pomocą którego inni mogą go zainstalować pip install my-package. Jeśli zależności dla mojego pakietu nie ma na liście w moim pakiecie / setup.py, nie są one instalowane przez pip install my-package. Nie udało mi się ustalić, w jaki sposób dostarczyć pakiet dla innych, który zawiera zależności, bez wyraźnego podawania ich w pliku setup.py. Chciałbym wiedzieć, czy ktoś wymyślił, jak zachować SUCHOŚĆ, pozwalając innym na instalowanie zależności mojego pakietu + bez pobierania pliku wymagań i ręcznego wywoływania pip install -r my-package/requirements.txt.
Malina
2
@Malina Pakiet tutaj można idealnie zainstalować bez requirements.txt. To o to chodzi. Zaktualizowałem pytanie, aby wszystko było bardziej jasne. Zaktualizowany również przestarzały link do posta na blogu.
famousgarkin
więc po uruchomieniu setup.py wywoła wymagania.txt dla określonych wersji plików wymienionych w stup.py?
dtracers
Odwrotnie jest @dtracers. wymagania.txt wskazują na sam pakiet, w którym można pobrać zależności setup.py. Tak więc podczas instalowania przy użyciu wymagań działa, a podczas instalowania przez pip, działa również - w obu przypadkach przy użyciu zależności pliku setup.py, ale także pozwalając na instalowanie większej liczby rzeczy podczas korzystania z wymagań.
Txt
20

Używanie parse_requirementsjest problematyczne, ponieważ API pip nie jest publicznie udokumentowane i obsługiwane. W pipie 1.6 ta funkcja faktycznie się porusza, więc istniejące jej zastosowania prawdopodobnie się zepsują.

Bardziej niezawodnym sposobem na wyeliminowanie powielania między setup.pyi requirements.txtjest określenie zależności w pliku, setup.pya następnie umieszczenie go -e .w requirements.txtpliku. Niektóre informacje od jednego z pipprogramistów o tym, dlaczego jest to lepszy sposób, są dostępne tutaj: https://caremad.io/blog/setup-vs-requirement/

Wilfredo Sánchez Vega
źródło
@Tommy Wypróbuj: caremad.io/2013/07/setup-vs-requirement To ten sam link, który opublikowano w innej odpowiedzi.
Amit
18

Większość pozostałych odpowiedzi powyżej nie działa z bieżącą wersją API pipa. Oto poprawny * sposób na zrobienie tego z bieżącą wersją pip (6.0.8 w momencie pisania, działał również w 7.1.2. Możesz sprawdzić swoją wersję za pomocą pip -V).

from pip.req import parse_requirements
from pip.download import PipSession

install_reqs = parse_requirements(<requirements_path>, session=PipSession())

reqs = [str(ir.req) for ir in install_reqs]

setup(
    ...
    install_requires=reqs
    ....
)

* Prawidłowo, ponieważ jest to sposób na użycie parse_requirements z bieżącym pipem. Prawdopodobnie nie jest to najlepszy sposób, aby to zrobić, ponieważ, jak wspomniano powyżej, pip naprawdę nie utrzymuje interfejsu API.

fabianvf
źródło
14

Zainstaluj bieżący pakiet w Travis. Pozwala to uniknąć użycia requirements.txtpliku. Na przykład:

language: python
python:
  - "2.7"
  - "2.6"
install:
  - pip install -q -e .
script:
  - python runtests.py
vdboor
źródło
2
To zdecydowanie najlepsza kombinacja „poprawnych” i „praktycznych”. Dodałbym, że jeśli po przejściu testów możesz skłonić Travisa do wygenerowania pip freezepliku wymagań.txt i wyeksportowania go gdzieś jako artefaktu (takiego jak S3 lub coś takiego), to byłbyś świetnym sposobem na wielokrotne instalowanie dokładnie tego, co przetestowany.
Jonathan Hanson,
4

from pip.req import parse_requirements nie działało dla mnie i myślę, że dotyczy to pustych wierszy w pliku wymagań.txt, ale ta funkcja działa

def parse_requirements(requirements):
    with open(requirements) as f:
        return [l.strip('\n') for l in f if l.strip('\n') and not l.startswith('#')]

reqs = parse_requirements(<requirements_path>)

setup(
    ...
    install_requires=reqs,
    ...
)
Diego Navarro
źródło
4

Jeśli nie chcesz zmuszać użytkowników do instalowania pip, możesz naśladować jego zachowanie za pomocą:

import sys

from os import path as p

try:
    from setuptools import setup, find_packages
except ImportError:
    from distutils.core import setup, find_packages


def read(filename, parent=None):
    parent = (parent or __file__)

    try:
        with open(p.join(p.dirname(parent), filename)) as f:
            return f.read()
    except IOError:
        return ''


def parse_requirements(filename, parent=None):
    parent = (parent or __file__)
    filepath = p.join(p.dirname(parent), filename)
    content = read(filename, parent)

    for line_number, line in enumerate(content.splitlines(), 1):
        candidate = line.strip()

        if candidate.startswith('-r'):
            for item in parse_requirements(candidate[2:].strip(), filepath):
                yield item
        else:
            yield candidate

setup(
...
    install_requires=list(parse_requirements('requirements.txt'))
)
reubano
źródło
4

Następujący interfejs stał się przestarzały w pip 10:

from pip.req import parse_requirements
from pip.download import PipSession

Więc zmieniłem to na zwykłą analizę tekstu:

with open('requirements.txt', 'r') as f:
    install_reqs = [
        s for s in [
            line.split('#', 1)[0].strip(' \t\n') for line in f
        ] if s != ''
    ]
Dmitrij Sintsov
źródło
To proste podejście działa przez ponad 90% czasu. Dla osób używających Python 3.6+ napisałem odpowiedź, która jest jej pathlibodmianą .
Acumenus
3

To proste podejście odczytuje plik wymagań z setup.py. Jest to odmiana Odpowiedź Dmitiry S. . Ta odpowiedź jest kompatybilna tylko z Python 3.6+.

Za DS , requirements.txtmoże udokumentować konkretne wymagania z numerami konkretnej wersji, a setup.pymoże udokumentować abstrakcyjne wymagania z luźnych zakresów wersji.

Poniżej znajduje się fragment mojego setup.py.

import distutils.text_file
from pathlib import Path
from typing import List

def _parse_requirements(filename: str) -> List[str]:
    """Return requirements from requirements file."""
    # Ref: https://stackoverflow.com/a/42033122/
    return distutils.text_file.TextFile(filename=str(Path(__file__).with_name(filename))).readlines()

setup(...
      install_requires=_parse_requirements('requirements.txt'),
   ...)

Pamiętaj, że distutils.text_file.TextFileusunie komentarze. Z mojego doświadczenia wynika, że ​​najwyraźniej nie musisz podejmować żadnych specjalnych kroków w celu dołączenia pliku wymagań.

Acumenus
źródło
2

UWAŻAJ NA parse_requirementsZACHOWANIE!

Pamiętaj, że pip.req.parse_requirementszmieni podkreślenia na myślniki. Rozwścieczyło mnie to przez kilka dni, zanim to odkryłem. Przykład demonstrujący:

from pip.req import parse_requirements  # tested with v.1.4.1

reqs = '''
example_with_underscores
example-with-dashes
'''

with open('requirements.txt', 'w') as f:
    f.write(reqs)

req_deps = parse_requirements('requirements.txt')
result = [str(ir.req) for ir in req_deps if ir.req is not None]
print result

produkuje

['example-with-underscores', 'example-with-dashes']
MikeTwo
źródło
1
Użyj niebezpiecznej nazwy, aby uzyskać wersję podkreślenia:[ir.req.unsafe_name for ir in req_deps if ir.req is not None]
alanjds,
5
Jak wskazano w innym miejscu, PIP to aplikacja, a nie biblioteka. Nie ma publicznie uzgodnionego interfejsu API, a importowanie go do kodu nie jest obsługiwanym przypadkiem użycia. Nic dziwnego, że ma nieoczekiwane zachowanie; jego funkcje wewnętrzne nigdy nie były przeznaczone do użycia w ten sposób.
Jonathan Hanson
1

Stworzyłem do tego funkcję wielokrotnego użytku. W rzeczywistości analizuje cały katalog plików wymagań i ustawia je na dodatkowe wymagania.

Najnowsze zawsze dostępne tutaj: https://gist.github.com/akatrevorjay/293c26fefa24a7b812f5

import glob
import itertools
import os

# This is getting ridiculous
try:
    from pip._internal.req import parse_requirements
    from pip._internal.network.session import PipSession
except ImportError:
    try:
        from pip._internal.req import parse_requirements
        from pip._internal.download import PipSession
    except ImportError:
        from pip.req import parse_requirements
        from pip.download import PipSession


def setup_requirements(
        patterns=[
            'requirements.txt', 'requirements/*.txt', 'requirements/*.pip'
        ],
        combine=True):
    """
    Parse a glob of requirements and return a dictionary of setup() options.
    Create a dictionary that holds your options to setup() and update it using this.
    Pass that as kwargs into setup(), viola

    Any files that are not a standard option name (ie install, tests, setup) are added to extras_require with their
    basename minus ext. An extra key is added to extras_require: 'all', that contains all distinct reqs combined.

    Keep in mind all literally contains `all` packages in your extras.
    This means if you have conflicting packages across your extras, then you're going to have a bad time.
    (don't use all in these cases.)

    If you're running this for a Docker build, set `combine=True`.
    This will set `install_requires` to all distinct reqs combined.

    Example:

    >>> import setuptools
    >>> _conf = dict(
    ...     name='mainline',
    ...     version='0.0.1',
    ...     description='Mainline',
    ...     author='Trevor Joynson <[email protected],io>',
    ...     url='https://trevor.joynson.io',
    ...     namespace_packages=['mainline'],
    ...     packages=setuptools.find_packages(),
    ...     zip_safe=False,
    ...     include_package_data=True,
    ... )
    >>> _conf.update(setup_requirements())
    >>> # setuptools.setup(**_conf)

    :param str pattern: Glob pattern to find requirements files
    :param bool combine: Set True to set install_requires to extras_require['all']
    :return dict: Dictionary of parsed setup() options
    """
    session = PipSession()

    # Handle setuptools insanity
    key_map = {
        'requirements': 'install_requires',
        'install': 'install_requires',
        'tests': 'tests_require',
        'setup': 'setup_requires',
    }
    ret = {v: set() for v in key_map.values()}
    extras = ret['extras_require'] = {}
    all_reqs = set()

    files = [glob.glob(pat) for pat in patterns]
    files = itertools.chain(*files)

    for full_fn in files:
        # Parse
        reqs = {
            str(r.req)
            for r in parse_requirements(full_fn, session=session)
            # Must match env marker, eg:
            #   yarl ; python_version >= '3.0'
            if r.match_markers()
        }
        all_reqs.update(reqs)

        # Add in the right section
        fn = os.path.basename(full_fn)
        barefn, _ = os.path.splitext(fn)
        key = key_map.get(barefn)

        if key:
            ret[key].update(reqs)
            extras[key] = reqs

        extras[barefn] = reqs

    if 'all' not in extras:
        extras['all'] = list(all_reqs)

    if combine:
        extras['install'] = ret['install_requires']
        ret['install_requires'] = list(all_reqs)

    def _listify(dikt):
        ret = {}

        for k, v in dikt.items():
            if isinstance(v, set):
                v = list(v)
            elif isinstance(v, dict):
                v = _listify(v)
            ret[k] = v

        return ret

    ret = _listify(ret)

    return ret


__all__ = ['setup_requirements']

if __name__ == '__main__':
    reqs = setup_requirements()
    print(reqs)
trevorj
źródło
bardzo dobrze! nawet obsługuje rekurencyjne wymagania z najnowszym pipem :)
amohr
@amohr Thanks! Niedawno zaktualizowałem go do jeszcze późniejszego pipa, nie jestem pewien, dlaczego zachowują się tak, jakimi są, przenosząc rzeczy do pip._internal... Jeśli nie zapewnisz użytecznego zewnętrznego interfejsu API, nie powinieneś łamać tych wszystkich które wykorzystują wszystko, co zapewniasz.
trevorj
0

Inne możliwe rozwiązanie ...

def gather_requirements(top_path=None):
    """Captures requirements from repo.

    Expected file format is: requirements[-_]<optional-extras>.txt

    For example:

        pip install -e .[foo]

    Would require:

        requirements-foo.txt

        or

        requirements_foo.txt

    """
    from pip.download import PipSession
    from pip.req import parse_requirements
    import re

    session = PipSession()
    top_path = top_path or os.path.realpath(os.getcwd())
    extras = {}
    for filepath in tree(top_path):
        filename = os.path.basename(filepath)
        basename, ext = os.path.splitext(filename)
        if ext == '.txt' and basename.startswith('requirements'):
            if filename == 'requirements.txt':
                extra_name = 'requirements'
            else:
                _, extra_name = re.split(r'[-_]', basename, 1)
            if extra_name:
                reqs = [str(ir.req) for ir in parse_requirements(filepath, session=session)]
                extras.setdefault(extra_name, []).extend(reqs)
    all_reqs = set()
    for key, values in extras.items():
        all_reqs.update(values)
    extras['all'] = list(all_reqs)
    return extras

a następnie użyć ...

reqs = gather_requirements()
install_reqs = reqs.pop('requirements', [])
test_reqs = reqs.pop('test', [])
...
setup(
    ...
    'install_requires': install_reqs,
    'test_requires': test_reqs,
    'extras_require': reqs,
    ...
)
Brian Bruggeman
źródło
skąd treepochodzi?
Francesco Boi,
@FrancescoBoi, jeśli trochę mi wybaczysz, że nie przedstawiłem w pełni działającego rozwiązania ... drzewo to tak naprawdę skan lokalnego systemu plików (bardzo podobny do polecenia „drzewo” w systemie Linux). Ponadto moje powyższe rozwiązanie może nie działać w tym momencie całkowicie, ponieważ pip jest ciągle aktualizowany, a ja używałem wewnętrznych elementów pip.
Brian Bruggeman
0

Nie poleciłbym robić czegoś takiego. Jak wspomniano wiele razy install_requiresi requirements.txtzdecydowanie nie powinna to być ta sama lista. Ale ponieważ istnieje wiele mylących odpowiedzi obejmujących prywatne wewnętrzne interfejsy API pip , warto spojrzeć na rozsądniejsze alternatywy ...

Pip nie musi analizować requirements.txtpliku ze skryptu setuptools setup.py . Projekt setuptools zawiera już wszystkie niezbędne narzędzia w pakiecie najwyższego poziomupkg_resources .

Może mniej więcej wyglądać tak:

#!/usr/bin/env python3

import pathlib

import pkg_resources
import setuptools

with pathlib.Path('requirements.txt').open() as requirements_txt:
    install_requires = [
        str(requirement)
        for requirement
        in pkg_resources.parse_requirements(requirements_txt)
    ]

setuptools.setup(
    install_requires=install_requires,
)
sinoroc
źródło
W przypadku, gdy nie jesteś świadomy, powodem, dla którego wielu (łącznie ze mną) używa pipanalizowania składni, a nie pkg_resourcesprzed rokiem 2015, są błędy, takie jak github.com/pypa/setuptools/issues/470 . Ta dokładna jest obecnie naprawiona, ale nadal boję się jej użyć, ponieważ obie implementacje wydają się być opracowywane osobno.
trevorj
@trevorj Dzięki za wskazanie tego, nie wiedziałem. Faktem jest, że obecnie działa, a zaangażowanie Pipa wydaje mi się absurdalnym pomysłem (szczególnie w ten sposób). Spójrz na inne odpowiedzi, większość wydaje się być niewielkimi odmianami tego samego niedoinformowanego pomysłu, bez ostrzeżenia. A nowicjusze mogą po prostu podążać za tym trendem. Mamy nadzieję, że inicjatywy takie jak PEP517 i PEP518 odciągną społeczność od tego szaleństwa.
sinoroc
-1

Cross wysyłając moją odpowiedź z tego pytania SO, dla innego prostego rozwiązania w wersji pip.

try:  # for pip >= 10
    from pip._internal.req import parse_requirements
    from pip._internal.download import PipSession
except ImportError:  # for pip <= 9.0.3
    from pip.req import parse_requirements
    from pip.download import PipSession

requirements = parse_requirements(os.path.join(os.path.dirname(__file__), 'requirements.txt'), session=PipSession())

if __name__ == '__main__':
    setup(
        ...
        install_requires=[str(requirement.req) for requirement in requirements],
        ...
    )

Następnie po prostu wprowadź wszystkie swoje wymagania w requirements.txtkatalogu głównym projektu.

Scrotch
źródło
-1

Ja to zrobiłem:

import re

def requirements(filename):
    with open(filename) as f:
        ll = f.read().splitlines()
    d = {}
    for l in ll:
        k, v = re.split(r'==|>=', l)
        d[k] = v
    return d

def packageInfo():
    try:
        from pip._internal.operations import freeze
    except ImportError:
        from pip.operations import freeze

    d = {}
    for kv in freeze.freeze():
        k, v = re.split(r'==|>=', kv)
        d[k] = v
    return d

req = getpackver('requirements.txt')
pkginfo = packageInfo()

for k, v in req.items():
    print(f'{k:<16}: {v:<6} -> {pkginfo[k]}')
yoonghm
źródło
-2

Kolejny parse_requirementshack, który analizuje również znaczniki środowiska w extras_require:

from collections import defaultdict
from pip.req import parse_requirements

requirements = []
extras = defaultdict(list)
for r in parse_requirements('requirements.txt', session='hack'):
    if r.markers:
        extras[':' + str(r.markers)].append(str(r.req))
    else:
        requirements.append(str(r.req))

setup(
    ...,
    install_requires=requirements,
    extras_require=extras
)

Powinien obsługiwać zarówno sdist, jak i binarne dists.

Jak twierdzą inni, parse_requirementsma kilka niedociągnięć, więc nie należy tego robić w projektach publicznych, ale może wystarczyć w przypadku projektów wewnętrznych / osobistych.

Tuukka Mustonen
źródło
pip 20.1 zmienił API i znaczniki nie są już dostępne przez parse_requirements(), więc to się nie udaje.
Tuukka Mustonen
-3

Oto kompletny hack (przetestowany z pip 9.0.1) oparty na odpowiedzi Romaina, który analizuje requirements.txti filtruje go zgodnie z obecnymi znacznikami środowiska :

from pip.req import parse_requirements

requirements = []
for r in parse_requirements('requirements.txt', session='hack'):
    # check markers, such as
    #
    #     rope_py3k    ; python_version >= '3.0'
    #
    if r.match_markers():
        requirements.append(str(r.req))

print(requirements)
anatoly techtonik
źródło
1
To tylko częściowo prawda. Jeśli zadzwonisz r.match_markers(), faktycznie oceniasz znaczniki, co jest słuszne dla sdist. Jednakże, jeśli budujesz binarkę binarną (np. Koło), pakiet wyświetli tylko te biblioteki, które pasują do twojego środowiska kompilacji.
Tuukka Mustonen,
@TuukkaMustonen, więc gdzie to znaleźć wheel environment(jeśli to jest rzecz, którą człowiek próbuje zrobić), aby ocenić markery przeciwko niemu?
anatoly techtonik
Zobacz stackoverflow.com/a/41172125/165629, który powinien również obsługiwać bdist_wheel. Nie ocenia markerów, tylko je dodaje extras_require.
Tuukka Mustonen,