PyPi / Versionierung modularer Scripts?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

Hallo, warum läuft hier eigentlich kein Python-basiertes Forum?,

ich bin Python-Neuling. Ich arbeite seit Jahren hauptberuflich als PHP- und JS-/CSS-Heini, lerne aber gern mal neuen Firlefanz. Vor ein paar Wochen habe ich mir, um mein tägliches Hantieren mit JavaScript-Bibliotheken zu vereinfachen, eigentlich ein lokales Javascript schreiben wollen, dann aber festgestellt, dass Python zum Parsen von Websites offenbar viel besser geeignet ist. Seitdem lese ich mich immer tiefer ein und kann mich des Eindrucks nicht erwehren, dass die Sprache trotz der fehlenden Klammern gewisse Vorzüge gegenüber meiner "Heimatsprache" PHP bietet. Aber ich schweife von meinem Hauptanliegen ab:

Mein Skript besteht im Wesentlichen aus etwas Pythoncode und jeder Menge JSON-Dateien (so eine Art Wörterbuch), die sich theoretisch jederzeit ändern/vermehren können. Allmählich hat es einen Stand erreicht, an dem ich den nächsten Punkt meiner Python-Erkundungstour abhaken möchte, nämlich die Veröffentlichung auf PyPi. Aktuell laufen Installation und (immerhin automatisches) Update per git-Aufruf, was vielleicht nicht ganz optimal ist.

Dafür muss ich mir aber eine Version ausdenken, und das ist das Problem: Das Skript selbst kann jahrelang unverändert bleiben, es könnte nur neue JSON-Dateien erhalten. Wenn ich dafür jedes Mal die Versionsnummer erhöhe, bin ich womöglich sehr schnell bei Version 57.0.0. Oder 1.0.57. :K

Gibt es da eine elegante Lösung? Kann ich vielleicht einfach die JSON-Dateien von irgendwo "nachladen"?
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@grum.py: die Vorteile von PHP gegenüber Python lassen sich an einem halben Finger abzählen, bei anderen Sprachen braucht man da immerhin eine Hand.
Um was für ein Programm handelt es sich denn? Für was sind die JSON-Dateien gut? Warum gehören die zum Programm-Code?
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

Außerhalb der Webentwicklung ist PHP noch so'ne Sache. Es wurde ja für Webkrams entwickelt, ist somit eher mit ASP.net als mit Perl/Python/Ruby zu vergleichen, richtig?

Es ist, mehr oder weniger, ein Skript, das für mich nachguckt, ob meine JS-Bibliotheken ausgetauscht werden sollten.
https://github.com/dertuxmalwieder/libcheck.py

Wahrscheinlich sind da trotz mehrfacher Revision noch Anfängerfehler drin, aber darum soll's nicht gehen. Die JSON-Dateien in /defs gehören nicht zum Programmcode, aber sind notwendiger Bestandteil des Skripts.
BlackJack

@grum.py: Erwartest Du da tatsächlich so viele Änderungen? Oder wolltest Du nach jeder einzelnen hinzugefügten JavaScript-Bibliothek eine neue Version Deines Programms veröffentlichen?
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

Ich hab' da nur Scripts drin, die ich kenne und/oder selbst nutze. Ich gehe fest davon aus, dass früher oder später ein Schwung von neuen Scripts kommen wird - oder dass irgendwelche mal ein Update brauchen, neue Website oder sowas. Und dann? :K
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Warum nicht die Versionsnummer immer wieder hochzählen?!?

Ist halt die Frage, wie man sie oganisiert...

Ich mache es meist so: Bsp: v1.2.3

Dabei wird:
v1.2.3 nur dann geändert, wenn das Projekt quasi einem rewrite gleichkommt ;)
v1.2.3 wird erhöht, wenn sich die API ändert, externe, die das Projekt nutzen, irgendwas machen müßen
v1.2.3 und zuletzt wird immer beim neuen PyPi Upload erhöht...

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

