Especificadores de dependência

Este documento descreve o formato dos especificadores de dependência conforme originalmente especificado na PEP 508.

O trabalho de uma dependência é permitir que ferramentas como pip 1 encontrem o pacote certo para instalar. Às vezes, isso é muito vago – apenas especificando um nome – e, às vezes, muito específico – referindo-se a um arquivo específico a ser instalado. Às vezes, as dependências são relevantes apenas em uma plataforma, ou apenas algumas versões são aceitáveis, então a linguagem permite descrever todos esses casos.

A linguagem definida é um formato compacto baseado em linha que já está em uso generalizado em arquivos de requisitos pip, embora não especifiquemos a manipulação de opção de linha de comando que esses arquivos permitem. Há uma ressalva: o formulário de referência de URL, especificado em PEP 440, não é realmente implementado em pip, mas como PEP 440 é aceito, usamos esse formato em vez do formato nativo atual de pip.

Especificação

Exemplos

Todos os recursos do idioma mostrados com uma pesquisa baseada em nome

requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < "2.7"

Uma pesquisa baseada em URL mínima

pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686

Conceitos

Uma especificação de dependência sempre especifica um nome de distribuição. Pode incluir extras, que expandem as dependências da distribuição nomeada para habilitar recursos opcionais. A versão instalada pode ser controlada usando limites de versão ou fornecendo a URL para um artefato específico para instalação. Finalmente, a dependência pode ser condicionada usando marcadores de ambiente.

Gramática

Primeiro, abordamos brevemente a gramática e, posteriormente, detalhamos a semântica de cada seção.

Uma especificação de distribuição é escrita em texto ASCII. Usamos uma gramática de parsley 2 para fornecer uma gramática precisa. Espera-se que a especificação seja incorporada a um sistema maior que ofereça enquadramento, como comentários, suporte a várias linhas por meio de continuações ou outros recursos semelhantes.

A gramática completa, incluindo anotações para construir uma árvore de análise útil, está incluída no final deste documento.

As versões podem ser especificadas de acordo com as regras da PEP 440. (Nota: URI é definido em std-66):

version_cmp   = wsp* '<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '==='
version       = wsp* ( letterOrDigit | '-' | '_' | '.' | '*' | '+' | '!' )+
version_one   = version_cmp version wsp*
version_many  = version_one (wsp* ',' version_one)*
versionspec   = ( '(' version_many ')' ) | version_many
urlspec       = '@' wsp* <URI_reference>

Os marcadores de ambiente permitem que uma especificação só tenha efeito em alguns ambientes

marker_op     = version_cmp | (wsp* 'in') | (wsp* 'not' wsp+ 'in')
python_str_c  = (wsp | letter | digit | '(' | ')' | '.' | '{' | '}' |
                 '-' | '_' | '*' | '#' | ':' | ';' | ',' | '/' | '?' |
                 '[' | ']' | '!' | '~' | '`' | '@' | '$' | '%' | '^' |
                 '&' | '=' | '+' | '|' | '<' | '>' )
dquote        = '"'
squote        = '\\''
python_str    = (squote (python_str_c | dquote)* squote |
                 dquote (python_str_c | squote)* dquote)
env_var       = ('python_version' | 'python_full_version' |
                 'os_name' | 'sys_platform' | 'platform_release' |
                 'platform_system' | 'platform_version' |
                 'platform_machine' | 'platform_python_implementation' |
                 'implementation_name' | 'implementation_version' |
                 'extra' # ONLY when defined by a containing layer
                 )
marker_var    = wsp* (env_var | python_str)
marker_expr   = marker_var marker_op marker_var
              | wsp* '(' marker wsp* ')'
marker_and    = marker_expr wsp* 'and' marker_expr
              | marker_expr
marker_or     = marker_and wsp* 'or' marker_and
                  | marker_and
marker        = marker_or
quoted_marker = ';' wsp* marker

Componentes opcionais de uma distribuição podem ser especificados usando o campo extras:

identifier_end = letterOrDigit | (('-' | '_' | '.' )* letterOrDigit)
identifier    = letterOrDigit identifier_end*
name          = identifier
extras_list   = identifier (wsp* ',' wsp* identifier)*
extras        = '[' wsp* extras_list? wsp* ']'

Restrições de nomes para extras são definidas na PEP 685.

Fornecendo uma regra para requisitos baseados em nome

name_req      = name wsp* extras? wsp* versionspec? wsp* quoted_marker?

E uma regra para especificações de referência direta

url_req       = name wsp* extras? wsp* urlspec wsp+ quoted_marker?

Levando à regra unificada que pode especificar uma dependência.:

specification = wsp* ( url_req | name_req ) wsp*

Espaço em branco

O espaço em branco sem quebra de linha é principalmente opcional, sem significado semântico. A única exceção é detectar o fim de um requisito de URL.

Nomes

Nomes de distribuição Python são atualmente definidos na PEP 345. Os nomes atuam como o identificador primário para distribuições. Eles estão presentes em todas as especificações de dependência e são suficientes para serem uma especificação por conta própria. No entanto, PyPI impõe restrições estritas aos nomes: eles devem corresponder a uma regex que não diferencie maiúsculas de minúsculas ou não serão aceitos. Assim, neste documento, limitamos os valores aceitáveis para identificadores a essa regex. Uma redefinição completa do nome pode ocorrer em um futuro PEP de metadados. A regex (executada com re.IGNORECASE) é

^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$

Extras

Um extra é uma parte opcional de uma distribuição. As distribuições podem especificar quantos extras desejarem, e cada extra resulta na declaração de dependências adicionais da distribuição quando o extra é usado em uma especificação de dependência. Por exemplo

requests[security]

União de extras nas dependências que definem com as dependências da distribuição à qual estão anexados. O exemplo acima resultaria na instalação de solicitações e nas próprias dependências das solicitações, além de quaisquer dependências listadas no extra de “security” das solicitações.

Se vários extras forem listados, todas as dependências serão unidas.

Versões

Consulte PEP 440 para obter mais detalhes sobre números de versão e comparações de versão. As especificações de versão limitam as versões de uma distribuição que podem ser usadas. Eles se aplicam apenas a distribuições pesquisadas pelo nome, e não por meio de um URL. A comparação de versões também é usada no recurso de marcadores. Os colchetes opcionais em torno de uma versão estão presentes para compatibilidade com PEP 345, mas não devem ser gerados, apenas aceitos.

Marcadores de ambiente

Os marcadores de ambiente permitem que uma especificação de dependência forneça uma regra que descreva quando a dependência deve ser usada. Por exemplo, considere um pacote que precisa de argparse. No Python 2.7, o argparse está sempre presente. Em versões mais antigas Python, ele deve ser instalado como uma dependência. Isso pode ser expresso como

argparse;python_version<"2.7"

Uma expressão de marcador é avaliada como True ou False. Quando for avaliado como False, a especificação de dependência deve ser ignorada.

A linguagem do marcador é inspirada no próprio Python, escolhido pela capacidade de avaliá-lo com segurança sem executar código arbitrário que pode se tornar uma vulnerabilidade de segurança. Os marcadores foram padronizados pela primeira vez na PEP 345. Este documento corrige alguns problemas que foram observados no projeto descrito em PEP 426.

As comparações em expressões de marcador são tipadas pelo operador de comparação. Os operadores <marker_op> que não estão em <version_cmp> executam o mesmo que para strings no Python. Os operadores <version_cmp> usam as regras de comparação de versão PEP 440 quando são definidas (ou seja, quando ambos os lados têm um especificador de versão válido). Se não houver comportamento da PEP 440 definido e o operador existir no Python, o operador voltará ao comportamento do Python. Caso contrário, um erro deve ser levantado. Por exemplo, o seguinte resultará em erros

"dog" ~= "fred"
python_version ~= "surprise"

As constantes fornecidas pelo usuário são sempre codificadas como strings com as aspas ' ou ". Observe que os escapes de barra invertida não são definidos, mas as implementações existentes os suportam. Eles não estão incluídos nesta especificação porque adicionam complexidade e não há nenhuma necessidade observável para eles hoje.Da mesma forma, não definimos suporte a caracteres não-ASCII: espera-se que todas as variáveis de tempo de execução às quais nos referimos sejam somente ASCII.

