Conversation
|
What does "nl" mean for those variant comparators? And why do they mean the left-hand side is less than the right-hand no matter what? And what no e.g., |
|
Hi @brettcannon, The "nl" in those variant comparators comes from dpkg --compare-versions, which I originally intended to support as a drop-in replacement. From the manpage:
Regarding the lack of an eq-nl operator, it's likely because version equality doesn't depend on whether one of the versions is empty—if both versions are non-empty, eq handles it naturally. However, I decided to remove support for I also removed the non-textual operators So, while the tool is still inspired by dpkg, it’s not a compatible API anymore, but it is simpler and still useful. |
|
I don’t really mind, but aren’t non-textual operators more readable than |
Co-authored-by: Brett Cannon <brett@python.org>
dd7f329 to
f497782
Compare
|
I feel if we do this we should have a subcommand |
Updated the argument parser description and replaced sys.exit with raise SystemExit.
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
f497782 to
7d3694f
Compare
|
I also used AI in CoPilot to add a CLI output (for a followup). It took a few prompts to get the 3.14+ color & dark mode looking nice: Followup PR could be based on:diff --git a/docs/conf.py b/docs/conf.py
index 9419c41..bc034f7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -3,6 +3,10 @@
# for complete details.
import os
+import re
+import subprocess
+from docutils import nodes
+from docutils.parsers.rst import Directive
# -- Project information loading ----------------------------------------------
@@ -104,3 +108,111 @@ intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"pypug": ("https://packaging.python.org/", None),
}
+
+
+# -- Custom directives --------------------------------------------------------
+
+
+def ansi_to_html(text):
+ """Convert ANSI color codes to HTML."""
+ # ANSI color code to HTML color mapping
+ ansi_colors = {
+ "30": "#000000", # black
+ "31": "#dc3545", # red
+ "32": "#28a745", # green
+ "33": "#ffc107", # yellow
+ "34": "#007bff", # blue
+ "35": "#6f42c1", # magenta
+ "36": "#17a2b8", # cyan
+ "37": "#f8f9fa", # white
+ "90": "#6c757d", # bright black (gray)
+ "91": "#ff6b6b", # bright red
+ "92": "#51cf66", # bright green
+ "93": "#ffd43b", # bright yellow
+ "94": "#4d7fff", # bright blue
+ "95": "#da77f2", # bright magenta
+ "96": "#15aabf", # bright cyan
+ "97": "#ffffff", # bright white
+ }
+
+ # Pattern to match ANSI escape sequences
+ ansi_escape_pattern = re.compile(r"\x1b\[([0-9;]*)m")
+
+ def replace_ansi(match):
+ codes = match.group(1).split(";") if match.group(1) else ["0"]
+ html_parts = []
+
+ for code in codes:
+ if code == "0":
+ # Reset
+ return "</span>"
+ elif code == "1":
+ # Bold
+ html_parts.append('<span style="font-weight: bold;">')
+ elif code in ansi_colors:
+ # Foreground color
+ color = ansi_colors[code]
+ html_parts.append(f'<span style="color: {color};">')
+
+ return "".join(html_parts) if html_parts else ""
+
+ # Replace ANSI codes with HTML
+ html = ansi_escape_pattern.sub(replace_ansi, text)
+
+ # Clean up any remaining unclosed spans
+ html = html.replace("</span></span>", "</span>")
+
+ return html
+
+
+class ShowCliDirective(Directive):
+ """Include the output of a CLI command in the documentation."""
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+
+ def run(self):
+ """Execute the command and return its output as a code block."""
+ command = self.arguments[0]
+ try:
+ # Run the command and capture output
+ result = subprocess.run(
+ command,
+ shell=True,
+ capture_output=True,
+ text=True,
+ cwd=_BASE_DIR,
+ env={**os.environ, "FORCE_COLOR": "1"},
+ )
+ output = result.stdout or result.stderr
+ except Exception as e:
+ return [
+ nodes.error(
+ None,
+ nodes.paragraph(text=f"Error running command: {command}\n{e}"),
+ )
+ ]
+
+ # Convert ANSI codes to HTML
+ html_output = ansi_to_html(output)
+
+ # Create a raw HTML node with the colored output
+ raw_html = nodes.raw("", html_output, format="html")
+ literal_block = nodes.literal_block(output, raw_html)
+ literal_block["language"] = "text"
+
+ # Return as a container with pre styling
+ container = nodes.container()
+ container += nodes.raw(
+ "",
+ f'<pre style="background-color: var(--color-background-secondary, #f5f5f5); color: var(--color-foreground-primary, inherit); padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 0.9em; font-weight: normal; line-height: 1.4;">{html_output}</pre>',
+ format="html",
+ )
+ return [container]
+
+
+def setup(app):
+ """Register custom directives."""
+ app.add_directive("show-cli", ShowCliDirective)
diff --git a/docs/version.rst b/docs/version.rst
index 2adf336..7210a3d 100644
--- a/docs/version.rst
+++ b/docs/version.rst
@@ -46,9 +46,21 @@ Usage
True
+Command Line Interface
+----------------------
+
+The ``packaging.version`` module can be used as a command-line tool:
+
+.. show-cli:: python -m packaging.version --help
+
+You can compare two versions:
+
+.. show-cli:: python -m packaging.version compare --help
+
Reference
---------
.. automodule:: packaging.version
:members:
:special-members:
Edit: I have a better version of this (without the bold bug) in scikit-build/scikit-build-core#1218. |
0f002ce to
28eab80
Compare
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
28eab80 to
d78e7ef
Compare
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>


I want to propose this simple CLI tool in the
packaging.versionmodule to perform semantic version comparisons directly from the command line. This utility is modeled afterdpkg --compare-versionsbut designed to be platform-agnostic, filling a gap for non-Debian users and providing a Python-native solution.Version comparison is a common requirement in deployment scripts, package management, and development workflows. Currently, developers resort to complex shell scripts or third-party tools to compare versions. Examples of community solutions include intricate bash functions and snippets found on Stack Overflow and GitHub Gists that, while functional for basic cases, vary in reliability and can be unnecessarily complex 1, 2, 3.
My proposal leverages the robustness of the
packaging.version.Versionclass for parsing and comparing semantic versions, supporting standard comparison operators (e.g., lt, gt, eq) and provide straightforward syntax that resembles the mentioned dpkg commandUsage