Weil das bei einem (optimistischen) Ansturm an neuen Definitionsdateien sehr bald Version 1.0.3346064564 werden könnte. Gut, 57 war realistischer...

Und eigentlich müssen Leute ja bei jedem neuen Definitionsupdate was machen, nämlich die Definitionen neu runterladen. Damit erhöht sich doch mindestens immer die zweite und nicht die dritte Stelle, oder?
BlackJack

@grum.py: Wie gesagt, das ist eine Frage davon was bei Dir ein Update auslöst. Man kann auch einmal im Monat schauen ob genug zusammengekommen ist um eine neue Version heraus zu bringen.

Nach meiner Lesart reden wir über die dritte Stelle in der Versionsnummer, denn der Benutzer muss ja an der Art wie er das verwendet nichts ändern, das sind ja nur kleine „patchlevel“ Updates die keinerlei neue Funktionalität liefern.

Andere Projekte die regelmässig Datenaktualisierungen haben verwenden eine datumsbasierte Versionierung. `pytz` zum Beispiel ist gerade bei Version 2015.2 weil im Februar dieses Jahres eine neue Version heraus kam weil sich die Datenbasis verändert hat.
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

So weit ich gelesen habe, hätte PyPi aber gern eine "semantische" Versionsnummer. 2015.2.12 für das Update von 12. Februar wäre natürlich machbar, aber verstößt gegen diese Regel. Oder habe ich da was missverstanden? :K

Wie macht pytz das mit den Daten?
BlackJack

@grum.py: `pytz` ist eine Python-Implementierung von Olson's Timezone Database, das braucht man wenn man mit Datumsangaben rechnen möchte und dabei Zeitzonen und deren Besonderheiten berücksichtigen will. Vieles davon wird ja in einzelnen Ländern per Gesetz geregelt und das ändert sich auch ab und zu mal, weshalb regelmässig Ergänzungen an der Datenbasis vorgenommen werden müssen.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Welche Rolle spielt dass wie hoch die Versionsnummer wird? Selbst wenn du Version 0.1.325 erreichst ist dass doch kein Problem.
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

Ich find' das nicht hübsch. Das sieht so unfertig aus. Daher ja meine Frage, ob man das auch irgendwie so packen kann, dass man geänderte Definitionsdateien nicht jedes Mal neu verpacken und die Version hochzählen muss.

Ein Versionsschema, das man dem Nutzer erst erklären muss, ist ein blödes.
BlackJack

@grum.py: Was muss man denn da erklären? Das wäre doch eine ganz gewöhnliche semantisches Versionierung wie sie viele, viele andere Packages/Module auch verwenden. Das einzige „ungewöhnliche“ wäre das das „patchlevel“ etwas höher wird als bei den meisten anderen, aber das muss man doch keinem erklären solange derjenige Versionsnummern grundsätzlich verstanden hat, also das x.y.(z + 1) eine grössere/neuere Version als x.y.z ist.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Eine Versionsnummer soll ja nicht irgendwelchen ästhetischen Zwecken gerecht werden, sondern eine sinnvolle Einordung einer neuen Version erlauben. Insofern ist major.minor.patch, wobei major rückwärtsinkompatibel ist, minor neue Features bringt und patch alles andere repräsentiert eigentlich dass einzig sinnvolle Schema. Es sei den Releases sind nicht an Features sondern an Zeiträume gebunden siehe z.B. Ubuntu.

Übrigens gibt es inzwischen auch Ansätze bei jedem Commit automatisiert ein neues Release zu erzeugen. Bei einer solchen Vorgehensweise kommen noch wesentlich "unfertigere" Versionsnummern zustande an denen sich auch niemand stört.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Klar mittlerweile wird so was wie v0.1.43 als unfertig angesehen... Wenn das stört kann man auch einfach v1 machen, sobalt das Projekt sinnvoll nutzbar ist... :wink:

Ich finde es unästhetisch, wenn die erste Zahl zu noch wird... Wie z.b. bei firefox.... Pip macht es anscheinend ebenfalls so...

Dumm dabei finde ich außerdem, das man so keine grossen Änderungen erkennen kann...

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

DasIch hat geschrieben: Insofern ist major.minor.patch, wobei major rückwärtsinkompatibel ist, minor neue Features bringt und patch alles andere repräsentiert eigentlich dass einzig sinnvolle Schema.
Womit ich wieder das Problem habe, dass neue Definitionsdateien eigentlich neue Features sind und das Skript selbst so ziemlich fertig ist. :K
DasIch hat geschrieben:Übrigens gibt es inzwischen auch Ansätze bei jedem Commit automatisiert ein neues Release zu erzeugen.
Das klingt gut. (Ich committe ja beinahe nur, wenn ich weiß, dass das, was ich da gebaut habe, auch funktioniert.) Gibt es einen Link dazu?
BlackJack

@grum.py: Definitionsdateien sind keine neuen Features. Das sind doch bloss Daten, das Programm ändert sich nicht, seine Funktionalität ändert sich nicht, der Benutzer muss beim Aufruf des Programms gegenüber der Vorgängerversion nichts beachten und hat auch keine neuen Einstellungen oder Kommandozeilenoptionen.

Ein paar Anmerkungen zum Code:

Beim Quelltext fällt als erstes auf das die Einrückung von den üblichen vier Leerzeichen pro Ebene abweicht.

Es gibt in der Standardbibliothek ein `logging`-Modul, da sollte man sich keine eigenen `debuglog()`-Funktionen basteln müssen.

Die `main()` gibt entweder eine Zahl oder `None` zurück, das ist unschön. Und im *Fehlerfall* sollte ein Prozess nicht den Exit-Code 0 zurückgeben, denn das ist der Code für „lief fehlerfrei“.

Ein nacktes ``except:`` ohne eine konkrete Ausnahme ist in der Regel keine vernünftige Ausnahmebehandlung. Damit kann man sich sehr schwer zu findende Fehler einhandeln weil *alle* Ausnahmen gleich behandelt werden, auch solche mit denen man an der Stelle gar nicht gerechnet hat. Ähnliches gilt für `Exception` — das ist sehr schwammig, da sollte man mindestens den kompletten Traceback protokollieren.

Was soll das `h` bei `hConfig` bedeuten? Abkürzungen und kryptische Prä- und Postfixe bei Namen sind nicht so toll.

Die Aufteilung des Codes ist eigenartig. Die Funktionen werden nicht nacheinander aufgerufen sondern es ist eine Aufrufkette so das `parse_lib()` deutlich mehr macht als einfach nur zu Parsen und ein Ergebnis zu liefern sondern auch die Ausgabe aufruft, die wiederum einen Wahrheitswert liefert was man von einer reinen Ausgabefunktion nicht erwarten würde. In der Ausgabefunktion werden auch Entscheidungen getroffen die eigentlich *vor* der Ausgabefunktion stattfinden sollten. Warum heist das erste Argument `thislib`, also was hat das `this` dort zu suchen? Die Funktion gibt auch wieder entweder einen Wahrheitswert oder `None` zurück → komische API. Statt ``matches[0]`` für die Versionsnummer zu nehmen wäre eine benannte Gruppe besser, dann muss man keine Klammern zählen oder darauf achten wo man „non capturing“ Gruppen verwendet. Wobei ich gerade sehe das `findall()` verwendet wird, davon dann immer nur der erste Treffer — womit `findall()` irgendwie unsinnig ist.

Der erste Kommentar in `parse_library()` ist inhaltlich falsch und zwei von drei Kommentaren in ``except``-Zweigen weil die offenbar einfach *kopiert* worden sind und inhaltlich nicht mehr zu der neuen Code-Stelle passen.

Pfadteile sollte man mit `os.path.join()` zusammensetzen. Und den gleichen Pfad sollte man nur einmal zusammensetzen und nicht mehrfach.

