Standardowy sposób osadzenia wersji w pakiecie Pythona?

264

Czy istnieje standardowy sposób na powiązanie ciągu wersji z pakietem Pythona w taki sposób, że mógłbym wykonać następujące czynności?

import foo
print foo.version

Wyobrażam sobie, że istnieje sposób na odzyskanie tych danych bez dodatkowego kodowania, ponieważ łańcuchy drobne / główne są setup.pyjuż określone . Alternatywne rozwiązanie, które znalazłem, miałem import __version__w swoim, foo/__init__.pya następnie __version__.pywygenerowałem setup.py.

Dimitri Tcaciuc
źródło
7
Do Twojej wiadomości, jest bardzo dobry przegląd na: packaging.python.org/en/latest/…
ionelmc
1
Wersję zainstalowanego pakietu można pobrać z metadanych za pomocą setuptools , więc w wielu przypadkach setup.pywystarczy wstawienie tylko wersji . Zobacz to pytanie .
saaj
2
Do Twojej dyspozycji jest w zasadzie 5 typowych wzorców utrzymywania jednego źródła prawdy (zarówno podczas instalacji, jak i wykonywania) dla numeru wersji.
KF Lin
@ionelmc Dokumentacja Pythona wymienia 7 różnych opcji pojedynczego kwaśności . Czy nie jest to sprzeczne z koncepcją „ jednego źródła prawdy ”?
Stevoisiak,
@StevenVascellaro nie jesteś pewien, o co pytasz. Jest tam tak wiele sposobów, ponieważ przewodnik po opakowaniach nie chce być opiniowany.
ionelmc

Odpowiedzi:

136

Nie jest to bezpośrednia odpowiedź na twoje pytanie, ale powinieneś rozważyć nazywanie go __version__, nie version.

To prawie quasi-standard. Wiele modułów w standardowej bibliotece używa __version__, a to jest także używane w wielu modułach innych firm, więc jest to quasi-standard.

Zwykle __version__jest sznurkiem, ale czasami jest również zmiennoprzecinkowy lub krotkę.

Edycja: jak wspomniał S.Lott (Dziękuję!), PEP 8 mówi wprost:

Nazwy modułów na poziomie modułu

LEVEL „dunders” moduł (czyli nazwy z dwóch wiodących i dwa podkreślenia krocząca), takie jak __all__, __author__, __version__, itd. Powinny być umieszczone po docstring modułem ale zanim jakiekolwiek instrukcje importu z wyjątkiem __future__importu.

Należy również upewnić się, że numer wersji jest zgodny z formatem opisanym w PEP 440 ( PEP 386 poprzednia wersja tego standardu).

oefe
źródło
9
Powinien to być ciąg znaków i mieć informację o wersji dla wersji krotkowej.
James Antill
James: Dlaczego __version_info__konkretnie? (Które „wymyśla” twoje słowo z podwójnym podkreśleniem.) [Kiedy James skomentował, podkreślenia nie zrobiły nic w komentarzach, teraz wskazują na nacisk, więc James też naprawdę napisał __version_info__. --- wyd.]
Możesz zobaczyć coś o tym, co wersja powinna powiedzieć na paczkach.python.org/distribute/... Ta strona dotyczy dystrybucji, ale znaczenie numeru wersji staje się de facto standardem.
sienkiew
2
Dobrze. Wydaje się, że te PEP są sobie sprzeczne. Cóż, PEP 8 mówi „if” i „crud”, więc tak naprawdę nie poleca używania słowa kluczowego VCS. Ponadto, jeśli kiedykolwiek przełączysz się na inny VCS, stracisz informacje o wersji. Dlatego sugerowałbym użycie informacji o wersji zgodnej z PEP 386/440 osadzonej w jednym pliku źródłowym, przynajmniej w przypadku większych projektów.
oefe
2
Gdzie można umieścić tę wersję . Biorąc pod uwagę, że jest to zaakceptowana wersja, chciałbym zobaczyć te dodatkowe informacje tutaj.
darkgaze
120

Używam jednego _version.pypliku jako „niegdyś kanonicznego miejsca” do przechowywania informacji o wersji:

  1. Zapewnia __version__atrybut.

  2. Zapewnia standardową wersję metadanych. Dlatego zostanie wykryty przez pkg_resourcesinne narzędzia przetwarzające metadane pakietu (EGG-INFO i / lub PKG-INFO, PEP 0345).

  3. Podczas importowania pakietu nie importuje pakietu (ani niczego innego), co może powodować problemy w niektórych sytuacjach. (Zobacz poniższe komentarze dotyczące problemów, jakie może to powodować).

  4. Jest tylko jedno miejsce, w którym zapisany jest numer wersji, więc jest tylko jedno miejsce, aby go zmienić, gdy zmienia się numer wersji, i istnieje mniejsze prawdopodobieństwo niespójności wersji.

Oto jak to działa: „jednym kanonicznym miejscem” do przechowywania numeru wersji jest plik .py o nazwie „_version.py”, który znajduje się w pakiecie Pythona, na przykład w myniftyapp/_version.py. Ten plik jest modułem Python, ale plik setup.py go nie importuje! (Spowodowałoby to porażkę funkcji 3.) Zamiast tego plik setup.py wie, że zawartość tego pliku jest bardzo prosta, na przykład:

