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.

The language defined is a compact line based format which is already in widespread use in pip requirements files, though we do not specify the command line option handling that those files permit. There is one caveat - the URL reference form, specified in Versioning specifier specification is not actually implemented in pip, but we use that format rather than pip’s current native format.

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.

Versions may be specified according to the rules of the Version specifier specification. (Note: URI is defined in 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,tests]

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#

See the Version specifier specification for more detail on both version numbers and version comparisons. Version specifications limit the versions of a distribution that can be used. They only apply to distributions looked up by name, rather than via a URL. Version comparison are also used in the markers feature. The optional brackets around a version are present for compatibility with PEP 345 but should not be generated, only accepted.

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.

Comparisons in marker expressions are typed by the comparison operator. The <marker_op> operators that are not in <version_cmp> perform the same as they do for strings in Python. The <version_cmp> operators use the version comparison rules of the Version specifier specification when those are defined (that is when both sides have a valid version specifier). If there is no defined behaviour of this specification and the operator exists in Python, then the operator falls back to the Python behaviour. Otherwise an error should be raised. e.g. the following will result in errors:

"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#