uv-development-toggle 0.2.1__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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Description: this syncs the project with the upstream copier template
3
+
4
+ uv tool run --with jinja2_shell_extension \
5
+ copier@latest update --vcs-ref=HEAD --trust --skip-tasks --skip-answered
@@ -0,0 +1,2 @@
1
+ # this exact file name is required in this exact location if updates are to work
2
+ {"_commit": "v0.2.1-27-gb520f68", "_src_path": "https://github.com/iloveitaly/python-package-template", "project_name": "uv-development-toggle", "full_name": "Michael Bianco", "email": "mike@mikebian.co", "github_username": "iloveitaly"}
@@ -0,0 +1,14 @@
1
+ layout uv
2
+
3
+ # if you are using orb for local development, this will work just fine
4
+ export DATABASE_HOST=${DATABASE_HOST:-postgres.$(basename $PWD).python-starter-template.orb.local}
5
+ export REDIS_HOST=${REDIS_HOST:-redis.$(basename $PWD).python-starter-template.orb.local}
6
+
7
+ export POSTGRES_USER=root
8
+ export POSTGRES_PASSWORD=password
9
+ export POSTGRES_DB=development
10
+
11
+ export DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/development
12
+ export REDIS_URL=redis://${REDIS_HOST}:6379/1
13
+
14
+ export LOG_LEVEL=DEBUG
@@ -0,0 +1,12 @@
1
+ version: 2
2
+
3
+ updates:
4
+ - package-ecosystem: "github-actions"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+
9
+ - package-ecosystem: "uv"
10
+ directory: "/"
11
+ schedule:
12
+ interval: "weekly"
@@ -0,0 +1,65 @@
1
+ name: Build and Publish to PyPI
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ - master
7
+
8
+ # by default, permissions are read-only, read + write is required for git pushes
9
+ permissions:
10
+ contents: write
11
+
12
+ env:
13
+ PIP_DEFAULT_TIMEOUT: 60
14
+ PIP_RETRIES: 5
15
+
16
+ jobs:
17
+ # if you want to test across multiple python versions
18
+ matrix-test:
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ python-version: ["3.13", "3.12", "3.11", "3.10", "3.9"]
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: jdx/mise-action@v2
26
+ - run: mise use python@${{ matrix.python-version }}
27
+ - run: uv sync
28
+ - name: Make sure the CLI runs properly
29
+ run: uv run ${{ github.event.repository.name }} --help
30
+ - run: uv run pytest
31
+
32
+ build-and-publish:
33
+ needs: matrix-test
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: jdx/mise-action@v2
38
+ - run: uv sync
39
+ - name: Conventional Changelog Action
40
+ id: changelog
41
+ uses: TriPSs/conventional-changelog-action@v6
42
+ with:
43
+ github-token: ${{ secrets.github_token }}
44
+ version-file: "./pyproject.toml"
45
+ version-path: "project.version"
46
+ fallback-version: "0.1.0"
47
+ output-file: "CHANGELOG.md"
48
+
49
+ # NOTE must run after versioning otherwise the right version won't be pushed
50
+ - name: Build distribution package
51
+ if: ${{ steps.changelog.outputs.skipped == 'false' }}
52
+ run: uv build
53
+
54
+ - name: Publish to PyPI
55
+ if: ${{ steps.changelog.outputs.skipped == 'false' }}
56
+ # `gh secret set PYPI_API_TOKEN --app actions --body $PYPI_API_TOKEN`
57
+ run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
58
+
59
+ - name: Github Release
60
+ if: ${{ steps.changelog.outputs.skipped == 'false' }}
61
+ uses: softprops/action-gh-release@v2
62
+ with:
63
+ # output options: https://github.com/TriPSs/conventional-changelog-action#outputs
64
+ body: ${{ steps.changelog.outputs.clean_changelog }}
65
+ tag_name: ${{ steps.changelog.outputs.tag }}
@@ -0,0 +1,16 @@
1
+ name: Repository Metadata Sync
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+
7
+ jobs:
8
+ repo_sync:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Fetching Local Repository
12
+ uses: actions/checkout@v4
13
+ - name: Repository Metadata Sync
14
+ uses: iloveitaly/github-actions-metadata-sync@main
15
+ with:
16
+ TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
@@ -0,0 +1,129 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
@@ -0,0 +1,2 @@
1
+ python 3.13.2
2
+ uv 0.6.6
@@ -0,0 +1,36 @@
1
+ {
2
+ "[python]": {
3
+ "editor.formatOnSave": true,
4
+ "editor.defaultFormatter": "charliermarsh.ruff",
5
+ "editor.codeActionsOnSave": {
6
+ "source.fixAll": "explicit",
7
+ "source.organizeImports": "explicit"
8
+ },
9
+ "editor.tabSize": 4
10
+ },
11
+ "[toml]": {
12
+ "editor.formatOnSave": true,
13
+ "editor.tabSize": 4
14
+ },
15
+ "python.analysis.autoFormatStrings": true,
16
+
17
+ // for import autosuggest
18
+ "python.analysis.indexing": true,
19
+ "python.analysis.autoImportCompletions": true,
20
+
21
+ "python.analysis.packageIndexDepths": [
22
+ {
23
+ "name": "",
24
+ "depth": 3,
25
+ "includeAllSymbols": true
26
+ }
27
+ ],
28
+
29
+ "cSpell.words": ["openai", "httpx"],
30
+
31
+ "files.exclude": {
32
+ ".ruff_cache": true,
33
+ ".pytest_cache": true,
34
+ ".venv": true
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ ## [0.2.1](https://github.com/iloveitaly/uv-development-toggle/compare/v0.2.0...v0.2.1) (2025-03-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * clarify argparse help messages for module toggling ([fe9200b](https://github.com/iloveitaly/uv-development-toggle/commit/fe9200b4c886e24b028ec61444c79bd0e5eb7faf))
7
+
8
+
9
+
10
+ # [0.2.0](https://github.com/iloveitaly/uv-development-toggle/compare/d6ebea655086f9d0d430a1f01062222e96796a55...v0.2.0) (2025-03-19)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * adjust module path resolution with env variable ([d6ebea6](https://github.com/iloveitaly/uv-development-toggle/commit/d6ebea655086f9d0d430a1f01062222e96796a55))
16
+ * handle missing GitHub URL more robustly in script.py ([f9a4111](https://github.com/iloveitaly/uv-development-toggle/commit/f9a4111a207a094153d8a8525377a674b9ceb34b))
17
+ * handle missing project_urls in get_pypi_homepage function ([2dc7a62](https://github.com/iloveitaly/uv-development-toggle/commit/2dc7a624a188f38b3d63680fdfa739303643addf))
18
+ * handle missing pyproject.toml in toggle_module_source ([9c55ee8](https://github.com/iloveitaly/uv-development-toggle/commit/9c55ee8895db5dfa426e6800a81ce627c42f9179))
19
+ * handle module naming variations in toggle_module_source function ([db36fa2](https://github.com/iloveitaly/uv-development-toggle/commit/db36fa26b6d2b6f0cbd87b3b8aa3da081becbf2d))
20
+ * prioritize GitHub links in get_pypi_homepage function ([91fc748](https://github.com/iloveitaly/uv-development-toggle/commit/91fc748219166f8c15c48624dab00d70c7198e6a))
21
+ * specify VCS reference in copier update script ([a206262](https://github.com/iloveitaly/uv-development-toggle/commit/a2062628e189fc67569fe3f479401222d8d6769d))
22
+
23
+
24
+ ### Features
25
+
26
+ * add PyPI force option in toggle_module_source function ([9fbd995](https://github.com/iloveitaly/uv-development-toggle/commit/9fbd9958b68a5d37dd042259eff797d566920a8e))
27
+ * conditional git init and add import tests ([8757f65](https://github.com/iloveitaly/uv-development-toggle/commit/8757f65c38e717b9389f50fa14e65f556b91504e))
28
+
29
+
30
+
@@ -0,0 +1,7 @@
1
+ setup:
2
+ uv venv && uv sync
3
+ @echo "activate: source ./.venv/bin/activate"
4
+
5
+ clean:
6
+ rm -rf *.egg-info
7
+ rm -rf .venv
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: uv-development-toggle
3
+ Version: 0.2.1
4
+ Summary: Easily toggle between development and production packages with uv. Quickly close development packages locally.
5
+ Project-URL: Repository, https://github.com/iloveitaly/uv-development-toggle
6
+ Author-email: Michael Bianco <mike@mikebian.co>
7
+ Keywords: development,package,uv
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: tomlkit>=0.13.2
10
+ Description-Content-Type: text/markdown
11
+
12
+
13
+ # Python Development Source Toggler
14
+
15
+ A utility script for easily switching between local development and published sources for Python packages in your `pyproject.toml`.
16
+
17
+ ## Features
18
+
19
+ - Automatically toggles between local development paths and GitHub sources
20
+ - Preserves TOML file comments and structure
21
+ - Automatically clones repositories when switching to local development
22
+ - Supports branch tracking
23
+ - Falls back to PyPI metadata if direct GitHub repository is not found
24
+ - Integrates with GitHub CLI for username detection
25
+
26
+ ## Installation
27
+
28
+
29
+ pip install -r requirements.txt
30
+
31
+
32
+ ## Usage
33
+
34
+ To toggle a module named "activemodel":
35
+
36
+ ```shell
37
+ pip install uv-development-toggle
38
+ uv-development-toggle activemodel --published
39
+ ```
40
+
41
+ This will:
42
+
43
+ 1. Check if the package exists in your `PYTHON_DEVELOPMENT_TOGGLE` directory
44
+ 2. If switching to local and the repository doesn't exist, clone it automatically (attempts to determine the repo URL from pypi information)
45
+ 3. Update your `pyproject.toml` with the appropriate source configuration
46
+ 4. Preserve any existing branch information when toggling
47
+
48
+ ### Arguments
49
+
50
+ - `MODULE_NAME`: The name of the Python module to toggle
51
+ - `--local`: Force using local development path
52
+ - `--published`: Force using published source
53
+
54
+ ### Environment Variables
55
+
56
+ - `PYTHON_DEVELOPMENT_TOGGLE`: Directory for local development repositories (default: "pypi")
@@ -0,0 +1,45 @@
1
+
2
+ # Python Development Source Toggler
3
+
4
+ A utility script for easily switching between local development and published sources for Python packages in your `pyproject.toml`.
5
+
6
+ ## Features
7
+
8
+ - Automatically toggles between local development paths and GitHub sources
9
+ - Preserves TOML file comments and structure
10
+ - Automatically clones repositories when switching to local development
11
+ - Supports branch tracking
12
+ - Falls back to PyPI metadata if direct GitHub repository is not found
13
+ - Integrates with GitHub CLI for username detection
14
+
15
+ ## Installation
16
+
17
+
18
+ pip install -r requirements.txt
19
+
20
+
21
+ ## Usage
22
+
23
+ To toggle a module named "activemodel":
24
+
25
+ ```shell
26
+ pip install uv-development-toggle
27
+ uv-development-toggle activemodel --published
28
+ ```
29
+
30
+ This will:
31
+
32
+ 1. Check if the package exists in your `PYTHON_DEVELOPMENT_TOGGLE` directory
33
+ 2. If switching to local and the repository doesn't exist, clone it automatically (attempts to determine the repo URL from pypi information)
34
+ 3. Update your `pyproject.toml` with the appropriate source configuration
35
+ 4. Preserve any existing branch information when toggling
36
+
37
+ ### Arguments
38
+
39
+ - `MODULE_NAME`: The name of the Python module to toggle
40
+ - `--local`: Force using local development path
41
+ - `--published`: Force using published source
42
+
43
+ ### Environment Variables
44
+
45
+ - `PYTHON_DEVELOPMENT_TOGGLE`: Directory for local development repositories (default: "pypi")
@@ -0,0 +1,68 @@
1
+ # NOTE some limitations of copier:
2
+ #
3
+ # - any extensions must be installed manually
4
+ # - you cannot use dst_path as default answers
5
+
6
+ _min_copier_version: 9.4.1
7
+
8
+ _answers_file: .copier/.copier-answers.yml
9
+
10
+ _jinja_extensions:
11
+ - jinja2_shell_extension.ShellExtension
12
+
13
+ _message_after_copy: |
14
+ Next steps:
15
+
16
+ 1. Customize secrets in .envrc
17
+ 2. Run `direnv allow`
18
+ 3. Set PYPI_API_TOKEN on GH actions
19
+ 4. Set GH_TOKEN on GH actions
20
+
21
+ project_name:
22
+ type: str
23
+ help: Dash separated project slug
24
+ default: "{{ \"basename $(pwd)\" | shell() | trim | regex_replace(' ', '-') | regex_replace('_', '-') }}"
25
+ validator: >-
26
+ {% if not (project_name | regex_search('^[a-z][a-z0-9-_]+$')) %}
27
+ project_name must start with a letter, followed one or more letters, digits or dashes all lowercase.
28
+ {% endif %}
29
+
30
+ # https://github.com/superlinear-ai/substrate/blob/main/copier.yml
31
+ project_name_snake_case:
32
+ when: false
33
+ default: "{{ project_name | lower | replace('-', '_') }}"
34
+
35
+ full_name:
36
+ type: str
37
+ help: Full name of the project owner (for pypi)
38
+ default: "{{ \"git config --global user.name\" | shell() | trim }}"
39
+
40
+ email:
41
+ type: str
42
+ help: Email of the project owner (for pypi)
43
+ default: "{{ \"git config --global user.email\" | shell() | trim }}"
44
+
45
+ github_username:
46
+ type: str
47
+ help: GitHub username of the project owner (for pypi)
48
+ default: "{{ \"gh api user --jq '.login'\" | shell() | trim }}"
49
+
50
+ _exclude:
51
+ - TODO
52
+ - /.git
53
+ - /README.md
54
+ - /CHANGELOG.md
55
+ - /LICENSE
56
+ - /uv.lock
57
+ - /metadata.json
58
+
59
+ _tasks:
60
+ - '[ ! -d .git ] && git init'
61
+ - touch README.md
62
+ - uv sync
63
+ - git add -A
64
+ - git commit -m "🎉 Initial commit"
65
+ # - ["{{ _copier_python }}", .copier/bootstrap.py]
66
+
67
+ # although it's annoying to have the .copier-answers.yml file in the root, it allows `copier update`
68
+ # to work properly
@@ -0,0 +1,36 @@
1
+ # ports are not exposed locally, instead dynamic orb domains are used to avoid clobbering localhost ports
2
+ services:
3
+ # port 5432
4
+ postgres:
5
+ image: postgres:latest
6
+ environment:
7
+ POSTGRES_USER: root
8
+ POSTGRES_PASSWORD: password
9
+ POSTGRES_DB: development
10
+ ports:
11
+ - ${CI:+5432}:5432
12
+ volumes:
13
+ - app_postgres_data:/var/lib/postgresql/data/
14
+ healthcheck:
15
+ # if you customize the default user at all, this healthcheck with clutter your logs
16
+ # since the default username + database does not match up
17
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
18
+ interval: 2s
19
+ timeout: 5s
20
+ retries: 3
21
+
22
+ # port 6379
23
+ redis:
24
+ image: redis:latest
25
+ restart: always
26
+ ports:
27
+ - ${CI:+6379}:6379
28
+ healthcheck:
29
+ test: ["CMD-SHELL", "redis-cli ping"]
30
+ interval: 2s
31
+ timeout: 5s
32
+ retries: 3
33
+
34
+ volumes:
35
+ # NOTE redis data is not persisted, assumed to be ephemeral
36
+ app_postgres_data:
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "uv-development-toggle"
3
+ version = "0.2.1"
4
+ description = "Easily toggle between development and production packages with uv. Quickly close development packages locally."
5
+ keywords = ["uv", "development", "package"]
6
+ readme = "README.md"
7
+ requires-python = ">=3.9"
8
+ dependencies = ["tomlkit>=0.13.2"]
9
+ authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
10
+ urls = { "Repository" = "https://github.com/iloveitaly/uv-development-toggle" }
11
+
12
+ # additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
13
+ [project.scripts]
14
+ uv-development-toggle = "uv_development_toggle:main"
15
+
16
+ # https://github.com/astral-sh/uv/issues/5200
17
+ [tool.uv]
18
+ package = true
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [dependency-groups]
25
+ dev = ["pytest>=8.3.3"]
@@ -0,0 +1,236 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from urllib.request import urlopen
9
+
10
+ import tomlkit
11
+
12
+ logging.basicConfig(level=logging.DEBUG)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def get_github_username() -> str:
17
+ logger.debug("Attempting to get GitHub username")
18
+ # Try gh cli first
19
+ try:
20
+ result = subprocess.run(["gh", "api", "user"], capture_output=True, text=True)
21
+ if result.returncode == 0:
22
+ username = json.loads(result.stdout)["login"]
23
+ logger.debug(f"Found username via gh cli: {username}")
24
+ return username
25
+ except FileNotFoundError:
26
+ logger.debug("gh cli not found, trying git config")
27
+
28
+ # Fall back to git config
29
+ try:
30
+ result = subprocess.run(
31
+ ["git", "config", "user.name"], capture_output=True, text=True
32
+ )
33
+ if result.returncode == 0:
34
+ username = result.stdout.strip()
35
+ logger.debug(f"Found username via git config: {username}")
36
+ return username
37
+ except FileNotFoundError:
38
+ logger.debug("git not found")
39
+
40
+ return None
41
+
42
+
43
+ def check_github_repo_exists(username: str, repo: str) -> bool:
44
+ logger.debug(f"Checking if repo exists: {username}/{repo}")
45
+ try:
46
+ urlopen(f"https://github.com/{username}/{repo}")
47
+ logger.debug("Repository found")
48
+ return True
49
+ except:
50
+ logger.debug("Repository not found")
51
+ return False
52
+
53
+
54
+ def get_pypi_info(package_name: str) -> dict:
55
+ logger.debug(f"Fetching PyPI data for {package_name}")
56
+ try:
57
+ with urlopen(f"https://pypi.org/pypi/{package_name}/json") as response:
58
+ data = json.loads(response.read())
59
+ logger.debug("Successfully fetched PyPI data")
60
+ return data
61
+ except:
62
+ logger.debug("Failed to fetch PyPI data")
63
+ return {}
64
+
65
+
66
+ def get_pypi_homepage(package_name: str) -> str:
67
+ data = get_pypi_info(package_name)
68
+ homepage = data.get("info", {}).get("home_page", "")
69
+
70
+ # Ensure homepage is not None
71
+ homepage = homepage or ""
72
+
73
+ # Check if homepage contains github.com
74
+ if homepage and "github.com" in homepage:
75
+ return homepage
76
+
77
+ # Check all project URLs for GitHub links
78
+ project_urls = data.get("info", {}).get("project_urls") or {}
79
+
80
+ # First try the repository link as priority
81
+ if (
82
+ "repository" in project_urls
83
+ and project_urls["repository"]
84
+ and "github.com" in project_urls["repository"]
85
+ ):
86
+ return project_urls["repository"]
87
+
88
+ # Then look in all other project links
89
+ for url_name, url in project_urls.items():
90
+ if url and "github.com" in url:
91
+ return url
92
+
93
+ # Return homepage even if it's not a GitHub URL, or empty string
94
+ return homepage
95
+
96
+
97
+ def clone_repo(github_url: str, target_path: Path):
98
+ logger.info(f"Cloning {github_url} into {target_path}")
99
+ subprocess.run(["git", "clone", github_url, str(target_path)], check=True)
100
+
101
+
102
+ def get_current_branch(repo_path: Path) -> str:
103
+ result = subprocess.run(
104
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
105
+ capture_output=True,
106
+ text=True,
107
+ cwd=str(repo_path),
108
+ )
109
+ return result.stdout.strip()
110
+
111
+
112
+ def toggle_module_source(
113
+ module_name: str,
114
+ force_local: bool = False,
115
+ force_published: bool = False,
116
+ force_pypi: bool = False,
117
+ ):
118
+ pyproject_path = Path("pyproject.toml")
119
+
120
+ # Check if the pyproject.toml exists
121
+ if not pyproject_path.exists():
122
+ logger.error("No pyproject.toml found, are you in the right folder?")
123
+ sys.exit(1)
124
+
125
+ # Read with tomlkit to preserve comments and structure
126
+ with open(pyproject_path) as f:
127
+ config = tomlkit.load(f)
128
+
129
+ sources = config["tool"]["uv"]["sources"]
130
+ current_source = sources.get(module_name, {})
131
+
132
+ # Handle PyPI option
133
+ if force_pypi:
134
+ # For PyPI, we remove the source entry or set it to {} to use default PyPI source
135
+ if module_name in sources:
136
+ logger.info(f"Removing custom source for {module_name} to use PyPI version")
137
+ del sources[module_name]
138
+ else:
139
+ logger.info(f"Already using PyPI version for {module_name}")
140
+
141
+ # Write back with preserved comments
142
+ with open(pyproject_path, "w") as f:
143
+ tomlkit.dump(config, f)
144
+
145
+ return
146
+
147
+ dev_toggle_dir = os.environ.get("PYTHON_DEVELOPMENT_TOGGLE", "pypi")
148
+ local_path_default = Path(f"{dev_toggle_dir}/{module_name}")
149
+ local_path_dash = Path(f"{dev_toggle_dir}/{module_name.replace('_', '-')}")
150
+ local_path_underscore = Path(f"{dev_toggle_dir}/{module_name.replace('-', '_')}")
151
+
152
+ if local_path_default.exists():
153
+ local_path = local_path_default
154
+ elif local_path_dash.exists():
155
+ local_path = local_path_dash
156
+ elif local_path_underscore.exists():
157
+ local_path = local_path_underscore
158
+ else:
159
+ local_path = local_path_default
160
+
161
+ # Get current branch if local repo exists
162
+ current_branch = None
163
+ if local_path.exists():
164
+ current_branch = get_current_branch(local_path)
165
+ if current_branch in ("master", "main"):
166
+ current_branch = None
167
+
168
+ # Try to find the correct GitHub source
169
+ github_url = None
170
+ username = get_github_username()
171
+
172
+ if username and check_github_repo_exists(username, module_name):
173
+ github_url = f"https://github.com/{username}/{module_name}.git"
174
+ else:
175
+ pypi_homepage = get_pypi_homepage(module_name)
176
+ if "github.com" in pypi_homepage:
177
+ github_url = f"{pypi_homepage}.git"
178
+
179
+ if not github_url:
180
+ logger.warning(f"Could not determine GitHub URL for {module_name}")
181
+
182
+ if not local_path.exists():
183
+ logger.info(
184
+ f"Local path {local_path} does not exist and gh url failed, exiting"
185
+ )
186
+ sys.exit(1)
187
+
188
+ # Add branch to github_url if available
189
+ if current_branch:
190
+ published_source = {"git": github_url, "rev": current_branch}
191
+ else:
192
+ published_source = {"git": github_url}
193
+
194
+ local_source = {"path": str(local_path), "editable": True}
195
+
196
+ if force_local or (not force_published and "git" in current_source):
197
+ new_source = local_source
198
+ if not local_path.exists():
199
+ logger.info(f"Local path {local_path} does not exist")
200
+ clone_repo(github_url, local_path)
201
+ else:
202
+ new_source = published_source
203
+
204
+ sources[module_name] = new_source
205
+
206
+ # Write back with preserved comments
207
+ with open(pyproject_path, "w") as f:
208
+ tomlkit.dump(config, f)
209
+
210
+ logger.info(f"Set {module_name} source to: {new_source}")
211
+
212
+ # TODO should run something like, but make it more isolated since this will drop other groups that were previously installed
213
+ # uv sync --upgrade-package starlette-context
214
+
215
+
216
+ def main():
217
+ parser = argparse.ArgumentParser()
218
+ parser.add_argument("module", help="Module name to toggle")
219
+ parser.add_argument("--local", action="store_true", help="Force local path")
220
+ parser.add_argument(
221
+ "--published",
222
+ action="store_true",
223
+ help="Force published github source",
224
+ )
225
+ parser.add_argument(
226
+ "--pypi",
227
+ action="store_true",
228
+ help="Force PyPI published version",
229
+ )
230
+
231
+ args = parser.parse_args()
232
+ toggle_module_source(args.module, args.local, args.published, args.pypi)
233
+
234
+
235
+ if __name__ == "__main__":
236
+ main()
File without changes
@@ -0,0 +1,8 @@
1
+ """Test uv-development-toggle."""
2
+
3
+ import uv_development_toggle
4
+
5
+
6
+ def test_import() -> None:
7
+ """Test that the can be imported."""
8
+ assert isinstance(uv_development_toggle.__name__, str)
@@ -0,0 +1,8 @@
1
+ """Test {{ project_name }}."""
2
+
3
+ import {{ project_name_snake_case }}
4
+
5
+
6
+ def test_import() -> None:
7
+ """Test that the {{ project_type }} can be imported."""
8
+ assert isinstance({{ project_name_snake_case }}.__name__, str)
@@ -0,0 +1,132 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.9"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "exceptiongroup"
16
+ version = "1.2.2"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "iniconfig"
25
+ version = "2.0.0"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "packaging"
34
+ version = "24.2"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "pluggy"
43
+ version = "1.5.0"
44
+ source = { registry = "https://pypi.org/simple" }
45
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
46
+ wheels = [
47
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
48
+ ]
49
+
50
+ [[package]]
51
+ name = "pytest"
52
+ version = "8.3.5"
53
+ source = { registry = "https://pypi.org/simple" }
54
+ dependencies = [
55
+ { name = "colorama", marker = "sys_platform == 'win32'" },
56
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
57
+ { name = "iniconfig" },
58
+ { name = "packaging" },
59
+ { name = "pluggy" },
60
+ { name = "tomli", marker = "python_full_version < '3.11'" },
61
+ ]
62
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
63
+ wheels = [
64
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
65
+ ]
66
+
67
+ [[package]]
68
+ name = "tomli"
69
+ version = "2.2.1"
70
+ source = { registry = "https://pypi.org/simple" }
71
+ sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
72
+ wheels = [
73
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
74
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
75
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
76
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
77
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
78
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
79
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
80
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
81
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
82
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
83
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
84
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
85
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
86
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
87
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
88
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
89
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
90
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
91
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
92
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
93
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
94
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
95
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
96
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
97
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
98
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
99
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
100
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
101
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
102
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
103
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
104
+ ]
105
+
106
+ [[package]]
107
+ name = "tomlkit"
108
+ version = "0.13.2"
109
+ source = { registry = "https://pypi.org/simple" }
110
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "uv-development-toggle"
117
+ version = "0.2.0"
118
+ source = { editable = "." }
119
+ dependencies = [
120
+ { name = "tomlkit" },
121
+ ]
122
+
123
+ [package.dev-dependencies]
124
+ dev = [
125
+ { name = "pytest" },
126
+ ]
127
+
128
+ [package.metadata]
129
+ requires-dist = [{ name = "tomlkit", specifier = ">=0.13.2" }]
130
+
131
+ [package.metadata.requires-dev]
132
+ dev = [{ name = "pytest", specifier = ">=8.3.3" }]
@@ -0,0 +1,243 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from urllib.request import urlopen
9
+
10
+ import tomlkit
11
+
12
+ logging.basicConfig(
13
+ level=os.environ.get("LOG_LEVEL", "INFO").upper(),
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def get_github_username() -> str:
20
+ logger.debug("Attempting to get GitHub username")
21
+ # Try gh cli first
22
+ try:
23
+ result = subprocess.run(["gh", "api", "user"], capture_output=True, text=True)
24
+ if result.returncode == 0:
25
+ username = json.loads(result.stdout)["login"]
26
+ logger.debug(f"Found username via gh cli: {username}")
27
+ return username
28
+ except FileNotFoundError:
29
+ logger.debug("gh cli not found, trying git config")
30
+
31
+ # Fall back to git config
32
+ try:
33
+ result = subprocess.run(
34
+ ["git", "config", "user.name"], capture_output=True, text=True
35
+ )
36
+ if result.returncode == 0:
37
+ username = result.stdout.strip()
38
+ logger.debug(f"Found username via git config: {username}")
39
+ return username
40
+ except FileNotFoundError:
41
+ logger.debug("git not found")
42
+
43
+ return None
44
+
45
+
46
+ def check_github_repo_exists(username: str, repo: str) -> bool:
47
+ logger.debug(f"Checking if repo exists: {username}/{repo}")
48
+ try:
49
+ urlopen(f"https://github.com/{username}/{repo}")
50
+ logger.debug("Repository found")
51
+ return True
52
+ except:
53
+ logger.debug("Repository not found")
54
+ return False
55
+
56
+
57
+ def get_pypi_info(package_name: str) -> dict:
58
+ logger.debug(f"Fetching PyPI data for {package_name}")
59
+ try:
60
+ with urlopen(f"https://pypi.org/pypi/{package_name}/json") as response:
61
+ data = json.loads(response.read())
62
+ logger.debug("Successfully fetched PyPI data")
63
+ return data
64
+ except:
65
+ logger.debug("Failed to fetch PyPI data")
66
+ return {}
67
+
68
+
69
+ def get_pypi_homepage(package_name: str) -> str:
70
+ data = get_pypi_info(package_name)
71
+ homepage = data.get("info", {}).get("home_page", "")
72
+
73
+ # Ensure homepage is not None
74
+ homepage = homepage or ""
75
+
76
+ # Check if homepage contains github.com
77
+ if homepage and "github.com" in homepage:
78
+ return homepage
79
+
80
+ # Check all project URLs for GitHub links
81
+ project_urls = data.get("info", {}).get("project_urls") or {}
82
+
83
+ # First try the repository link as priority
84
+ if (
85
+ "repository" in project_urls
86
+ and project_urls["repository"]
87
+ and "github.com" in project_urls["repository"]
88
+ ):
89
+ return project_urls["repository"]
90
+
91
+ # Then look in all other project links
92
+ for url_name, url in project_urls.items():
93
+ if url and "github.com" in url:
94
+ return url
95
+
96
+ # Return homepage even if it's not a GitHub URL, or empty string
97
+ return homepage
98
+
99
+
100
+ def clone_repo(github_url: str, target_path: Path):
101
+ logger.info(f"Cloning {github_url} into {target_path}")
102
+ subprocess.run(["git", "clone", github_url, str(target_path)], check=True)
103
+
104
+
105
+ def get_current_branch(repo_path: Path) -> str:
106
+ result = subprocess.run(
107
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
108
+ capture_output=True,
109
+ text=True,
110
+ cwd=str(repo_path),
111
+ )
112
+ return result.stdout.strip()
113
+
114
+
115
+ def toggle_module_source(
116
+ module_name: str,
117
+ force_local: bool = False,
118
+ force_published: bool = False,
119
+ force_pypi: bool = False,
120
+ ):
121
+ pyproject_path = Path("pyproject.toml")
122
+
123
+ # Check if the pyproject.toml exists
124
+ if not pyproject_path.exists():
125
+ logger.error("No pyproject.toml found, are you in the right folder?")
126
+ sys.exit(1)
127
+
128
+ # Read with tomlkit to preserve comments and structure
129
+ with open(pyproject_path) as f:
130
+ config = tomlkit.load(f)
131
+
132
+ sources = config["tool"]["uv"]["sources"]
133
+ current_source = sources.get(module_name, {})
134
+
135
+ # Handle PyPI option
136
+ if force_pypi:
137
+ # For PyPI, we remove the source entry or set it to {} to use default PyPI source
138
+ if module_name in sources:
139
+ logger.info(f"Removing custom source for {module_name} to use PyPI version")
140
+ del sources[module_name]
141
+ else:
142
+ logger.info(f"Already using PyPI version for {module_name}")
143
+
144
+ # Write back with preserved comments
145
+ with open(pyproject_path, "w") as f:
146
+ tomlkit.dump(config, f)
147
+
148
+ return
149
+
150
+ dev_toggle_dir = os.environ.get("PYTHON_DEVELOPMENT_TOGGLE", "pypi")
151
+ local_path_default = Path(f"{dev_toggle_dir}/{module_name}")
152
+ local_path_dash = Path(f"{dev_toggle_dir}/{module_name.replace('_', '-')}")
153
+ local_path_underscore = Path(f"{dev_toggle_dir}/{module_name.replace('-', '_')}")
154
+
155
+ if local_path_default.exists():
156
+ local_path = local_path_default
157
+ elif local_path_dash.exists():
158
+ local_path = local_path_dash
159
+ elif local_path_underscore.exists():
160
+ local_path = local_path_underscore
161
+ else:
162
+ local_path = local_path_default
163
+
164
+ # Get current branch if local repo exists
165
+ current_branch = None
166
+ if local_path.exists():
167
+ current_branch = get_current_branch(local_path)
168
+ if current_branch in ("master", "main"):
169
+ current_branch = None
170
+
171
+ # Try to find the correct GitHub source
172
+ github_url = None
173
+ username = get_github_username()
174
+
175
+ if username and check_github_repo_exists(username, module_name):
176
+ github_url = f"https://github.com/{username}/{module_name}.git"
177
+ else:
178
+ pypi_homepage = get_pypi_homepage(module_name)
179
+ if "github.com" in pypi_homepage:
180
+ github_url = f"{pypi_homepage}.git"
181
+
182
+ if not github_url:
183
+ logger.warning(f"Could not determine GitHub URL for {module_name}")
184
+
185
+ if not local_path.exists():
186
+ logger.info(
187
+ f"Local path {local_path} does not exist and gh url failed, exiting"
188
+ )
189
+ sys.exit(1)
190
+
191
+ # Add branch to github_url if available
192
+ if current_branch:
193
+ published_source = {"git": github_url, "rev": current_branch}
194
+ else:
195
+ published_source = {"git": github_url}
196
+
197
+ local_source = {"path": str(local_path), "editable": True}
198
+
199
+ if force_local or (not force_published and "git" in current_source):
200
+ new_source = local_source
201
+ if not local_path.exists():
202
+ logger.info(f"Local path {local_path} does not exist")
203
+ clone_repo(github_url, local_path)
204
+ else:
205
+ new_source = published_source
206
+
207
+ sources[module_name] = new_source
208
+
209
+ # Write back with preserved comments
210
+ with open(pyproject_path, "w") as f:
211
+ tomlkit.dump(config, f)
212
+
213
+ logger.info(f"Set {module_name} source to: {new_source}")
214
+
215
+ # TODO should run something like, but make it more isolated since this will drop other groups that were previously installed
216
+ # uv sync --upgrade-package starlette-context
217
+
218
+
219
+ def main():
220
+ parser = argparse.ArgumentParser()
221
+ parser.add_argument("module", help="Module name in pyproject.toml to toggle")
222
+ parser.add_argument(
223
+ "--local",
224
+ action="store_true",
225
+ help="Use local editable path, and clone repo if necessary",
226
+ )
227
+ parser.add_argument(
228
+ "--published",
229
+ action="store_true",
230
+ help="Use github source",
231
+ )
232
+ parser.add_argument(
233
+ "--pypi",
234
+ action="store_true",
235
+ help="Use PyPI published version",
236
+ )
237
+
238
+ args = parser.parse_args()
239
+ toggle_module_source(args.module, args.local, args.published, args.pypi)
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()