As variáveis na gramática do marcador, como “os_name”, resolvem os valores pesquisados no tempo de execução do Python. Com exceção de “extra” todos os valores são definidos em todas as versões Python hoje – é um erro na implementação de marcadores se um valor não for definido.

Variáveis desconhecidas devem gerar um erro em vez de resultar em uma comparação avaliada como True ou False.

As variáveis cujo valor não pode ser calculado em uma determinada implementação do Python devem ser avaliadas como 0 para versões e uma string vazia para todas as outras variáveis.

A variável “extra” é especial. É usada pelo wheels para sinalizar quais especificações se aplicam a um determinado extra no arquivo METADATA do wheel, mas como o arquivo METADATA é baseado em uma versão de rascunho da PEP 426, não há especificação atual para isso. Independentemente disso, fora de um contexto em que esse tratamento especial esteja ocorrendo, a variável “extra” deve resultar em um erro como todas as outras variáveis desconhecidas.

Marcador

Equivalente no Python

Valores de amostra

os_name

os.name

posix, java

sys_platform

sys.platform

linux, linux2, darwin, java1.8.0_51 (observe que “linux” é do Python3 e “linux2” do Python2)

platform_machine

platform.machine()

x86_64

platform_python_implementation

platform.python_implementation()

CPython, Jython

platform_release

platform.release()

3.14.1-x86_64-linode39, 14.5.0, 1.8.0_51

platform_system

platform.system()

Linux, Windows, Java

platform_version

platform.version()

#1 SMP Fri Apr 25 13:07:35 EDT 2014 Java HotSpot(TM) 64-Bit Server VM, 25.51-b03, Oracle Corporation Darwin Kernel Version 14.5.0: Wed Jul 29 02:18:53 PDT 2015; root:xnu-2782.40.9~2/RELEASE_X86_64

python_version

'.'.join(platform.python_version_tuple()[:2])

3.4, 2.7

python_full_version

platform.python_version()

3.4.0, 3.5.0b1

implementation_name

sys.implementation.name

cpython

implementation_version

veja a definição abaixo

3.4.0, 3.5.0b1

extra

Um erro, exceto quando definido pelo contexto que interpreta a especificação.

test

A variável do marcador implementation_version é derivada de sys.implementation.version:

def format_full_version(info):
    version = '{0.major}.{0.minor}.{0.micro}'.format(info)
    kind = info.releaselevel
    if kind != 'final':
        version += kind[0] + str(info.serial)
    return version

if hasattr(sys, 'implementation'):
    implementation_version = format_full_version(sys.implementation.version)
else:
    implementation_version = "0"

Esta seção de marcadores de ambiente, inicialmente definida na PEP 508, substitui a seção de marcadores de ambiente na PEP 345.

Gramática completa

A gramática completa do persley

wsp           = ' ' | '\t'
version_cmp   = wsp* <'<=' | '<' | '!=' | '==' | '>=' | '>' | '~=' | '==='>
version       = wsp* <( letterOrDigit | '-' | '_' | '.' | '*' | '+' | '!' )+>
version_one   = version_cmp:op version:v wsp* -> (op, v)
version_many  = version_one:v1 (wsp* ',' version_one)*:v2 -> [v1] + v2
versionspec   = ('(' version_many:v ')' ->v) | version_many
urlspec       = '@' wsp* <URI_reference>
marker_op     = version_cmp | (wsp* 'in') | (wsp* 'not' wsp+ 'in')
python_str_c  = (wsp | letter | digit | '(' | ')' | '.' | '{' | '}' |
                 '-' | '_' | '*' | '#' | ':' | ';' | ',' | '/' | '?' |
                 '[' | ']' | '!' | '~' | '`' | '@' | '$' | '%' | '^' |
                 '&' | '=' | '+' | '|' | '<' | '>' )