``pass`` macht nur in ansonsten leeren Blöcken Sinn. Und an der Stelle sollte ganz bestimmt auch etwas anderes passieren, denn wenn die Webseite nicht geladen werden konnte, dann läuft der nachfolgende Code unweigerlich in einen `NameError` weil `matches` dann nicht definiert ist, was dann von einem wieder sehr groben ``except Exception`` „behandelt“ wird.

Und auch die `parse_library()`-Funktion gibt `True`, `False`, oder `None` zurück. Kann es sein dass das mit den Wahrheitswerten so ein PHP-Ding ist? Eine `print_*()`-Funktion sollte gar nichts zurückgeben und eine `parse_*()`-Funktion sollte das Ergebnis des Parsens zurückgeben. Über Misserfolge informiert man den Aufrufer über Ausnahmen und nicht über einen boole'schen Rückgabewert.
grum.py
User
Beiträge: 137
Registriert: Montag 11. Mai 2015, 15:27

BlackJack hat geschrieben:der Benutzer muss beim Aufruf des Programms gegenüber der Vorgängerversion nichts beachten und hat auch keine neuen Einstellungen oder Kommandozeilenoptionen.
Doch, er kann seine Konfigurationsdatei ggf. um die neu hinzugefügten Bibliotheken erweitern.
BlackJack hat geschrieben:Beim Quelltext fällt als erstes auf das die Einrückung von den üblichen vier Leerzeichen pro Ebene abweicht.
Der Standard war mir neu. Gibt es da einen "Auto-Formatter", um mir solcherlei Fauxpas künftig zu ersparen?
BlackJack hat geschrieben:Es gibt in der Standardbibliothek ein `logging`-Modul, da sollte man sich keine eigenen `debuglog()`-Funktionen basteln müssen.
Die genau das Gleiche tut?
BlackJack hat geschrieben:Und im *Fehlerfall* sollte ein Prozess nicht den Exit-Code 0 zurückgeben, denn das ist der Code für „lief fehlerfrei“.
Danke, ich ändere das mal.
BlackJack hat geschrieben:Ein nacktes ``except:`` ohne eine konkrete Ausnahme ist in der Regel keine vernünftige Ausnahmebehandlung.
Das habe ich (ebenso wie ``Exception``) eigentlich weitgehend eliminiert, es ist nur noch da, wo alles Mögliche auftreten kann, vorhanden.
BlackJack hat geschrieben:Was soll das `h` bei `hConfig` bedeuten?
"handle". Entschuldige - Python ist bei Weitem nicht meine erste Sprache. Ich ändere das auch mal.
BlackJack hat geschrieben:Die Aufteilung des Codes ist eigenartig.
Ist "historisch gewachsen" (guck' lieber nicht ins git-Log)... ;)
BlackJack hat geschrieben:Warum heist das erste Argument `thislib`, also was hat das `this` dort zu suchen?
"Die, die gerade dran ist". Oder verstehe ich meinen eigenen Code falsch?
BlackJack hat geschrieben:Die Funktion gibt auch wieder entweder einen Wahrheitswert oder `None` zurück → komische API.
Ist tatsächlich nicht dafür gemacht, dass sie von Drittanbieterprogrammen mitbenutzt wird. 'tschuldigung! Ich ändere das gleich mit.
BlackJack hat geschrieben:Statt ``matches[0]`` für die Versionsnummer zu nehmen wäre eine benannte Gruppe besser, dann muss man keine Klammern zählen oder darauf achten wo man „non capturing“ Gruppen verwendet. Wobei ich gerade sehe das `findall()` verwendet wird, davon dann immer nur der erste Treffer — womit `findall()` irgendwie unsinnig ist.
Gibt es auch ein einfaches ``find``, das ich dann mit ``match`` füttern könnte?
BlackJack hat geschrieben:Der erste Kommentar in `parse_library()` ist inhaltlich falsch und zwei von drei Kommentaren in ``except``-Zweigen weil die offenbar einfach *kopiert* worden sind und inhaltlich nicht mehr zu der neuen Code-Stelle passen.
Der erste: Naja, eigentlich nicht, die Funktion wird ja in einer Schleife aufgerufen; aber ich ändere den schnell. Die anderen beiden stimmen: Das kann nur passieren, wenn die Definitionsdatei kaputt ist. ;) (Ich gebe zu, sie könnten ausführlicher sein.)
BlackJack hat geschrieben:Pfadteile sollte man mit `os.path.join()` zusammensetzen. Und den gleichen Pfad sollte man nur einmal zusammensetzen und nicht mehrfach.
Du hast Recht, danke.
BlackJack hat geschrieben:``pass`` macht nur in ansonsten leeren Blöcken Sinn.
Ah, ich dachte, damit könnte ich den Fehler als abgefangen kennzeichnen. Was tut es denn?
BlackJack hat geschrieben:Und an der Stelle sollte ganz bestimmt auch etwas anderes passieren, denn wenn die Webseite nicht geladen werden konnte, dann läuft der nachfolgende Code unweigerlich in einen `NameError` weil `matches` dann nicht definiert ist, was dann von einem wieder sehr groben ``except Exception`` „behandelt“ wird.
Ja, ``return False`` wäre vermutlich klüger, danke. Obwohl ...
BlackJack hat geschrieben:Und auch die `parse_library()`-Funktion gibt `True`, `False`, oder `None` zurück.
Gleich nicht mehr. :)
BlackJack hat geschrieben:Kann es sein dass das mit den Wahrheitswerten so ein PHP-Ding ist?
Ja, tatsächlich. Wobei man in PHP ja auf viele "strenge" Sprachkonsturkte gern mal verzichtet. Rückgabewerte? Im Bestfall is' alles "void", wir machen dafür nicht ständig ``try`` und ``catch`` ...
Danke für die Aufklärung. Ich korrigiere das Gröbste gleich. :)
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@grum.py: für meinen Geschmack hast Du es mit den Funktionen etwas übertrieben, "print_metadata" oder "print_pkg_info", wobei zweitere auch noch einen falschen Doc-String hat. Natürlich kann man mit logging exakt die selbe Ausgabe bekommen wie Dein debuglog, aber warum sollte man sich die Mühe machen, da die Default-Ausgabe ausreichend ist. Bei print_library_output hattest Du noch nicht gelernt, dass es format gibt, das würde ich nachziehen. Die Funktion sollte auch check_version_and_output_package_information heißen. Ich würde auch kein match-Ergebnis als Parameter übergeben, sondern eine newest_version, dann ist auch am Variablennamen klar, was da gemacht wird. "not <" ist eigentlich ein ">=".
Statt die verschiedenen Paketmanager per copy-paste im Code zu haben, sollten sie als Struktur z.B. in Deiner Config-Datei stehen.
"parse_library" sollte eigentlich "check_library" heißen und läuft immer noch mit einem undefinierten matches weiter. Welche Exception soll denn in "print_library_output" auftreten, die Du versuchst abzufangen? Warum verwendest Du bei config_file io.open und warum heißt das Fileobjekt data_file?
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

grum.py hat geschrieben:Rückgabewerte? Im Bestfall is' alles "void", wir machen dafür nicht ständig ``try`` und ``catch`` ...
Fehlerbehandlung über Rückgabewerte ist doof...

Ein Sinnloses Beispiel:

Code: Alles auswählen

class ExampleException(Exception):
    pass

def fehler():
    raise ExampleException("Hello Exception!")

def foo():
    fehler()

def bar():
    foo()


if __name__ == "__main__":
    try:
        bar()
    except ExampleException as err:
        print("Ein Fehler: %s" % err)
Ausgabe ist dann: Ein Fehler: Hello Exception!

Dabei muß man ja nicht überall ein try...except machen. Das ist ja gerade das Nette daran...

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Antworten