simple-module-cli 0.0.32__tar.gz → 0.0.34__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/PKG-INFO +1 -1
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/pyproject.toml +1 -1
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/package_update.py +40 -65
- simple_module_cli-0.0.34/simple_module_cli/pypi.py +55 -0
- simple_module_cli-0.0.34/simple_module_cli/requirements.py +232 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/main.py +5 -2
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/conftest.py +38 -1
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_package_update.py +38 -41
- simple_module_cli-0.0.34/tests/test_cli_package_update_pins.py +279 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_css_build.py +12 -3
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/.gitignore +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/LICENSE +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/README.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/_module_build.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/_module_host.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/_templating.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/add_cmd.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/app_project.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/cli.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/docker_assets.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/git_source.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/module_cmd.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/new.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/pyproject_edit.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/Makefile.snippet +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/dockerignore +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/host-flat.Dockerfile +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/docker/host.Dockerfile +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/module-assets.ts +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/client_app/vite.dev-url.ts +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/pages/Index.tsx.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/tests/test___PACKAGE__.py.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/update_cmd.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_add_cmd.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_add_spec_parse.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_docker_assets.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_git_scan.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_new.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_new_git_module.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_new_migration_bootstrap.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_new_scaffold_layout.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_pyproject_edit.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_update_sources.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_vite_port_env.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_cli_wizard_git.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_create_module_ci_skip.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_cmd_build.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_cmd_e2e.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_cmd_verify.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_css_assets.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_npm_aliases.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_module_pages_manifest.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_scaffolding_module_js.py +0 -0
- {simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/tests/test_skills_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.34
|
|
4
4
|
Summary: Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host.
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Walks the project's ``pyproject.toml`` (and any ``[tool.uv.workspace]`` members),
|
|
4
4
|
finds every dependency whose distribution name starts with ``simple_module_`` /
|
|
5
|
-
``simple-module-``, queries PyPI for the latest non-yanked release, and
|
|
6
|
-
the constraint
|
|
5
|
+
``simple-module-``, queries PyPI for the latest non-yanked release, and points
|
|
6
|
+
the constraint at it.
|
|
7
|
+
|
|
8
|
+
The rewrite preserves the pin style you wrote: ``==0.0.32`` becomes
|
|
9
|
+
``==0.0.33``, ``>=0.0.32`` becomes ``>=0.0.33``, and an upper bound that
|
|
10
|
+
excludes the latest release skips the dependency instead of being dropped.
|
|
11
|
+
``--loosen`` restores the older behaviour of rewriting every constraint to
|
|
12
|
+
``name>=<latest>``.
|
|
7
13
|
|
|
8
14
|
Dependencies whose ``[tool.uv.sources]`` entry points at a workspace member, a
|
|
9
15
|
local path, a git ref, or a URL are left untouched — those aren't installed
|
|
@@ -12,11 +18,7 @@ from PyPI.
|
|
|
12
18
|
|
|
13
19
|
from __future__ import annotations
|
|
14
20
|
|
|
15
|
-
import json
|
|
16
21
|
import re
|
|
17
|
-
import urllib.error
|
|
18
|
-
import urllib.request
|
|
19
|
-
from collections.abc import Callable
|
|
20
22
|
from concurrent.futures import ThreadPoolExecutor
|
|
21
23
|
from dataclasses import dataclass
|
|
22
24
|
from pathlib import Path
|
|
@@ -26,17 +28,12 @@ import tomlkit
|
|
|
26
28
|
import typer
|
|
27
29
|
from tomlkit.items import Array, Table
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
from simple_module_cli.pypi import Fetcher, default_fetcher, fetch_latest
|
|
32
|
+
from simple_module_cli.requirements import parse_requirement, rewrite_requirement
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
__all__ = ["package_update", "run_update"]
|
|
32
35
|
|
|
33
|
-
_PYPI_URL = "https://pypi.org/pypi/{name}/json"
|
|
34
36
|
_SM_PREFIX_RE = re.compile(r"^simple[_-]module[_-]", re.IGNORECASE)
|
|
35
|
-
# PEP 440 release segments contain only digits + dots; any letter signals
|
|
36
|
-
# a pre/post/dev release (a, b, rc, post, dev). Coarser than packaging.version
|
|
37
|
-
# but `packaging` isn't a CLI dep (see test_no_framework_deps.py).
|
|
38
|
-
_PRE_RELEASE_RE = re.compile(r"[a-zA-Z]")
|
|
39
|
-
_REQ_OPS = ("===", "==", ">=", "<=", "!=", "~=", ">", "<")
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
@dataclass(frozen=True)
|
|
@@ -60,14 +57,8 @@ def _is_sm_package(name: str) -> bool:
|
|
|
60
57
|
|
|
61
58
|
def _dep_name(spec: str) -> str | None:
|
|
62
59
|
"""Extract the distribution name from a PEP 508 requirement string."""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for op in _REQ_OPS:
|
|
66
|
-
if op in base:
|
|
67
|
-
base = base.split(op, 1)[0]
|
|
68
|
-
break
|
|
69
|
-
name = base.strip()
|
|
70
|
-
return name or None
|
|
60
|
+
parsed = parse_requirement(spec)
|
|
61
|
+
return parsed.name if parsed else None
|
|
71
62
|
|
|
72
63
|
|
|
73
64
|
def _is_local_source(source: Any) -> bool:
|
|
@@ -89,40 +80,6 @@ def _get_uv_section(doc: tomlkit.TOMLDocument, key: str) -> dict[str, Any] | Non
|
|
|
89
80
|
return section if isinstance(section, dict) else None
|
|
90
81
|
|
|
91
82
|
|
|
92
|
-
def _fetch_latest(name: str, *, include_pre: bool, fetcher: Fetcher) -> str | None:
|
|
93
|
-
try:
|
|
94
|
-
data = fetcher(_PYPI_URL.format(name=name))
|
|
95
|
-
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
96
|
-
return None
|
|
97
|
-
releases = data.get("releases") or {}
|
|
98
|
-
candidates: list[str] = []
|
|
99
|
-
for version, files in releases.items():
|
|
100
|
-
if not files:
|
|
101
|
-
continue
|
|
102
|
-
if any(f.get("yanked") for f in files):
|
|
103
|
-
continue
|
|
104
|
-
if not include_pre and _PRE_RELEASE_RE.search(version):
|
|
105
|
-
continue
|
|
106
|
-
candidates.append(version)
|
|
107
|
-
if candidates:
|
|
108
|
-
return max(candidates, key=_version_key)
|
|
109
|
-
info = data.get("info") or {}
|
|
110
|
-
return info.get("version")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _version_key(v: str) -> tuple[int, ...]:
|
|
114
|
-
parts: list[int] = []
|
|
115
|
-
for part in v.split("."):
|
|
116
|
-
digits = re.match(r"\d+", part)
|
|
117
|
-
parts.append(int(digits.group()) if digits else 0)
|
|
118
|
-
return tuple(parts)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _default_fetcher(url: str) -> dict[str, Any]:
|
|
122
|
-
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
123
|
-
return json.loads(resp.read().decode("utf-8"))
|
|
124
|
-
|
|
125
|
-
|
|
126
83
|
def _workspace_member_dirs(root_pyproject: Path, doc: tomlkit.TOMLDocument) -> list[Path]:
|
|
127
84
|
workspace = _get_uv_section(doc, "workspace")
|
|
128
85
|
if workspace is None:
|
|
@@ -166,6 +123,7 @@ def _process_file(
|
|
|
166
123
|
doc: tomlkit.TOMLDocument,
|
|
167
124
|
*,
|
|
168
125
|
cache: dict[str, str | None],
|
|
126
|
+
loosen: bool = False,
|
|
169
127
|
) -> tuple[list[Change], list[Skip], tomlkit.TOMLDocument | None]:
|
|
170
128
|
project = doc.get("project")
|
|
171
129
|
if not isinstance(project, (dict, Table)):
|
|
@@ -190,11 +148,13 @@ def _process_file(
|
|
|
190
148
|
if latest is None:
|
|
191
149
|
skips.append(Skip(path, name, "not found on PyPI"))
|
|
192
150
|
continue
|
|
193
|
-
|
|
194
|
-
if
|
|
151
|
+
result = rewrite_requirement(dep_str, latest, loosen=loosen)
|
|
152
|
+
if result.spec is None:
|
|
153
|
+
if result.reason:
|
|
154
|
+
skips.append(Skip(path, name, result.reason))
|
|
195
155
|
continue
|
|
196
|
-
deps[idx] =
|
|
197
|
-
changes.append(Change(path, name, dep_str.strip(),
|
|
156
|
+
deps[idx] = result.spec
|
|
157
|
+
changes.append(Change(path, name, dep_str.strip(), result.spec))
|
|
198
158
|
|
|
199
159
|
return changes, skips, doc if changes else None
|
|
200
160
|
|
|
@@ -226,6 +186,7 @@ def run_update(
|
|
|
226
186
|
*,
|
|
227
187
|
dry_run: bool = False,
|
|
228
188
|
include_pre: bool = False,
|
|
189
|
+
loosen: bool = False,
|
|
229
190
|
fetcher: Fetcher | None = None,
|
|
230
191
|
) -> None:
|
|
231
192
|
"""Programmatic entry point — separated from the Typer command for testing."""
|
|
@@ -234,7 +195,7 @@ def run_update(
|
|
|
234
195
|
typer.echo(f"ERROR: {root} not found.", err=True)
|
|
235
196
|
raise typer.Exit(code=1)
|
|
236
197
|
|
|
237
|
-
fetch = fetcher or
|
|
198
|
+
fetch = fetcher or default_fetcher
|
|
238
199
|
root_doc = tomlkit.parse(root.read_text(encoding="utf-8"))
|
|
239
200
|
files: list[tuple[Path, tomlkit.TOMLDocument]] = [(root, root_doc)]
|
|
240
201
|
for member in _workspace_member_dirs(root, root_doc):
|
|
@@ -246,7 +207,7 @@ def run_update(
|
|
|
246
207
|
if unique_names:
|
|
247
208
|
with ThreadPoolExecutor(max_workers=min(8, len(unique_names))) as pool:
|
|
248
209
|
results = pool.map(
|
|
249
|
-
lambda n: (n,
|
|
210
|
+
lambda n: (n, fetch_latest(n, include_pre=include_pre, fetcher=fetch)),
|
|
250
211
|
unique_names,
|
|
251
212
|
)
|
|
252
213
|
cache = dict(results)
|
|
@@ -256,7 +217,7 @@ def run_update(
|
|
|
256
217
|
pending: list[tuple[Path, tomlkit.TOMLDocument]] = []
|
|
257
218
|
|
|
258
219
|
for file, doc in files:
|
|
259
|
-
changes, skips, new_doc = _process_file(file, doc, cache=cache)
|
|
220
|
+
changes, skips, new_doc = _process_file(file, doc, cache=cache, loosen=loosen)
|
|
260
221
|
all_changes.extend(changes)
|
|
261
222
|
all_skips.extend(skips)
|
|
262
223
|
if new_doc is not None:
|
|
@@ -282,6 +243,20 @@ def package_update(
|
|
|
282
243
|
bool,
|
|
283
244
|
typer.Option("--include-pre", help="Include pre-release versions."),
|
|
284
245
|
] = False,
|
|
246
|
+
loosen: Annotated[
|
|
247
|
+
bool,
|
|
248
|
+
typer.Option(
|
|
249
|
+
"--loosen",
|
|
250
|
+
help="Rewrite every constraint to `>=<latest>` instead of keeping its operator.",
|
|
251
|
+
),
|
|
252
|
+
] = False,
|
|
285
253
|
) -> None:
|
|
286
|
-
"""Update all simple_module_* dependencies to the latest PyPI versions.
|
|
287
|
-
|
|
254
|
+
"""Update all simple_module_* dependencies to the latest PyPI versions.
|
|
255
|
+
|
|
256
|
+
Each constraint keeps the operator it already has — `==0.0.32` becomes
|
|
257
|
+
`==0.0.33`, `>=0.0.32` becomes `>=0.0.33` — so bumping versions never
|
|
258
|
+
silently changes a project's pinning policy. A dependency whose upper
|
|
259
|
+
bound excludes the latest release is reported and left alone. Pass
|
|
260
|
+
`--loosen` to rewrite everything to `>=<latest>` instead.
|
|
261
|
+
"""
|
|
262
|
+
run_update(path, dry_run=dry_run, include_pre=include_pre, loosen=loosen)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""PyPI release lookup for ``smpy package-update``.
|
|
2
|
+
|
|
3
|
+
Split out of ``package_update`` so the network half is separately testable and
|
|
4
|
+
neither file approaches the 300-line cap. The ``Fetcher`` indirection is what
|
|
5
|
+
lets the CLI tests run without touching the network.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from simple_module_cli.requirements import version_key
|
|
18
|
+
|
|
19
|
+
__all__ = ["PYPI_URL", "Fetcher", "default_fetcher", "fetch_latest"]
|
|
20
|
+
|
|
21
|
+
Fetcher = Callable[[str], dict[str, Any]]
|
|
22
|
+
|
|
23
|
+
PYPI_URL = "https://pypi.org/pypi/{name}/json"
|
|
24
|
+
|
|
25
|
+
# PEP 440 release segments contain only digits + dots; any letter signals
|
|
26
|
+
# a pre/post/dev release (a, b, rc, post, dev). Coarser than packaging.version
|
|
27
|
+
# but `packaging` isn't a CLI dep (see test_no_framework_deps.py).
|
|
28
|
+
_PRE_RELEASE_RE = re.compile(r"[a-zA-Z]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_latest(name: str, *, include_pre: bool, fetcher: Fetcher) -> str | None:
|
|
32
|
+
"""Latest non-yanked release of ``name``, or ``None`` if PyPI doesn't have it."""
|
|
33
|
+
try:
|
|
34
|
+
data = fetcher(PYPI_URL.format(name=name))
|
|
35
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
36
|
+
return None
|
|
37
|
+
releases = data.get("releases") or {}
|
|
38
|
+
candidates: list[str] = []
|
|
39
|
+
for version, files in releases.items():
|
|
40
|
+
if not files:
|
|
41
|
+
continue
|
|
42
|
+
if any(f.get("yanked") for f in files):
|
|
43
|
+
continue
|
|
44
|
+
if not include_pre and _PRE_RELEASE_RE.search(version):
|
|
45
|
+
continue
|
|
46
|
+
candidates.append(version)
|
|
47
|
+
if candidates:
|
|
48
|
+
return max(candidates, key=version_key)
|
|
49
|
+
info = data.get("info") or {}
|
|
50
|
+
return info.get("version")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def default_fetcher(url: str) -> dict[str, Any]:
|
|
54
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
55
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""PEP 508 requirement rewriting for ``smpy package-update``.
|
|
2
|
+
|
|
3
|
+
Split out of ``package_update`` so the version-bump rules live in one place
|
|
4
|
+
with their own tests, and so neither file approaches the 300-line cap.
|
|
5
|
+
|
|
6
|
+
The rule that matters: bumping a dependency changes its *version*, not its
|
|
7
|
+
*pin style*. ``simple_module_core==0.0.32`` becomes ``==0.0.33``, not
|
|
8
|
+
``>=0.0.33`` — a host that pins exactly has made a deliberate choice, and the
|
|
9
|
+
published module wheels pin exactly too, so loosening the host leaves the
|
|
10
|
+
effective version decided by whichever wheel pins hardest. ``--loosen``
|
|
11
|
+
restores the old blanket ``>=`` rewrite for anyone who wants it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
__all__ = ["ParsedRequirement", "RewriteResult", "parse_requirement", "rewrite_requirement"]
|
|
20
|
+
|
|
21
|
+
# Longest-first: ``===`` must win over ``==``, and ``>=`` over ``>``.
|
|
22
|
+
_OPS = ("===", "==", "~=", "!=", ">=", "<=", ">", "<")
|
|
23
|
+
_CLAUSE_RE = re.compile(rf"\s*({'|'.join(re.escape(op) for op in _OPS)})\s*([^,]+)")
|
|
24
|
+
|
|
25
|
+
#: Operators naming a plain floor the bump should raise. ``>`` is deliberately
|
|
26
|
+
#: absent: ``>0.0.31`` is already satisfied by anything newer, and rewriting it
|
|
27
|
+
#: to ``>0.0.33`` would exclude the very version being installed. ``~=`` is
|
|
28
|
+
#: absent too — it carries an implicit ceiling, handled separately below.
|
|
29
|
+
_FLOOR_OPS = frozenset({"==", "===", ">="})
|
|
30
|
+
|
|
31
|
+
#: Suffix marking a PEP 440 wildcard (``==1.4.*``, ``!=1.0.*``). These match on
|
|
32
|
+
#: a *prefix* of the release segments, so they must never go through the
|
|
33
|
+
#: numeric comparison — ``version_key`` maps ``*`` to 0, which would read
|
|
34
|
+
#: ``1.0.*`` as ``1.0.0`` and quietly answer the wrong question.
|
|
35
|
+
_WILDCARD = ".*"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ParsedRequirement:
|
|
40
|
+
"""A PEP 508 requirement split into the parts the rewrite cares about."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
#: Extras bracket including the brackets (``"[redis]"``), or ``""``.
|
|
44
|
+
extras: str
|
|
45
|
+
#: ``(operator, version)`` pairs in source order.
|
|
46
|
+
clauses: tuple[tuple[str, str], ...]
|
|
47
|
+
#: Environment marker including the leading ``;``, or ``""``.
|
|
48
|
+
marker: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class RewriteResult:
|
|
53
|
+
"""Outcome of a rewrite: a new spec, or a reason there isn't one."""
|
|
54
|
+
|
|
55
|
+
#: The rewritten requirement, or ``None`` when nothing should change.
|
|
56
|
+
spec: str | None
|
|
57
|
+
#: Human-readable reason when ``spec`` is ``None`` and the user should be
|
|
58
|
+
#: told (an excluded latest); ``None`` when the no-op is unremarkable.
|
|
59
|
+
reason: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def version_key(version: str) -> tuple[int, ...]:
|
|
63
|
+
"""Coarse PEP 440 release-segment ordering.
|
|
64
|
+
|
|
65
|
+
``packaging`` isn't a CLI dependency (see ``test_no_framework_deps.py``),
|
|
66
|
+
so this compares the numeric release segments only. Good enough to answer
|
|
67
|
+
"does the latest release trip this upper bound", which is all it's for.
|
|
68
|
+
"""
|
|
69
|
+
parts: list[int] = []
|
|
70
|
+
for part in version.split("."):
|
|
71
|
+
digits = re.match(r"\d+", part)
|
|
72
|
+
parts.append(int(digits.group()) if digits else 0)
|
|
73
|
+
return tuple(parts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _compare(left: str, right: str) -> int:
|
|
77
|
+
"""Three-way compare two versions on their release segments, zero-padded."""
|
|
78
|
+
a, b = version_key(left), version_key(right)
|
|
79
|
+
width = max(len(a), len(b))
|
|
80
|
+
a += (0,) * (width - len(a))
|
|
81
|
+
b += (0,) * (width - len(b))
|
|
82
|
+
return (a > b) - (a < b)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_requirement(spec: str) -> ParsedRequirement | None:
|
|
86
|
+
"""Split a PEP 508 requirement string, or ``None`` if the name is unusable."""
|
|
87
|
+
base, sep, marker = spec.partition(";")
|
|
88
|
+
base = base.strip()
|
|
89
|
+
marker = f";{marker}" if sep else ""
|
|
90
|
+
|
|
91
|
+
head, bracket, rest = base.partition("[")
|
|
92
|
+
if bracket:
|
|
93
|
+
extras_body, closed, tail = rest.partition("]")
|
|
94
|
+
if not closed:
|
|
95
|
+
return None
|
|
96
|
+
extras = f"[{extras_body}]"
|
|
97
|
+
remainder = tail
|
|
98
|
+
else:
|
|
99
|
+
extras = ""
|
|
100
|
+
# No extras: the name runs until the first operator character.
|
|
101
|
+
match = re.match(r"[^<>=!~]*", head)
|
|
102
|
+
name_end = match.end() if match else 0
|
|
103
|
+
head, remainder = head[:name_end], head[name_end:]
|
|
104
|
+
|
|
105
|
+
name = head.strip()
|
|
106
|
+
if not name:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
clauses = tuple((op, version.strip()) for op, version in _CLAUSE_RE.findall(remainder))
|
|
110
|
+
return ParsedRequirement(name=name, extras=extras, clauses=clauses, marker=marker)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _render(parsed: ParsedRequirement, clauses: tuple[tuple[str, str], ...]) -> str:
|
|
114
|
+
body = ",".join(f"{op}{version}" for op, version in clauses)
|
|
115
|
+
return f"{parsed.name}{parsed.extras}{body}{parsed.marker}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _matches_wildcard(version: str, pattern: str) -> bool:
|
|
119
|
+
"""Does ``version`` fall under a PEP 440 wildcard like ``1.4.*``?"""
|
|
120
|
+
prefix = version_key(pattern[: -len(_WILDCARD)])
|
|
121
|
+
release = version_key(version)
|
|
122
|
+
return len(release) >= len(prefix) and release[: len(prefix)] == prefix
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _compatible_band(version: str) -> str | None:
|
|
126
|
+
"""The wildcard a ``~=`` clause implies, or ``None`` if it has none.
|
|
127
|
+
|
|
128
|
+
``~=1.4`` means ``>=1.4, ==1.*``; ``~=1.4.2`` means ``>=1.4.2, ==1.4.*``.
|
|
129
|
+
A single-segment ``~=1`` is invalid PEP 440 and has no band to derive.
|
|
130
|
+
"""
|
|
131
|
+
parts = version.split(".")
|
|
132
|
+
if len(parts) < 2:
|
|
133
|
+
return None
|
|
134
|
+
return ".".join(parts[:-1]) + _WILDCARD
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _allows(op: str, version: str, latest: str) -> bool:
|
|
138
|
+
"""Would this clause still be satisfied by ``latest``?
|
|
139
|
+
|
|
140
|
+
Floors (``==``/``===``/``>=``) are always "allowed": the rewrite moves them
|
|
141
|
+
*to* ``latest``, so they can't exclude it. Everything else either carries a
|
|
142
|
+
ceiling of its own or is left verbatim, and has to be checked.
|
|
143
|
+
"""
|
|
144
|
+
if op in _FLOOR_OPS and not version.endswith(_WILDCARD):
|
|
145
|
+
return True
|
|
146
|
+
if version.endswith(_WILDCARD):
|
|
147
|
+
# ``==1.0.*`` allows anything under the prefix; ``!=1.0.*`` allows
|
|
148
|
+
# anything outside it.
|
|
149
|
+
inside = _matches_wildcard(latest, version)
|
|
150
|
+
return inside if op in ("==", "===") else not inside
|
|
151
|
+
if op == "~=":
|
|
152
|
+
band = _compatible_band(version)
|
|
153
|
+
return band is not None and _matches_wildcard(latest, band)
|
|
154
|
+
cmp = _compare(latest, version)
|
|
155
|
+
if op == "<":
|
|
156
|
+
return cmp < 0
|
|
157
|
+
if op == "<=":
|
|
158
|
+
return cmp <= 0
|
|
159
|
+
if op == "!=":
|
|
160
|
+
return cmp != 0
|
|
161
|
+
# ``>`` — already satisfied by anything newer than the pin.
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _excluded_by(parsed: ParsedRequirement, latest: str) -> str | None:
|
|
166
|
+
"""Return the clause that rules ``latest`` out, or ``None`` if it's allowed."""
|
|
167
|
+
for op, version in parsed.clauses:
|
|
168
|
+
if not _allows(op, version, latest):
|
|
169
|
+
return f"{op}{version}"
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _bump(op: str, version: str, latest: str) -> str:
|
|
174
|
+
"""The version this clause should carry after the update.
|
|
175
|
+
|
|
176
|
+
Only a plain floor moves, and only *upwards*: PyPI's latest can legitimately
|
|
177
|
+
be older than what a project already asks for (a version bumped in the
|
|
178
|
+
workspace but not yet published), and rewriting ``==0.0.33`` to ``==0.0.32``
|
|
179
|
+
would force an actual downgrade on the next ``uv sync``.
|
|
180
|
+
|
|
181
|
+
A wildcard band (``==1.0.*``, ``!=1.0.*``) that already allows ``latest`` is
|
|
182
|
+
left exactly as written — narrowing it to a single release would be a policy
|
|
183
|
+
change, not a version bump. ``~=`` moves within its own width for the same
|
|
184
|
+
reason: ``~=1.4`` becomes ``~=1.5``, never ``~=1.5.0``, because the latter
|
|
185
|
+
narrows the implied band from ``==1.*`` to ``==1.5.*`` and would make the
|
|
186
|
+
next run report the following minor as excluded.
|
|
187
|
+
"""
|
|
188
|
+
if version.endswith(_WILDCARD):
|
|
189
|
+
return version
|
|
190
|
+
if op == "~=":
|
|
191
|
+
segments = len(version.split("."))
|
|
192
|
+
parts = latest.split(".")
|
|
193
|
+
if len(parts) < segments:
|
|
194
|
+
return version
|
|
195
|
+
candidate = ".".join(parts[:segments])
|
|
196
|
+
return candidate if _compare(candidate, version) > 0 else version
|
|
197
|
+
if op in _FLOOR_OPS:
|
|
198
|
+
return latest if _compare(latest, version) > 0 else version
|
|
199
|
+
return version
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def rewrite_requirement(spec: str, latest: str, *, loosen: bool = False) -> RewriteResult:
|
|
203
|
+
"""Point ``spec`` at ``latest``, preserving its pin style unless ``loosen``.
|
|
204
|
+
|
|
205
|
+
Returns a ``RewriteResult`` whose ``spec`` is ``None`` when the requirement
|
|
206
|
+
already asks for ``latest`` (or for something newer), when it carries no
|
|
207
|
+
floor to raise, or when an upper bound excludes ``latest`` (the last
|
|
208
|
+
carries a ``reason``).
|
|
209
|
+
"""
|
|
210
|
+
parsed = parse_requirement(spec)
|
|
211
|
+
if parsed is None:
|
|
212
|
+
return RewriteResult(None)
|
|
213
|
+
|
|
214
|
+
if loosen or not parsed.clauses:
|
|
215
|
+
# Nothing to preserve — an unconstrained dependency has no pin style,
|
|
216
|
+
# so it gets the tool's default floor. Extras and markers survive.
|
|
217
|
+
new = f"{parsed.name}{parsed.extras}>={latest}{parsed.marker}"
|
|
218
|
+
return RewriteResult(None) if new == spec.strip() else RewriteResult(new)
|
|
219
|
+
|
|
220
|
+
blocker = _excluded_by(parsed, latest)
|
|
221
|
+
if blocker is not None:
|
|
222
|
+
return RewriteResult(None, reason=f"{latest} excluded by {blocker}")
|
|
223
|
+
|
|
224
|
+
bumped = tuple((op, _bump(op, version, latest)) for op, version in parsed.clauses)
|
|
225
|
+
if bumped == parsed.clauses:
|
|
226
|
+
# Nothing here names a floor to raise — a bare ``>``/``<``/``!=``, or a
|
|
227
|
+
# wildcard band that already covers ``latest``. Rewriting either would
|
|
228
|
+
# invent a constraint the author didn't ask for.
|
|
229
|
+
return RewriteResult(None)
|
|
230
|
+
|
|
231
|
+
new = _render(parsed, bumped)
|
|
232
|
+
return RewriteResult(None) if new == spec.strip() else RewriteResult(new)
|
{simple_module_cli-0.0.32 → simple_module_cli-0.0.34}/simple_module_cli/templates/host/main.py
RENAMED
|
@@ -33,12 +33,15 @@ if str(_HOST_DIR) not in sys.path:
|
|
|
33
33
|
os.chdir(_REPO_ROOT)
|
|
34
34
|
load_dotenv_into_environ(_REPO_ROOT / ".env")
|
|
35
35
|
|
|
36
|
-
from simple_module_hosting import
|
|
36
|
+
from simple_module_hosting import create_app, merge_host_settings # noqa: E402
|
|
37
37
|
from simple_module_hosting.logging import setup_logging # noqa: E402
|
|
38
38
|
|
|
39
39
|
from routes import router as host_router # noqa: E402
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
# merge_host_settings, not Settings(): the host knobs (log level, trusted
|
|
42
|
+
# proxy, auth provider, DB pool) are DB-backed, and create_app's own fallback
|
|
43
|
+
# does not fire because settings is passed to it explicitly below.
|
|
44
|
+
settings = merge_host_settings()
|
|
42
45
|
|
|
43
46
|
setup_logging(
|
|
44
47
|
level=settings.log_level,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Fixtures shared by the
|
|
1
|
+
"""Fixtures shared by the tests in this directory.
|
|
2
2
|
|
|
3
3
|
These test files have no ``__init__.py`` (test basenames are globally unique
|
|
4
4
|
instead), so a plain helper import across files is not available — a fixture is
|
|
@@ -10,10 +10,47 @@ from __future__ import annotations
|
|
|
10
10
|
import subprocess
|
|
11
11
|
import sys
|
|
12
12
|
from pathlib import Path
|
|
13
|
+
from urllib.error import HTTPError
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def fake_pypi():
|
|
20
|
+
"""Return a factory building a `package-update` fetcher stub.
|
|
21
|
+
|
|
22
|
+
Takes `{dist_name: latest_version}` and answers only for those names;
|
|
23
|
+
anything else raises the 404 the real fetcher would see. Keeps the CLI
|
|
24
|
+
tests off the network.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def factory(versions: dict[str, str]):
|
|
28
|
+
def fetcher(url: str) -> dict:
|
|
29
|
+
name = url.rsplit("/json", 1)[0].rsplit("/", 1)[1]
|
|
30
|
+
if name not in versions:
|
|
31
|
+
raise HTTPError(url, 404, "not found", {}, None)
|
|
32
|
+
latest = versions[name]
|
|
33
|
+
return {"info": {"version": latest}, "releases": {latest: [{"yanked": False}]}}
|
|
34
|
+
|
|
35
|
+
return fetcher
|
|
36
|
+
|
|
37
|
+
return factory
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def write_pyproject():
|
|
42
|
+
"""Return a factory writing a minimal pyproject with the given deps."""
|
|
43
|
+
|
|
44
|
+
def factory(path: Path, *deps: str) -> None:
|
|
45
|
+
body = "".join(f' "{d}",\n' for d in deps)
|
|
46
|
+
path.write_text(
|
|
47
|
+
f'[project]\nname = "x"\nversion = "0"\ndependencies = [\n{body}]\n',
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return factory
|
|
52
|
+
|
|
53
|
+
|
|
17
54
|
@pytest.fixture
|
|
18
55
|
def make_importable_module(tmp_path: Path):
|
|
19
56
|
"""Return a factory creating a real importable package bound to a ModuleBase.
|