dquote        = '"'
squote        = '\\''
python_str    = (squote <(python_str_c | dquote)*>:s squote |
                 dquote <(python_str_c | squote)*>:s dquote) -> s
env_var       = ('python_version' | 'python_full_version' |
                 'os_name' | 'sys_platform' | 'platform_release' |
                 'platform_system' | 'platform_version' |
                 'platform_machine' | 'platform_python_implementation' |
                 'implementation_name' | 'implementation_version' |
                 'extra' # ONLY when defined by a containing layer
                 ):varname -> lookup(varname)
marker_var    = wsp* (env_var | python_str)
marker_expr   = marker_var:l marker_op:o marker_var:r -> (o, l, r)
              | wsp* '(' marker:m wsp* ')' -> m
marker_and    = marker_expr:l wsp* 'and' marker_expr:r -> ('and', l, r)
              | marker_expr:m -> m
marker_or     = marker_and:l wsp* 'or' marker_and:r -> ('or', l, r)
                  | marker_and:m -> m
marker        = marker_or
quoted_marker = ';' wsp* marker
identifier_end = letterOrDigit | (('-' | '_' | '.' )* letterOrDigit)
identifier    = < letterOrDigit identifier_end* >
name          = identifier
extras_list   = identifier:i (wsp* ',' wsp* identifier)*:ids -> [i] + ids
extras        = '[' wsp* extras_list?:e wsp* ']' -> e
name_req      = (name:n wsp* extras?:e wsp* versionspec?:v wsp* quoted_marker?:m
                 -> (n, e or [], v or [], m))
url_req       = (name:n wsp* extras?:e wsp* urlspec:v (wsp+ | end) quoted_marker?:m
                 -> (n, e or [], v, m))
specification = wsp* ( url_req | name_req ):s wsp* -> s
# The result is a tuple - name, list-of-extras,
# list-of-version-constraints-or-a-url, marker-ast or None


URI_reference = <URI | relative_ref>
URI           = scheme ':' hier_part ('?' query )? ( '#' fragment)?
hier_part     = ('//' authority path_abempty) | path_absolute | path_rootless | path_empty
absolute_URI  = scheme ':' hier_part ( '?' query )?
relative_ref  = relative_part ( '?' query )? ( '#' fragment )?
relative_part = '//' authority path_abempty | path_absolute | path_noscheme | path_empty
scheme        = letter ( letter | digit | '+' | '-' | '.')*
authority     = ( userinfo '@' )? host ( ':' port )?
userinfo      = ( unreserved | pct_encoded | sub_delims | ':')*
host          = IP_literal | IPv4address | reg_name
port          = digit*
IP_literal    = '[' ( IPv6address | IPvFuture) ']'
IPvFuture     = 'v' hexdig+ '.' ( unreserved | sub_delims | ':')+
IPv6address   = (
                  ( h16 ':'){6} ls32
                  | '::' ( h16 ':'){5} ls32
                  | ( h16 )?  '::' ( h16 ':'){4} ls32
                  | ( ( h16 ':')? h16 )? '::' ( h16 ':'){3} ls32
                  | ( ( h16 ':'){0,2} h16 )? '::' ( h16 ':'){2} ls32
                  | ( ( h16 ':'){0,3} h16 )? '::' h16 ':' ls32
                  | ( ( h16 ':'){0,4} h16 )? '::' ls32
                  | ( ( h16 ':'){0,5} h16 )? '::' h16
                  | ( ( h16 ':'){0,6} h16 )? '::' )
h16           = hexdig{1,4}
ls32          = ( h16 ':' h16) | IPv4address
IPv4address   = dec_octet '.' dec_octet '.' dec_octet '.' dec_octet
nz            = ~'0' digit
dec_octet     = (
                  digit # 0-9
                  | nz digit # 10-99
                  | '1' digit{2} # 100-199
                  | '2' ('0' | '1' | '2' | '3' | '4') digit # 200-249
                  | '25' ('0' | '1' | '2' | '3' | '4' | '5') )# %250-255
