diff --git a/examples/packaging/README.md b/examples/packaging/README.md
new file mode 100644
index 0000000..fcedd7e
--- /dev/null
+++ b/examples/packaging/README.md
@@ -0,0 +1,18 @@
+# packaging Examples
+
+Each sub-directory contains a self-contained example. The order in
+which the examples are to appear is specified in `order.json` (an
+array of directory names in the expected order).
+
+In each example directory you'll find:
+
+* `config.toml` - must conform to the specification outlined here:
+ https://docs.pyscript.net/latest/user-guide/configuration/ This is
+ parsed and ultimately turned into a JSON representation as part of
+ the package's API object.
+* `setup.py` - Python code for contextual and environmental setup,
+ NOT SEEN BY THE END USER, but is run before the `code.py` code is
+ evaluated. Allows us to create useful (IPython) shims, avoid
+ repeating boilerplate and whatnot.
+* `code.py` - the actual code added to the editor which forms the
+ practical example of using the package.
diff --git a/examples/packaging/order.json b/examples/packaging/order.json
new file mode 100644
index 0000000..d2a0161
--- /dev/null
+++ b/examples/packaging/order.json
@@ -0,0 +1,4 @@
+[
+ "versions_and_specifiers",
+ "requirements_and_markers"
+]
diff --git a/examples/packaging/requirements_and_markers/code.py b/examples/packaging/requirements_and_markers/code.py
new file mode 100644
index 0000000..f323272
--- /dev/null
+++ b/examples/packaging/requirements_and_markers/code.py
@@ -0,0 +1,97 @@
+# ---------------------------------------------------------------------
+# Parsing PEP 508 requirement strings and evaluating environment markers.
+# ---------------------------------------------------------------------
+import pandas as pd
+from packaging.requirements import Requirement
+from packaging.markers import Marker
+from packaging.version import Version
+
+
+heading("Parsing requirement strings")
+note(
+ "Each line in a requirements.txt follows PEP 508. "
+ "Requirement breaks one apart into its name, extras, "
+ "version specifier, and optional environment marker."
+)
+
+requirement_lines = [
+ "requests>=2.31,<3",
+ "django[bcrypt,argon2]>=4.2,<5",
+ "numpy>=1.26 ; python_version >= '3.10'",
+ "pywin32 ; sys_platform == 'win32'",
+ "rich",
+]
+
+parsed_rows = []
+for line in requirement_lines:
+ req = Requirement(line)
+ parsed_rows.append({
+ "raw": line,
+ "name": req.name,
+ "extras": ", ".join(sorted(req.extras)) or "-",
+ "specifier": str(req.specifier) or "(any)",
+ "marker": str(req.marker) if req.marker else "-",
+ })
+
+display(pd.DataFrame(parsed_rows), append=True)
+
+
+heading("Evaluating environment markers")
+note(
+ "Markers are tiny boolean expressions evaluated against the "
+ "current Python environment. You can also evaluate them against "
+ "a fabricated environment to ask 'would this install on Windows "
+ "with Python 3.9?'."
+)
+
+marker = Marker("python_version >= '3.10' and sys_platform != 'win32'")
+
+environments = [
+ {"python_version": "3.9", "sys_platform": "linux"},
+ {"python_version": "3.10", "sys_platform": "linux"},
+ {"python_version": "3.12", "sys_platform": "darwin"},
+ {"python_version": "3.12", "sys_platform": "win32"},
+]
+
+eval_rows = []
+for env in environments:
+ eval_rows.append({
+ **env,
+ "marker_holds": marker.evaluate(environment=env),
+ })
+
+note(f"Evaluating: {marker}")
+display(pd.DataFrame(eval_rows), append=True)
+
+
+heading("Putting it together: would this requirement install here?")
+note(
+ "Combine a parsed requirement with a candidate version and a "
+ "fabricated environment to predict whether a resolver would "
+ "pick it."
+)
+
+req = Requirement(
+ "numpy>=1.26,<2 ; python_version >= '3.10'"
+)
+
+candidate_versions = ["1.25.2", "1.26.4", "1.99.0", "2.0.0"]
+target_env = {"python_version": "3.11", "sys_platform": "linux"}
+
+resolution_rows = []
+marker_holds = req.marker.evaluate(environment=target_env)
+for raw in candidate_versions:
+ v = Version(raw)
+ in_specifier = v in req.specifier
+ resolution_rows.append({
+ "candidate": raw,
+ "in specifier": in_specifier,
+ "marker holds": marker_holds,
+ "would install": in_specifier and marker_holds,
+ })
+
+note(
+ f"Requirement: {req}
"
+ f"Target environment: {target_env}"
+)
+display(pd.DataFrame(resolution_rows), append=True)
diff --git a/examples/packaging/requirements_and_markers/config.toml b/examples/packaging/requirements_and_markers/config.toml
new file mode 100644
index 0000000..a525f65
--- /dev/null
+++ b/examples/packaging/requirements_and_markers/config.toml
@@ -0,0 +1 @@
+packages = ["packaging", "pandas"]
diff --git a/examples/packaging/requirements_and_markers/setup.py b/examples/packaging/requirements_and_markers/setup.py
new file mode 100644
index 0000000..6850fc0
--- /dev/null
+++ b/examples/packaging/requirements_and_markers/setup.py
@@ -0,0 +1,21 @@
+"""Lightweight setup for the second example. Inherits the notebook
+namespace established by the first example."""
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+def heading(text, level=2):
+ display(HTML(f"
{text}
"), append=True) + diff --git a/examples/packaging/versions_and_specifiers/code.py b/examples/packaging/versions_and_specifiers/code.py new file mode 100644 index 0000000..5945b41 --- /dev/null +++ b/examples/packaging/versions_and_specifiers/code.py @@ -0,0 +1,85 @@ +""" +A first look at the `packaging` library. + +`packaging` is the canonical implementation of Python packaging +interoperability standards (PEP 440, PEP 508, PEP 425, and friends). +If you have ever wondered how pip decides whether "1.10.0" is newer +than "1.9.0", or whether "2.0.0rc1" satisfies ">=2,<3", this is the +library doing the work. + +Docs: https://packaging.pypa.io/ +""" +import pandas as pd +from IPython.core.display import display, HTML + +# Package imports for this example. +from packaging.version import Version, InvalidVersion +from packaging.specifiers import SpecifierSet + + +heading("Parsing and comparing versions") +note( + "PEP 440 says version strings have a precise structure: an " + "optional epoch, a release segment, and pre/post/dev tags. " + "Version parses them and compares them correctly, "
+ "even when string ordering would get it wrong."
+)
+
+raw_versions = [
+ "1.0",
+ "1.0.0",
+ "1.0.1",
+ "1.0a1", # alpha pre-release
+ "1.0rc2", # release candidate
+ "1.0.post1", # post-release
+ "1.0.dev3", # development release
+ "2!1.0", # epoch 2 -- jumps ahead of any non-epoch version
+ "1.10", # newer than 1.9, despite shorter string sort
+]
+
+parsed = [Version(v) for v in raw_versions]
+ordered = sorted(parsed)
+
+table = pd.DataFrame({
+ "version": [str(v) for v in ordered],
+ "is_prerelease": [v.is_prerelease for v in ordered],
+ "is_postrelease": [v.is_postrelease for v in ordered],
+ "release_tuple": [v.release for v in ordered],
+})
+note("Sorted from oldest to newest by PEP 440 rules:")
+display(table, append=True)
+
+# Invalid versions raise a clear exception.
+try:
+ Version("not-a-version")
+except InvalidVersion as exc:
+ note(f"Version('not-a-version') raises: {exc}")
+
+
+heading("Matching versions against a specifier")
+note(
+ "A SpecifierSet is the comma-separated constraint "
+ "you write in a requirements file, like >=1.0,<2. "
+ "By default it excludes pre-releases unless you opt in."
+)
+
+constraint = SpecifierSet(">=1.0,<2")
+candidates = ["0.9", "1.0", "1.0rc2", "1.5", "1.99", "2.0", "2!1.0"]
+
+rows = []
+for raw in candidates:
+ v = Version(raw)
+ rows.append({
+ "candidate": raw,
+ f"matches '{constraint}'": v in constraint,
+ "matches (with prereleases)": constraint.contains(
+ v, prereleases=True,
+ ),
+ })
+display(pd.DataFrame(rows), append=True)
+
+note(
+ "Notice 1.0rc2 only matches when pre-releases are "
+ "allowed, and the epoch-bumped 2!1.0 sorts above "
+ "2.0 so it falls outside the upper bound."
+)
diff --git a/examples/packaging/versions_and_specifiers/config.toml b/examples/packaging/versions_and_specifiers/config.toml
new file mode 100644
index 0000000..a525f65
--- /dev/null
+++ b/examples/packaging/versions_and_specifiers/config.toml
@@ -0,0 +1 @@
+packages = ["packaging", "pandas"]
diff --git a/examples/packaging/versions_and_specifiers/setup.py b/examples/packaging/versions_and_specifiers/setup.py
new file mode 100644
index 0000000..bb018e1
--- /dev/null
+++ b/examples/packaging/versions_and_specifiers/setup.py
@@ -0,0 +1,42 @@
+"""
+Shim IPython's display API onto PyScript so example code written in a
+Jupyter/IPython idiom runs unmodified in the browser.
+"""
+
+import sys
+import types
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ """Wrap pyscript.display so output lands in the example target."""
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+ipython = types.ModuleType("IPython")
+core = types.ModuleType("IPython.core")
+core_display = types.ModuleType("IPython.core.display")
+core_display.display = display
+core_display.HTML = HTML
+ipython.core = core
+core.display = core_display
+ipython.version_info = (9, 0, 2, '')
+ipython.get_ipython = lambda: None
+ipython.display = core_display
+sys.modules["IPython"] = ipython
+sys.modules["IPython.core"] = core
+sys.modules["IPython.core.display"] = core_display
+sys.modules["IPython.display"] = core_display
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}
"), append=True)