__version__ = "3.6.5"

I tak plik setup.py otwiera plik i analizuje go za pomocą kodu takiego jak:

import re
VERSIONFILE="myniftyapp/_version.py"
verstrline = open(VERSIONFILE, "rt").read()
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
    verstr = mo.group(1)
else:
    raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))

Następnie plik setup.py przekazuje ten ciąg znaków jako wartość argumentu „version” setup(), co spełnia funkcję 2.

Aby spełnić funkcję 1, możesz poprosić pakiet (w czasie wykonywania, a nie w czasie instalacji!) Zaimportować plik _version z myniftyapp/__init__.pynastępującego:

from _version import __version__

Oto przykład tej techniki , której używam od lat.

Kod w tym przykładzie jest nieco bardziej skomplikowany, ale uproszczony przykład, który napisałem w tym komentarzu, powinien być kompletną implementacją.

Oto przykładowy kod importowania wersji .

Jeśli zauważysz coś złego w tym podejściu, daj mi znać.

Zooko
źródło
8
Czy mógłbyś opisać problemy, które motywują # 3? Glyph powiedział, że ma to coś wspólnego z „setuptools lubi udawać, że twój kod nie jest nigdzie w systemie, gdy działa setup.py”, ale szczegóły pomogłyby mi i innym.
Ivan Kozik,
2
@Iva W jakiej kolejności to narzędzie powinno to zrobić? Nie może (w dzisiejszym systemie setuptools / pip / virtualenv) nawet wiedzieć, jakie deps, dopóki nie oceni twojego setup.py. Ponadto, jeśli spróbowałby wykonać pełną głębokość na początku i wykonać wszystkie operacje depresji przed wykonaniem tej operacji, utknąłby, gdyby istniały operacje depresji kołowej. Ale jeśli spróbuje zbudować ten pakiet przed zainstalowaniem zależności, to jeśli zaimportujesz pakiet z twojego setup.py, niekoniecznie będzie w stanie zaimportować jego deps lub odpowiednie wersje swoich deps.
Zooko
3
Czy mógłbyś pisać? plik „version.py” z „setup.py” zamiast go analizować? To wydaje się prostsze.
Jonathan Hartley
3
Jonathan Hartley: Zgadzam się, że napisanie pliku „version.py” zamiast parsowania byłoby nieco łatwiejsze dla pliku „setup.py”, ale otworzyłoby okno niespójności podczas edytowania pliku setup.py mieć nową wersję, ale jeszcze nie uruchomił setup.py, aby zaktualizować plik version.py. Innym powodem umieszczenia wersji kanonicznej w małym osobnym pliku jest to, że ułatwia innym narzędziom, takim jak narzędzia odczytujące stan kontroli wersji, zapisanie pliku wersji.
Zooko
3
Podobne podejście ma miejsce execfile("myniftyapp/_version.py")w pliku setup.py, zamiast próbować ręcznie parsować kod wersji. Sugerowane w stackoverflow.com/a/2073599/647002 - dyskusja też może być pomocna.
medmundowie
97

Przepisane 2017-05

Po ponad dziesięciu latach pisania kodu Python i zarządzania różnymi pakietami doszedłem do wniosku, że majsterkowanie może nie jest najlepszym podejściem.

Zacząłem używać pbrpakietu do zarządzania wersjami w moich pakietach. Jeśli używasz git jako SCM, będzie to pasować do twojej pracy jak magia, oszczędzając twoje tygodnie pracy (będziesz zaskoczony, jak skomplikowany może być problem).

Na dzień dzisiejszy pbr zajmuje 11 miejsce wśród najczęściej używanych pakietów Pythona, a osiągnięcie tego poziomu nie zawierało żadnych brudnych sztuczek: było tylko jedno: naprawienie typowego problemu z pakowaniem w bardzo prosty sposób.

pbr może znieść więcej obciążeń związanych z utrzymaniem pakietu, nie ogranicza się do wersji, ale nie zmusza cię do przyjęcia wszystkich jego zalet.

Aby dać ci wyobrażenie o tym, jak to wygląda zaimplementować pbr w jednym zatwierdzeniu, spójrz na opakowanie pbr

Prawdopodobnie zauważyłeś, że wersja wcale nie jest przechowywana w repozytorium. PBR wykrywa go z gałęzi i tagów Git.

Nie musisz się martwić, co się stanie, gdy nie będziesz mieć repozytorium git, ponieważ pbr „kompiluje” i buforuje wersję podczas pakowania lub instalowania aplikacji, więc nie ma zależności od środowiska uruchomieniowego od git.

Stare rozwiązanie

Oto najlepsze rozwiązanie, jakie do tej pory widziałem, i wyjaśnia również, dlaczego:

Wewnątrz yourpackage/version.py:

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '0.12'

Wewnątrz yourpackage/__init__.py:

from .version import __version__

Wewnątrz setup.py:

