{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Authoring a package\n",
    "\n",
    "Once you have more than one `.py` file you want to share — even just\n",
    "between two of your own projects — it's worth turning your code into\n",
    "a proper installable package. This tutorial walks through the modern,\n",
    "`pyproject.toml`-driven approach using **hatchling** as the build\n",
    "backend.\n",
    "\n",
    "**Time commitment:** 20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- You've worked through the earlier Learn pages, especially\n",
    "  [Packages and namespaces](https://agilearn.co.uk/guides/packages-and-packaging/learn/02-packages-and-namespaces)\n",
    "- You're comfortable creating a virtual environment\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Lay a project out in the canonical `src/` shape\n",
    "- Write a working `pyproject.toml` with metadata and dependencies\n",
    "- Install your package in editable mode for development"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "!!! note \"Browser note\"\n",
    "    The commands below run in your terminal, not in this page's browser kernel.\n",
    "    Pyodide doesn't expose a shell, so the bash blocks here are for reference —\n",
    "    copy them into a real terminal to follow along.\n",
    "\n",
    "            We'll still use the page's virtual filesystem to write files\n",
    "            and demonstrate structure — only the `pip install -e .` step\n",
    "            and similar shell commands need a real terminal."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `src/` layout\n",
    "\n",
    "The recommended layout for a new package looks like this:\n",
    "\n",
    "```text\n",
    "my-package/\n",
    "    pyproject.toml\n",
    "    README.md\n",
    "    LICENSE\n",
    "    src/\n",
    "        my_package/\n",
    "            __init__.py\n",
    "            core.py\n",
    "    tests/\n",
    "        test_core.py\n",
    "```\n",
    "\n",
    "Two things to note:\n",
    "\n",
    "1. The **distribution name** (`my-package`) and the **import name**\n",
    "   (`my_package`) can differ. The distribution name is what you\n",
    "   publish to PyPI; the import name is the directory under `src/`.\n",
    "   Hyphens are common in distribution names; the import name must be\n",
    "   a valid Python identifier, so it uses underscores.\n",
    "2. The package itself lives under `src/`, not at the top level. This\n",
    "   is *the* most common layout question for new authors — the next\n",
    "   section explains why.\n",
    "\n",
    "Project layout has its own [reference page](https://agilearn.co.uk/guides/packages-and-packaging/reference/project-layout)\n",
    "with the trade-offs in detail."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building the layout on the page\n",
    "\n",
    "We'll create the file tree in Pyodide's virtual filesystem so the\n",
    "rest of the notebook can refer to real files."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, textwrap\n",
    "\n",
    "os.makedirs(\"my-package/src/my_package\", exist_ok=True)\n",
    "os.makedirs(\"my-package/tests\", exist_ok=True)\n",
    "\n",
    "open(\"my-package/src/my_package/__init__.py\", \"w\").write(\n",
    "    '__version__ = \"0.1.0\"\\n'\n",
    "    'from .core import greet\\n'\n",
    ")\n",
    "\n",
    "open(\"my-package/src/my_package/core.py\", \"w\").write(textwrap.dedent('''\n",
    "    def greet(name: str) -> str:\n",
    "        # Return a friendly greeting.\n",
    "        return f\"Hello, {name}!\"\n",
    "''').lstrip())\n",
    "\n",
    "open(\"my-package/tests/test_core.py\", \"w\").write(textwrap.dedent('''\n",
    "    from my_package import greet\n",
    "\n",
    "    def test_greet():\n",
    "        assert greet(\"Ada\") == \"Hello, Ada!\"\n",
    "''').lstrip())\n",
    "\n",
    "os.system(\"ls -R my-package\")  # Pyodide may or may not have `ls`; fine either way."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `pyproject.toml`\n",
    "\n",
    "`pyproject.toml` is the single configuration file that describes\n",
    "your package — its name, version, dependencies, build system, and\n",
    "any tool-specific configuration. It's defined by PEP 621 (project\n",
    "metadata) and PEP 517 (build system).\n",
    "\n",
    "Here's a minimal one for our example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyproject = textwrap.dedent('''\n",
    "    [build-system]\n",
    "    requires = [\"hatchling\"]\n",
    "    build-backend = \"hatchling.build\"\n",
    "\n",
    "    [project]\n",
    "    name = \"my-package\"\n",
    "    version = \"0.1.0\"\n",
    "    description = \"A friendly greeting library.\"\n",
    "    readme = \"README.md\"\n",
    "    requires-python = \">=3.10\"\n",
    "    authors = [{ name = \"Ada Lovelace\", email = \"ada@example.com\" }]\n",
    "    license = { text = \"MIT\" }\n",
    "    dependencies = []\n",
    "\n",
    "    [project.optional-dependencies]\n",
    "    test = [\"pytest\"]\n",
    "''').lstrip()\n",
    "\n",
    "open(\"my-package/pyproject.toml\", \"w\").write(pyproject)\n",
    "print(pyproject)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Two tables do the work.\n",
    "\n",
    "**`[build-system]`** tells `pip` how to build your package. We've\n",
    "chosen [hatchling](https://hatch.pypa.io) — a modern, lightweight\n",
    "build backend that needs no configuration for the standard `src/`\n",
    "layout. Setuptools is the classic alternative; flit, poetry-core,\n",
    "and pdm-backend are others.\n",
    "\n",
    "**`[project]`** carries the metadata. `name`, `version`, and at\n",
    "least a `description` are the essentials. `requires-python`\n",
    "constrains which Python versions your package supports.\n",
    "`dependencies` is a list of strings using the same syntax as `pip\n",
    "install` arguments (`\"requests>=2.30\"`).\n",
    "\n",
    "See the [pyproject.toml field reference](https://agilearn.co.uk/guides/packages-and-packaging/reference/pyproject-toml-field-reference)\n",
    "for the full set of fields."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Editable installs\n",
    "\n",
    "For a package you're actively developing, you don't want to\n",
    "`pip install` it after every change. Editable installs solve that:\n",
    "\n",
    "```bash\n",
    "# From inside the project root, with your venv activated:\n",
    "pip install -e .\n",
    "```\n",
    "\n",
    "The `-e` (editable) flag installs a thin link rather than a copy. Any\n",
    "edit to your source files is picked up next time you run Python — no\n",
    "re-install needed. This is *the* standard development workflow once\n",
    "a project is package-shaped.\n",
    "\n",
    "After `pip install -e .` you can `import my_package` from anywhere\n",
    "in the venv."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Adding dependencies\n",
    "\n",
    "Dependencies live in the `[project]` table:\n",
    "\n",
    "```toml\n",
    "[project]\n",
    "# ...\n",
    "dependencies = [\n",
    "    \"requests>=2.30\",\n",
    "    \"rich\",\n",
    "]\n",
    "```\n",
    "\n",
    "After editing `pyproject.toml`, run `pip install -e .` again to pick\n",
    "up the new dependencies. From this point on, anyone who installs\n",
    "your package gets `requests` and `rich` automatically."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Optional dependencies\n",
    "\n",
    "Tooling that's only needed in development — `pytest`, `mypy`,\n",
    "`black` — belongs in `[project.optional-dependencies]`:\n",
    "\n",
    "```toml\n",
    "[project.optional-dependencies]\n",
    "test = [\"pytest>=7\", \"pytest-cov\"]\n",
    "dev = [\"mypy\", \"ruff\"]\n",
    "```\n",
    "\n",
    "Install a group with the bracket syntax:\n",
    "\n",
    "```bash\n",
    "pip install -e \".[test]\"          # base deps + the test group\n",
    "pip install -e \".[test,dev]\"      # base deps + both groups\n",
    "```\n",
    "\n",
    "This is also how libraries offer optional integrations — for example,\n",
    "`pandas[excel]` pulls in the bits needed for Excel support, and only\n",
    "those."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Versioning\n",
    "\n",
    "The `version` field is a string that other tools — and humans —\n",
    "will read. Most Python packages follow [Semantic Versioning](https://semver.org/):\n",
    "`MAJOR.MINOR.PATCH`, where breaking changes bump MAJOR, new features\n",
    "bump MINOR, and fixes bump PATCH.\n",
    "\n",
    "For now, set it manually in `pyproject.toml` and bump it when you\n",
    "release. Tools like `hatch version` or `bump-my-version` automate\n",
    "this if it gets tedious."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap and next steps\n",
    "\n",
    "- The standard layout has `pyproject.toml` at the root and the code\n",
    "  under `src/<import_name>/`.\n",
    "- Use hatchling as the build backend unless you have a reason to\n",
    "  choose otherwise.\n",
    "- `pip install -e .` is the developer's friend — install once,\n",
    "  edit freely.\n",
    "\n",
    "With your package working locally, the last step is to share it —\n",
    "[Building and publishing](https://agilearn.co.uk/guides/packages-and-packaging/learn/06-building-and-publishing)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}