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) + + +def note(text): + 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) + + +def note(text): + display(HTML(f"

{text}

"), append=True)