exec(open('yourpackage/version.py').read())
setup(
    ...
    version=__version__,
    ...

Jeśli znasz inne podejście, które wydaje się lepsze, daj mi znać.

sorin
źródło
12
Err, nie. execfile () nie istnieje w Pythonie 3, więc lepiej jest użyć exec (open (). read ()).
Christophe Vu-Brugier
4
dlaczego nie from .version import __version__w setup.py?
Aprillion
4
@Aprillion Ponieważ pakiet nie jest ładowany, gdy setup.pyjest uruchomiony - spróbuj, pojawi się błąd (a przynajmniej tak zrobiłem :-))
darthbith
3
Link do pbr powoduje złą bramę.
MERose
4
pbr to bez wątpienia świetne narzędzie, ale nie udało ci się odpowiedzieć na to pytanie. Jak uzyskać dostęp do bieżącej wersji lub zainstalowanego pakietu za pośrednictwem bpr .
nad2000
29

Zgodnie z odroczonym PEP 396 (Numery wersji modułów) istnieje proponowany sposób, aby to zrobić. Opisuje, wraz z uzasadnieniem, (co prawda opcjonalny) standard dla kolejnych modułów. Oto fragment:

3) Gdy moduł (lub pakiet) zawiera numer wersji, wersja POWINNA być dostępna w __version__atrybucie.

4) W przypadku modułów, które znajdują się w pakiecie przestrzeni nazw, moduł POWINIEN zawierać __version__atrybut. Sam pakiet przestrzeni nazw NIE POWINIEN zawierać własnego __version__atrybutu.

5) Wartość __version__atrybutu POWINNA być ciągiem.

Dziwne
źródło
13
Że PEP nie jest akceptowany / standaryzowany, ale odraczany (z powodu braku zainteresowania). Dlatego nieco mylące jest stwierdzenie, że „ istnieje standardowy sposób ” przez nią określony.
tkacz
@weaver: Oh my! Nauczyłem się czegoś nowego. Nie wiedziałem, że muszę to sprawdzić.
Dziwne,
4
Zredagowano, aby zauważyć, że nie jest to standard. Teraz czuję się zawstydzony, ponieważ zgłosiłem prośby o dodanie funkcji do projektów, prosząc ich o przestrzeganie tego „standardu”.
Dziwne,
1
Być może powinieneś przejąć prace normalizacyjne na tym PEP, ponieważ wydajesz się zainteresowany :)
tkacz
To działałoby w przypadku wersjonowania pojedynczego modułu, ale nie jestem pewien, czy miałoby to zastosowanie do wersjonowania pełnego projektu.
Stevoisiak,
21

Chociaż prawdopodobnie jest to zdecydowanie za późno, istnieje nieco prostsza alternatywa dla poprzedniej odpowiedzi:

__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)

(I byłoby dość łatwo przekonwertować automatycznie zwiększające części numerów wersji na ciąg znaków za pomocą str() .)

Oczywiście z tego, co widziałem, ludzie używają podczas używania czegoś takiego jak wcześniej wspomniana wersja __version_info__i jako taki przechowują ją jako krotkę int; nie widzę jednak sensu, aby to robić, ponieważ wątpię, by były sytuacje, w których można by wykonywać operacje matematyczne, takie jak dodawanie i odejmowanie części numerów wersji w jakimkolwiek celu oprócz ciekawości lub automatycznej inkrementacji (a nawet wtedy, int()i str()może być używany dość łatwo). (Z drugiej strony istnieje możliwość, że czyjś kod spodziewa się krotki numerycznej zamiast krotkowej i tym samym zawiedzie.)

Jest to oczywiście mój własny pogląd iz chęcią bym podoba innym wkład w użycie krotki numerycznej.


Jak przypomniał mi Shezi, (leksykalne) porównania ciągów liczbowych niekoniecznie mają taki sam wynik jak bezpośrednie porównania numeryczne; do tego potrzebne byłyby początkowe zera. W końcu przechowywanie __version_info__(lub jakkolwiek by to nazwano) jako krotkę wartości całkowitych pozwoliłoby na bardziej wydajne porównania wersji.

UKŁUCIE
źródło
12
fajnie (+1), ale czy nie wolałbyś liczb zamiast łańcuchów? np.__version_info__ = (1,2,3)
orip
3
Porównanie ciągów może stać się niebezpieczne, gdy numery wersji przekraczają 9, ponieważ na przykład „10” <„2”.
D Coetzee,
13
Robię to również, ale nieco poprawiłem, aby zająć się __version_info__ = (0, 1, 0) __version__ = '.'.join(map(str, __version_info__))
ints
2
Problem w __version__ = '.'.join(__version_info__)tym __version_info__ = ('1', '2', 'beta'), że stanie się 1.2.beta, nie 1.2betalub1.2 beta
nagisa
4
Myślę, że problem z tym podejściem polega na tym, gdzie umieścić wiersze kodu deklarującego __version__. Jeśli znajdują się w pliku setup.py, Twój program nie może zaimportować ich ze swojego pakietu. Być może nie jest to dla ciebie problem, w takim przypadku w porządku. Jeśli przejdą do twojego programu, plik setup.py może je zaimportować, ale nie powinien, ponieważ plik setup.py jest uruchamiany podczas instalacji, gdy zależności programu nie są jeszcze zainstalowane (plik setup.py służy do określenia, jakie są zależności .) Stąd odpowiedź Zooko: ręcznie parsuj wartość z pliku źródłowego produktu, bez importowania pakietu produktu
Jonathan Hartley
11

