Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/packaging/README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions examples/packaging/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"versions_and_specifiers",
"requirements_and_markers"
]
97 changes: 97 additions & 0 deletions examples/packaging/requirements_and_markers/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>requirements.txt</code> follows PEP 508. "
"<code>Requirement</code> 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: <code>{marker}</code>")
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: <code>{req}</code><br>"
f"Target environment: <code>{target_env}</code>"
)
display(pd.DataFrame(resolution_rows), append=True)
1 change: 1 addition & 0 deletions examples/packaging/requirements_and_markers/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["packaging", "pandas"]
21 changes: 21 additions & 0 deletions examples/packaging/requirements_and_markers/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

85 changes: 85 additions & 0 deletions examples/packaging/versions_and_specifiers/code.py
Original file line number Diff line number Diff line change
@@ -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. "
"<code>Version</code> 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"<code>Version('not-a-version')</code> raises: <em>{exc}</em>")


heading("Matching versions against a specifier")
note(
"A <code>SpecifierSet</code> is the comma-separated constraint "
"you write in a requirements file, like <code>&gt;=1.0,&lt;2</code>. "
"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 <code>1.0rc2</code> only matches when pre-releases are "
"allowed, and the epoch-bumped <code>2!1.0</code> sorts above "
"<code>2.0</code> so it falls outside the upper bound."
)
1 change: 1 addition & 0 deletions examples/packaging/versions_and_specifiers/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["packaging", "pandas"]
41 changes: 41 additions & 0 deletions examples/packaging/versions_and_specifiers/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
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.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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)