reg_name = ( unreserved | pct_encoded | sub_delims)*
path = (
        path_abempty # begins with '/' or is empty
        | path_absolute # begins with '/' but not '//'
        | path_noscheme # begins with a non-colon segment
        | path_rootless # begins with a segment
        | path_empty ) # zero characters
path_abempty  = ( '/' segment)*
path_absolute = '/' ( segment_nz ( '/' segment)* )?
path_noscheme = segment_nz_nc ( '/' segment)*
path_rootless = segment_nz ( '/' segment)*
path_empty    = pchar{0}
segment       = pchar*
segment_nz    = pchar+
segment_nz_nc = ( unreserved | pct_encoded | sub_delims | '@')+
                # non-zero-length segment without any colon ':'
pchar         = unreserved | pct_encoded | sub_delims | ':' | '@'
query         = ( pchar | '/' | '?')*
fragment      = ( pchar | '/' | '?')*
pct_encoded   = '%' hexdig
unreserved    = letter | digit | '-' | '.' | '_' | '~'
reserved      = gen_delims | sub_delims
gen_delims    = ':' | '/' | '?' | '#' | '(' | ')?' | '@'
sub_delims    = '!' | '$' | '&' | '\\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
hexdig        = digit | 'a' | 'A' | 'b' | 'B' | 'c' | 'C' | 'd' | 'D' | 'e' | 'E' | 'f' | 'F'

Um programa de teste – se a gramática estiver em uma string grammar:

import os
import sys
import platform

from parsley import makeGrammar

grammar = """
    wsp ...
    """
tests = [
    "A",
    "A.B-C_D",
    "aa",
    "name",
    "name<=1",
    "name>=3",
    "name>=3,<2",
    "name@http://foo.com",
    "name [fred,bar] @ http://foo.com ; python_version=='2.7'",
    "name[quux, strange];python_version<'2.7' and platform_version=='2'",
    "name; os_name=='a' or os_name=='b'",
    # Should parse as (a and b) or c
    "name; os_name=='a' and os_name=='b' or os_name=='c'",
    # Overriding precedence -> a and (b or c)
    "name; os_name=='a' and (os_name=='b' or os_name=='c')",
    # should parse as a or (b and c)
    "name; os_name=='a' or os_name=='b' and os_name=='c'",
    # Overriding precedence -> (a or b) and c
    "name; (os_name=='a' or os_name=='b') and os_name=='c'",
    ]

def format_full_version(info):
    version = '{0.major}.{0.minor}.{0.micro}'.format(info)
    kind = info.releaselevel
    if kind != 'final':
        version += kind[0] + str(info.serial)
    return version

if hasattr(sys, 'implementation'):
    implementation_version = format_full_version(sys.implementation.version)
    implementation_name = sys.implementation.name
else:
    implementation_version = '0'
    implementation_name = ''
bindings = {
    'implementation_name': implementation_name,
    'implementation_version': implementation_version,
    'os_name': os.name,
    'platform_machine': platform.machine(),
    'platform_python_implementation': platform.python_implementation(),
    'platform_release': platform.release(),
    'platform_system': platform.system(),
    'platform_version': platform.version(),
    'python_full_version': platform.python_version(),
    'python_version': '.'.join(platform.python_version_tuple()[:2]),
    'sys_platform': sys.platform,
}

compiled = makeGrammar(grammar, {'lookup': bindings.__getitem__})
for test in tests:
    parsed = compiled(test).specification()
    print("%s -> %s" % (test, parsed))

Resumo das alterações à PEP 508

As seguintes alterações foram feitas com base no feedback após sua implementação inicial:

  • A definição de python_version foi alterada de platform.python_version()[:3] para '.'.join(platform.python_version_tuple()[:2]), para acomodar potenciais futuras versões do Python com versões principais e secundárias de 2 dígitos (por exemplo, 3.10). 3

Referências

1

pip, o instalador recomendado para pacotes Python (http://pip.readthedocs.org/en/stable/)

2

A biblioteca de PEG do parsley. (https://pypi.python.org/pypi/parsley/)

3

Versões futuras do Python podem ser problemáticas com a definição da variável do marcador de ambiente python_version (https://github.com/python/peps/issues/560)