Wiele z tych rozwiązań ignoruje gittagi wersji, co wciąż oznacza, że ​​musisz śledzić wersję w wielu miejscach (źle). Do tego podszedłem z następującymi celami:

  • Wszystkie odniesienia do wersji Pythona uzyskaj ze znacznika w gitrepozytorium
  • Zautomatyzuj git tag/ pushi setup.py uploadkroki za pomocą jednego polecenia, które nie wymaga żadnych danych wejściowych.

Jak to działa:

  1. Z make releasepolecenia ostatnia otagowana wersja w repozytorium git zostaje znaleziona i zwiększona. Tag został zepchnięty z powrotem do origin.

  2. W Makefilesklepach wersja w src/_version.pyktórym zostanie on odczytany przez setup.py, a także zawarte w komunikacie. Nie sprawdzaj _version.pykontroli źródła!

  3. setup.pypolecenie odczytuje ciąg nowej wersji z package.__version__.

Detale:

Makefile

# remove optional 'v' and trailing hash "v1.0-N-HASH" -> "v1.0-N"
git_describe_ver = $(shell git describe --tags | sed -E -e 's/^v//' -e 's/(.*)-.*/\1/')
git_tag_ver      = $(shell git describe --abbrev=0)
next_patch_ver = $(shell python versionbump.py --patch $(call git_tag_ver))
next_minor_ver = $(shell python versionbump.py --minor $(call git_tag_ver))
next_major_ver = $(shell python versionbump.py --major $(call git_tag_ver))

.PHONY: ${MODULE}/_version.py
${MODULE}/_version.py:
    echo '__version__ = "$(call git_describe_ver)"' > $@

.PHONY: release
release: test lint mypy
    git tag -a $(call next_patch_ver)
    $(MAKE) ${MODULE}/_version.py
    python setup.py check sdist upload # (legacy "upload" method)
    # twine upload dist/*  (preferred method)
    git push origin master --tags

releaseCel zawsze zwiększa 3rd wersji cyfrę, ale można użyć next_minor_verlub next_major_ver, aby zwiększyć pozostałych cyfr. Polecenia polegają na versionbump.pyskrypcie sprawdzonym w katalogu głównym repozytorium

versionbump.py

"""An auto-increment tool for version strings."""

import sys
import unittest

import click
from click.testing import CliRunner  # type: ignore

__version__ = '0.1'

MIN_DIGITS = 2
MAX_DIGITS = 3


@click.command()
@click.argument('version')
@click.option('--major', 'bump_idx', flag_value=0, help='Increment major number.')
@click.option('--minor', 'bump_idx', flag_value=1, help='Increment minor number.')
@click.option('--patch', 'bump_idx', flag_value=2, default=True, help='Increment patch number.')
def cli(version: str, bump_idx: int) -> None:
    """Bumps a MAJOR.MINOR.PATCH version string at the specified index location or 'patch' digit. An
    optional 'v' prefix is allowed and will be included in the output if found."""
    prefix = version[0] if version[0].isalpha() else ''
    digits = version.lower().lstrip('v').split('.')

    if len(digits) > MAX_DIGITS:
        click.secho('ERROR: Too many digits', fg='red', err=True)
        sys.exit(1)

    digits = (digits + ['0'] * MAX_DIGITS)[:MAX_DIGITS]  # Extend total digits to max.
    digits[bump_idx] = str(int(digits[bump_idx]) + 1)  # Increment the desired digit.

    # Zero rightmost digits after bump position.
    for i in range(bump_idx + 1, MAX_DIGITS):
        digits[i] = '0'
    digits = digits[:max(MIN_DIGITS, bump_idx + 1)]  # Trim rightmost digits.
    click.echo(prefix + '.'.join(digits), nl=False)


if __name__ == '__main__':
    cli()  # pylint: disable=no-value-for-parameter

To sprawia, że ​​ciężkie podnoszenie, jak przetwarzać i zwiększać numer wersji git.

__init__.py

my_module/_version.pyPlik jest importowany do my_module/__init__.py. Umieść tutaj dowolną konfigurację instalacji statycznej, którą chcesz dystrybuować z modułem.

from ._version import __version__
__author__ = ''
__email__ = ''

setup.py

Ostatnim krokiem jest odczytanie informacji o wersji z my_modulemodułu.

from setuptools import setup, find_packages

pkg_vars  = {}

with open("{MODULE}/_version.py") as fp:
    exec(fp.read(), pkg_vars)

setup(
    version=pkg_vars['__version__'],
    ...
    ...
)

Oczywiście, aby wszystko to działało, musisz mieć co najmniej jeden tag wersji w repozytorium, aby rozpocząć.

