Requisitos de referencia.txt para la instalación requiere kwarg en setuptools setup.py archivo?


Tengo un archivo requirements.txt que estoy usando con Travis-CI. Parece tonto duplicar los requisitos tanto en requirements.txt como en setup.py, así que esperaba pasar un controlador de archivo a install_requires kwarg en setuptools.setup.

Es esto posible? Si es así, ¿cómo debería hacerlo?

Aquí está mi requirements.txt archivo:

guessit>=0.5.2
tvdb_api>=1.8.2
hachoir-metadata>=1.3.3
hachoir-core>=1.3.3
hachoir-parser>=1.3.4
Author: A-B-B, 2013-01-18

16 answers

Puede darle la vuelta y enumerar las dependencias en setup.py y tener un solo carácter - un punto . - en requirements.txt en su lugar.


Alternativamente, incluso si no se aconseja, todavía es posible analizar el archivo requirements.txt (si no se refiere a ningún requisito externo por URL) con el siguiente hack (probado con pip 9.0.1):

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

Esto no filtra [marcadores de entorno] ( https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers ) aunque.


En versiones antiguas de pip, más específicamente anteriores a 6.0, hay una API pública que se puede usar para lograr esto. Un requisito archivo puede contener comentarios (#) y puede incluir algunos otros archivos (--requirement o -r). Por lo tanto, si realmente desea analizar un requirements.txt puede usar el analizador 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
)
 199
Author: Romain Hardouin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-22 13:28:48

A primera vista, parece que requirements.txt y setup.py son duplicados tontos, pero es importante entender que mientras que la forma es similar, la función prevista es muy diferente.

El objetivo de un autor de paquetes, al especificar dependencias, es decir "donde quiera que instale este paquete, estos son los otros paquetes que necesita, para que este paquete funcione."

Por el contrario, el autor de la implementación (que puede ser la misma persona en un momento diferente) tiene una job, en el que dicen "aquí está la lista de paquetes que hemos reunido y probado y que ahora necesito instalar".

El autor del paquete escribe para una amplia variedad de escenarios, porque están poniendo su trabajo allí para ser utilizado de maneras que pueden no saber, y no tienen forma de saber qué paquetes se instalarán junto con su paquete. Para ser un buen vecino y evitar conflictos de versiones de dependencias con otros paquetes, deben especificar un rango tan amplio de las versiones de dependencias que posiblemente puedan funcionar. Esto es lo que hace install_requires en setup.py.

El autor de la implementación escribe para un objetivo muy diferente y muy específico: una sola instancia de una aplicación o servicio instalado, instalado en un equipo en particular. Para controlar con precisión una implementación y asegurarse de que se prueban e implementan los paquetes correctos, el autor de la implementación debe especificar la versión exacta y la ubicación de origen de cada paquete que se va a instalar, incluidas las dependencias y dependencias de dependencias. Con esta especificación, una implementación se puede aplicar de forma repetida a varias máquinas o probar en una máquina de prueba, y el autor de la implementación puede estar seguro de que los mismos paquetes se implementan cada vez. Esto es lo que hace un requirements.txt.

Así que puede ver que, aunque ambos parecen una gran lista de paquetes y versiones, estas dos cosas tienen trabajos muy diferentes. ¡Y definitivamente es fácil mezclar esto y equivocarse! Pero la forma correcta de pensar en esto es que requirements.txt es una "respuesta" a la "pregunta" planteada por los requisitos en todos los archivos de paquetes setup.py. En lugar de escribirlo a mano, a menudo se genera diciéndole a pip que mire todos los archivos setup.py en un conjunto de paquetes deseados, encuentre un conjunto de paquetes que crea que se ajusta a todos los requisitos, y luego, después de que estén instalados, "congele" esa lista de paquetes en un archivo de texto (de ahí viene el nombre pip freeze).

Así que la conclusión:

  • setup.py debe declarar las versiones de dependencia más flexibles posibles que aún son viables. Su trabajo es decir con qué puede funcionar un paquete en particular.
  • requirements.txt es un manifiesto de implementación que define un trabajo de instalación completo, y no debe considerarse como vinculado a ningún paquete. Su trabajo es declarar una lista exhaustiva de todos los paquetes necesarios para hacer que un deployment funcione.
  • Debido a que estas dos cosas tienen un contenido y razones de existencia tan diferentes, no es factible simplemente copiar una en el otro.

Referencias:

 113
Author: Jonathan Hanson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-12 13:31:44

No puede tomar un controlador de archivo. El argumento install_requires puede ser solo una cadena o una lista de cadenas.

Por supuesto, puede leer su archivo en el script de instalación y pasarlo como una lista de cadenas a install_requires.

import os
from setuptools import setup

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

setup(...
install_requires=required,
...)
 69
Author: Fredrick Brennan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-18 13:14:47