git tag -a v0.0.1
cmcginty
źródło
1
faktycznie - cały ten wątek zapomina, że ​​VCS jest bardzo ważny w tej dyskusji. tylko obs: inkrementacja wersji powinna pozostać procesem ręcznym, więc preferowanym sposobem byłoby 1. ręczne utworzenie i pchnięcie tagu 2. pozwól narzędziom VCS odkryć ten tag i przechowywać go w razie potrzeby (wow - ten interfejs edycji SO naprawdę mnie paraliżuje - Musiałem edytować to kilkanaście razy tylko po to, by poradzić sobie z nowymi
liniami, A JESZCZE
Nie trzeba używać, versionbump.pygdy mamy niesamowity pakiet bumpversion dla Pythona.
Oran
@Oran Spojrzałem na wersjębump. Dokumenty nie są bardzo jasne i nie radzą sobie zbyt dobrze z tagowaniem. W moich testach wydaje się, że przechodzi w stany, które powodują awarię: subprocess.CalledProcessError: Command '[' git ',' commit ',' -F ',' / var / folder / rl / tjyk4hns7kndnx035p26wg692g_7t8 / T / tmppishngbo '] 'zwrócił niezerowy status wyjścia 1.
cmcginty
1
Dlaczego nie _version.pynależy śledzić za pomocą kontroli wersji?
Stevoisiak,
1
@StevenVascellaro To komplikuje proces wydania. Teraz musisz się upewnić, że tagi i zatwierdzenia odpowiadają wartości w _version.py. Użycie tagu jednej wersji jest czystszym IMO i oznacza, że ​​możesz utworzyć wydanie bezpośrednio z interfejsu użytkownika github.
cmcginty,
10

Używam pliku JSON w pakiecie reż. To pasuje do wymagań Zooko.

Wewnątrz pkg_dir/pkg_info.json:

{"version": "0.1.0"}

Wewnątrz setup.py:

from distutils.core import setup
import json

with open('pkg_dir/pkg_info.json') as fp:
    _info = json.load(fp)

setup(
    version=_info['version'],
    ...
    )

Wewnątrz pkg_dir/__init__.py:

import json
from os.path import dirname

with open(dirname(__file__) + '/pkg_info.json') as fp:
    _info = json.load(fp)

__version__ = _info['version']

Podaję także inne informacje pkg_info.json, takie jak autor. Lubię używać JSON, ponieważ mogę zautomatyzować zarządzanie metadanymi.

Andy Lee
źródło
Czy mógłbyś wyjaśnić, w jaki sposób używać JSON do automatyzacji zarządzania metadanymi? Dzięki!
ryanjdillon
6

Warto również zauważyć, że oprócz tego, że __version__jest się pół-standardowym. w pythonie jest __version_info__to krotka, w prostych przypadkach możesz po prostu zrobić coś takiego:

__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])

... i możesz pobrać __version__ciąg z pliku lub cokolwiek innego.

James Antill
źródło
1
Czy masz jakieś odniesienia / linki dotyczące pochodzenia tego użycia __version_info__?
Craig McQueen,
3
Jest to to samo mapowanie co sys.version na sys.version_info. Tak więc: docs.python.org/library/sys.html#sys.version_info
James Antill
7
Łatwiej jest wykonać mapowanie w innym kierunku ( __version_info__ = (1, 2, 3)i __version__ = '.'.join(map(str, __version_info__))).
Eric O Lebigot,
2
@EOL - __version__ = '.'.join(str(i) for i in __version_info__)- nieco dłuższy, ale bardziej pythoniczny.
ArtOfWarfare
2
Nie jestem pewien, czy to, co proponujesz, jest wyraźnie bardziej pytoniczne, ponieważ zawiera zmienną fikcyjną, która nie jest tak naprawdę potrzebna i której znaczenie jest trochę trudne do wyrażenia ( inie ma znaczenia, version_numjest trochę długie i dwuznaczne…). Uważam nawet istnienie map()w Pythonie za silną wskazówkę, że należy go tutaj użyć, ponieważ musimy tutaj zrobić typowy przypadek map()użycia (użycie z istniejącą funkcją) - nie widzę wielu innych uzasadnionych zastosowań.
Eric O Lebigot
5

Wydaje się, że nie ma standardowego sposobu na osadzenie ciągu wersji w pakiecie Pythona. Większość pakietów, które widziałem, używa jakiegoś wariantu twojego rozwiązania, np. Eitner

  1. Osadź wersję setup.pyi setup.pywygeneruj moduł (np. version.py) Zawierający tylko informacje o wersji, który jest importowany przez twój pakiet lub

  2. Odwrotnie: umieść informacje o wersji w samym pakiecie i zaimportuj ją , aby ustawić wersję setup.py

dF.
źródło
Podoba mi się twoje zalecenie, ale jak wygenerować ten moduł z setup.py?
sorin
1
Podoba mi się pomysł opcji (1), wydaje się to prostsze niż odpowiedź Zooko na parsowanie numeru wersji z pliku. Ale nie możesz zagwarantować, że wersja.py zostanie utworzona, gdy programista po prostu sklonuje twoje repozytorium. O ile nie zalogujesz się w wersji.py, co otworzy małe zmarszczki, że możesz zmienić numer wersji w setup.py, zatwierdzić, zwolnić, a następnie (ukośnik zapomnieć) zatwierdzić zmianę w wersji.py.
Jonathan Hartley
Przypuszczalnie możesz wygenerować moduł używając czegoś takiego jak „” z otwartym („mypackage / version.py”, „w”) jako fp: fp.write („__ wersja__ == '% s' \ n"% (__version__,) ) „” „
Jonathan Hartley
1
Myślę, że opcja 2. może ulec awarii podczas instalacji, jak zauważono w komentarzach do odpowiedzi JAB.
Jonathan Hartley
Co z tym? Umieszczasz __version__ = „0.0.1” ”(gdzie wersja jest oczywiście ciągiem znaków) w __init__.py” głównego pakietu oprogramowania. Następnie przejdź do punktu 2: W instalacji wykonujesz z pakietu .__ init__ import __version__ jako v, a następnie ustawiasz zmienną v jako wersję pliku setup.py. Następnie zaimportuj mypack jako mój, wydrukuj moją wersję .__ wersja__ wydrukuje wersję. Wersja będzie przechowywana tylko w jednym miejscu, dostępnym z całego kodu, w pliku, który nie importuje niczego innego związanego z oprogramowaniem.
SEF,
5

strzała radzi sobie z tym w ciekawy sposób.

Teraz (od 2e5031b )

W arrow/__init__.py:

__version__ = 'x.y.z'

W setup.py:

from arrow import __version__

setup(
    name='arrow',
    version=__version__,
    # [...]
)

Przed

W arrow/__init__.py:

__version__ = 'x.y.z'
VERSION = __version__

W setup.py:

def grep(attrname):
    pattern = r"{0}\W*=\W*'([^']+)'".format(attrname)
    strval, = re.findall(pattern, file_text)
    return strval

file_text = read(fpath('arrow/__init__.py'))

setup(
    name='arrow',
    version=grep('__version__'),
    # [...]
)
Anto
źródło
co jest file_text?
ely
2
zaktualizowane rozwiązanie jest w rzeczywistości szkodliwe. Gdy plik setup.py jest uruchomiony, niekoniecznie zobaczy wersję pakietu z lokalnej ścieżki pliku. Może wrócić do poprzednio zainstalowanej wersji, np. Z działania pip install -e .w gałęzi programistycznej lub coś innego podczas testowania. setup.py absolutnie nie powinien polegać na importowaniu pakietu, który jest w trakcie instalowania, aby określić parametry wdrożenia. Yikes.
ely
Tak, nie wiem, dlaczego strzałka postanowiła zrezygnować z tego rozwiązania. Ponadto komunikat zatwierdzenia mówi „Zaktualizowano setup.py ze współczesnymi standardami Python ” ... 🤷
Anto
4

Widziałem też inny styl:

>>> django.VERSION
(1, 1, 0, 'final', 0)
L1ker
źródło
1
Tak, też widziałem. BTW każda odpowiedź ma inny styl, więc teraz nie wiem, który styl jest „standardem”. Szukam wspomnianych PEP ...
kbec
Widzimy inny sposób; Klient Python Mongo używa zwykłej wersji, bez podkreślników. To działa; $ python >>> import pymongo >>> pymongo.version '2.7'
AnneTheAgile
Implementacja .VERSIONnie oznacza, że ​​nie musisz implementować __version__.
Acumenus
Czy wymaga to wdrożenia djangow projekcie?
Stevoisiak,
3

Korzystanie setuptoolsipbr

Nie ma standardowego sposobu zarządzania wersją, ale standardowy sposób zarządzania pakietami to setuptools.

Najlepszym rozwiązaniem, jakie znalazłem ogólnie do zarządzania wersją, jest użycie setuptoolsz pbrrozszerzeniem. To jest teraz mój standardowy sposób zarządzania wersją.

Ustawienie projektu na pełne pakowanie może być przesadą w przypadku prostych projektów, ale jeśli potrzebujesz zarządzać wersją, prawdopodobnie jesteś na odpowiednim poziomie, aby wszystko skonfigurować. Dzięki temu Twój pakiet można zwolnić w PyPi, dzięki czemu każdy może go pobrać i używać z Pip.

PBR przenosi większość metadanych z setup.pynarzędzi do setup.cfgpliku, który jest następnie wykorzystywany jako źródło większości metadanych, które mogą obejmować wersję. Umożliwia to spakowanie metadanych do pliku wykonywalnego przy użyciu czegoś takiego jak w pyinstallerrazie potrzeby (jeśli tak, prawdopodobnie będziesz potrzebować tych informacji ) i oddziela metadane od innych skryptów zarządzania / konfiguracji pakietu. Możesz bezpośrednio zaktualizować ciąg wersji setup.cfgręcznie, a zostanie on pobrany do *.egg-infofolderu podczas budowania wydań pakietu. Skrypty mogą następnie uzyskać dostęp do wersji z metadanych przy użyciu różnych metod (procesy te opisano w poniższych sekcjach).

Podczas korzystania z Git dla VCS / SCM ta konfiguracja jest jeszcze lepsza, ponieważ pobierze wiele metadanych z Git, dzięki czemu Twoje repozytorium może być głównym źródłem prawdy dla niektórych metadanych, w tym wersji, autorów, dzienników zmian, itp. Dla konkretnej wersji utworzy ciąg wersji dla bieżącego zatwierdzenia na podstawie znaczników git w repozytorium.

Ponieważ PBR pobierze wersję, autora, dziennik zmian i inne informacje bezpośrednio z repozytorium git, więc niektóre metadane setup.cfgmożna pominąć i wygenerować automatycznie za każdym razem, gdy tworzona jest dystrybucja pakietu (za pomocą setup.py)

Aktualna wersja w czasie rzeczywistym

setuptoolspobierze najnowsze informacje w czasie rzeczywistym, używając setup.py:

python setup.py --version

Spowoduje to pobranie najnowszej wersji z setup.cfgpliku lub z repozytorium git na podstawie ostatniego zatwierdzenia i znaczników istniejących w repozytorium. To polecenie nie aktualizuje jednak wersji w dystrybucji.

Aktualizacja wersji

Po utworzeniu dystrybucji za pomocą setup.py( py setup.py sdistnp.) Wszystkie bieżące informacje zostaną wyodrębnione i zapisane w dystrybucji. Zasadniczo uruchamia to setup.py --versionpolecenie, a następnie przechowuje informacje o wersji w package.egg-infofolderze w zestawie plików przechowujących metadane dystrybucji.

Uwaga na temat procesu aktualizacji metadanych wersji:

Jeśli nie używasz pbr do pobierania danych wersji z git, po prostu zaktualizuj plik setup.cfg bezpośrednio informacjami o nowej wersji (dość łatwe, ale upewnij się, że jest to standardowa część procesu wydania).

Jeśli używasz git i nie musisz tworzyć dystrybucji źródłowej lub binarnej (za pomocą python setup.py sdistlub jednego z python setup.py bdist_xxxpoleceń), najprostszym sposobem na aktualizację informacji git repo w <mypackage>.egg-infofolderze metadanych jest po prostu uruchomienie python setup.py installpolecenia. Spowoduje to uruchomienie wszystkich funkcji PBR związanych z pobieraniem metadanych z repozytorium git i aktualizację lokalnego .egg-infofolderu, zainstalowanie plików wykonywalnych skryptu dla wszystkich zdefiniowanych przez ciebie punktów wejścia oraz innych funkcji, które można zobaczyć z danych wyjściowych po uruchomieniu tego polecenia.

Zauważ, że .egg-infofolder jest generalnie wykluczony z przechowywania w samym repozytorium git w standardowych .gitignoreplikach Pythona (takich jak Gitignore.IO ), ponieważ można go wygenerować ze źródła. Jeśli jest to wykluczone, upewnij się, że masz standardowy „proces wydawania”, aby zaktualizować metadane lokalnie przed wydaniem, a każdy pakiet przesyłany do PyPi.org lub w inny sposób rozpowszechniany musi zawierać te dane, aby mieć poprawną wersję. Jeśli chcesz, aby repozytorium Git zawierało te informacje, możesz wykluczyć ignorowanie określonych plików (np. Dodać !*.egg-info/PKG_INFOdo .gitignore)

Dostęp do wersji ze skryptu

Możesz uzyskać dostęp do metadanych z bieżącej wersji w skryptach Python w samym pakiecie. Na przykład w przypadku wersji istnieje kilka sposobów, aby to zrobić:

## This one is a new built-in as of Python 3.8.0 should become the standard
from importlib-metadata import version

v0 = version("mypackage")
print('v0 {}'.format(v0))

## I don't like this one because the version method is hidden
import pkg_resources  # part of setuptools

v1 = pkg_resources.require("mypackage")[0].version
print('v1 {}'.format(v1))

# Probably best for pre v3.8.0 - the output without .version is just a longer string with
# both the package name, a space, and the version string
import pkg_resources  # part of setuptools

v2 = pkg_resources.get_distribution('mypackage').version
print('v2 {}'.format(v2))

## This one seems to be slower, and with pyinstaller makes the exe a lot bigger
from pbr.version import VersionInfo

v3 = VersionInfo('mypackage').release_string()
print('v3 {}'.format(v3))

Możesz umieścić jeden z nich bezpośrednio w swoim __init__.pypakiecie, aby wyodrębnić informacje o wersji w następujący sposób, podobnie do niektórych innych odpowiedzi:

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version
LightCC
źródło
Ponownie sformatowane głosowanie w dół, aby bezpośrednio odpowiedzieć na pytanie.
LightCC
1

Po kilku godzinach prób znalezienia najprostszego niezawodnego rozwiązania, oto części:

utwórz plik version.py Wewnątrz folderu paczki „/ mypackage”:

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '1.2.7'

w setup.py:

exec(open('mypackage/version.py').read())
setup(
    name='mypackage',
    version=__version__,

w głównym folderze init .py:

from .version import __version__

exec()Funkcja uruchamia skrypt poza jakichkolwiek importu, ponieważ setup.py jest prowadzony przed moduł może być importowany. Nadal wystarczy zarządzać numerem wersji w jednym pliku w jednym miejscu, ale niestety nie ma go w pliku setup.py. (to wada, ale brak błędów importu to wada)

Marc Maxmeister
źródło
1

Wiele pracy nad jednolitym wersjonowaniem i wspieraniem konwencji zostało zakończonych od pierwszego pytania . Smaczne opcje są teraz szczegółowo opisane w Podręczniku użytkownika Python Packaging . Warto również zauważyć, że schematy numerów wersji są stosunkowo surowe w Pythonie na PEP 440 , więc utrzymanie rozsądku jest niezwykle ważne, jeśli pakiet zostanie wydany w sklepie z serami .

Oto skrócony podział opcji wersji:

  1. Przeczytaj plik w setup.py( setuptools ) i pobierz wersję.
  2. Użyj zewnętrznego narzędzia do budowania (aby zaktualizować zarówno __init__.pykontrolę źródła, jak i bump2version , zmiany lub zest.releaser .
  3. Ustaw wartość na __version__zmienną globalną w określonym module.
  4. Umieść wartość w prostym pliku tekstowym WERSJI, aby plik setup.py i kod mogły zostać odczytane.
  5. Ustaw wartość za pomocą setup.pywydania i użyj importlib.metadata, aby pobrać ją w czasie wykonywania. (Uwaga, istnieją wersje wcześniejsze niż 3.8 i nowsze).
  6. Ustaw wartość __version__w sample/__init__.pyi importu w próbce setup.py.
  7. Użyj setuptools_scm, aby wyodrębnić kontrolę wersji z kontroli źródła, aby była to referencja kanoniczna, a nie kod.

Zauważ, że (7) może być najnowocześniejszym podejściem (metadane kompilacji są niezależne od kodu publikowanego przez automatyzację). Również Uwaga , że jeśli ustawienie służy do wydania pakietu że prosty python3 setup.py --versionzgłosi wersję bezpośrednio.

ingyhere
źródło
-1

Jeśli chodzi o to, co jest warte, jeśli używasz distutils NumPy, numpy.distutils.misc_util.Configurationma make_svn_version_py()metodę, która osadza numer zmiany package.__svn_version__w zmiennej version.

Matt
źródło
Czy możesz podać więcej szczegółów lub przykład, jak to by działało?
Stevoisiak,
Hmm W 2020 r. Jest to (zawsze było?) Przeznaczenie dla FORTRAN . Pakiet „numpy.distutils jest częścią NumPy rozszerzającą standardowe distutils Pythona do obsługi źródeł Fortran.”
ingyhere
-1
  1. Używaj version.pypliku tylko z __version__ = <VERSION>parametrami w pliku. W setup.pypliku zaimportuj __version__parametr i umieść jego wartość w setup.pypliku w następujący sposób: version=__version__
  2. Innym sposobem jest użycie tylko setup.pypliku z version=<CURRENT_VERSION>- CURRENT_VERSION jest zakodowany na stałe.

Ponieważ nie chcemy ręcznie zmieniać wersji w pliku za każdym razem, gdy tworzymy nowy znacznik (gotowy do wydania nowej wersji pakietu), możemy użyć następujących ...

Bardzo polecam pakiet bumpversion . Używam go od lat do wypuszczania wersji.

zacznij od dodania version=<VERSION>do setup.pypliku, jeśli jeszcze go nie masz.

Za każdym razem, gdy wybierasz wersję, powinieneś używać takiego krótkiego skryptu:

bumpversion (patch|minor|major) - choose only one option
git push
git push --tags

Następnie dodaj jeden plik do repozytorium o nazwie .bumpversion.cfg:

[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = {new_version}
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]

Uwaga:

  • Możesz użyć __version__parametru w version.pypliku tak, jak sugerowano to w innych postach i zaktualizować plik bumpversion w następujący sposób: [bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
  • Państwo musi git commit albo git resetwszystko w swoim repo, inaczej dostaniesz brudne błąd repo.
  • Upewnij się, że środowisko wirtualne zawiera pakiet bumpversion, bez niego nie będzie działać.
Oran
źródło
@cmcginty Przepraszam za opóźnienie, sprawdź moją odpowiedź ^^^ - pamiętaj, że musisz zatwierdzić lub zresetować wszystko w repozytorium i upewnić się, że środowisko wirtualne zawiera pakiet bumpversion, bez niego nie będzie działać. Użyj najnowszej wersji.
Oran
Nie jestem pewien, jakie rozwiązanie jest tutaj sugerowane. Czy zalecamy ręczne śledzenie wersji za pomocą version.py, czy śledzenie jej za pomocą bumpversion?
Stevoisiak
@StevenVascellaro Sugeruję używanie wersji bumpversion, nigdy nie używaj ręcznej wersji. To, co próbowałem wyjaśnić, to to, że możesz skierować bumpversion do aktualizacji wersji w pliku setup.py lub jeszcze lepiej użyć go do aktualizacji pliku version.py. Częstszą praktyką jest aktualizacja pliku version.py i przeniesienie __version__wartości parametru do pliku setup.py. Moje rozwiązanie jest wykorzystywane w produkcji i jest to powszechna praktyka. Uwaga: dla jasności użycie bumpversion jako części skryptu jest najlepszym rozwiązaniem, włóż go do CI i będzie to działanie automatyczne.
Oran
-3

Jeśli używasz CVS (lub RCS) i chcesz szybkiego rozwiązania, możesz użyć:

__version__ = "$Revision: 1.1 $"[11:-2]
__version_info__ = tuple([int(s) for s in __version__.split(".")])

(Oczywiście numer wersji zostanie zastąpiony przez CVS.)

Daje to wersję do wydruku i informacje o wersji, których możesz użyć, aby sprawdzić, czy importowany moduł ma co najmniej oczekiwaną wersję:

import my_module
assert my_module.__version_info__ >= (1, 1)
Martin Ibert
źródło
W jakim pliku zalecasz zapisywanie zapisywania __version__? Jak zwiększyć numer wersji za pomocą tego rozwiązania?
Stevoisiak,