Los archivos de requisitos usan un formato pip expandido, que solo es útil si necesita complementar su setup.py con restricciones más fuertes, por ejemplo, especificar las URL exactas de las que deben provenir algunas de las dependencias, o la salida de pip freeze para congelar todo el paquete establecido en versiones de trabajo conocidas. Si no necesita las restricciones adicionales, utilice solo un setup.py. Si siente que realmente necesita enviar un requirements.txt de todos modos, puede hacerlo en una sola línea:

.

Será válido y referir exactamente al contenido del setup.py que está en el mismo directorio.

 58
Author: Tobu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-30 11:24:31

Aunque no es una respuesta exacta a la pregunta, recomiendo la publicación del blog de Donald Stufft en https://caremad.io/2013/07/setup-vs-requirement / para una buena toma de este problema. Lo he estado usando con gran éxito.

En resumen, requirements.txt no es una alternativa setup.py, sino un complemento de implementación. Mantenga una abstracción apropiada de las dependencias de paquetes en setup.py. Establecer requirements.txt o más de ellos para obtener versiones específicas de dependencias de paquetes para desarrollo, pruebas, o producción.

Por ejemplo, con paquetes incluidos en el repositorio en deps/:

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

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

Pip ejecuta setup.py del paquete e instala las versiones específicas de las dependencias declaradas en install_requires. No hay duplicidad y el propósito de ambos artefactos se conserva.

 38
Author: famousgarkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-15 17:15:11

La mayoría de las otras respuestas anteriores no funcionan con la versión actual de la API de pip. Esta es la forma correcta* de hacerlo con la versión actual de pip (6.0.8 en el momento de escribir, también trabajó en 7.1.2. Puede comprobar su versión con 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
    ....
)

* Correcto, ya que es la forma de usar parse_requirements con el pip actual. Todavía probablemente no es la mejor manera de hacerlo, ya que, como posters dijo anteriormente, pip realmente no mantiene una API.

 20
Author: fabianvf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-26 20:44:25

Usar parse_requirements es problemático porque la API pip no está documentada ni soportada públicamente. En pip 1.6, esa función se está moviendo, por lo que es probable que los usos existentes se rompan.

Una forma más confiable de eliminar la duplicación entre setup.py y requirements.txt es especificar sus dependencias en setup.py y luego poner -e . en su archivo requirements.txt. Puede encontrar información de uno de los desarrolladores de pip sobre por qué es una mejor manera de hacerlo aquí: https://caremad.io/blog/setup-vs-requirement /

 17
Author: Wilfredo Sánchez Vega,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-26 01:31:31

Instale el paquete actual en Travis. Esto evita el uso de un archivo requirements.txt. Por ejemplo:

language: python
python:
  - "2.7"
  - "2.6"
install:
  - pip install -q -e .
script:
  - python runtests.py
 14
Author: vdboor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-21 16:23:17

Si no desea forzar a sus usuarios a instalar pip, puede emular su comportamiento con esto:

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'))
)
 4
Author: reubano,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-05 17:37:02

from pip.req import parse_requirements no funcionó para mí y creo que es para las líneas en blanco en mis requisitos.txt, pero esta función funciona

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,
    ...
)
 3
Author: Diego Navarro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-08 19:18:45

¡CUIDADO CON EL COMPORTAMIENTO!

Tenga en cuenta que pip.req.parse_requirements cambiará los guiones bajos a guiones. Esto me enfureció unos días antes de descubrirlo. Ejemplo de demostración:

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

Produce

['example-with-underscores', 'example-with-dashes']
 3
Author: MikeTwo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-26 18:44:20

He creado una función reutilizable para esto. En realidad, analiza un directorio completo de archivos de requisitos y los establece en extras_require.

Último siempre disponible aquí: https://gist.github.com/akatrevorjay/293c26fefa24a7b812f5

import glob
import itertools
import os

from setuptools import find_packages, setup

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:

    >>> _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=find_packages(),
    ...     zip_safe=False,
    ...     include_package_data=True,
    ... )
    >>> _conf.update(setup_requirements())
    >>> 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
 2
Author: trevorj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-02 02:08:59

Otra posible solución...

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

Y luego usar...

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,
    ...
)
 1
Author: Brian Bruggeman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-20 19:12:41

Otro hack parse_requirements que también analiza los marcadores de entorno en 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
)

Debería soportar tanto sdist como dists binarios.

Como han dicho otros, parse_requirements tiene varias deficiencias, por lo que esto no es lo que debe hacer en proyectos públicos, pero puede ser suficiente para proyectos internos/personales.

 0
Author: Tuukka Mustonen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-15 19:42:19

La siguiente interfaz quedó obsoleta en el pip 10:

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

Así que lo cambié al simple análisis de texto:

with open('requirements.txt', 'r') as f:
    install_reqs = [
        s for s in [
            line.strip(' \n') for line in f
        ] if not s.startswith('#') and s != ''
    ]
 0
Author: Dmitriy Sintsov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-16 10:28:31

Aquí hay un hack completo (probado con pip 9.0.1) basado en la respuesta de Romain que analiza requirements.txt y lo filtra de acuerdo con los marcadores de entorno actuales :

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)
 -1
Author: anatoly techtonik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:23